taco_it 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|