Commits

Kaspar Schiess committed 9fe101e

Implements most of the EDN spec

Comments (0)

Files changed (5)

 
 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
       or { src.match('false').as(false) }.value
   end
   def string src
-    src.match('"') or return Par.bottom
+    src.match('"').bottom? and return Par.bottom
 
     str = src.match(/.*(?=(?<!\\)")/)
 
-    src.match('"').as(Par.result(str))
+    src.match('"').as(str)
   end
   def char src
-    src.match('\\') or return Par.bottom  
+    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     { char(src) }.value
+      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
 
 
     expect(source).to be_at_eos
     expect(result).not_to eq(Par.bottom)
-    if expected 
-      expect(result).to eq(expected)
-    end
   end
   def parses_into str, expected
     source = Par.source(str)
 
     expect(source).to be_at_eos
     expect(result).not_to eq(Par.bottom)
-    expect(result).to eq(expected)
+    result.should == expected
   end
   def fails str
     expect(subject.call(Par.source(str))).to eq(Par.bottom)
     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('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

lib/par/result.rb

+require 'par/result/conversion'
+
 module Par
   class Result
+    include Conversion
+
     attr_reader :obj
 
     def initialize obj
     def maybe
       self
     end
-    def as replacement=nil, &block
-      unless block_given?
-        return Result.new(replacement)
-      end
-
-      return Result.new(block.call(@obj))
-    end
 
     def new obj
       self.class.new obj

lib/par/result/conversion.rb

+module Par
+  module Conversion
+    def as obj=nil, &block
+      obj = block.call(@obj || self) if block_given?
+
+      return obj if obj == Par.top || obj == Par.bottom
+      return obj if obj.kind_of? Result
+      return Result.new(obj)
+    end
+  end # module Conversion
+end # module Par

lib/par/result/top.rb

+require 'par/result/conversion'
+
 module Par
   class Top
+    include Conversion
+
     def >> other
       other
     end
     def bottom?
       false
     end
-    def as obj=nil, &block
-      unless block_given?
-        return Result.new(obj)
-      end
-      return Result.new(block.call(self))
-    end
   end
 end

spec/lib/par/result_spec.rb

   context 'a simple result' do
     let(:r) { result 'r' }
 
+    it 'should allow equality comparison as part of an array' do
+      [r, r].should == ['r', 'r']
+      ['r', 'r'].should == [r, r]
+    end
     it 'should not be bottom' do 
       r.should_not be_bottom
     end
     it 'allows transforming object contents' do
       r.as { |o| o + 's' }.should == 'rs'
     end
+    it 'allows conversion into another value' do
+      r.as('v').should == result('v')
+
+      r.as(result('v')).obj.should eql('v')
+
+      r.as(bottom).should eql(bottom)
+      r.as(top).should eql(top)
+    end
   end
   context 'given 2 results' do
     let(:a) { result 'foo' }
     it "allows transformation into an object" do
       top.as { |o| 'obj' }.should == 'obj'
     end 
+    it "does correct value conversion" do
+      top.as(bottom).should eql(bottom)
+      top.as(top).should eql(top)      
+    end
   end
 end