Source

par / examples / edn.rb


# A (partial) implementation of EDN in par. 
# https://github.com/edn-format/edn

require 'rspec/autorun'

$:.unshift File.dirname(__FILE__) + "/../lib"
require 'par'

module EDN

  class Id
    attr_reader :name

    def initialize name
      @name = name
    end
    def eql? other
      other.kind_of?(self.class) &&
        other.name == self.name
    end 
    def hash
      self.name.hash
    end
    def == other
      eql? other
    end
  end

  def nil src
    src.match('nil').as(nil)
  end
  def bool src
    src.either { src.match('true').as(true) }.
      or { src.match('false').as(false) }.value
  end
  def string src
    src.match('"').bottom? and return Par.bottom

    str = src.match(/.*(?=(?<!\\)")/)

    src.match('"').as(str)
  end
  def char src
    src.match('\\').bottom? and return Par.bottom  
  
    return "\n" unless src.cr { src.match('newline') }.bottom?

    src.match(/./)
  end
  def integer src
    src.match(/\d+/).as { |o| o.to_i }
  end
  def symbol src
    src.match(/:[[:alnum:]]+/).as { |o| o[1..-1].to_sym }
  end
  def identifier src
    src.match(/[[:alnum:]]+/).as { |o| Id.new(o) }
  end

  def value src
    src.
      either { self.nil(src) }.
      or     { bool(src) }.
      or     { string(src) }.
      or     { integer(src) }.
      or     { symbol(src) }.
      or     { char(src) }.
      or     { identifier(src) }.
      or     { list(src) }.
      or     { map(src) }.
      value
  end

  def list src
    src.cr {
      src.match(/\(|\[/).bottom? and return Par.bottom

      values = greedy { 
        v = value(src) 
        src.match(/\s*/).as(v) }

      src.match(/\)|\]/).bottom? and return Par.bottom

      Par.result(values.map(&:obj))
    }
  end
  alias vector list

  def map src
    result = {}

    src.cr {
      src.match('{').bottom? and return Par.bottom

      loop do
        k, v = pair(src) { value(src) }
        break if k.bottom?

        result[k.obj] = v.obj

        src.match(/,\s*/).bottom? and break
      end

      src.match('}').bottom? and return Par.bottom
    }

    result
  end

  def pair src, &block
    v1 = block.call
    s = src.match(/\s+/)
    v2 = block.call

    return Par.bottom if s.bottom? || v1.bottom? || v2.bottom?    
    return v1, v2
  end
  def greedy &block
    accum = []

    loop do
      v = block.call
      break if v.bottom?

      accum << v
    end

    return accum 
  end
end

describe EDN do
  include EDN 

  def parses str
    source = Par.source(str)
    result = subject.call(source)

    expect(source).to be_at_eos
    expect(result).not_to eq(Par.bottom)
  end
  def parses_into str, expected
    source = Par.source(str)
    result = subject.call(source)

    expect(source).to be_at_eos
    expect(result).not_to eq(Par.bottom)
    result.should == expected
  end
  def fails str
    expect(subject.call(Par.source(str))).to eq(Par.bottom)
  end    

  describe '#nil' do
    subject { method(:nil) }

    it { parses_into 'nil', nil }
    it { fails 'foo' }
  end
  describe '#bool' do
    subject { method(:bool) }

    it { parses_into('true', true) }
    it { parses_into('false', false) }
    it { fails 'foo' }
  end
  describe '#string' do
    subject { method(:string) }

    it { fails 'foo' }
    it { parses_into '"foo"', 'foo' }
    it { fails '"foo' }
  end
  describe '#char' do
    subject { method(:char) }

    it { parses_into '\c', ?c }
    it { parses_into '\newline', "\n" }
  end
  describe '#integer' do
    subject { method(:integer) }
    
    it { parses_into '123', 123 }
  end

  describe '#value' do
    subject { method(:value) }

    it { parses_into '\c', ?c }
    it { parses_into '"foo"', 'foo' }
    it { parses_into('true', true) }
    it { parses_into 'nil', nil }
  end

  describe '#list' do
    subject { method(:list) }

    it { fails '( 1 2 3' }
    it { parses_into '(1 2 true false)', [1, 2, true, false] }
  end
  describe '#list' do
    subject { method(:vector) }

    it { fails '[ 1 2 3]' }
    it { parses_into '(1 2 true false)', [1, 2, true, false] }
  end

  describe '#map' do
    subject { method(:map) }

    it "parses correct map" do
      result = subject.call(Par.source('{:a 1, "foo" :bar, five four}'))
      result[:a].should == 1
      result['foo'].should == :bar
      result[EDN::Id.new('five')].should == EDN::Id.new('four')
    end
  end
end