class Talk::Context

Attributes

classname[R]
final_validations[R]
postprocesses[R]
properties[R]
references[R]
registrations[R]
registry[R]
tags[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
load_child_tags(name, params) click to toggle source
# 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) 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