Class: Talk::Context

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/context.rb,
lib/context_class.rb

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tag, file, line) ⇒ Context

Returns a new instance of Context.



10
11
12
13
14
15
16
17
# File 'lib/context.rb', line 10

def initialize(tag, file, line)
  @tag = tag
  @file = file
  @line = line
  @contents = {}

  @property_words = []
end

Class Attribute Details

.classnameObject (readonly)

Returns the value of attribute classname.



4
5
6
# File 'lib/context_class.rb', line 4

def classname
  @classname
end

.final_validationsObject (readonly)

Returns the value of attribute final_validations.



6
7
8
# File 'lib/context_class.rb', line 6

def final_validations
  @final_validations
end

.postprocessesObject (readonly)

Returns the value of attribute postprocesses.



6
7
8
# File 'lib/context_class.rb', line 6

def postprocesses
  @postprocesses
end

.propertiesObject (readonly)

Returns the value of attribute properties.



4
5
6
# File 'lib/context_class.rb', line 4

def properties
  @properties
end

.referencesObject (readonly)

Returns the value of attribute references.



5
6
7
# File 'lib/context_class.rb', line 5

def references
  @references
end

.registrationsObject (readonly)

Returns the value of attribute registrations.



5
6
7
# File 'lib/context_class.rb', line 5

def registrations
  @registrations
end

.registryObject (readonly)

Returns the value of attribute registry.



8
9
10
# File 'lib/context_class.rb', line 8

def registry
  @registry
end

.tagsObject (readonly)

Returns the value of attribute tags.



4
5
6
# File 'lib/context_class.rb', line 4

def tags
  @tags
end

.transformsObject (readonly)

Returns the value of attribute transforms.



4
5
6
# File 'lib/context_class.rb', line 4

def transforms
  @transforms
end

.validationsObject (readonly)

Returns the value of attribute validations.



6
7
8
# File 'lib/context_class.rb', line 6

def validations
  @validations
end

Instance Attribute Details

#fileObject (readonly)

Returns the value of attribute file.



6
7
8
# File 'lib/context.rb', line 6

def file
  @file
end

#lineObject (readonly)

Returns the value of attribute line.



6
7
8
# File 'lib/context.rb', line 6

def line
  @line
end

#tagObject (readonly)

Returns the value of attribute tag.



6
7
8
# File 'lib/context.rb', line 6

def tag
  @tag
end

Class Method Details

.add_key_support(name) ⇒ Object

Support stuff; avoid invoking directly



142
143
144
145
# File 'lib/context_class.rb', line 142

def add_key_support(name)
  @transforms[name] = []
  @validations[name] = []
end

.add_property_allowed(name, allowed) ⇒ Object



163
164
165
166
167
168
169
# File 'lib/context_class.rb', line 163

def add_property_allowed(name, allowed)
  ref = "#{@classname}.#{name}"
  norm_allow = normalize_allowed(name, allowed).join(", ")
  errmsg  = "#{ref}: must be one of #{norm_allow}"

  validate( errmsg, name, lambda { |c,v| norm_allow.include? v } )
end

.add_property_params(name, params) ⇒ Object



158
159
160
161
# File 'lib/context_class.rb', line 158

def add_property_params(name, params)
  defaults = { :length => 1, :name => name }
  @properties[name] = defaults.merge(params)
end

.add_property_required(name) ⇒ Object



171
172
173
174
175
176
# File 'lib/context_class.rb', line 171

def add_property_required(name)
  ref = "#{@classname}.#{name}"
  errmsg = "#{ref}: required property cannot be omitted"

  validate_final( errmsg, lambda { |c| c.has_key? name } )
end

.add_property_support(name, params) ⇒ Object



147
148
149
150
151
152
153
154
155
156
# File 'lib/context_class.rb', line 147

def add_property_support(name, params)
  defaults = { :required => true }
  params = defaults.merge(params)
  
  add_key_support(name)
  add_property_params(name, params)
  add_property_transform(name, params[:transform]) unless params[:transform].nil?
  add_property_allowed(name, params[:allowed]) if params.has_key?(:allowed)
  add_property_required(name) if params[:required]
end

.add_property_transform(name, transform) ⇒ Object



178
179
180
# File 'lib/context_class.rb', line 178

def add_property_transform(name, transform)
  @transforms[name].push transform
end

.add_tag_required(name) ⇒ Object



202
203
204
205
206
# File 'lib/context_class.rb', line 202

def add_tag_required(name)
  ref = "#{@classname}->@#{name}"
  errmsg = "#{ref}: required tag cannot be omitted"
  validate_final( errmsg, lambda { |c| c.key_multiplicity(name) >= 1 } )
end

.add_tag_singular(name) ⇒ Object



196
197
198
199
200
# File 'lib/context_class.rb', line 196

def add_tag_singular(name)
  ref = "#{@classname}->@#{name}"
  errmsg = "#{ref}: tag may only be added once"
  validate_final( errmsg, lambda { |c| c.key_multiplicity(name) <= 1 } )
end

.add_tag_support(name, params) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/context_class.rb', line 182

def add_tag_support(name, params)
  add_key_support(name)
  params[:class] = name unless params.has_key?(:class) # ||= won't work since class might be nil

  add_tag_singular(name) unless params[:multi]
  add_tag_required(name) if params[:required]
  postprocess(lambda do |c|
    return if c.has_key? name
    tag = c.start_tag(name, c.file, c.line)
    tag.parse(params[:default])
    c.end_tag(tag)
  end) if params[:default]
end

.all_contextsObject

Subclassing magic



104
105
106
107
# File 'lib/context_class.rb', line 104

def all_contexts
  path = File.join(File.dirname(__FILE__), "contexts/*.rb");
  Dir[path].collect { |file| context_for_name(name) }
end

.bridge_tag_to_property(name) ⇒ Object



73
74
75
76
77
78
79
80
81
# File 'lib/context_class.rb', line 73

def bridge_tag_to_property(name)
  fixed_keys = { required: false, length:[0,nil] }
  allowed_keys = [:transform, :context]

  # the new property will have parameters pre-defined fixed_keys, and also
  # parameters imported from the tag listed in allowed_keys
  params = allowed_keys.inject(fixed_keys) { |c, k| c.merge( k => @tags[name][k] ) }
  property( name, params )
end

.canonical_path_for_name(name) ⇒ Object



133
134
135
# File 'lib/context_class.rb', line 133

def canonical_path_for_name(name)
  File.absolute_path(File.join(File.dirname(__FILE__), "contexts", File.basename(name.to_s, ".rb")) + ".rb")
end

.classname_for_filename(name) ⇒ Object

/path/to/file_name.rb to FileName



137
138
139
# File 'lib/context_class.rb', line 137

def classname_for_filename(name) # /path/to/file_name.rb to FileName
  File.basename(name.to_s, ".rb").split('_').collect { |word| word.capitalize }.join("")
end

.context_for_name(name) ⇒ Object



109
110
111
# File 'lib/context_class.rb', line 109

def context_for_name(name)
  predefined_context_for_name(name) || make_context(name)
end

.has_tag?(tag) ⇒ Boolean

Returns:

  • (Boolean)


95
96
97
# File 'lib/context_class.rb', line 95

def has_tag?(tag)
  @tags.has_key?(tag)
end

.initialize(classname) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/context_class.rb', line 10

def initialize(classname)
  @classname = classname
  @properties = {}
  @property_map = []
  @tags = {}
  @transforms = {}

  @registrations = []
  @references = []

  @validations = {}
  @final_validations = []
  @postprocesses = []
end

.load_child_tags(name, params) ⇒ Object



208
209
210
# File 'lib/context_class.rb', line 208

def load_child_tags(name, params)
  @tags.each_value { |tag| Context.context_for_name(tag[:class]) unless tag[:class].nil? }
end

.make_context(name) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/context_class.rb', line 118

def make_context(name)
  new_classname = classname_for_filename(name)
  
  subclass = Class.new(Talk::Context) do
    initialize(new_classname)
  end

  source_file = canonical_path_for_name(name)
  subclass.class_eval( IO.read(source_file), source_file )

  props = Talk.instance_variable_get("@contexts")
  props = Talk.instance_variable_set("@contexts", {}) if props.nil?
  props[new_classname] = subclass
end

.normalize_allowed(name, allowed) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/context_class.rb', line 212

def normalize_allowed(name, allowed)
  new_allowed = []
  remap = {}

  allowed.each do |v|
    vv = [*v] # vv == [ v ] if v is scalar, vv == v if v is already an array
    new_allowed += vv
    vv.each { |u| remap[u] = vv[0] }
  end

  add_property_transform(name, lambda do |c,v|
    return remap[v] if remap.has_key? v
    v
  end)
  new_allowed
end

.postprocess(block) ⇒ Object



60
61
62
# File 'lib/context_class.rb', line 60

def postprocess(block)
  @postprocesses.push block
end

.predefined_context_for_name(name) ⇒ Object



113
114
115
116
# File 'lib/context_class.rb', line 113

def predefined_context_for_name(name)
  props = Talk.instance_variable_get("@contexts")
  props.nil? ? nil : props[classname_for_filename(name)]
end

.property(name, params = {}) ⇒ Object

Stuff to be used by context definitions All of this is documented in ./README.md



27
28
29
30
31
# File 'lib/context_class.rb', line 27

def property(name, params={})
  raise "Duplicate property definition #{name} in #{@classname}" if @properties.has_key?(name)
  @property_map.push(name)
  add_property_support(name, params)
end

.property_at_index(idx) ⇒ Object

Convenience and support methods for instance methods



85
86
87
88
89
# File 'lib/context_class.rb', line 85

def property_at_index(idx)
  return nil unless idx < @property_map.length

  return @properties[@property_map[idx]]
end

.reference(name, namespace, params = {}) ⇒ Object



56
57
58
# File 'lib/context_class.rb', line 56

def reference(name, namespace, params={})
  @references.push({ namespace:namespace, name:name, params: params })
end

.register(namespace, params = {}) ⇒ Object



50
51
52
53
54
# File 'lib/context_class.rb', line 50

def register(namespace, params={})
  defaults = { name: :name, delimiter: nil, namespace: namespace }
  params = defaults.merge(params)
  @registrations.push(params)
end

.tag(name, params = {}) ⇒ Object



33
34
35
36
37
38
# File 'lib/context_class.rb', line 33

def tag(name, params={})
  raise "Duplicate tag definition #{name} in #{@classname}" if @properties.has_key?(name)
  @tags[name] = params
  add_tag_support(name, params)
  load_child_tags(name, params)
end

.tag_description(params = {}) ⇒ Object



40
41
42
43
44
# File 'lib/context_class.rb', line 40

def tag_description(params={})
  params = { :class => :string, :required => true, :bridge => true }.merge(params)
  tag(:description, params)
  bridge_tag_to_property :description if params[:bridge]
end

.tag_endObject



46
47
48
# File 'lib/context_class.rb', line 46

def tag_end
  tag(:end, { :class => nil })
end

.tag_is_singular?(tag) ⇒ Boolean

Returns:

  • (Boolean)


99
100
101
# File 'lib/context_class.rb', line 99

def tag_is_singular?(tag)
  has_tag? tag and (@tags[tag][:multi] == false or @tags[tag][:multi].nil?)
end

.unique_key_for_tag(key) ⇒ Object



91
92
93
# File 'lib/context_class.rb', line 91

def unique_key_for_tag(key)
  @tags[key][:unique]
end

.validate(errmsg, name, block) ⇒ Object



64
65
66
67
# File 'lib/context_class.rb', line 64

def validate(errmsg, name, block)
  @validations[name] ||= []
  @validations[name].push( { message: errmsg, block: block } )
end

.validate_final(errmsg, block) ⇒ Object



69
70
71
# File 'lib/context_class.rb', line 69

def validate_final(errmsg, block)
  @final_validations.push( { message: errmsg, block: block })
end

Instance Method Details

#[](key) ⇒ Object



71
72
73
# File 'lib/context.rb', line 71

def [](key)
  @contents[key.to_sym]
end

#[]=(key, value) ⇒ Object



75
76
77
78
79
80
81
82
83
# File 'lib/context.rb', line 75

def []=(key, value)
  if value.is_a? Array then
    value = value.map { |v| validated_value_for_key(key, transformed_value_for_key(key, value)) }
  else
    value = validated_value_for_key(key, transformed_value_for_key(key, value))
  end

  @contents[key.to_sym] = value
end

#add_tag(context) ⇒ Object



85
86
87
88
89
# File 'lib/context.rb', line 85

def add_tag(context)
  key = context.tag
  self[key.to_sym] ||= []
  self[key.to_sym].push validated_value_for_key(key, transformed_value_for_key(key, context))
end

#check_child_uniqueness(child) ⇒ Object

Support for parser



93
94
95
96
97
98
99
100
101
102
# File 'lib/context.rb', line 93

def check_child_uniqueness(child)
  # we could do this as a validator, but then we'd lose ability to show sibling info
  return unless self.has_key? child.tag
  key = self.class.unique_key_for_tag(child.tag)

  self[child.tag].each do |sibling|
    errmsg = "Child tag @#{child.tag} must have unique #{key} value; previously used in sibling at line #{sibling.line}"
    parse_error(errmsg, child.file, child.line) if child[key] == sibling[key]
  end
end

#closeObject



54
55
56
57
58
# File 'lib/context.rb', line 54

def close
  process_property_words
  postprocess
  register
end

#crossreferenceObject



136
137
138
139
140
141
142
143
# File 'lib/context.rb', line 136

def crossreference
  self.class.references.each do |r|
    namespace = namespace_for_reference(r)
    [*self[r[:name]]].each do |referenced_name|
      crossreference_value(referenced_name, namespace) unless reference_skipped?(referenced_name, r[:params])
    end
  end
end

#crossreference_value(value, namespace) ⇒ Object



130
131
132
133
134
# File 'lib/context.rb', line 130

def crossreference_value(value, namespace)
  value  = value[:value] if value.is_a? Context
  registered = Registry.registered?(value, namespace)
  parse_error("no symbol '#{value}' in #{namespace}") unless registered
end

#descriptionObject



240
241
242
# File 'lib/context.rb', line 240

def description
  "@#{tag} #{file}:#{line}"
end

#eachObject

Operators and other standard-ish public methods



67
68
69
# File 'lib/context.rb', line 67

def each
  @contents.each { |k,v| yield k,v }
end

#end_tag(context) ⇒ Object



33
34
35
36
37
# File 'lib/context.rb', line 33

def end_tag(context)
  context.close
  check_child_uniqueness(context) if self.class.unique_key_for_tag(context.tag)
  add_tag(context)
end

#final_validationObject



117
118
119
# File 'lib/context.rb', line 117

def final_validation
  self.class.final_validations.each { |v| parse_error(v[:message]) unless v[:block].call(self) }
end

#finalizeObject



60
61
62
63
# File 'lib/context.rb', line 60

def finalize
  final_validation
  crossreference
end

#has_key?(key) ⇒ Boolean

Returns:

  • (Boolean)


39
40
41
# File 'lib/context.rb', line 39

def has_key?(key)
  @contents.has_key?(key.to_sym)
end

#has_tag?(tag) ⇒ Boolean

Returns:

  • (Boolean)


43
44
45
# File 'lib/context.rb', line 43

def has_tag?(tag)
  self.class.has_tag?(tag)
end

#hashify_value(v) ⇒ Object



270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/context.rb', line 270

def hashify_value(v)
  # cache method list to provide big speedup
  @class_methods ||= {}
  @class_methods[v.class] ||= v.methods

  return v.to_val if @class_methods[v.class].include? :to_val
  return v.to_h if @class_methods[v.class].include? :to_h
  return v.to_f if(v.is_a?(Fixnum) || v.is_a?(Float))
  return v if (v == true || v == false)

  v.to_s
end

#key_multiplicity(key) ⇒ Object



47
48
49
50
51
52
# File 'lib/context.rb', line 47

def key_multiplicity(key)
  key = key.to_sym
  return 0 unless @contents.has_key?(key) and not @contents[key].nil?
  return 1 unless @contents[key].is_a? Array or @contents[key].is_a? Hash
  return @contents[key].length
end

#namespace_for_reference(reg) ⇒ Object



125
126
127
128
# File 'lib/context.rb', line 125

def namespace_for_reference(reg)
  return reg[:namespace].call(self) if reg[:namespace].methods.include? :call
  reg[:namespace]
end

#parse(word, file = nil, line = nil) ⇒ Object

Parser interface



21
22
23
# File 'lib/context.rb', line 21

def parse(word, file=nil, line=nil)
  @property_words.push word
end

#parse_error(message, file = nil, line = nil) ⇒ Object

Output



214
215
216
# File 'lib/context.rb', line 214

def parse_error(message, file=nil, line=nil)
  Talk::Parser.error(@tag, file || @file, line || @line, message)
end

#pluralize(num, word, suffix = "s") ⇒ Object



208
209
210
# File 'lib/context.rb', line 208

def pluralize(num, word, suffix="s")
  num == 1 ? word : word + suffix
end

#postprocessObject



113
114
115
# File 'lib/context.rb', line 113

def postprocess
  self.class.postprocesses.each { |p| p.call(self) }
end

#process_property_wordsObject



104
105
106
107
108
109
110
111
# File 'lib/context.rb', line 104

def process_property_words
  ranges = property_ranges
  ranges.each_with_index do |range, idx|
    property = self.class.property_at_index(idx)
    value = @property_words[range[0] .. range[1]].join(" ")
    self[property[:name]] = value
  end
end

#property_range_for_variable_len(offset, word_count, prop_def) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/context.rb', line 195

def property_range_for_variable_len(offset, word_count, prop_def)
  words_left = word_count - offset
  min = prop_def[:length][0]
  max = prop_def[:length][1]
  meets_min = words_left >= min
  meets_max = max.nil? or words_left <= max

  parse_error("Property #{prop_def[:name]} takes at least #{min} #{pluralize min, 'word'}; got #{words_left}") unless meets_min
  parse_error("Property #{prop_def[:name]} takes at most #{max} #{pluralize min, 'word'}; got #{words_left}") unless meets_max

  [ offset, word_count-1 ]
end

#property_rangesObject

Property manipulation



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/context.rb', line 167

def property_ranges
  word_count = @property_words.length
  ranges = []

  self.class.properties.each do |prop_name, prop_def|
    len = prop_def[:length]
    offset = ranges.empty? ? 0 : ranges.last[1]+1
    msg_start = "@#{self.tag} property '#{prop_name}' "

    if len.is_a? Array then
      new_range = property_range_for_variable_len(offset, word_count, prop_def)
    else
      if offset >= word_count then
        parse_error(msg_start+"cannot be omitted") if prop_def[:required]
        new_range = [1, 0]
      else
        length_ok = (word_count - offset >= len)
        parse_error(msg_start+"got #{word_count-offset} of #{len} words") unless length_ok
        new_range = [offset, offset+len-1]
      end        
    end

    ranges.push new_range if new_range[1] >= new_range[0]
  end

  ranges
end

#reference_skipped?(ref_value, params) ⇒ Boolean

Returns:

  • (Boolean)


145
146
147
148
149
150
# File 'lib/context.rb', line 145

def reference_skipped?(ref_value, params)
  ref_value = ref_value[:value] if ref_value.is_a? Context
  return false if params[:skip].nil?
  return params[:skip].include? ref_value if params[:skip].is_a? Array
  return params[:skip] == ref_value
end

#registerObject



121
122
123
# File 'lib/context.rb', line 121

def register
  self.class.registrations.each { |r| Registry.add(self[r[:name]], r[:namespace], self.file, self.line, r[:delimiter]) }
end

#render(indent_level = 0) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/context.rb', line 226

def render(indent_level=0)
  indent = "\t" * indent_level
  str = indent + "@" + self.tag.to_s + ' ' + @property_words.join(' ') + "\n"
  @contents.each do |key, value|
    if value.is_a? Array then
      str = value.inject(str) { |s, element| s + render_element(indent_level+1, key, element) }
    else
      render_element(indent_level+1, key, value)
    end
  end

  str
end

#render_element(indent_level, key, element) ⇒ Object



218
219
220
221
222
223
224
# File 'lib/context.rb', line 218

def render_element(indent_level, key, element)
    if element.methods.include? :render then
      element.render(indent_level)
    else
      "\t" * indent_level + "#{key.to_s} -> '#{element.to_s}'\n"
    end
end

#start_tag(tag, file, line) ⇒ Object



25
26
27
28
29
30
31
# File 'lib/context.rb', line 25

def start_tag(tag, file, line)
  parse_error("Unsupported tag @#{tag}", file, line) unless self.class.tags.has_key?(tag)
  tag_class = self.class.tags[tag][:class]

  # @end tags use a nil class
  tag_class.nil? ? nil : Context.context_for_name(tag_class).new(tag, file, line)
end

#to_hObject



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/context.rb', line 248

def to_h
  dict = {}
  @contents.each do |k,v|
    if v.is_a? Array then
      if self.class.tag_is_singular? k and v.length > 0
        dict[k] = hashify_value(v[0])
      else
        dict[k] = v.map { |u| hashify_value(u) }
      end
    else
      dict[k] = hashify_value(v)
    end
  end

  dict[:__meta] ||= {}
  dict[:__meta][:file] = @file
  dict[:__meta][:tag] = @tag
  dict[:__meta][:line] = @line

  dict
end

#to_sObject



244
245
246
# File 'lib/context.rb', line 244

def to_s
  render
end

#transformed_value_for_key(key, value) ⇒ Object

Key manipulation



154
155
156
157
158
# File 'lib/context.rb', line 154

def transformed_value_for_key(key, value)
  transforms = self.class.transforms[key]
  transforms.each { |t| value = t.call(self, value) } unless transforms.nil?
  value
end

#validated_value_for_key(key, value) ⇒ Object



160
161
162
163
# File 'lib/context.rb', line 160

def validated_value_for_key(key, value)
  self.class.validations[key].each { |v| parse_error(v[:message]) unless v[:block].call(self, value) }
  value
end