talk 2.0.1 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +6 -0
- data/Gemfile +10 -0
- data/README.md +6 -0
- data/Rakefile +9 -0
- data/features/class-field.feature +226 -0
- data/features/class.feature +95 -0
- data/features/enumeration-constant.feature +76 -0
- data/features/enumeration.feature +35 -0
- data/features/glossary-term.feature +46 -0
- data/features/glossary.feature +35 -0
- data/features/step_definitions/class-field.rb +74 -0
- data/features/step_definitions/class.rb +50 -0
- data/features/step_definitions/enumeration-constant.rb +42 -0
- data/features/step_definitions/enumeration.rb +29 -0
- data/features/step_definitions/glossary-term.rb +31 -0
- data/features/step_definitions/glossary.rb +23 -0
- data/features/support/env.rb +261 -0
- data/lib/context.rb +282 -0
- data/lib/context_class.rb +224 -0
- data/lib/contexts/README.md +274 -0
- data/lib/contexts/base.rb +6 -0
- data/lib/contexts/boolean.rb +5 -0
- data/lib/contexts/class.rb +11 -0
- data/lib/contexts/constant.rb +5 -0
- data/lib/contexts/enumeration.rb +20 -0
- data/lib/contexts/field.rb +47 -0
- data/lib/contexts/glossary.rb +7 -0
- data/lib/contexts/inherits.rb +3 -0
- data/lib/contexts/map.rb +7 -0
- data/lib/contexts/meta.rb +2 -0
- data/lib/contexts/method.rb +15 -0
- data/lib/contexts/numeric.rb +5 -0
- data/lib/contexts/protocol.rb +8 -0
- data/lib/contexts/reference.rb +6 -0
- data/lib/contexts/string.rb +5 -0
- data/lib/contexts/target.rb +11 -0
- data/lib/contexts/term.rb +11 -0
- data/lib/languages/java/java.rb +145 -0
- data/lib/languages/java/templates/class.java.erb +22 -0
- data/lib/languages/java/templates/enumeration.java.erb +10 -0
- data/lib/languages/java/templates/glossary.java.erb +8 -0
- data/lib/languages/language.rb +172 -0
- data/lib/languages/objc/objc.rb +162 -0
- data/lib/languages/objc/templates/TalkClasses.h.erb +3 -0
- data/lib/languages/objc/templates/TalkClassesForward.h.erb +2 -0
- data/lib/languages/objc/templates/TalkConstants.h.erb +22 -0
- data/lib/languages/objc/templates/TalkObjectList.h.erb +4 -0
- data/lib/languages/objc/templates/class.h.erb +21 -0
- data/lib/languages/objc/templates/class.m.erb +43 -0
- data/lib/parse_error.rb +4 -0
- data/lib/parser.rb +119 -0
- data/lib/registry.rb +151 -0
- data/lib/talk.rb +5 -0
- data/talk.gemspec +18 -0
- metadata +71 -3
data/lib/context.rb
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
require 'context_class'
|
2
|
+
require 'registry'
|
3
|
+
|
4
|
+
module Talk
|
5
|
+
class Context
|
6
|
+
attr_reader :tag, :file, :line
|
7
|
+
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(tag, file, line)
|
11
|
+
@tag = tag
|
12
|
+
@file = file
|
13
|
+
@line = line
|
14
|
+
@contents = {}
|
15
|
+
|
16
|
+
@property_words = []
|
17
|
+
end
|
18
|
+
|
19
|
+
## Parser interface
|
20
|
+
|
21
|
+
def parse(word, file, line)
|
22
|
+
@property_words.push word
|
23
|
+
end
|
24
|
+
|
25
|
+
def start_tag(tag, file, line)
|
26
|
+
parse_error("Unsupported tag @#{tag}", file, line) unless self.class.tags.has_key?(tag)
|
27
|
+
tag_class = self.class.tags[tag][:class]
|
28
|
+
|
29
|
+
# @end tags use a nil class
|
30
|
+
tag_class.nil? ? nil : Context.context_for_name(tag_class).new(tag, file, line)
|
31
|
+
end
|
32
|
+
|
33
|
+
def end_tag(context)
|
34
|
+
context.close
|
35
|
+
check_child_uniqueness(context) if self.class.unique_key_for_tag(context.tag)
|
36
|
+
add_tag(context)
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_key?(key)
|
40
|
+
@contents.has_key?(key.to_sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_tag?(tag)
|
44
|
+
self.class.has_tag?(tag)
|
45
|
+
end
|
46
|
+
|
47
|
+
def key_multiplicity(key)
|
48
|
+
key = key.to_sym
|
49
|
+
return 0 unless @contents.has_key?(key) and not @contents[key].nil?
|
50
|
+
return 1 unless @contents[key].is_a? Array or @contents[key].is_a? Hash
|
51
|
+
return @contents[key].length
|
52
|
+
end
|
53
|
+
|
54
|
+
def close
|
55
|
+
process_property_words
|
56
|
+
postprocess
|
57
|
+
register
|
58
|
+
end
|
59
|
+
|
60
|
+
def finalize
|
61
|
+
final_validation
|
62
|
+
crossreference
|
63
|
+
end
|
64
|
+
|
65
|
+
## Operators and other standard-ish public methods
|
66
|
+
|
67
|
+
def each
|
68
|
+
@contents.each { |k,v| yield k,v }
|
69
|
+
end
|
70
|
+
|
71
|
+
def [](key)
|
72
|
+
@contents[key.to_sym]
|
73
|
+
end
|
74
|
+
|
75
|
+
def []=(key, value)
|
76
|
+
if value.is_a? Array then
|
77
|
+
value = value.map { |v| validated_value_for_key(key, transformed_value_for_key(key, value)) }
|
78
|
+
else
|
79
|
+
value = validated_value_for_key(key, transformed_value_for_key(key, value))
|
80
|
+
end
|
81
|
+
|
82
|
+
@contents[key.to_sym] = value
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_tag(context)
|
86
|
+
key = context.tag
|
87
|
+
self[key.to_sym] ||= []
|
88
|
+
self[key.to_sym].push validated_value_for_key(key, transformed_value_for_key(key, context))
|
89
|
+
end
|
90
|
+
|
91
|
+
## Support for parser
|
92
|
+
|
93
|
+
def check_child_uniqueness(child)
|
94
|
+
# we could do this as a validator, but then we'd lose ability to show sibling info
|
95
|
+
return unless self.has_key? child.tag
|
96
|
+
key = self.class.unique_key_for_tag(child.tag)
|
97
|
+
|
98
|
+
self[child.tag].each do |sibling|
|
99
|
+
errmsg = "Child tag @#{child.tag} must have unique #{key} value; previously used in sibling at line #{sibling.line}"
|
100
|
+
parse_error(errmsg, child.file, child.line) if child[key] == sibling[key]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def process_property_words
|
105
|
+
ranges = property_ranges
|
106
|
+
ranges.each_with_index do |range, idx|
|
107
|
+
property = self.class.property_at_index(idx)
|
108
|
+
value = @property_words[range[0] .. range[1]].join(" ")
|
109
|
+
self[property[:name]] = value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def postprocess
|
114
|
+
self.class.postprocesses.each { |p| p.call(self) }
|
115
|
+
end
|
116
|
+
|
117
|
+
def final_validation
|
118
|
+
self.class.final_validations.each { |v| parse_error(v[:message]) unless v[:block].call(self) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def register
|
122
|
+
self.class.registrations.each { |r| Registry.add(self[r[:name]], r[:namespace], self.file, self.line, r[:delimiter]) }
|
123
|
+
end
|
124
|
+
|
125
|
+
def namespace_for_reference(reg)
|
126
|
+
return reg[:namespace].call(self) if reg[:namespace].methods.include? :call
|
127
|
+
reg[:namespace]
|
128
|
+
end
|
129
|
+
|
130
|
+
def crossreference_value(value, namespace)
|
131
|
+
value = value[:value] if value.is_a? Context
|
132
|
+
registered = Registry.registered?(value, namespace)
|
133
|
+
parse_error("no symbol '#{value}' in #{namespace}") unless registered
|
134
|
+
end
|
135
|
+
|
136
|
+
def crossreference
|
137
|
+
self.class.references.each do |r|
|
138
|
+
namespace = namespace_for_reference(r)
|
139
|
+
[*self[r[:name]]].each do |referenced_name|
|
140
|
+
crossreference_value(referenced_name, namespace) unless reference_skipped?(referenced_name, r[:params])
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def reference_skipped?(ref_value, params)
|
146
|
+
ref_value = ref_value[:value] if ref_value.is_a? Context
|
147
|
+
return false if params[:skip].nil?
|
148
|
+
return params[:skip].include? ref_value if params[:skip].is_a? Array
|
149
|
+
return params[:skip] == ref_value
|
150
|
+
end
|
151
|
+
|
152
|
+
## Key manipulation
|
153
|
+
|
154
|
+
def transformed_value_for_key(key, value)
|
155
|
+
transforms = self.class.transforms[key]
|
156
|
+
transforms.each { |t| value = t.call(self, value) } unless transforms.nil?
|
157
|
+
value
|
158
|
+
end
|
159
|
+
|
160
|
+
def validated_value_for_key(key, value)
|
161
|
+
self.class.validations[key].each { |v| parse_error(v[:message]) unless v[:block].call(self, value) }
|
162
|
+
value
|
163
|
+
end
|
164
|
+
|
165
|
+
## Property manipulation
|
166
|
+
|
167
|
+
def property_ranges
|
168
|
+
word_count = @property_words.length
|
169
|
+
ranges = []
|
170
|
+
|
171
|
+
self.class.properties.each do |prop_name, prop_def|
|
172
|
+
len = prop_def[:length]
|
173
|
+
offset = ranges.empty? ? 0 : ranges.last[1]+1
|
174
|
+
msg_start = "@#{self.tag} property '#{prop_name}' "
|
175
|
+
|
176
|
+
if len.is_a? Array then
|
177
|
+
new_range = property_range_for_variable_len(offset, word_count, prop_def)
|
178
|
+
else
|
179
|
+
if offset >= word_count then
|
180
|
+
parse_error(msg_start+"cannot be omitted") if prop_def[:required]
|
181
|
+
new_range = [1, 0]
|
182
|
+
else
|
183
|
+
length_ok = (word_count - offset >= len)
|
184
|
+
parse_error(msg_start+"got #{word_count-offset} of #{len} words") unless length_ok
|
185
|
+
new_range = [offset, offset+len-1]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
ranges.push new_range if new_range[1] >= new_range[0]
|
190
|
+
end
|
191
|
+
|
192
|
+
ranges
|
193
|
+
end
|
194
|
+
|
195
|
+
def property_range_for_variable_len(offset, word_count, prop_def)
|
196
|
+
words_left = word_count - offset
|
197
|
+
min = prop_def[:length][0]
|
198
|
+
max = prop_def[:length][1]
|
199
|
+
meets_min = words_left >= min
|
200
|
+
meets_max = max.nil? or words_left <= max
|
201
|
+
|
202
|
+
parse_error("Property #{prop_def[:name]} takes at least #{min} #{pluralize min, 'word'}; got #{words_left}") unless meets_min
|
203
|
+
parse_error("Property #{prop_def[:name]} takes at most #{max} #{pluralize min, 'word'}; got #{words_left}") unless meets_max
|
204
|
+
|
205
|
+
[ offset, word_count-1 ]
|
206
|
+
end
|
207
|
+
|
208
|
+
def pluralize(num, word, suffix="s")
|
209
|
+
num == 1 ? word : word + suffix
|
210
|
+
end
|
211
|
+
|
212
|
+
## Output
|
213
|
+
|
214
|
+
def parse_error(message, file=nil, line=nil)
|
215
|
+
Talk::Parser.error(@tag, file || @file, line || @line, message)
|
216
|
+
end
|
217
|
+
|
218
|
+
def render_element(indent_level, key, element)
|
219
|
+
if element.methods.include? :render then
|
220
|
+
element.render(indent_level)
|
221
|
+
else
|
222
|
+
"\t" * indent_level + "#{key.to_s} -> '#{element.to_s}'\n"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def render(indent_level=0)
|
227
|
+
indent = "\t" * indent_level
|
228
|
+
str = indent + "@" + self.tag.to_s + ' ' + @property_words.join(' ') + "\n"
|
229
|
+
@contents.each do |key, value|
|
230
|
+
if value.is_a? Array then
|
231
|
+
str = value.inject(str) { |s, element| s + render_element(indent_level+1, key, element) }
|
232
|
+
else
|
233
|
+
render_element(indent_level+1, key, value)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
str
|
238
|
+
end
|
239
|
+
|
240
|
+
def description
|
241
|
+
"@#{tag} #{file}:#{line}"
|
242
|
+
end
|
243
|
+
|
244
|
+
def to_s
|
245
|
+
render
|
246
|
+
end
|
247
|
+
|
248
|
+
def to_h
|
249
|
+
dict = {}
|
250
|
+
@contents.each do |k,v|
|
251
|
+
if v.is_a? Array then
|
252
|
+
if self.class.tag_is_singular? k and v.length > 0
|
253
|
+
dict[k] = hashify_value(v[0])
|
254
|
+
else
|
255
|
+
dict[k] = v.map { |u| hashify_value(u) }
|
256
|
+
end
|
257
|
+
else
|
258
|
+
dict[k] = hashify_value(v)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
dict[:__meta] ||= {}
|
263
|
+
dict[:__meta][:file] = @file
|
264
|
+
dict[:__meta][:tag] = @tag
|
265
|
+
dict[:__meta][:line] = @line
|
266
|
+
|
267
|
+
dict
|
268
|
+
end
|
269
|
+
|
270
|
+
def hashify_value(v)
|
271
|
+
# cache method list to provide big speedup
|
272
|
+
@class_methods ||= {}
|
273
|
+
@class_methods[v.class] ||= v.methods
|
274
|
+
|
275
|
+
return v.to_val if @class_methods[v.class].include? :to_val
|
276
|
+
return v.to_h if @class_methods[v.class].include? :to_h
|
277
|
+
return v.to_f if v.is_a? Fixnum or v.is_a? Float
|
278
|
+
|
279
|
+
v.to_s
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module Talk
|
2
|
+
class Context
|
3
|
+
class << self
|
4
|
+
attr_reader :classname, :properties, :tags, :transforms
|
5
|
+
attr_reader :registrations, :references
|
6
|
+
attr_reader :validations, :final_validations, :postprocesses
|
7
|
+
|
8
|
+
attr_reader :registry
|
9
|
+
|
10
|
+
def initialize(classname)
|
11
|
+
@classname = classname
|
12
|
+
@properties = {}
|
13
|
+
@property_map = []
|
14
|
+
@tags = {}
|
15
|
+
@transforms = {}
|
16
|
+
|
17
|
+
@registrations = []
|
18
|
+
@references = []
|
19
|
+
|
20
|
+
@validations = {}
|
21
|
+
@final_validations = []
|
22
|
+
@postprocesses = []
|
23
|
+
end
|
24
|
+
|
25
|
+
## Stuff to be used by context definitions
|
26
|
+
## All of this is documented in ./README.md
|
27
|
+
def property(name, params={})
|
28
|
+
raise "Duplicate property definition #{name} in #{@classname}" if @properties.has_key?(name)
|
29
|
+
@property_map.push(name)
|
30
|
+
add_property_support(name, params)
|
31
|
+
end
|
32
|
+
|
33
|
+
def tag(name, params={})
|
34
|
+
raise "Duplicate tag definition #{name} in #{@classname}" if @properties.has_key?(name)
|
35
|
+
@tags[name] = params
|
36
|
+
add_tag_support(name, params)
|
37
|
+
load_child_tags(name, params)
|
38
|
+
end
|
39
|
+
|
40
|
+
def tag_description(params={})
|
41
|
+
params = { :class => :string, :required => true, :bridge => true }.merge(params)
|
42
|
+
tag(:description, params)
|
43
|
+
bridge_tag_to_property :description if params[:bridge]
|
44
|
+
end
|
45
|
+
|
46
|
+
def tag_end
|
47
|
+
tag(:end, { :class => nil })
|
48
|
+
end
|
49
|
+
|
50
|
+
def register(namespace, params={})
|
51
|
+
defaults = { name: :name, delimiter: nil, namespace: namespace }
|
52
|
+
params = defaults.merge(params)
|
53
|
+
@registrations.push(params)
|
54
|
+
end
|
55
|
+
|
56
|
+
def reference(name, namespace, params={})
|
57
|
+
@references.push({ namespace:namespace, name:name, params: params })
|
58
|
+
end
|
59
|
+
|
60
|
+
def postprocess(block)
|
61
|
+
@postprocesses.push block
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate(errmsg, name, block)
|
65
|
+
@validations[name] ||= []
|
66
|
+
@validations[name].push( { message: errmsg, block: block } )
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_final(errmsg, block)
|
70
|
+
@final_validations.push( { message: errmsg, block: block })
|
71
|
+
end
|
72
|
+
|
73
|
+
def bridge_tag_to_property(name)
|
74
|
+
fixed_keys = { required: false, length:[0,nil] }
|
75
|
+
allowed_keys = [:transform, :context]
|
76
|
+
|
77
|
+
# the new property will have parameters pre-defined fixed_keys, and also
|
78
|
+
# parameters imported from the tag listed in allowed_keys
|
79
|
+
params = allowed_keys.inject(fixed_keys) { |c, k| c.merge( k => @tags[name][k] ) }
|
80
|
+
property( name, params )
|
81
|
+
end
|
82
|
+
|
83
|
+
## Convenience and support methods for instance methods
|
84
|
+
|
85
|
+
def property_at_index(idx)
|
86
|
+
return nil unless idx < @property_map.length
|
87
|
+
|
88
|
+
return @properties[@property_map[idx]]
|
89
|
+
end
|
90
|
+
|
91
|
+
def unique_key_for_tag(key)
|
92
|
+
@tags[key][:unique]
|
93
|
+
end
|
94
|
+
|
95
|
+
def has_tag?(tag)
|
96
|
+
@tags.has_key?(tag)
|
97
|
+
end
|
98
|
+
|
99
|
+
def tag_is_singular?(tag)
|
100
|
+
has_tag? tag and (@tags[tag][:multi] == false or @tags[tag][:multi].nil?)
|
101
|
+
end
|
102
|
+
|
103
|
+
## Subclassing magic
|
104
|
+
def all_contexts
|
105
|
+
path = File.join(File.dirname(__FILE__), "contexts/*.rb");
|
106
|
+
Dir[path].collect { |file| context_for_name(name) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def context_for_name(name)
|
110
|
+
predefined_context_for_name(name) || make_context(name)
|
111
|
+
end
|
112
|
+
|
113
|
+
def predefined_context_for_name(name)
|
114
|
+
props = Talk.instance_variable_get("@contexts")
|
115
|
+
props.nil? ? nil : props[classname_for_filename(name)]
|
116
|
+
end
|
117
|
+
|
118
|
+
def make_context(name)
|
119
|
+
new_classname = classname_for_filename(name)
|
120
|
+
|
121
|
+
subclass = Class.new(Talk::Context) do
|
122
|
+
initialize(new_classname)
|
123
|
+
end
|
124
|
+
|
125
|
+
source_file = canonical_path_for_name(name)
|
126
|
+
subclass.class_eval( IO.read(source_file), source_file )
|
127
|
+
|
128
|
+
props = Talk.instance_variable_get("@contexts")
|
129
|
+
props = Talk.instance_variable_set("@contexts", {}) if props.nil?
|
130
|
+
props[new_classname] = subclass
|
131
|
+
end
|
132
|
+
|
133
|
+
def canonical_path_for_name(name)
|
134
|
+
File.absolute_path(File.join(File.dirname(__FILE__), "contexts", File.basename(name.to_s, ".rb")) + ".rb")
|
135
|
+
end
|
136
|
+
|
137
|
+
def classname_for_filename(name) # /path/to/file_name.rb to FileName
|
138
|
+
File.basename(name.to_s, ".rb").split('_').collect { |word| word.capitalize }.join("")
|
139
|
+
end
|
140
|
+
|
141
|
+
## Support stuff; avoid invoking directly
|
142
|
+
def add_key_support(name)
|
143
|
+
@transforms[name] = []
|
144
|
+
@validations[name] = []
|
145
|
+
end
|
146
|
+
|
147
|
+
def add_property_support(name, params)
|
148
|
+
defaults = { :required => true }
|
149
|
+
params = defaults.merge(params)
|
150
|
+
|
151
|
+
add_key_support(name)
|
152
|
+
add_property_params(name, params)
|
153
|
+
add_property_transform(name, params[:transform]) unless params[:transform].nil?
|
154
|
+
add_property_allowed(name, params[:allowed]) if params.has_key?(:allowed)
|
155
|
+
add_property_required(name) if params[:required]
|
156
|
+
end
|
157
|
+
|
158
|
+
def add_property_params(name, params)
|
159
|
+
defaults = { :length => 1, :name => name }
|
160
|
+
@properties[name] = defaults.merge(params)
|
161
|
+
end
|
162
|
+
|
163
|
+
def add_property_allowed(name, allowed)
|
164
|
+
ref = "#{@classname}.#{name}"
|
165
|
+
norm_allow = normalize_allowed(name, allowed).join(", ")
|
166
|
+
errmsg = "#{ref}: must be one of #{norm_allow}"
|
167
|
+
|
168
|
+
validate( errmsg, name, lambda { |c,v| norm_allow.include? v } )
|
169
|
+
end
|
170
|
+
|
171
|
+
def add_property_required(name)
|
172
|
+
ref = "#{@classname}.#{name}"
|
173
|
+
errmsg = "#{ref}: required property cannot be omitted"
|
174
|
+
|
175
|
+
validate_final( errmsg, lambda { |c| c.has_key? name } )
|
176
|
+
end
|
177
|
+
|
178
|
+
def add_property_transform(name, transform)
|
179
|
+
@transforms[name].push transform
|
180
|
+
end
|
181
|
+
|
182
|
+
def add_tag_support(name, params)
|
183
|
+
add_key_support(name)
|
184
|
+
params[:class] = name unless params.has_key?(:class) # ||= won't work since class might be nil
|
185
|
+
|
186
|
+
add_tag_singular(name) unless params[:multi]
|
187
|
+
add_tag_required(name) if params[:required]
|
188
|
+
end
|
189
|
+
|
190
|
+
def add_tag_singular(name)
|
191
|
+
ref = "#{@classname}->@#{name}"
|
192
|
+
errmsg = "#{ref}: tag may only be added once"
|
193
|
+
validate_final( errmsg, lambda { |c| c.key_multiplicity(name) <= 1 } )
|
194
|
+
end
|
195
|
+
|
196
|
+
def add_tag_required(name)
|
197
|
+
ref = "#{@classname}->@#{name}"
|
198
|
+
errmsg = "#{ref}: required tag cannot be omitted"
|
199
|
+
validate_final( errmsg, lambda { |c| c.key_multiplicity(name) >= 1 } )
|
200
|
+
end
|
201
|
+
|
202
|
+
def load_child_tags(name, params)
|
203
|
+
@tags.each_value { |tag| Context.context_for_name(tag[:class]) unless tag[:class].nil? }
|
204
|
+
end
|
205
|
+
|
206
|
+
def normalize_allowed(name, allowed)
|
207
|
+
new_allowed = []
|
208
|
+
remap = {}
|
209
|
+
|
210
|
+
allowed.each do |v|
|
211
|
+
vv = [*v] # vv == [ v ] if v is scalar, vv == v if v is already an array
|
212
|
+
new_allowed += vv
|
213
|
+
vv.each { |u| remap[u] = vv[0] }
|
214
|
+
end
|
215
|
+
|
216
|
+
add_property_transform(name, lambda do |c,v|
|
217
|
+
return remap[v] if remap.has_key? v
|
218
|
+
v
|
219
|
+
end)
|
220
|
+
new_allowed
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|