taco_it 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/taco +4 -743
- data/lib/taco/change.rb +75 -0
- data/lib/taco/cli.rb +107 -0
- data/lib/taco/defaults/tacorc +10 -0
- data/lib/taco/issue.rb +240 -0
- data/lib/taco/schema.rb +208 -0
- data/lib/taco/taco.rb +158 -0
- data/lib/taco/tacorc.rb +24 -0
- data/lib/taco.rb +4 -922
- metadata +10 -3
data/lib/taco/change.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
# FIXME: put this in a namespace
|
5
|
+
class Change
|
6
|
+
class Invalid < Exception; end
|
7
|
+
|
8
|
+
attr_reader :created_at
|
9
|
+
attr_accessor :attribute
|
10
|
+
attr_accessor :old_value
|
11
|
+
attr_accessor :new_value
|
12
|
+
|
13
|
+
def initialize(args={})
|
14
|
+
args.each do |attr, value|
|
15
|
+
raise ArgumentError.new("Unknown attribute #{attr}") unless self.respond_to?(attr)
|
16
|
+
|
17
|
+
case attr.to_sym
|
18
|
+
when :created_at
|
19
|
+
value = Time.parse(value) unless value.is_a?(Time)
|
20
|
+
when :attribute
|
21
|
+
value = value.to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
instance_variable_set("@#{attr.to_s}", value)
|
25
|
+
end
|
26
|
+
|
27
|
+
@created_at = Time.parse(@created_at) if @created_at.is_a?(String)
|
28
|
+
@created_at = timescrub(@created_at || Time.now)
|
29
|
+
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.from_json(the_json)
|
34
|
+
begin
|
35
|
+
hash = JSON.parse(the_json)
|
36
|
+
rescue JSON::ParserError => e
|
37
|
+
raise Change::Invalid.new(e.to_s)
|
38
|
+
end
|
39
|
+
|
40
|
+
Change.new(hash)
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid?(opts={})
|
44
|
+
# old_value is optional!
|
45
|
+
#
|
46
|
+
valid = created_at && attribute && new_value
|
47
|
+
raise Invalid if opts[:raise] && !valid
|
48
|
+
valid
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_json(state=nil)
|
52
|
+
valid? :raise => true
|
53
|
+
hash = { :created_at => created_at, :attribute => attribute, :old_value => old_value, :new_value => new_value }
|
54
|
+
JSON.pretty_generate(hash)
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s(opts={})
|
58
|
+
if opts[:simple]
|
59
|
+
"#{attribute} : #{old_value} => #{new_value}"
|
60
|
+
else
|
61
|
+
fields = [ date(created_at), attribute, old_value || '[nil]', new_value ]
|
62
|
+
"%10s : %12s : %s => %s" % fields
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def date(t)
|
68
|
+
t.strftime "%Y/%m/%d %H:%M:%S"
|
69
|
+
end
|
70
|
+
|
71
|
+
def timescrub(t)
|
72
|
+
Time.new t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
data/lib/taco/cli.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'taco/issue'
|
2
|
+
require 'taco/taco'
|
3
|
+
require 'taco/tacorc'
|
4
|
+
|
5
|
+
class TacoCLI
|
6
|
+
RETRY_NAME = '.taco_retry.txt'
|
7
|
+
|
8
|
+
TACORC_NAME = '.tacorc'
|
9
|
+
INDEX_ERB_NAME = '.index.html.erb'
|
10
|
+
|
11
|
+
DEFAULT_TACORC_NAME = 'tacorc'
|
12
|
+
DEFAULT_INDEX_ERB_NAME = 'index.html.erb'
|
13
|
+
DEFAULTS_HOME = File.realpath(File.join(File.dirname(__FILE__), 'defaults/'))
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@taco = Taco.new
|
17
|
+
|
18
|
+
@retry_path = File.join(@taco.home, RETRY_NAME)
|
19
|
+
|
20
|
+
@tacorc_path = File.join(@taco.home, TACORC_NAME)
|
21
|
+
@index_erb_path = File.join(@taco.home, INDEX_ERB_NAME)
|
22
|
+
|
23
|
+
if File.exist? @tacorc_path
|
24
|
+
rc = TacoRc.new @tacorc_path
|
25
|
+
rc.update_schema! Issue
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def init!
|
30
|
+
out = @taco.init!
|
31
|
+
|
32
|
+
FileUtils.copy(File.join(DEFAULTS_HOME, DEFAULT_TACORC_NAME), @tacorc_path)
|
33
|
+
FileUtils.copy(File.join(DEFAULTS_HOME, DEFAULT_INDEX_ERB_NAME), @index_erb_path)
|
34
|
+
|
35
|
+
out + "\nPlease edit the config file at #{@tacorc_path}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def list(args)
|
39
|
+
the_list = @taco.list(:short_ids => true, :filters => args).map do |issue, short_id|
|
40
|
+
"#{short_id} : #{issue.priority} : #{issue.summary}"
|
41
|
+
end
|
42
|
+
return "Found no issues." unless the_list.size > 0
|
43
|
+
the_list.join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def new!(args, opts)
|
47
|
+
editor_opts = if opts[:retry]
|
48
|
+
raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
|
49
|
+
{ :template => open(@retry_path) { |f| f.read } }
|
50
|
+
elsif args.size == 0
|
51
|
+
{ :template => Issue.new.to_template }
|
52
|
+
elsif args.size == 1
|
53
|
+
{ :from_file => args[0] }
|
54
|
+
end
|
55
|
+
|
56
|
+
if issue = IssueEditor.new(@taco, @retry_path).new_issue!(editor_opts)
|
57
|
+
"Created Issue #{issue.id}"
|
58
|
+
else
|
59
|
+
"Aborted."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def show(args, opts)
|
64
|
+
if opts[:all]
|
65
|
+
filters = args.select { |arg| arg.include? ':' }
|
66
|
+
args = @taco.list(:filters => filters).map(&:id)
|
67
|
+
end
|
68
|
+
|
69
|
+
args.map { |id| @taco.read(id).to_s(opts) }.join("\n\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def edit!(args, opts)
|
73
|
+
ie = IssueEditor.new @taco, @retry_path
|
74
|
+
|
75
|
+
if opts[:retry]
|
76
|
+
raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
|
77
|
+
template = open(@retry_path) { |f| f.read }
|
78
|
+
end
|
79
|
+
|
80
|
+
if issue = ie.edit_issue!(@taco.read(args[0]), :template => template)
|
81
|
+
"Updated Issue #{issue.id}"
|
82
|
+
else
|
83
|
+
"Aborted."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def template(opts)
|
88
|
+
if opts[:defaults]
|
89
|
+
(Issue::TEMPLATE % Issue.new.to_hash).strip
|
90
|
+
else
|
91
|
+
Issue::TEMPLATE.gsub(/%{.*?}/, '').strip
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def html
|
96
|
+
require 'erb'
|
97
|
+
|
98
|
+
issues = @taco.list
|
99
|
+
ERB.new(open(@index_erb_path) { |f| f.read }).result(binding)
|
100
|
+
end
|
101
|
+
|
102
|
+
def push(opts)
|
103
|
+
opts[:message] ||= 'turn and face the strange'
|
104
|
+
cmd = "git add . && git commit -am '#{opts[:message]}' && git push"
|
105
|
+
system(cmd)
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Empty lines and lines beginning with # will be ignored.
|
2
|
+
#
|
3
|
+
|
4
|
+
#
|
5
|
+
# schema_attr_update <attribute name>, [ default: <default attribute value> ], [ validate: <list of type-correct valid values, or lambda validator> ]
|
6
|
+
#
|
7
|
+
|
8
|
+
schema_attr_update :kind, default: 'Defect', validate: [ 'Defect', 'FeatReq' ]
|
9
|
+
schema_attr_update :status, default: 'Open', validate: [ 'Open', 'Closed' ]
|
10
|
+
schema_attr_update :priority, default: 3, validate: [ 1, 2, 3, 4, 5 ]
|
data/lib/taco/issue.rb
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'taco/schema'
|
2
|
+
require 'taco/change'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
class Issue
|
6
|
+
include Comparable
|
7
|
+
include Schema
|
8
|
+
|
9
|
+
alias_method :schema_valid?, :valid?
|
10
|
+
|
11
|
+
attr_reader :changelog
|
12
|
+
|
13
|
+
schema_attr :id, class: String, settable: false
|
14
|
+
schema_attr :created_at, class: Time, settable: false
|
15
|
+
schema_attr :updated_at, class: Time, settable: false
|
16
|
+
|
17
|
+
schema_attr :summary, class: String, settable: true
|
18
|
+
schema_attr :kind, class: String, settable: true
|
19
|
+
schema_attr :status, class: String, settable: true
|
20
|
+
schema_attr :owner, class: String, settable: true
|
21
|
+
schema_attr :priority, class: Fixnum, settable: true
|
22
|
+
schema_attr :description, class: String, settable: true
|
23
|
+
|
24
|
+
TEMPLATE =<<-EOT.strip
|
25
|
+
# Lines beginning with # will be ignored.
|
26
|
+
Summary : %{summary}
|
27
|
+
Kind : %{kind}
|
28
|
+
Status : %{status}
|
29
|
+
Priority : %{priority}
|
30
|
+
Owner : %{owner}
|
31
|
+
|
32
|
+
# Everything between the --- lines is Issue Description
|
33
|
+
---
|
34
|
+
%{description}
|
35
|
+
---
|
36
|
+
EOT
|
37
|
+
|
38
|
+
class Invalid < Exception; end
|
39
|
+
class NotFound < Exception; end
|
40
|
+
|
41
|
+
def initialize(attributes={}, changelog=[])
|
42
|
+
attributes = Hash[attributes.map { |k, v| [ k.to_sym, v ] }]
|
43
|
+
|
44
|
+
@new = attributes[:created_at].nil? && attributes[:id].nil?
|
45
|
+
|
46
|
+
@changelog = []
|
47
|
+
|
48
|
+
attributes.each do |attr, value|
|
49
|
+
schema_attr = self.class.schema_attributes[attr]
|
50
|
+
raise ArgumentError.new("unknown attribute: #{attr}") unless schema_attr
|
51
|
+
if schema_attr[:settable]
|
52
|
+
self.send "#{attr}=", attributes[attr]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
self.id = attributes[:id] || SecureRandom.uuid.gsub('-', '')
|
57
|
+
self.created_at = attributes[:created_at] || Time.now
|
58
|
+
self.updated_at = attributes[:updated_at] || Time.now
|
59
|
+
|
60
|
+
if changelog.size > 0
|
61
|
+
@changelog = changelog.map do |thing|
|
62
|
+
if thing.is_a? Change
|
63
|
+
thing
|
64
|
+
else
|
65
|
+
Change.new thing
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def schema_attribute_change(attribute, old_value, new_value)
|
74
|
+
if self.class.schema_attributes[attribute][:settable]
|
75
|
+
self.updated_at = Time.now
|
76
|
+
@changelog << Change.new(:attribute => attribute, :old_value => old_value, :new_value => new_value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def new?
|
81
|
+
@new
|
82
|
+
end
|
83
|
+
|
84
|
+
def <=>(other)
|
85
|
+
if self.class.schema_attributes.all? { |attr, opts| self.send(attr) == other.send(attr) }
|
86
|
+
r = 0
|
87
|
+
else
|
88
|
+
if self.created_at == other.created_at
|
89
|
+
r = self.id <=> other.id
|
90
|
+
else
|
91
|
+
r = self.created_at <=> other.created_at
|
92
|
+
end
|
93
|
+
|
94
|
+
# this clause should not return 0, we've already established inequality
|
95
|
+
#
|
96
|
+
r = -1 if r == 0
|
97
|
+
end
|
98
|
+
|
99
|
+
r
|
100
|
+
end
|
101
|
+
|
102
|
+
def inspect
|
103
|
+
fields = self.class.schema_attributes.map do |attr, opts|
|
104
|
+
"@#{attr}=#{self.send(attr).inspect}"
|
105
|
+
end.join ', '
|
106
|
+
|
107
|
+
"#<#{self.class}:0x%016x %s>" % [ object_id, fields ]
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_s(opts={})
|
111
|
+
text = <<-EOT.strip
|
112
|
+
ID : #{id}
|
113
|
+
Created At : #{date(created_at)}
|
114
|
+
Updated At : #{date(updated_at)}
|
115
|
+
|
116
|
+
Summary : #{summary}
|
117
|
+
Kind : #{kind}
|
118
|
+
Status : #{status}
|
119
|
+
Priority : #{priority}
|
120
|
+
Owner : #{owner}
|
121
|
+
|
122
|
+
---
|
123
|
+
#{description}
|
124
|
+
EOT
|
125
|
+
|
126
|
+
if opts[:changelog]
|
127
|
+
changelog_str = changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")
|
128
|
+
text << %Q|\n---\n\n#{changelog_str}|
|
129
|
+
end
|
130
|
+
|
131
|
+
text
|
132
|
+
end
|
133
|
+
|
134
|
+
def valid?(opts={})
|
135
|
+
valid = schema_valid?
|
136
|
+
error = schema_errors.first
|
137
|
+
raise Invalid.new("attribute #{error.first}: #{error[1].inspect} is not a valid value") if !valid && opts[:raise]
|
138
|
+
valid
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_json(state=nil)
|
142
|
+
valid? :raise => true
|
143
|
+
hash = { :issue => self.to_hash, :changelog => changelog }
|
144
|
+
JSON.pretty_generate(hash)
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_template
|
148
|
+
if new?
|
149
|
+
header = "# New Issue\n#"
|
150
|
+
body = TEMPLATE % self.to_hash
|
151
|
+
footer = ""
|
152
|
+
else
|
153
|
+
header =<<-EOT
|
154
|
+
# Edit Issue
|
155
|
+
#
|
156
|
+
# ID : #{id}
|
157
|
+
# Created At : #{created_at}
|
158
|
+
# Updated At : #{updated_at}
|
159
|
+
#
|
160
|
+
EOT
|
161
|
+
body = TEMPLATE % self.to_hash
|
162
|
+
|
163
|
+
footer =<<-EOT
|
164
|
+
# ChangeLog
|
165
|
+
#
|
166
|
+
#{changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")}
|
167
|
+
EOT
|
168
|
+
end
|
169
|
+
|
170
|
+
(header + "\n" + body + "\n\n" + footer).strip
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.from_json(the_json)
|
174
|
+
begin
|
175
|
+
hash = JSON.parse(the_json)
|
176
|
+
rescue JSON::ParserError => e
|
177
|
+
raise Issue::Invalid.new(e.to_s)
|
178
|
+
end
|
179
|
+
|
180
|
+
Issue.new(hash['issue'], hash['changelog'])
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.from_template(text)
|
184
|
+
issue = { :description => '' }
|
185
|
+
reading_description = false
|
186
|
+
|
187
|
+
text.lines.each_with_index do |line, index|
|
188
|
+
next if line =~ /^#/ || (!reading_description && line =~ /^\s*$/)
|
189
|
+
|
190
|
+
if line =~ /^---$/
|
191
|
+
# FIXME: this means that there can be multiple description blocks in the template!
|
192
|
+
#
|
193
|
+
reading_description = !reading_description
|
194
|
+
next
|
195
|
+
end
|
196
|
+
|
197
|
+
if !reading_description && line =~ /^(\w+)\s*:\s*(.*)$/
|
198
|
+
key, value = $1.downcase.to_sym, $2.strip
|
199
|
+
|
200
|
+
if schema_attributes.include?(key) && schema_attributes[key][:settable]
|
201
|
+
issue[key] = value
|
202
|
+
else
|
203
|
+
raise ArgumentError.new("Unknown Issue attribute: #{key} on line #{index+1}") unless schema_attributes.include?(key)
|
204
|
+
raise ArgumentError.new("Cannot set write-protected Issue attribute: #{key} on line #{index+1}")
|
205
|
+
end
|
206
|
+
elsif reading_description
|
207
|
+
issue[:description] += line
|
208
|
+
else
|
209
|
+
raise ArgumentError.new("Cannot parse line #{index+1}")
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
Issue.new(issue)
|
214
|
+
end
|
215
|
+
|
216
|
+
def update_from_template!(text)
|
217
|
+
new_issue = Issue.from_template(text)
|
218
|
+
|
219
|
+
attrs = self.class.schema_attributes.map do |attr, opts|
|
220
|
+
if opts[:settable] && self.send(attr) != (new_value = new_issue.send(attr))
|
221
|
+
self.send("#{attr}=", new_value)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
self
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
def date(t)
|
230
|
+
t.strftime "%Y/%m/%d %H:%M:%S"
|
231
|
+
end
|
232
|
+
|
233
|
+
def dup
|
234
|
+
raise NoMethodError.new
|
235
|
+
end
|
236
|
+
|
237
|
+
def clone
|
238
|
+
raise NoMethodError.new
|
239
|
+
end
|
240
|
+
end
|
data/lib/taco/schema.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
# Schema characteristics
|
4
|
+
#
|
5
|
+
# attributes have default validations, coercions, and transformations
|
6
|
+
#
|
7
|
+
# FIXME: document
|
8
|
+
|
9
|
+
module Schema
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
Hash[self.class.schema_attributes.map do |attr, opts|
|
16
|
+
[ attr, send(attr) ]
|
17
|
+
end]
|
18
|
+
end
|
19
|
+
|
20
|
+
def schema_errors
|
21
|
+
@errors || []
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid?
|
25
|
+
@errors = nil
|
26
|
+
|
27
|
+
self.class.schema_attributes.each do |attr, opts|
|
28
|
+
if opts[:validate].nil?
|
29
|
+
case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
|
30
|
+
when 'String'
|
31
|
+
opts[:validate] = lambda { |v| v !~ /\A\s*\Z/ }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
if opts[:validate]
|
36
|
+
value = eval(attr.to_s)
|
37
|
+
|
38
|
+
valid = if opts[:validate].is_a?(Array)
|
39
|
+
opts[:validate].include? value
|
40
|
+
elsif opts[:validate].is_a?(Proc)
|
41
|
+
opts[:validate].call(value)
|
42
|
+
end
|
43
|
+
|
44
|
+
unless valid
|
45
|
+
@errors = [ [ attr, value ] ]
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
def schema_attributes
|
56
|
+
@schema_attrs
|
57
|
+
end
|
58
|
+
|
59
|
+
def schema_attr_remove(name)
|
60
|
+
raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
|
61
|
+
@schema_attrs.delete(name)
|
62
|
+
self.send(:remove_method, name)
|
63
|
+
self.send(:remove_method, "#{name}=".to_s)
|
64
|
+
end
|
65
|
+
|
66
|
+
def schema_attr_replace(name, opts)
|
67
|
+
raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
|
68
|
+
schema_attr_remove(name)
|
69
|
+
schema_attr(name, opts)
|
70
|
+
end
|
71
|
+
|
72
|
+
def schema_attr_update(name, opts)
|
73
|
+
raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
|
74
|
+
raise KeyError.new("attribute #{name}: cannot update non-settable attribute") unless @schema_attrs[name][:settable]
|
75
|
+
schema_attr_replace(name, @schema_attrs[name].merge(opts))
|
76
|
+
end
|
77
|
+
|
78
|
+
def schema_attr(name, opts)
|
79
|
+
@schema_attrs ||= {}
|
80
|
+
|
81
|
+
raise TypeError.new("attribute #{name}: missing or invalid :class") unless opts[:class].is_a?(Class)
|
82
|
+
|
83
|
+
if opts[:default].nil?
|
84
|
+
opts[:default] = case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
|
85
|
+
when 'String'
|
86
|
+
''
|
87
|
+
when 'Fixnum'
|
88
|
+
0
|
89
|
+
when 'Time'
|
90
|
+
lambda { Time.new }
|
91
|
+
else
|
92
|
+
raise ArgumentError.new("Sorry, no default default exists for #{opts[:class]}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
unless opts[:default].is_a?(opts[:class]) || opts[:default].is_a?(Proc)
|
97
|
+
raise TypeError.new("attribute #{name}: invalid :default")
|
98
|
+
end
|
99
|
+
|
100
|
+
if opts[:validate]
|
101
|
+
unless opts[:validate].is_a?(Array) || opts[:validate].is_a?(Proc)
|
102
|
+
raise ArgumentError.new("attribute #{name}: expecting Array or Proc for :validate")
|
103
|
+
end
|
104
|
+
|
105
|
+
if opts[:validate].is_a?(Array)
|
106
|
+
raise TypeError.new("attribute #{name}: wrong type in :validate Array") unless opts[:validate].all? { |v| v.is_a?(opts[:class]) }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
raise ArgumentError.new("attribute #{name}: already exists") if @schema_attrs[name]
|
111
|
+
|
112
|
+
@schema_attrs[name] = opts
|
113
|
+
|
114
|
+
value_getter = if opts[:default].is_a?(Proc)
|
115
|
+
%Q(
|
116
|
+
opts = self.class.schema_attributes[:#{name}]
|
117
|
+
value = opts[:default].call
|
118
|
+
raise TypeError.new("attribute #{name}: expected type #{opts[:class]}, received \#{value.class}") unless opts[:class] == value.class
|
119
|
+
)
|
120
|
+
else
|
121
|
+
%Q(value = #{opts[:default].inspect})
|
122
|
+
end
|
123
|
+
module_eval %Q(
|
124
|
+
def #{name}
|
125
|
+
if @#{name}.nil?
|
126
|
+
#{value_getter}
|
127
|
+
self.#{name}= value
|
128
|
+
end
|
129
|
+
@#{name}
|
130
|
+
end
|
131
|
+
)
|
132
|
+
|
133
|
+
unless opts[:coerce] == false # possible values are false=no-coerce, nil=default-coerce, Proc=custom-coerce
|
134
|
+
case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
|
135
|
+
when 'Fixnum'
|
136
|
+
unless opts[:coerce].is_a? Proc
|
137
|
+
# the default coercion for Fixnum
|
138
|
+
opts[:coerce] = lambda do |value|
|
139
|
+
unless value.is_a?(Fixnum) # FIXME: this "unless value.is_a?(same class as 'when')" is copy-pasta. fix it.
|
140
|
+
raise TypeError.new("attribute #{name}: cannot coerce from \#{value.class}") unless value.is_a?(String)
|
141
|
+
i = value.to_i
|
142
|
+
raise TypeError.new("attribute #{name}: failed to coerce from \#{value}") unless i.to_s == value
|
143
|
+
value = i
|
144
|
+
end
|
145
|
+
value
|
146
|
+
end
|
147
|
+
end
|
148
|
+
when 'Time'
|
149
|
+
unless opts[:coerce].is_a? Proc
|
150
|
+
# the default coercion for Time
|
151
|
+
opts[:coerce] = lambda do |value|
|
152
|
+
unless value.is_a?(Time) # FIXME: this "unless value.is_a?(same class as 'when')" is copy-pasta. fix it.
|
153
|
+
raise TypeError.new("attribute #{name}: cannot coerce from \#{value.class}") unless value.is_a?(String)
|
154
|
+
begin
|
155
|
+
value = Time.parse(value)
|
156
|
+
rescue ArgumentError
|
157
|
+
raise TypeError.new("attribute #{name}: cannot coerce from \#{value.class}")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
value
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
coerce = 'value = opts[:coerce].call(value)' if opts[:coerce]
|
166
|
+
end
|
167
|
+
|
168
|
+
unless opts[:transform] == false # possible values are false=no-transform, nil=default-transform, Proc=custom-transform
|
169
|
+
case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
|
170
|
+
when 'String'
|
171
|
+
unless opts[:transform].is_a? Proc
|
172
|
+
# the default transform for String: remove excess whitespace
|
173
|
+
opts[:transform] = lambda { |s| s.strip }
|
174
|
+
end
|
175
|
+
when 'Time'
|
176
|
+
unless opts[:transform].is_a? Proc
|
177
|
+
# the default transform for Time: remove subsecond precision. subsec precision is not recorded in to_s, so unless
|
178
|
+
# we scrub it out, the following happens:
|
179
|
+
# foo.a_time = Time.new
|
180
|
+
# Time.parse(foo.a_time.to_s) == foo.a_time # returns false most of the time!
|
181
|
+
opts[:transform] = lambda { |t| Time.new t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset }
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
transform = 'value = opts[:transform].call(value)' if opts[:transform]
|
186
|
+
end
|
187
|
+
|
188
|
+
callback = %Q(
|
189
|
+
if self.respond_to? :schema_attribute_change
|
190
|
+
self.schema_attribute_change(:#{name}, @#{name}, value)
|
191
|
+
end
|
192
|
+
)
|
193
|
+
|
194
|
+
setter_method = %Q(
|
195
|
+
def #{name}=(value)
|
196
|
+
opts = self.class.schema_attributes[:#{name}]
|
197
|
+
#{coerce}
|
198
|
+
raise TypeError.new("attribute #{name}: expected type #{opts[:class]}, received \#{value.class}") unless opts[:class] == value.class
|
199
|
+
#{transform}
|
200
|
+
#{callback}
|
201
|
+
@#{name} = value
|
202
|
+
end
|
203
|
+
)
|
204
|
+
module_eval setter_method
|
205
|
+
module_eval "private :#{name}=" unless opts[:settable]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|