class Aspen::Compiler

Attributes

environment[R]
root[R]

Public Class Methods

new(root, environment = {}) click to toggle source

@param environment [Hash] @todo Make {environment} an Aspen::Environment

# File lib/aspen/compiler.rb, line 17
    def initialize(root, environment = {})
      @root = root
      @environment = environment
      @adapter = environment.fetch(:adapter, :cypher).to_sym
      # @todo FIXME: This is too much responsibility for the compiler.
      #   This should be delegated to an object and the later calls
      #   just messages to that object.
      @slug_counters = Hash.new { 1 }

      # @todo Move this into an Environment object—it should be set there.
      #   and here, just run environment.validate
      unless Aspen.available_formats.include?(@adapter)
        raise Aspen::ArgumentError, <<~MSG
          The adapter, also known as the output format, must be one of:
          #{Aspen.available_formats.join(', ')}.

          What Aspen received was #{@adapter}.
        MSG
      end
    end
render(root, environment = {}) click to toggle source

@param environment [Hash] @todo Make {environment} an Aspen::Environment

# File lib/aspen/compiler.rb, line 11
def self.render(root, environment = {})
  new(root, environment).render
end

Public Instance Methods

discourse() click to toggle source
# File lib/aspen/compiler.rb, line 42
def discourse
  @discourse ||= Discourse.from_hash(environment)
end
render() click to toggle source
# File lib/aspen/compiler.rb, line 38
def render
  visit(root)
end
visit(node) click to toggle source
# File lib/aspen/compiler.rb, line 46
def visit(node)
  short_name = node.class.to_s.split('::').last.downcase
  method_name = "visit_#{short_name}"
  send(method_name, node)
end
visit_attribute(node) click to toggle source
# File lib/aspen/compiler.rb, line 189
def visit_attribute(node)
  content = visit(node.content)
  type    = visit(node.type)
  content.send(type.converter)
end
visit_comment(node) click to toggle source

@returns [Symbol] :comment This acts as a signal so other methods know to reject comments.

# File lib/aspen/compiler.rb, line 205
def visit_comment(node)
  :comment
end
visit_content(node) click to toggle source
# File lib/aspen/compiler.rb, line 199
def visit_content(node)
  node.content
end
visit_customstatement(node) click to toggle source

@todo Get the labels back into here. Labelreg? typereg?

This is doing too much.
Can't we have typed attributes come from the Grammar?
# File lib/aspen/compiler.rb, line 76
def visit_customstatement(node)
  statement = visit(node.content)
  matcher   = discourse.grammar.matcher_for(statement)
  results   = matcher.captures(statement)
  template  = matcher.template
  typereg   = matcher.typereg
  labelreg  = matcher.labelreg

  nodes = []

  typed_results = results.inject({}) do |hash, elem|
    key, value = elem
    typed_value = case typereg[key]
    when :integer then value.to_i
    when :float   then value.to_f
    when :numeric then
      value.match?(/\./) ? value.to_f : value.to_i
    when :string  then "\"#{value}\""
    when :node    then
      # FIXME: This only handles short form.
      #   I think we were allowing grouped and Cypher form to fill
      #   in custom statement templates.
      # TODO: Add some object to nodes array.
      node = visit(
        Aspen::AST::Nodes::Node.new(
          attribute: value,
          label: labelreg[key]
        )
      )
      nodes << node
      node
    end
    hash[key] = typed_value
    hash
  end

  formatted_results = typed_results.inject({}) do |hash, elem|
    key, value = elem
    f_value = value.is_a?(Aspen::Node) ? value.nickname_node : value
    hash[key] = f_value

    # TODO: Trying to insert a p_id as well as p to be used in JSON identifiers.
    # if value.is_a?(Aspen::Node)
    #   hash["#{key}_id"] = value.nickname
    # end
    # puts "TYPED VALS: #{hash.inspect}"
    hash
  end

  slugs = template.scan(/{{{?(?<full>uniq_(?<name>\w+))}}}?/).uniq
  usable_results = if slugs.any?
    counts = slugs.map do |full, short|
      [full, "#{short}_#{@slug_counters[full]}"]
    end.to_h

    context = results.merge(counts)
    custom_statement = CustomStatement.new(
      nodes: nodes,
      cypher: Mustache.render(template.strip, formatted_results.merge(counts))
    )
    slugs.each do |full, _|
      @slug_counters[full] = @slug_counters[full] + 1
    end
    custom_statement
  else
    CustomStatement.new(
      nodes: nodes,
      cypher: Mustache.render(template.strip, formatted_results)
    )
  end
end
visit_edge(node) click to toggle source
# File lib/aspen/compiler.rb, line 163
def visit_edge(node)
  content = visit(node.content)
  unless discourse.allows_edge?(content)
    raise Aspen::Error, """
      Your narrative includes an edge called '#{content}',
      but only #{discourse.allowed_edges} are allowed.
    """
  end
  Aspen::Edge.new(
    content,
    mutual: discourse.mutual?(visit(node.content))
  )
end
visit_label(node) click to toggle source
# File lib/aspen/compiler.rb, line 177
def visit_label(node)
  content = visit(node.content)
  label = Maybe(content).value_or(discourse.default_label)
  unless discourse.allows_label?(label)
    raise Aspen::CompileError, """
      Your narrative includes a node with label '#{label}',
      but only #{discourse.allowed_labels} are allowed.
    """
  end
  label
end
visit_narrative(node) click to toggle source
# File lib/aspen/compiler.rb, line 52
def visit_narrative(node)
  # Instead of letting comments be `nil` and using `#compact`
  # to silently remove them, possibly hiding errors, we "compile"
  # comments as `:comment` and filter them explicitly
  statements = node.statements.map do |statement|
    # This will visit both regular and custom statements.
    visit(statement)
  end.reject { |elem| elem == :comment }

  renderer_klass = Kernel.const_get("Aspen::Renderers::#{@adapter.to_s.downcase.capitalize}Renderer")
  renderer_klass.new(statements, environment).render
end
visit_node(node) click to toggle source
# File lib/aspen/compiler.rb, line 148
def visit_node(node)
  # Get the label, falling back to the default label.
  label = visit(node.label)

  # Get the attribute name, falling back to the default attribute name.
  attribute_name  = Maybe(nil).value_or(discourse.default_attr_name(label))
  typed_attribute_value = visit(node.attribute)
  nickname = typed_attribute_value.to_s.downcase

  Aspen::Node.new(
    label: label,
    attributes: { attribute_name => typed_attribute_value }
  )
end
visit_statement(node) click to toggle source
# File lib/aspen/compiler.rb, line 65
def visit_statement(node)
  Statement.new(
    origin: visit(node.origin),
    edge: visit(node.edge),
    target: visit(node.target)
  )
end
visit_type(node) click to toggle source
# File lib/aspen/compiler.rb, line 195
def visit_type(node)
  node
end