talk 2.0.1 → 2.0.2

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