class Talk::Context
Attributes
classname[R]
final_validations[R]
postprocesses[R]
properties[R]
references[R]
registrations[R]
registry[R]
transforms[R]
validations[R]
file[R]
line[R]
tag[R]
Public Class Methods
add_key_support(name)
click to toggle source
Support stuff; avoid invoking directly
# File lib/context_class.rb, line 142 def add_key_support(name) @transforms[name] = [] @validations[name] = [] end
add_property_allowed(name, allowed)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# File lib/context_class.rb, line 178 def add_property_transform(name, transform) @transforms[name].push transform end
add_tag_required(name)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# 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_contexts()
click to toggle source
Subclassing magic
# 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)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# File lib/context_class.rb, line 109 def context_for_name(name) predefined_context_for_name(name) || make_context(name) end
has_tag?(tag)
click to toggle source
# File lib/context_class.rb, line 95 def has_tag?(tag) @tags.has_key?(tag) end
initialize(classname)
click to toggle source
# File lib/context_class.rb, line 10 def initialize(classname) @classname = classname @properties = {} @property_map = [] @tags = {} @transforms = {} @registrations = [] @references = [] @validations = {} @final_validations = [] @postprocesses = [] end
make_context(name)
click to toggle source
# 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
new(tag, file, line)
click to toggle source
# File lib/context.rb, line 10 def initialize(tag, file, line) @tag = tag @file = file @line = line @contents = {} @property_words = [] end
normalize_allowed(name, allowed)
click to toggle source
# 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)
click to toggle source
# File lib/context_class.rb, line 60 def postprocess(block) @postprocesses.push block end
predefined_context_for_name(name)
click to toggle source
# 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={})
click to toggle source
Stuff to be used by context definitions All of this is documented in ./README.md
# 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)
click to toggle source
Convenience and support methods for instance methods
# 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={})
click to toggle source
# File lib/context_class.rb, line 56 def reference(name, namespace, params={}) @references.push({ namespace:namespace, name:name, params: params }) end
register(namespace, params={})
click to toggle source
# 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={})
click to toggle source
# 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={})
click to toggle source
# 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_end()
click to toggle source
# File lib/context_class.rb, line 46 def tag_end tag(:end, { :class => nil }) end
tag_is_singular?(tag)
click to toggle source
# 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)
click to toggle source
# File lib/context_class.rb, line 91 def unique_key_for_tag(key) @tags[key][:unique] end
validate(errmsg, name, block)
click to toggle source
# 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)
click to toggle source
# File lib/context_class.rb, line 69 def validate_final(errmsg, block) @final_validations.push( { message: errmsg, block: block }) end
Public Instance Methods
[](key)
click to toggle source
# File lib/context.rb, line 71 def [](key) @contents[key.to_sym] end
[]=(key, value)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
Support for parser
# 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
close()
click to toggle source
# File lib/context.rb, line 54 def close process_property_words postprocess register end
crossreference()
click to toggle source
# 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)
click to toggle source
# 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
description()
click to toggle source
# File lib/context.rb, line 240 def description "@#{tag} #{file}:#{line}" end
each() { |k,v| ... }
click to toggle source
Operators and other standard-ish public methods
# File lib/context.rb, line 67 def each @contents.each { |k,v| yield k,v } end
end_tag(context)
click to toggle source
# 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_validation()
click to toggle source
# 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
finalize()
click to toggle source
# File lib/context.rb, line 60 def finalize final_validation crossreference end
has_key?(key)
click to toggle source
# File lib/context.rb, line 39 def has_key?(key) @contents.has_key?(key.to_sym) end
has_tag?(tag)
click to toggle source
# File lib/context.rb, line 43 def has_tag?(tag) self.class.has_tag?(tag) end
hashify_value(v)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
Parser
interface
# 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)
click to toggle source
Output
# 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")
click to toggle source
# File lib/context.rb, line 208 def pluralize(num, word, suffix="s") num == 1 ? word : word + suffix end
postprocess()
click to toggle source
# File lib/context.rb, line 113 def postprocess self.class.postprocesses.each { |p| p.call(self) } end
process_property_words()
click to toggle source
# 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)
click to toggle source
# 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_ranges()
click to toggle source
Property manipulation
# 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)
click to toggle source
# 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
register()
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# 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)
click to toggle source
# 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_h()
click to toggle source
# 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_s()
click to toggle source
# File lib/context.rb, line 244 def to_s render end
transformed_value_for_key(key, value)
click to toggle source
Key manipulation
# 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)
click to toggle source
# 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