talk 2.0.1 → 2.0.2
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.
- 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
|