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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +10 -0
  5. data/README.md +6 -0
  6. data/Rakefile +9 -0
  7. data/features/class-field.feature +226 -0
  8. data/features/class.feature +95 -0
  9. data/features/enumeration-constant.feature +76 -0
  10. data/features/enumeration.feature +35 -0
  11. data/features/glossary-term.feature +46 -0
  12. data/features/glossary.feature +35 -0
  13. data/features/step_definitions/class-field.rb +74 -0
  14. data/features/step_definitions/class.rb +50 -0
  15. data/features/step_definitions/enumeration-constant.rb +42 -0
  16. data/features/step_definitions/enumeration.rb +29 -0
  17. data/features/step_definitions/glossary-term.rb +31 -0
  18. data/features/step_definitions/glossary.rb +23 -0
  19. data/features/support/env.rb +261 -0
  20. data/lib/context.rb +282 -0
  21. data/lib/context_class.rb +224 -0
  22. data/lib/contexts/README.md +274 -0
  23. data/lib/contexts/base.rb +6 -0
  24. data/lib/contexts/boolean.rb +5 -0
  25. data/lib/contexts/class.rb +11 -0
  26. data/lib/contexts/constant.rb +5 -0
  27. data/lib/contexts/enumeration.rb +20 -0
  28. data/lib/contexts/field.rb +47 -0
  29. data/lib/contexts/glossary.rb +7 -0
  30. data/lib/contexts/inherits.rb +3 -0
  31. data/lib/contexts/map.rb +7 -0
  32. data/lib/contexts/meta.rb +2 -0
  33. data/lib/contexts/method.rb +15 -0
  34. data/lib/contexts/numeric.rb +5 -0
  35. data/lib/contexts/protocol.rb +8 -0
  36. data/lib/contexts/reference.rb +6 -0
  37. data/lib/contexts/string.rb +5 -0
  38. data/lib/contexts/target.rb +11 -0
  39. data/lib/contexts/term.rb +11 -0
  40. data/lib/languages/java/java.rb +145 -0
  41. data/lib/languages/java/templates/class.java.erb +22 -0
  42. data/lib/languages/java/templates/enumeration.java.erb +10 -0
  43. data/lib/languages/java/templates/glossary.java.erb +8 -0
  44. data/lib/languages/language.rb +172 -0
  45. data/lib/languages/objc/objc.rb +162 -0
  46. data/lib/languages/objc/templates/TalkClasses.h.erb +3 -0
  47. data/lib/languages/objc/templates/TalkClassesForward.h.erb +2 -0
  48. data/lib/languages/objc/templates/TalkConstants.h.erb +22 -0
  49. data/lib/languages/objc/templates/TalkObjectList.h.erb +4 -0
  50. data/lib/languages/objc/templates/class.h.erb +21 -0
  51. data/lib/languages/objc/templates/class.m.erb +43 -0
  52. data/lib/parse_error.rb +4 -0
  53. data/lib/parser.rb +119 -0
  54. data/lib/registry.rb +151 -0
  55. data/lib/talk.rb +5 -0
  56. data/talk.gemspec +18 -0
  57. metadata +71 -3
@@ -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