[DRE-commits] [ruby-parslet] 01/04: Imported Upstream 1.7.0
Jonas Genannt
genannt at moszumanska.debian.org
Wed Aug 19 13:40:58 UTC 2015
This is an automated email from the git hooks/post-receive script.
genannt pushed a commit to branch master
in repository ruby-parslet.
commit 0c9f3eba0ca3ccd5aa54ac5c4260ac7ae6907e4e
Author: Jonas Genannt <jonas at brachium-system.net>
Date: Wed Aug 19 15:31:54 2015 +0200
Imported Upstream 1.7.0
---
HISTORY.txt | 9 +-
README | 2 +-
checksums.yaml.gz | Bin 269 -> 0 bytes
lib/parslet.rb | 4 +-
lib/parslet/atoms/base.rb | 12 +-
lib/parslet/atoms/context.rb | 10 +-
lib/parslet/atoms/entity.rb | 13 +-
lib/parslet/atoms/sequence.rb | 2 +-
lib/parslet/cause.rb | 19 +-
lib/parslet/error_reporter.rb | 3 +-
lib/parslet/error_reporter/contextual.rb | 120 +++++++
lib/parslet/error_reporter/deepest.rb | 5 +
lib/parslet/error_reporter/tree.rb | 8 +-
lib/parslet/rig/rspec.rb | 2 +-
lib/parslet/slice.rb | 4 +-
lib/parslet/transform.rb | 31 +-
metadata.yml | 61 +++-
parslet.gemspec | 18 ++
spec/acceptance/examples_spec.rb | 37 +++
spec/acceptance/infix_parser_spec.rb | 112 +++++++
spec/acceptance/regression_spec.rb | 314 ++++++++++++++++++
spec/acceptance/repetition_and_maybe_spec.rb | 42 +++
spec/acceptance/unconsumed_input_spec.rb | 21 ++
spec/parslet/atom_results_spec.rb | 39 +++
spec/parslet/atoms/alternative_spec.rb | 26 ++
spec/parslet/atoms/base_spec.rb | 126 ++++++++
spec/parslet/atoms/capture_spec.rb | 21 ++
spec/parslet/atoms/combinations_spec.rb | 5 +
spec/parslet/atoms/dsl_spec.rb | 25 ++
spec/parslet/atoms/entity_spec.rb | 77 +++++
spec/parslet/atoms/infix_spec.rb | 5 +
spec/parslet/atoms/lookahead_spec.rb | 22 ++
spec/parslet/atoms/named_spec.rb | 4 +
spec/parslet/atoms/re_spec.rb | 14 +
spec/parslet/atoms/repetition_spec.rb | 24 ++
spec/parslet/atoms/scope_spec.rb | 26 ++
spec/parslet/atoms/sequence_spec.rb | 28 ++
spec/parslet/atoms/str_spec.rb | 15 +
spec/parslet/atoms/visitor_spec.rb | 80 +++++
spec/parslet/atoms_spec.rb | 429 +++++++++++++++++++++++++
spec/parslet/convenience_spec.rb | 48 +++
spec/parslet/error_reporter/contextual_spec.rb | 115 +++++++
spec/parslet/error_reporter/deepest_spec.rb | 73 +++++
spec/parslet/error_reporter/tree_spec.rb | 7 +
spec/parslet/export_spec.rb | 67 ++++
spec/parslet/expression/treetop_spec.rb | 74 +++++
spec/parslet/minilisp.citrus | 29 ++
spec/parslet/minilisp.tt | 29 ++
spec/parslet/parser_spec.rb | 31 ++
spec/parslet/parslet_spec.rb | 38 +++
spec/parslet/pattern_spec.rb | 272 ++++++++++++++++
spec/parslet/position_spec.rb | 14 +
spec/parslet/rig/rspec_spec.rb | 54 ++++
spec/parslet/scope_spec.rb | 45 +++
spec/parslet/slice_spec.rb | 144 +++++++++
spec/parslet/source/line_cache_spec.rb | 74 +++++
spec/parslet/source_spec.rb | 168 ++++++++++
spec/parslet/transform/context_spec.rb | 35 ++
spec/parslet/transform_spec.rb | 165 ++++++++++
spec/spec_helper.rb | 38 +++
60 files changed, 3299 insertions(+), 36 deletions(-)
diff --git a/HISTORY.txt b/HISTORY.txt
index ad81ce2..8f04f9f 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -3,11 +3,13 @@
- prsnt? and absnt? are now finally banned into oblivion. Wasting vocals for
the win.
-= 1.7 / ???
+= 1.7 / 12Mar2015
! Small speed gains from improvements on the hot spots.
+ + Contextual error reporter, a flavor of error reporting that emphasizes
+ context.
-= 1.6 / 1May2014
+= 1.6 / 1May2014, 13Okt14
+ EXPERIMENTAL: Parslet accelerators permit replacing parts of your parser
with optimized atoms using pattern matching. Look at
@@ -27,6 +29,9 @@
! A few small bug fixes and optimisations have been introduced. API should
remain unchanged.
+
+ + More lenient on the blankslate version.
+ + Modernizes the test suite to run with rspec again. (!)
= 1.5 / 27Dec2012
diff --git a/README b/README
index 90444b1..9d19787 100644
--- a/README
+++ b/README
@@ -11,7 +11,7 @@ the atoms of your language first; _parslet_ takes pride in making this
possible.
Eager to try this out? Please see the associated web site:
-http://kschiess.github.com/parslet
+http://kschiess.github.io/parslet
SYNOPSIS
diff --git a/checksums.yaml.gz b/checksums.yaml.gz
deleted file mode 100644
index 7fea2e9..0000000
Binary files a/checksums.yaml.gz and /dev/null differ
diff --git a/lib/parslet.rb b/lib/parslet.rb
index 2fef5ce..fb332bd 100644
--- a/lib/parslet.rb
+++ b/lib/parslet.rb
@@ -99,7 +99,7 @@ module Parslet
# root :twobar
# end
#
- def rule(name, &definition)
+ def rule(name, opts={}, &definition)
define_method(name) do
@rules ||= {} # <name, rule> memoization
return @rules[name] if @rules.has_key?(name)
@@ -109,7 +109,7 @@ module Parslet
self.instance_eval(&definition)
}
- @rules[name] = Atoms::Entity.new(name, &definition_closure)
+ @rules[name] = Atoms::Entity.new(name, opts[:label], &definition_closure)
end
end
end
diff --git a/lib/parslet/atoms/base.rb b/lib/parslet/atoms/base.rb
index e145ecf..ea11cb7 100644
--- a/lib/parslet/atoms/base.rb
+++ b/lib/parslet/atoms/base.rb
@@ -7,7 +7,10 @@ class Parslet::Atoms::Base
include Parslet::Atoms::Precedence
include Parslet::Atoms::DSL
include Parslet::Atoms::CanFlatten
-
+
+ # Parslet label as provided in grammar
+ attr_accessor :label
+
# Given a string or an IO object, this will attempt a parse of its contents
# and return a result. If the parse fails, a Parslet::ParseFailed exception
# will be thrown.
@@ -83,6 +86,8 @@ class Parslet::Atoms::Base
success, value = result = context.try_with_cache(self, source, consume_all)
if success
+ # Notify context
+ context.succ(source)
# If a consume_all parse was made and doesn't result in the consumption
# of all the input, that is considered an error.
if consume_all && source.chars_left>0
@@ -132,10 +137,11 @@ class Parslet::Atoms::Base
end
precedence BASE
def to_s(outer_prec=OUTER)
+ str = @label || to_s_inner(precedence)
if outer_prec < precedence
- "("+to_s_inner(precedence)+")"
+ "(#{str})"
else
- to_s_inner(precedence)
+ str
end
end
def inspect
diff --git a/lib/parslet/atoms/context.rb b/lib/parslet/atoms/context.rb
index 7167a74..fb71146 100644
--- a/lib/parslet/atoms/context.rb
+++ b/lib/parslet/atoms/context.rb
@@ -62,7 +62,15 @@ module Parslet::Atoms
return [false, @reporter.err(*args)] if @reporter
return [false, nil]
end
-
+
+ # Report a successful parse.
+ # @see ErrorReporter::Contextual
+ #
+ def succ(*args)
+ return [true, @reporter.succ(*args)] if @reporter
+ return [true, nil]
+ end
+
# Returns the current captures made on the input (see
# Parslet::Atoms::Base#capture). Use as follows:
#
diff --git a/lib/parslet/atoms/entity.rb b/lib/parslet/atoms/entity.rb
index 4df6050..00215f4 100644
--- a/lib/parslet/atoms/entity.rb
+++ b/lib/parslet/atoms/entity.rb
@@ -5,15 +5,16 @@
# * Be able to print things by their name, not by their sometimes
# complicated content.
#
-# You don't normally use this directly, instead you should generated it by
+# You don't normally use this directly, instead you should generate it by
# using the structuring method Parslet.rule.
#
class Parslet::Atoms::Entity < Parslet::Atoms::Base
attr_reader :name, :block
- def initialize(name, &block)
+ def initialize(name, label=nil, &block)
super()
@name = name
+ @label = label
@block = block
end
@@ -22,9 +23,11 @@ class Parslet::Atoms::Entity < Parslet::Atoms::Base
end
def parslet
- @parslet ||= @block.call.tap { |p|
- raise_not_implemented unless p
- }
+ return @parslet unless @parslet.nil?
+ @parslet = @block.call
+ raise_not_implemented if @parslet.nil?
+ @parslet.label = @label
+ @parslet
end
def to_s_inner(prec)
diff --git a/lib/parslet/atoms/sequence.rb b/lib/parslet/atoms/sequence.rb
index b556cc9..2d54b28 100644
--- a/lib/parslet/atoms/sequence.rb
+++ b/lib/parslet/atoms/sequence.rb
@@ -31,7 +31,7 @@ class Parslet::Atoms::Sequence < Parslet::Atoms::Base
unless success
return context.err(self, source, @error_msgs[:failed], [value])
end
-
+
result[idx+1] = value
end
diff --git a/lib/parslet/cause.rb b/lib/parslet/cause.rb
index ce5672c..8d2e48b 100644
--- a/lib/parslet/cause.rb
+++ b/lib/parslet/cause.rb
@@ -44,15 +44,22 @@ module Parslet
def self.format(source, pos, str, children=[])
self.new(str, source, pos, children)
end
-
+
+ # Update error message to include context provided by label
+ # Update all child causes too (the same context applies to all causes)
+ def set_label(l)
+ @context = " when parsing #{l}"
+ children.each { |c| c.set_label(l) }
+ end
+
def to_s
line, column = source.line_and_column(pos)
# Allow message to be a list of objects. Join them here, since we now
- # really need it.
- Array(message).map { |o|
- o.respond_to?(:to_slice) ?
- o.str.inspect :
- o.to_s }.join + " at line #{line} char #{column}."
+ # really need it.
+ Array(message).map { |o|
+ o.respond_to?(:to_slice) ?
+ o.str.inspect :
+ o.to_s }.join + " at line #{line} char #{column}#{@context}."
end
# Signals to the outside that the parse has failed. Use this in
diff --git a/lib/parslet/error_reporter.rb b/lib/parslet/error_reporter.rb
index 567c63a..3998e4e 100644
--- a/lib/parslet/error_reporter.rb
+++ b/lib/parslet/error_reporter.rb
@@ -4,4 +4,5 @@ module Parslet::ErrorReporter
end
require 'parslet/error_reporter/tree'
-require 'parslet/error_reporter/deepest'
\ No newline at end of file
+require 'parslet/error_reporter/deepest'
+require 'parslet/error_reporter/contextual'
diff --git a/lib/parslet/error_reporter/contextual.rb b/lib/parslet/error_reporter/contextual.rb
new file mode 100644
index 0000000..0fe1237
--- /dev/null
+++ b/lib/parslet/error_reporter/contextual.rb
@@ -0,0 +1,120 @@
+module Parslet
+ module ErrorReporter
+
+ # A reporter that tries to improve on the deepest error reporter by
+ # using heuristics to find the most relevant error and provide more
+ # context.
+ # The heuristic chooses the deepest error when parsing a sequence for which
+ # no alternative parsed successfully.
+ #
+ # Given the following parser:
+ #
+ # root(:call)
+ #
+ # rule(:call, label: 'call') {
+ # identifier >> str('.') >> method
+ # }
+ #
+ # rule(:method, label: 'method call') {
+ # identifier >> str('(') >> arguments.maybe >> str(')')
+ # }
+ #
+ # rule(:identifier, label: 'identifier') {
+ # match['[:alnum:]'].repeat(1)
+ # }
+ #
+ # rule(:arguments, label: 'method call arguments') {
+ # argument >> str(',') >> arguments | argument
+ # }
+ #
+ # rule(:argument) {
+ # call | identifier
+ # }
+ #
+ # and the following source:
+ #
+ # foo.bar(a,goo.baz(),c,)
+ #
+ # The contextual reporter returns the following causes:
+ #
+ # 0: Failed to match sequence (identifier '.' method call) at line 1 char 5
+ # when parsing method call arguments.
+ # 1: Failed to match sequence (identifier '(' method call arguments? ')') at
+ # line 1 char 22 when parsing method call arguments.
+ # 2: Failed to match [[:alnum:]] at line 1 char 23 when parsing method call
+ # arguments.
+ #
+ # (where 2 is a child cause of 1 and 1 a child cause of 0)
+ #
+ # The last piece used by the reporter is the (newly introduced) ability
+ # to attach a label to rules that describe a sequence in the grammar. The
+ # labels are used in two places:
+ # - In the "to_s" of Atom::Base so that any error message uses labels to
+ # refer to atoms
+ # - In the cause error messages to give information about which expression
+ # failed to parse
+ #
+ class Contextual < Deepest
+
+ def initialize
+ @last_reset_pos = 0
+ reset
+ end
+
+ # A sequence expression successfully parsed, reset all errors reported
+ # for previous expressions in the sequence (an alternative matched)
+ # Only reset errors if the position of the source that matched is higher
+ # than the position of the source that was last successful (so we keep
+ # errors that are the "deepest" but for which no alternative succeeded)
+ #
+ def succ(source)
+ source_pos = source.pos.bytepos
+ return if source_pos < @last_reset_pos
+ @last_reset_pos = source_pos
+ reset
+ end
+
+ # Reset deepest error and its position and sequence index
+ #
+ def reset
+ @deepest_cause = nil
+ @label_pos = -1
+ end
+
+ # Produces an error cause that combines the message at the current level
+ # with the errors that happened at a level below (children).
+ # Compute and set label used by Cause to produce error message.
+ #
+ # @param atom [Parslet::Atoms::Base] parslet that failed
+ # @param source [Source] Source that we're using for this parse. (line
+ # number information...)
+ # @param message [String, Array] Error message at this level.
+ # @param children [Array] A list of errors from a deeper level (or nil).
+ # @return [Cause] An error tree combining children with message.
+ #
+ def err(atom, source, message, children=nil)
+ cause = super(atom, source, message, children)
+ if (label = atom.respond_to?(:label) && atom.label)
+ update_label(label, source.pos.bytepos)
+ cause.set_label(@label)
+ end
+ cause
+ end
+
+ # Update error message label if given label is more relevant.
+ # A label is more relevant if the position of the matched source is
+ # bigger.
+ #
+ # @param label [String] label to apply if more relevant
+ # @param bytepos [Integer] position in source code of matched source
+ #
+ def update_label(label, bytepos)
+ if bytepos >= @label_pos
+ @label_pos = bytepos
+ @label = label
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/parslet/error_reporter/deepest.rb b/lib/parslet/error_reporter/deepest.rb
index 102b4f8..e6fa622 100644
--- a/lib/parslet/error_reporter/deepest.rb
+++ b/lib/parslet/error_reporter/deepest.rb
@@ -50,6 +50,11 @@ module Parslet
end
# Returns the cause that is currently deepest. Mainly for specs.
+
+ # Notification that an expression successfully parsed
+ # not used, see ErrorReporter::Contextual
+ def succ(source)
+ end
#
attr_reader :deepest_cause
diff --git a/lib/parslet/error_reporter/tree.rb b/lib/parslet/error_reporter/tree.rb
index 2fb2750..055363f 100644
--- a/lib/parslet/error_reporter/tree.rb
+++ b/lib/parslet/error_reporter/tree.rb
@@ -52,6 +52,12 @@ module Parslet
position = pos
Cause.format(source, position, message, children)
end
+
+ # Notification that an expression successfully parsed
+ # not used, see ErrorReporter::Contextual
+ def succ(source)
+ end
+
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/parslet/rig/rspec.rb b/lib/parslet/rig/rspec.rb
index 71f1aa7..df40508 100644
--- a/lib/parslet/rig/rspec.rb
+++ b/lib/parslet/rig/rspec.rb
@@ -52,7 +52,7 @@ RSpec::Matchers.define(:parse) do |input, opts|
# NOTE: This has a nodoc tag since the rdoc parser puts this into
# Object, a thing I would never allow.
- chain :as do |expected_output, &block|
+ chain :as do |expected_output=nil, &block|
as = expected_output
block = block
end
diff --git a/lib/parslet/slice.rb b/lib/parslet/slice.rb
index f8ad518..b2b78a6 100644
--- a/lib/parslet/slice.rb
+++ b/lib/parslet/slice.rb
@@ -57,6 +57,8 @@ class Parslet::Slice
def size
str.size
end
+
+ alias length size
# Concatenate two slices; it is assumed that the second slice begins
# where the first one ends. The offset of the resulting slice is the same
@@ -105,4 +107,4 @@ class Parslet::Slice
str.inspect << "@#{offset}"
end
-end
\ No newline at end of file
+end
diff --git a/lib/parslet/transform.rb b/lib/parslet/transform.rb
index b04fa29..6e6f1b2 100644
--- a/lib/parslet/transform.rb
+++ b/lib/parslet/transform.rb
@@ -124,7 +124,8 @@ class Parslet::Transform
#
def rule(expression, &block)
@__transform_rules ||= []
- @__transform_rules << [Parslet::Pattern.new(expression), block]
+ # Prepend new rules so they have higher precedence than older rules
+ @__transform_rules.unshift([Parslet::Pattern.new(expression), block])
end
# Allows accessing the class' rules
@@ -132,6 +133,11 @@ class Parslet::Transform
def rules
@__transform_rules || []
end
+
+ def inherited(subclass)
+ super
+ subclass.instance_variable_set(:@__transform_rules, rules.dup)
+ end
end
def initialize(&block)
@@ -149,16 +155,31 @@ class Parslet::Transform
# * a *transformation block*
#
def rule(expression, &block)
- @rules << [
- Parslet::Pattern.new(expression),
- block
- ]
+ # Prepend new rules so they have higher precedence than older rules
+ @rules.unshift([Parslet::Pattern.new(expression), block])
end
# Applies the transformation to a tree that is generated by Parslet::Parser
# or a simple parslet. Transformation will proceed down the tree, replacing
# parts/all of it with new objects. The resulting object will be returned.
#
+ # Using the context parameter, you can inject bindings for the transformation.
+ # This can be used to allow access to the outside world from transform blocks,
+ # like so:
+ #
+ # document = # some class that you act on
+ # transform.apply(tree, document: document)
+ #
+ # The above will make document available to all your action blocks:
+ #
+ # # Variant A
+ # rule(...) { document.foo(bar) }
+ # # Variant B
+ # rule(...) { |d| d[:document].foo(d[:bar]) }
+ #
+ # @param obj PORO ast to transform
+ # @param context start context to inject into the bindings.
+ #
def apply(obj, context=nil)
transform_elt(
case obj
diff --git a/metadata.yml b/metadata.yml
index c761d5a..7c0bcf4 100644
--- a/metadata.yml
+++ b/metadata.yml
@@ -1,29 +1,35 @@
--- !ruby/object:Gem::Specification
name: parslet
version: !ruby/object:Gem::Version
- version: 1.6.1
+ version: 1.7.0
platform: ruby
authors:
- Kaspar Schiess
autorequire:
bindir: bin
cert_chain: []
-date: 2014-05-22 00:00:00.000000000 Z
+date: 2015-08-19 00:00:00.000000000 Z
dependencies:
- !ruby/object:Gem::Dependency
name: blankslate
requirement: !ruby/object:Gem::Requirement
requirements:
- - - "~>"
+ - - ">="
- !ruby/object:Gem::Version
version: '2.0'
+ - - "<="
+ - !ruby/object:Gem::Version
+ version: '4.0'
type: :runtime
prerelease: false
version_requirements: !ruby/object:Gem::Requirement
requirements:
- - - "~>"
+ - - ">="
- !ruby/object:Gem::Version
version: '2.0'
+ - - "<="
+ - !ruby/object:Gem::Version
+ version: '4.0'
description:
email: kaspar.schiess at absurd.li
executables: []
@@ -118,6 +124,7 @@ files:
- lib/parslet/context.rb
- lib/parslet/convenience.rb
- lib/parslet/error_reporter.rb
+- lib/parslet/error_reporter/contextual.rb
- lib/parslet/error_reporter/deepest.rb
- lib/parslet/error_reporter/tree.rb
- lib/parslet/export.rb
@@ -134,7 +141,50 @@ files:
- lib/parslet/source.rb
- lib/parslet/source/line_cache.rb
- lib/parslet/transform.rb
-homepage: http://kschiess.github.com/parslet
+- parslet.gemspec
+- spec/acceptance/examples_spec.rb
+- spec/acceptance/infix_parser_spec.rb
+- spec/acceptance/regression_spec.rb
+- spec/acceptance/repetition_and_maybe_spec.rb
+- spec/acceptance/unconsumed_input_spec.rb
+- spec/parslet/atom_results_spec.rb
+- spec/parslet/atoms/alternative_spec.rb
+- spec/parslet/atoms/base_spec.rb
+- spec/parslet/atoms/capture_spec.rb
+- spec/parslet/atoms/combinations_spec.rb
+- spec/parslet/atoms/dsl_spec.rb
+- spec/parslet/atoms/entity_spec.rb
+- spec/parslet/atoms/infix_spec.rb
+- spec/parslet/atoms/lookahead_spec.rb
+- spec/parslet/atoms/named_spec.rb
+- spec/parslet/atoms/re_spec.rb
+- spec/parslet/atoms/repetition_spec.rb
+- spec/parslet/atoms/scope_spec.rb
+- spec/parslet/atoms/sequence_spec.rb
+- spec/parslet/atoms/str_spec.rb
+- spec/parslet/atoms/visitor_spec.rb
+- spec/parslet/atoms_spec.rb
+- spec/parslet/convenience_spec.rb
+- spec/parslet/error_reporter/contextual_spec.rb
+- spec/parslet/error_reporter/deepest_spec.rb
+- spec/parslet/error_reporter/tree_spec.rb
+- spec/parslet/export_spec.rb
+- spec/parslet/expression/treetop_spec.rb
+- spec/parslet/minilisp.citrus
+- spec/parslet/minilisp.tt
+- spec/parslet/parser_spec.rb
+- spec/parslet/parslet_spec.rb
+- spec/parslet/pattern_spec.rb
+- spec/parslet/position_spec.rb
+- spec/parslet/rig/rspec_spec.rb
+- spec/parslet/scope_spec.rb
+- spec/parslet/slice_spec.rb
+- spec/parslet/source/line_cache_spec.rb
+- spec/parslet/source_spec.rb
+- spec/parslet/transform/context_spec.rb
+- spec/parslet/transform_spec.rb
+- spec/spec_helper.rb
+homepage: http://kschiess.github.io/parslet
licenses:
- MIT
metadata: {}
@@ -161,4 +211,3 @@ signing_key:
specification_version: 4
summary: Parser construction library with great error reporting in Ruby.
test_files: []
-has_rdoc:
diff --git a/parslet.gemspec b/parslet.gemspec
new file mode 100644
index 0000000..572d37c
--- /dev/null
+++ b/parslet.gemspec
@@ -0,0 +1,18 @@
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = 'parslet'
+ s.version = '1.7.0'
+
+ s.authors = ['Kaspar Schiess']
+ s.email = 'kaspar.schiess at absurd.li'
+ s.extra_rdoc_files = ['README']
+ s.files = %w(HISTORY.txt LICENSE Rakefile README parslet.gemspec) + Dir.glob("{lib,spec,example}/**/*")
+ s.homepage = 'http://kschiess.github.io/parslet'
+ s.license = 'MIT'
+ s.rdoc_options = ['--main', 'README']
+ s.require_paths = ['lib']
+ s.summary = 'Parser construction library with great error reporting in Ruby.'
+
+ s.add_dependency 'blankslate', '>= 2.0', '<= 4.0'
+end
diff --git a/spec/acceptance/examples_spec.rb b/spec/acceptance/examples_spec.rb
new file mode 100644
index 0000000..7e0eae4
--- /dev/null
+++ b/spec/acceptance/examples_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'open3'
+
+describe "Regression on" do
+ Dir["example/*.rb"].each do |example|
+ context example do
+ # Generates a product path for a given example file.
+ def product_path(str, ext)
+ str.
+ gsub('.rb', ".#{ext}").
+ gsub('example/','example/output/')
+ end
+
+ it "runs successfully" do
+ stdin, stdout, stderr = Open3.popen3("ruby #{example}")
+
+ handle_map = {
+ stdout => :out,
+ stderr => :err
+ }
+ expectation_found = handle_map.any? do |io, ext|
+ name = product_path(example, ext)
+
+ if File.exists?(name)
+ io.read.strip.should == File.read(name).strip
+ true
+ end
+ end
+
+ unless expectation_found
+ fail "Example doesn't have either an .err or an .out file. "+
+ "Please create in examples/output!"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/acceptance/infix_parser_spec.rb b/spec/acceptance/infix_parser_spec.rb
new file mode 100644
index 0000000..c676a29
--- /dev/null
+++ b/spec/acceptance/infix_parser_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe 'Infix expression parsing' do
+ class InfixExpressionParser < Parslet::Parser
+ rule(:space) { match['\s'] }
+
+ def cts atom
+ atom >> space.repeat
+ end
+ def infix *args
+ Infix.new(*args)
+ end
+
+ rule(:mul_op) { match['*/'] >> str(' ').maybe }
+ rule(:add_op) { match['+-'] >> str(' ').maybe }
+ rule(:digit) { match['0-9'] }
+ rule(:integer) { cts digit.repeat(1) }
+
+ rule(:expression) { infix_expression(integer,
+ [mul_op, 2, :left],
+ [add_op, 1, :right]) }
+ end
+
+ let(:p) { InfixExpressionParser.new }
+ describe '#integer' do
+ let(:i) { p.integer }
+ it "parses integers" do
+ i.should parse('1')
+ i.should parse('123')
+ end
+ it "consumes trailing white space" do
+ i.should parse('1 ')
+ i.should parse('134 ')
+ end
+ it "doesn't parse floats" do
+ i.should_not parse('1.3')
+ end
+ end
+ describe '#multiplication' do
+ let(:m) { p.expression }
+ it "parses simple multiplication" do
+ m.should parse('1*2').as(l: '1', o: '*', r: '2')
+ end
+ it "parses simple multiplication with spaces" do
+ m.should parse('1 * 2').as(l: '1 ', o: '* ', r: '2')
+ end
+ it "parses division" do
+ m.should parse('1/2')
+ end
+ end
+ describe '#addition' do
+ let(:a) { p.expression }
+
+ it "parses simple addition" do
+ a.should parse('1+2')
+ end
+ it "parses complex addition" do
+ a.should parse('1+2+3-4')
+ end
+ it "parses a single element" do
+ a.should parse('1')
+ end
+ end
+
+ describe 'mixed operations' do
+ let(:mo) { p.expression }
+
+ describe 'inspection' do
+ it 'produces useful expressions' do
+ p.expression.parslet.inspect.should ==
+ "infix_expression(INTEGER, [MUL_OP, ADD_OP])"
+ end
+ end
+ describe 'right associativity' do
+ it 'produces trees that lean right' do
+ mo.should parse('1+2+3').as(
+ l: '1', o: '+', r: {l: '2', o: '+', r: '3'})
+ end
+ end
+ describe 'left associativity' do
+ it 'produces trees that lean left' do
+ mo.should parse('1*2*3').as(
+ l: {l:'1', o:'*', r:'2'}, o:'*', r:'3')
+ end
+ end
+ describe 'error handling' do
+ describe 'incomplete expression' do
+ it 'produces the right error' do
+ cause = catch_failed_parse {
+ mo.parse('1+') }
+
+ cause.ascii_tree.to_s.should == <<-ERROR
+INTEGER was expected at line 1 char 3.
+`- Failed to match sequence (DIGIT{1, } SPACE{0, }) at line 1 char 3.
+ `- Expected at least 1 of DIGIT at line 1 char 3.
+ `- Premature end of input at line 1 char 3.
+ ERROR
+ end
+ end
+ describe 'invalid operator' do
+ it 'produces the right error' do
+ cause = catch_failed_parse {
+ mo.parse('1%') }
+
+ cause.ascii_tree.to_s.should == <<-ERROR
+Don't know what to do with "%" at line 1 char 2.
+ ERROR
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/acceptance/regression_spec.rb b/spec/acceptance/regression_spec.rb
new file mode 100644
index 0000000..8ef5947
--- /dev/null
+++ b/spec/acceptance/regression_spec.rb
@@ -0,0 +1,314 @@
+# Encoding: UTF-8
+
+require 'spec_helper'
+
+require 'parslet'
+
+describe "Regressions from real examples" do
+ # This parser piece produces on the left a subtree that is keyed (a hash)
+ # and on the right a subtree that is a repetition of such subtrees. I've
+ # for now decided that these would merge into the repetition such that the
+ # return value is an array. This avoids maybe loosing keys/values in a
+ # hash merge.
+ #
+ class ArgumentListParser
+ include Parslet
+
+ rule :argument_list do
+ expression.as(:argument) >>
+ (comma >> expression.as(:argument)).repeat
+ end
+ rule :expression do
+ string
+ end
+ rule :string do
+ str('"') >>
+ (
+ str('\\') >> any |
+ str('"').absent? >> any
+ ).repeat.as(:string) >>
+ str('"') >> space?
+ end
+ rule :comma do
+ str(',') >> space?
+ end
+ rule :space? do
+ space.maybe
+ end
+ rule :space do
+ match("[ \t]").repeat(1)
+ end
+
+ def parse(str)
+ argument_list.parse(str)
+ end
+ end
+ describe ArgumentListParser do
+ let(:instance) { ArgumentListParser.new }
+ it "should have method expression" do
+ instance.should respond_to(:expression)
+ end
+ it 'should parse "arg1", "arg2"' do
+ result = ArgumentListParser.new.parse('"arg1", "arg2"')
+
+ result.size.should == 2
+ result.each do |r|
+ r[:argument]
+ end
+ end
+ it 'should parse "arg1", "arg2", "arg3"' do
+ result = ArgumentListParser.new.parse('"arg1", "arg2", "arg3"')
+
+ result.size.should == 3
+ result.each do |r|
+ r[:argument]
+ end
+ end
+ end
+
+ class ParensParser < Parslet::Parser
+ rule(:balanced) {
+ str('(').as(:l) >> balanced.maybe.as(:m) >> str(')').as(:r)
+ }
+
+ root(:balanced)
+ end
+ describe ParensParser do
+ let(:instance) { ParensParser.new }
+
+ context "statefulness: trying several expressions in sequence" do
+ it "should not be stateful" do
+ # NOTE: Since you've come here to read this, I'll explain why
+ # this is broken and not fixed: You're looking at the tuning branch,
+ # which rewrites a bunch of stuff - so I have failing tests to
+ # remind me of what is left to be done. And to remind you not to
+ # trust this code.
+ instance.parse('(())')
+ lambda {
+ instance.parse('((()))')
+ instance.parse('(((())))')
+ }.should_not raise_error
+ end
+ end
+ context "expression '(())'" do
+ let(:result) { instance.parse('(())') }
+
+ it "should yield a doubly nested hash" do
+ result.should be_a(Hash)
+ result.should have_key(:m)
+ result[:m].should be_a(Hash) # This was an array earlier
+ end
+ context "inner hash" do
+ let(:inner) { result[:m] }
+
+ it "should have nil as :m" do
+ inner[:m].should be_nil
+ end
+ end
+ end
+ end
+
+ class ALanguage < Parslet::Parser
+ root(:expressions)
+
+ rule(:expressions) { (line >> eol).repeat(1) | line }
+ rule(:line) { space? >> an_expression.as(:exp).repeat }
+ rule(:an_expression) { str('a').as(:a) >> space? }
+
+ rule(:eol) { space? >> match["\n\r"].repeat(1) >> space? }
+
+ rule(:space?) { space.repeat }
+ rule(:space) { multiline_comment.as(:multi) | line_comment.as(:line) | str(' ') }
+
+ rule(:line_comment) { str('//') >> (match["\n\r"].absent? >> any).repeat }
+ rule(:multiline_comment) { str('/*') >> (str('*/').absent? >> any).repeat >> str('*/') }
+ end
+ describe ALanguage do
+ def remove_indent(s)
+ s.to_s.lines.map { |l| l.chomp.strip }.join("\n")
+ end
+
+ it "should count lines correctly" do
+ cause = catch_failed_parse {
+ subject.parse('a
+ a a a
+ aaa // ff
+ /*
+ a
+ */
+ b
+ ')
+ }
+
+ remove_indent(cause.ascii_tree).should == remove_indent(%q(
+ Expected one of [(LINE EOL){1, }, LINE] at line 1 char 1.
+ |- Extra input after last repetition at line 7 char 11.
+ | `- Failed to match sequence (LINE EOL) at line 7 char 11.
+ | `- Failed to match sequence (SPACE? [\n\r]{1, } SPACE?) at line 7 char 11.
+ | `- Expected at least 1 of [\n\r] at line 7 char 11.
+ | `- Failed to match [\n\r] at line 7 char 11.
+ `- Don't know what to do with "\n " at line 1 char 2.).strip)
+ end
+ end
+
+ class BLanguage < Parslet::Parser
+ root :expression
+ rule(:expression) { b.as(:one) >> b.as(:two) }
+ rule(:b) { str('b') }
+ end
+ describe BLanguage do
+ it "should parse 'bb'" do
+ subject.should parse('bb').as(:one => 'b', :two => 'b')
+ end
+ it "should transform with binding constraint" do
+ transform = Parslet::Transform.new do |t|
+ t.rule(:one => simple(:b), :two => simple(:b)) { :ok }
+ end
+ transform.apply(subject.parse('bb')).should == :ok
+ end
+ end
+
+ class UnicodeLanguage < Parslet::Parser
+ root :gobble
+ rule(:gobble) { any.repeat }
+ end
+ describe UnicodeLanguage do
+ it "should parse UTF-8 strings" do
+ subject.should parse('éèäöü').as('éèäöü')
+ subject.should parse('RubyKaigi2009のテーマは、「変わる/変える」です。 前回の').as('RubyKaigi2009のテーマは、「変わる/変える」です。 前回の')
+ end
+ end
+
+ class UnicodeSentenceLanguage < Parslet::Parser
+ rule(:sentence) { (match('[^。]').repeat(1) >> str("。")).as(:sentence) }
+ rule(:sentences) { sentence.repeat }
+ root(:sentences)
+ end
+ describe UnicodeSentenceLanguage do
+ let(:string) {
+ "RubyKaigi2009のテーマは、「変わる/変える」です。 前回の" +
+ "RubyKaigi2008のテーマであった「多様性」の言葉の通り、 " +
+ "2008年はRubyそのものに関しても、またRubyの活躍する舞台に関しても、 " +
+ "ますます多様化が進みつつあります。RubyKaigi2008は、そのような " +
+ "Rubyの生態系をあらためて認識する場となりました。 しかし、" +
+ "こうした多様化が進む中、異なる者同士が単純に距離を 置いたままでは、" +
+ "その違いを認識したところであまり意味がありません。 異なる実装、" +
+ "異なる思想、異なる背景といった、様々な多様性を理解しつつ、 " +
+ "すり合わせるべきものをすり合わせ、変えていくべきところを " +
+ "変えていくことが、豊かな未来へとつながる道に違いありません。"
+ }
+
+ it "should parse sentences" do
+ subject.should parse(string)
+ end
+ end
+
+ class TwoCharLanguage < Parslet::Parser
+ root :twochar
+ rule(:twochar) { any >> str('2') }
+ end
+ describe TwoCharLanguage do
+ def di(s)
+ s.strip.to_s.lines.map { |l| l.chomp.strip }.join("\n")
+ end
+
+ it "should raise an error" do
+ error = catch_failed_parse {
+ subject.parse('123') }
+ di(error.ascii_tree).should == di(%q(
+ Failed to match sequence (. '2') at line 1 char 2.
+ `- Don't know what to do with "3" at line 1 char 3.
+ ))
+ end
+ end
+
+ # Issue #68: Extra input reporting, written by jmettraux
+ class RepetitionParser < Parslet::Parser
+ rule(:nl) { match('[\s]').repeat(1) }
+ rule(:nl?) { nl.maybe }
+ rule(:sp) { str(' ').repeat(1) }
+ rule(:sp?) { str(' ').repeat(0) }
+ rule(:line) { sp >> str('line') }
+ rule(:body) { ((line | block) >> nl).repeat(0) }
+ rule(:block) { sp? >> str('begin') >> sp >> match('[a-z]') >> nl >>
+ body >> sp? >> str('end') }
+ rule(:blocks) { nl? >> block >> (nl >> block).repeat(0) >> nl? }
+
+ root(:blocks)
+ end
+ describe RepetitionParser do
+ def di(s)
+ s.strip.to_s.lines.map { |l| l.chomp.strip }.join("\n")
+ end
+
+ it 'parses a block' do
+ subject.parse(%q{
+ begin a
+ end
+ })
+ end
+ it 'parses nested blocks' do
+ subject.parse(%q{
+ begin a
+ begin b
+ end
+ end
+ })
+ end
+ it 'parses successive blocks' do
+ subject.parse(%q{
+ begin a
+ end
+ begin b
+ end
+ })
+ end
+ it 'fails gracefully on a missing end' do
+ error = catch_failed_parse {
+ subject.parse(%q{
+ begin a
+ begin b
+ end
+ }) }
+
+ di(error.ascii_tree).should == di(%q(
+ Failed to match sequence (NL? BLOCK (NL BLOCK){0, } NL?) at line 2 char 11.
+ `- Failed to match sequence (SP? 'begin' SP [a-z] NL BODY SP? 'end') at line 5 char 9.
+ `- Premature end of input at line 5 char 9.
+ ))
+ end
+ it 'fails gracefully on a missing end (2)' do
+ error = catch_failed_parse {
+ subject.parse(%q{
+ begin a
+ end
+ begin b
+ begin c
+ end
+ }) }
+
+ di(error.ascii_tree).should == di(%q(
+ Failed to match sequence (NL? BLOCK (NL BLOCK){0, } NL?) at line 3 char 14.
+ `- Don't know what to do with "begin b\n " at line 4 char 11.
+ ))
+ end
+ it 'fails gracefully on a missing end (deepest reporter)' do
+ error = catch_failed_parse {
+ subject.parse(%q{
+ begin a
+ end
+ begin b
+ begin c
+ li
+ end
+ end
+ },
+ :reporter => Parslet::ErrorReporter::Deepest.new) }
+
+ di(error.ascii_tree).should == di(%q(
+ Failed to match sequence (NL? BLOCK (NL BLOCK){0, } NL?) at line 3 char 16.
+ `- Expected "end", but got "li\n" at line 6 char 17.
+ ))
+ end
+ end
+end
diff --git a/spec/acceptance/repetition_and_maybe_spec.rb b/spec/acceptance/repetition_and_maybe_spec.rb
new file mode 100644
index 0000000..8bfcab5
--- /dev/null
+++ b/spec/acceptance/repetition_and_maybe_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe "Tree output" do
+ extend Parslet
+
+ def self.hash_examples(h)
+ h.each do |atom, expected|
+ it "should convert #{atom} to #{expected.inspect}" do
+ atom.parse(input).should == expected
+ end
+ end
+ end
+
+ context "when parsing the empty string" do
+ let(:input) { '' }
+ hash_examples(
+ # No naming
+ str('a').maybe => '',
+ str('a').repeat => '',
+
+ # Named contents: maybe yields nil
+ str('a').maybe.as(:f) => {:f => nil},
+ str('a').repeat.as(:f) => {:f => []},
+
+ # Contents that aren't simple strings
+ (str('a') >> str('b')).maybe.as(:f) => {:f => nil},
+ (str('a') >> str('b')).repeat.as(:f) => {:f => []},
+
+ # The other way around: Contents would be tagged, but nil result isn't
+ (str('a') >> str('b')).as(:f).maybe => '',
+ (str('a') >> str('b')).as(:f).repeat => ''
+ )
+ end
+
+ context "when parsing 'aa'" do
+ let(:input) { 'aa' }
+ hash_examples(
+ # since they're not named, repetitions get merged together.
+ str('a').as(:a).repeat >> str('a').as(:a).repeat => [{:a=>'a'},{:a=>'a'}]
+ )
+ end
+end
\ No newline at end of file
diff --git a/spec/acceptance/unconsumed_input_spec.rb b/spec/acceptance/unconsumed_input_spec.rb
new file mode 100644
index 0000000..ffc48f8
--- /dev/null
+++ b/spec/acceptance/unconsumed_input_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe "Unconsumed input:" do
+ class RepeatingBlockParser < Parslet::Parser
+ root :expressions
+ rule(:expressions) { expression.repeat }
+ rule(:expression) { str('(') >> aab >> str(')') }
+ rule(:aab) { str('a').repeat(1) >> str('b') }
+ end
+ describe RepeatingBlockParser do
+ let(:parser) { described_class.new }
+ it "throws annotated error" do
+ error = catch_failed_parse { parser.parse('(aaac)') }
+ end
+ it "doesn't error out if prefix is true" do
+ expect {
+ parser.parse('(aaac)', :prefix => true)
+ }.not_to raise_error
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atom_results_spec.rb b/spec/parslet/atom_results_spec.rb
new file mode 100644
index 0000000..1dcfafa
--- /dev/null
+++ b/spec/parslet/atom_results_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe 'Result of a Parslet#parse' do
+ include Parslet; extend Parslet
+
+ describe "regression" do
+ [
+ # Behaviour with maybe-nil
+ [str('foo').maybe >> str('bar'), "bar", "bar"],
+ [str('bar') >> str('foo').maybe, "bar", 'bar'],
+
+ # These might be hard to understand; look at the result of
+ # str.maybe >> str
+ # and
+ # str.maybe >> str first.
+ [(str('f').maybe >> str('b')).repeat, "bb", "bb"],
+ [(str('b') >> str('f').maybe).repeat, "bb", 'bb'],
+
+ [str('a').as(:a) >> (str('b') >> str('c').as(:a)).repeat, 'abc',
+ [{:a=>'a'}, {:a=>'c'}]],
+
+ [str('a').as(:a).repeat >> str('b').as(:b).repeat, 'ab', [{:a=>'a'}, {:b=>'b'}]],
+
+ # Repetition behaviour / named vs. unnamed
+ [str('f').repeat, '', ''],
+ [str('f').repeat.as(:f), '', {:f => []}],
+
+ # Maybe behaviour / named vs. unnamed
+ [str('f').maybe, '', ''],
+ [str('f').maybe.as(:f), '', {:f => nil}],
+ ].each do |parslet, input, result|
+ context "#{parslet.inspect}" do
+ it "should parse \"#{input}\" into \"#{result}\"" do
+ parslet.parse(input).should == result
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/alternative_spec.rb b/spec/parslet/atoms/alternative_spec.rb
new file mode 100644
index 0000000..79bd296
--- /dev/null
+++ b/spec/parslet/atoms/alternative_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Alternative do
+ include Parslet
+
+ describe '| shortcut' do
+ let(:alternative) { str('a') | str('b') }
+
+ context "when chained with different atoms" do
+ before(:each) {
+ # Chain something else to the alternative parslet. If it modifies the
+ # parslet atom in place, we'll notice:
+
+ alternative | str('d')
+ }
+ let!(:chained) { alternative | str('c') }
+
+
+ it "is side-effect free" do
+ chained.should parse('c')
+ chained.should parse('a')
+ chained.should_not parse('d')
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/base_spec.rb b/spec/parslet/atoms/base_spec.rb
new file mode 100644
index 0000000..9940ac0
--- /dev/null
+++ b/spec/parslet/atoms/base_spec.rb
@@ -0,0 +1,126 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Base do
+ let(:parslet) { Parslet::Atoms::Base.new }
+ let(:context) { Parslet::Atoms::Context.new }
+
+ describe "<- #try(io)" do
+ it "should raise NotImplementedError" do
+ lambda {
+ parslet.try(flexmock(:io), context, false)
+ }.should raise_error(NotImplementedError)
+ end
+ end
+ describe "<- #flatten_sequence" do
+ [
+ # 9 possibilities for making a word of 2 letters from the alphabeth of
+ # A(rray), H(ash) and S(tring). Make sure that all results are valid.
+ #
+ ['a', 'b'], 'ab', # S S
+ [['a'], ['b']], ['a', 'b'], # A A
+ [{:a=>'a'}, {:b=>'b'}], {:a=>'a',:b=>'b'}, # H H
+
+ [{:a=>'a'}, ['a']], [{:a=>'a'}, 'a'], # H A
+ [{:a=>'a'}, 's'], {:a=>'a'}, # H S
+
+ [['a'], {:a=>'a'}], ['a', {:a=>'a'}], # A H (symmetric to H A)
+ [['a'], 'b'], ['a'], # A S
+
+ ['a', {:b=>'b'}], {:b=>'b'}, # S H (symmetric to H S)
+ ['a', ['b']], ['b'], # S A (symmetric to A S)
+
+ [nil, ['a']], ['a'], # handling of lhs nil
+ [nil, {:a=>'a'}], {:a=>'a'},
+ [['a'], nil], ['a'], # handling of rhs nil
+ [{:a=>'a'}, nil], {:a=>'a'}
+ ].each_slice(2) do |sequence, result|
+ context "for " + sequence.inspect do
+ it "should equal #{result.inspect}" do
+ parslet.flatten_sequence(sequence).should == result
+ end
+ end
+ end
+ end
+ describe "<- #flatten_repetition" do
+ def unnamed(obj)
+ parslet.flatten_repetition(obj, false)
+ end
+
+ it "should give subtrees precedence" do
+ unnamed([[{:a=>"a"}, {:m=>"m"}], {:a=>"a"}]).should == [{:a=>"a"}]
+ end
+ end
+ describe '#parse(source)' do
+ context "when given something that looks like a source" do
+ let(:source) { flexmock("source lookalike",
+ :line_and_column => [1,2],
+ :bytepos => 1,
+ :chars_left => 0) }
+
+ it "should not rewrap in a source" do
+ flexmock(Parslet::Source).
+ should_receive(:new => :source_created).never
+
+ begin
+ parslet.parse(source)
+ rescue NotImplementedError
+ end
+ end
+ end
+ end
+
+ context "when the parse fails, the exception" do
+ it "should contain a string" do
+ begin
+ Parslet.str('foo').parse('bar')
+ rescue Parslet::ParseFailed => ex
+ ex.message.should be_kind_of(String)
+ end
+ end
+ end
+ context "when not all input is consumed" do
+ let(:parslet) { Parslet.str('foo') }
+
+ it "should raise with a proper error message" do
+ error = catch_failed_parse {
+ parslet.parse('foobar') }
+
+ error.to_s.should == "Don't know what to do with \"bar\" at line 1 char 4."
+ end
+ end
+ context "when only parsing string prefix" do
+ let(:parslet) { Parslet.str('foo') >> Parslet.str('bar') }
+
+ it "returns the first half on a prefix parse" do
+ parslet.parse('foobarbaz', :prefix => true).should == 'foobar'
+ end
+ end
+
+ describe ':reporter option' do
+ let(:parslet) { Parslet.str('test') >> Parslet.str('ing') }
+ let(:reporter) { flexmock(:reporter) }
+
+ it "replaces the default reporter" do
+ cause = flexmock(:cause)
+
+ # Two levels of the parse, calling two different error reporting
+ # methods.
+ reporter.
+ should_receive(:err_at).once
+ reporter.
+ should_receive(:err => cause).once
+ reporter.
+ should_receive(:succ).once
+
+ # The final cause will be sent the #raise method.
+ cause.
+ should_receive(:raise).once.and_throw(:raise)
+
+ catch(:raise) {
+ parslet.parse('testung', :reporter => reporter)
+
+ fail "NEVER REACHED"
+ }
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/capture_spec.rb b/spec/parslet/atoms/capture_spec.rb
new file mode 100644
index 0000000..2f3a68a
--- /dev/null
+++ b/spec/parslet/atoms/capture_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Capture do
+ include Parslet
+
+ let(:context) { Parslet::Atoms::Context.new(nil) }
+
+ def inject string, parser
+ source = Parslet::Source.new(string)
+ parser.apply(source, context, true)
+ end
+
+ it "should capture simple results" do
+ inject 'a', str('a').capture(:a)
+ context.captures[:a].should == 'a'
+ end
+ it "should capture complex results" do
+ inject 'a', str('a').as(:b).capture(:a)
+ context.captures[:a].should == {:b => 'a'}
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/combinations_spec.rb b/spec/parslet/atoms/combinations_spec.rb
new file mode 100644
index 0000000..5edbfff
--- /dev/null
+++ b/spec/parslet/atoms/combinations_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe "Parslet combinations" do
+ include Parslet
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/dsl_spec.rb b/spec/parslet/atoms/dsl_spec.rb
new file mode 100644
index 0000000..1915bc1
--- /dev/null
+++ b/spec/parslet/atoms/dsl_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::DSL do
+ describe "deprecated methods" do
+ let(:parslet) { Parslet.str('foo') }
+ describe "<- #absnt?" do
+ slet(:absnt) { parslet.absnt? }
+ it '#bound_parslet' do
+ absnt.bound_parslet.should == parslet
+ end
+ it 'should be a negative lookahead' do
+ absnt.positive.should == false
+ end
+ end
+ describe "<- #prsnt?" do
+ slet(:prsnt) { parslet.prsnt? }
+ it '#bound_parslet' do
+ prsnt.bound_parslet.should == parslet
+ end
+ it 'should be a positive lookahead' do
+ prsnt.positive.should == true
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/entity_spec.rb b/spec/parslet/atoms/entity_spec.rb
new file mode 100644
index 0000000..fc6b028
--- /dev/null
+++ b/spec/parslet/atoms/entity_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Entity do
+ context "when constructed with str('bar') inside" do
+ let(:named) { Parslet::Atoms::Entity.new('name', &proc { Parslet.str('bar') }) }
+
+ it "should parse 'bar' without raising exceptions" do
+ named.parse('bar')
+ end
+ it "should raise when applied to 'foo'" do
+ lambda {
+ named.parse('foo')
+ }.should raise_error(Parslet::ParseFailed)
+ end
+
+ describe "#inspect" do
+ it "should return the name of the entity" do
+ named.inspect.should == 'NAME'
+ end
+ end
+ end
+ context "when constructed with empty block" do
+ let(:entity) { Parslet::Atoms::Entity.new('name', &proc { }) }
+
+ it "should raise NotImplementedError" do
+ lambda {
+ entity.parse('some_string')
+ }.should raise_error(NotImplementedError)
+ end
+ end
+
+ context "recursive definition parser" do
+ class RecDefParser
+ include Parslet
+ rule :recdef do
+ str('(') >> atom >> str(')')
+ end
+ rule :atom do
+ str('a') | str('b') | recdef
+ end
+ end
+ let(:parser) { RecDefParser.new }
+
+ it "should parse balanced parens" do
+ parser.recdef.parse("(((a)))")
+ end
+ it "should not throw 'stack level too deep' when printing errors" do
+ cause = catch_failed_parse { parser.recdef.parse('(((a))') }
+ cause.ascii_tree
+ end
+ end
+
+ context "when constructed with a label" do
+ let(:named) { Parslet::Atoms::Entity.new('name', 'label', &proc { Parslet.str('bar') }) }
+
+ it "should parse 'bar' without raising exceptions" do
+ named.parse('bar')
+ end
+ it "should raise when applied to 'foo'" do
+ lambda {
+ named.parse('foo')
+ }.should raise_error(Parslet::ParseFailed)
+ end
+
+ describe "#inspect" do
+ it "should return the label of the entity" do
+ named.inspect.should == 'label'
+ end
+ end
+
+ describe "#parslet" do
+ it "should set the label on the cached parslet" do
+ named.parslet.label.should == 'label'
+ end
+ end
+ end
+end
diff --git a/spec/parslet/atoms/infix_spec.rb b/spec/parslet/atoms/infix_spec.rb
new file mode 100644
index 0000000..203a3ff
--- /dev/null
+++ b/spec/parslet/atoms/infix_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Infix do
+
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/lookahead_spec.rb b/spec/parslet/atoms/lookahead_spec.rb
new file mode 100644
index 0000000..ac54ecd
--- /dev/null
+++ b/spec/parslet/atoms/lookahead_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Lookahead do
+ include Parslet
+
+ describe 'negative lookahead' do
+ it "influences the error tree" do
+ parser = str('f').absent? >> str('b')
+ cause = catch_failed_parse { parser.parse('f') }
+
+ cause.ascii_tree.should == "Failed to match sequence (!'f' 'b') at line 1 char 1.\n`- Input should not start with 'f' at line 1 char 1.\n"
+ end
+ end
+ describe 'positive lookahead' do
+ it "influences the error tree" do
+ parser = str('f').present? >> str('b')
+ cause = catch_failed_parse { parser.parse('b') }
+
+ cause.ascii_tree.should == "Failed to match sequence (&'f' 'b') at line 1 char 1.\n`- Input should start with 'f' at line 1 char 1.\n"
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/named_spec.rb b/spec/parslet/atoms/named_spec.rb
new file mode 100644
index 0000000..98ce14d
--- /dev/null
+++ b/spec/parslet/atoms/named_spec.rb
@@ -0,0 +1,4 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Named do
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/re_spec.rb b/spec/parslet/atoms/re_spec.rb
new file mode 100644
index 0000000..44f9cd7
--- /dev/null
+++ b/spec/parslet/atoms/re_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Re do
+ describe "construction" do
+ include Parslet
+
+ it "should allow match(str) form" do
+ match('[a]').should be_a(Parslet::Atoms::Re)
+ end
+ it "should allow match[str] form" do
+ match['a'].should be_a(Parslet::Atoms::Re)
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/repetition_spec.rb b/spec/parslet/atoms/repetition_spec.rb
new file mode 100644
index 0000000..0f6a8c1
--- /dev/null
+++ b/spec/parslet/atoms/repetition_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Repetition do
+ include Parslet
+
+ describe "repeat" do
+ let(:parslet) { str('a') }
+
+ describe "(min, max)" do
+ subject { parslet.repeat(1,2) }
+
+ it { should_not parse("") }
+ it { should parse("a") }
+ it { should parse("aa") }
+ end
+ describe "0 times" do
+ it "raises an ArgumentError" do
+ expect {
+ parslet.repeat(0,0)
+ }.to raise_error(ArgumentError)
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/scope_spec.rb b/spec/parslet/atoms/scope_spec.rb
new file mode 100644
index 0000000..2664bc8
--- /dev/null
+++ b/spec/parslet/atoms/scope_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Scope do
+ include Parslet
+ include Parslet::Atoms::DSL
+
+
+ let(:context) { Parslet::Atoms::Context.new(nil) }
+ let(:captures) { context.captures }
+
+ def inject string, parser
+ source = Parslet::Source.new(string)
+ parser.apply(source, context, true)
+ end
+
+ let(:aabb) {
+ scope {
+ match['ab'].capture(:f) >> dynamic { |s,c| str(c.captures[:f]) }
+ }
+ }
+ it "keeps values of captures outside" do
+ captures[:f] = 'old_value'
+ inject 'aa', aabb
+ captures[:f].should == 'old_value'
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/sequence_spec.rb b/spec/parslet/atoms/sequence_spec.rb
new file mode 100644
index 0000000..c2cec13
--- /dev/null
+++ b/spec/parslet/atoms/sequence_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Parslet::Atoms::Sequence do
+ include Parslet
+
+ let(:sequence) { described_class.new }
+
+ describe '>> shortcut' do
+ let(:sequence) { str('a') >> str('b') }
+
+ context "when chained with different atoms" do
+ before(:each) {
+ # Chain something else to the sequence parslet. If it modifies the
+ # parslet atom in place, we'll notice:
+
+ sequence >> str('d')
+ }
+ let!(:chained) { sequence >> str('c') }
+
+
+ it "is side-effect free" do
+ chained.should parse('abc')
+ chained.should_not parse('abdc')
+ end
+ end
+
+ end
+end
diff --git a/spec/parslet/atoms/str_spec.rb b/spec/parslet/atoms/str_spec.rb
new file mode 100644
index 0000000..455f82d
--- /dev/null
+++ b/spec/parslet/atoms/str_spec.rb
@@ -0,0 +1,15 @@
+# Encoding: UTF-8
+
+require 'spec_helper'
+
+describe Parslet::Atoms::Str do
+ def str(s)
+ described_class.new(s)
+ end
+
+ describe 'regression #1: multibyte characters' do
+ it "parses successfully (length check works)" do
+ str('あああ').should parse('あああ')
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms/visitor_spec.rb b/spec/parslet/atoms/visitor_spec.rb
new file mode 100644
index 0000000..c195cee
--- /dev/null
+++ b/spec/parslet/atoms/visitor_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Parslet::Atoms do
+ include Parslet
+ let(:visitor) { flexmock(:visitor) }
+
+ describe Parslet::Atoms::Str do
+ let(:parslet) { str('foo') }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_str).with('foo').once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe Parslet::Atoms::Re do
+ let(:parslet) { match['abc'] }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_re).with('[abc]').once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe Parslet::Atoms::Sequence do
+ let(:parslet) { str('a') >> str('b') }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_sequence).with(Array).once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe Parslet::Atoms::Repetition do
+ let(:parslet) { str('a').repeat(1,2) }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_repetition).with(:repetition, 1, 2, Parslet::Atoms::Base).once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe Parslet::Atoms::Alternative do
+ let(:parslet) { str('a') | str('b') }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_alternative).with(Array).once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe Parslet::Atoms::Named do
+ let(:parslet) { str('a').as(:a) }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_named).with(:a, Parslet::Atoms::Base).once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe Parslet::Atoms::Entity do
+ let(:parslet) { Parslet::Atoms::Entity.new('foo', &lambda {}) }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_entity).with('foo', Proc).once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe Parslet::Atoms::Lookahead do
+ let(:parslet) { str('a').absent? }
+ it "should call back visitor" do
+ visitor.should_receive(:visit_lookahead).with(false, Parslet::Atoms::Base).once
+
+ parslet.accept(visitor)
+ end
+ end
+ describe "< Parslet::Parser" do
+ let(:parslet) { Parslet::Parser.new }
+ it "calls back to visitor" do
+ visitor.should_receive(:visit_parser).with(:root).once
+
+ flexmock(parslet, :root => :root)
+ parslet.accept(visitor)
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/atoms_spec.rb b/spec/parslet/atoms_spec.rb
new file mode 100644
index 0000000..5810280
--- /dev/null
+++ b/spec/parslet/atoms_spec.rb
@@ -0,0 +1,429 @@
+require 'spec_helper'
+
+require 'timeout'
+require 'parslet'
+
+describe Parslet do
+ def not_parse
+ raise_error(Parslet::ParseFailed)
+ end
+
+ include Parslet
+ extend Parslet
+
+ def src(str); Parslet::Source.new str; end
+ let(:context) { Parslet::Atoms::Context.new }
+
+ describe "match('[abc]')" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = match('[abc]')
+ end
+
+ it "should parse {a,b,c}" do
+ parslet.parse('a')
+ parslet.parse('b')
+ parslet.parse('c')
+ end
+ it "should not parse d" do
+ cause = catch_failed_parse {
+ parslet.parse('d')
+ }
+ cause.to_s.should == "Failed to match [abc] at line 1 char 1."
+ end
+ it "should print as [abc]" do
+ parslet.inspect.should == "[abc]"
+ end
+ end
+ describe "match(['[a]').repeat(3)" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = match('[a]').repeat(3)
+ end
+
+ context "when failing on input 'aa'" do
+ let!(:cause) {
+ catch_failed_parse { parslet.parse('aa') }
+ }
+ it "should have a relevant cause" do
+ cause.to_s.should == "Expected at least 3 of [a] at line 1 char 1."
+ end
+ it "should have a tree with 2 nodes" do
+ cause.children.size.should == 1
+ end
+ end
+ it "should succeed on 'aaa'" do
+ parslet.parse('aaa')
+ end
+ it "should succeed on many 'a'" do
+ parslet.parse('a'*100)
+ end
+ it "should inspect as [a]{3, }" do
+ parslet.inspect.should == "[a]{3, }"
+ end
+ end
+ describe "str('foo')" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = str('foo')
+ end
+
+ it "should parse 'foo'" do
+ parslet.parse('foo')
+ end
+ it "should not parse 'bar'" do
+ cause = catch_failed_parse { parslet.parse('bar') }
+ cause.to_s.should ==
+ "Expected \"foo\", but got \"bar\" at line 1 char 1."
+ end
+ it "should inspect as 'foo'" do
+ parslet.inspect.should == "'foo'"
+ end
+ end
+ describe "str('foo').maybe" do
+ let(:parslet) { str('foo').maybe }
+
+ it "should parse a foo" do
+ parslet.parse('foo')
+ end
+ it "should leave pos untouched if there is no foo" do
+ source = src('bar')
+ parslet.apply(source, context)
+ source.pos.charpos.should == 0
+ end
+ it "should inspect as 'foo'?" do
+ parslet.inspect.should == "'foo'?"
+ end
+ context "when parsing 'foo'" do
+ subject { parslet.parse('foo') }
+
+ it { should == 'foo' }
+ end
+ context "when parsing ''" do
+ subject { parslet.parse('') }
+
+ it { should == '' }
+ end
+ end
+ describe "str('foo') >> str('bar')" do
+ let(:parslet) { str('foo') >> str('bar') }
+
+ context "when it fails on input 'foobaz'" do
+ let!(:cause) {
+ catch_failed_parse { parslet.parse('foobaz') }
+ }
+
+ it "should not parse 'foobaz'" do
+ cause.to_s.should == "Failed to match sequence ('foo' 'bar') at line 1 char 4."
+ end
+ it "should have 2 nodes in error tree" do
+ cause.children.size.should == 1
+ end
+ end
+ it "should parse 'foobar'" do
+ parslet.parse('foobar')
+ end
+ it "should inspect as ('foo' 'bar')" do
+ parslet.inspect.should == "'foo' 'bar'"
+ end
+ end
+ describe "str('foo') | str('bar')" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = str('foo') | str('bar')
+ end
+
+ context "when failing on input 'baz'" do
+ let!(:cause) {
+ catch_failed_parse { parslet.parse('baz') }
+ }
+
+ it "should have a sensible cause" do
+ cause.to_s.should == "Expected one of ['foo', 'bar'] at line 1 char 1."
+ end
+ it "should have an error tree with 3 nodes" do
+ cause.children.size.should == 2
+ end
+ end
+
+ it "should accept 'foo'" do
+ parslet.parse('foo')
+ end
+ it "should accept 'bar'" do
+ parslet.parse('bar')
+ end
+ it "should inspect as ('foo' / 'bar')" do
+ parslet.inspect.should == "'foo' / 'bar'"
+ end
+ end
+ describe "str('foo').present? (positive lookahead)" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = str('foo').present?
+ end
+
+ it "should inspect as &'foo'" do
+ parslet.inspect.should == "&'foo'"
+ end
+ context "when fed 'foo'" do
+ it "should parse" do
+ success, _ = parslet.apply(src('foo'), context)
+ success.should == true
+ end
+ it "should not change input position" do
+ source = src('foo')
+ parslet.apply(source, context)
+ source.pos.charpos.should == 0
+ end
+ end
+ context "when fed 'bar'" do
+ it "should not parse" do
+ lambda { parslet.parse('bar') }.should not_parse
+ end
+ end
+ describe "<- #parse" do
+ it "should return nil" do
+ parslet.apply(src('foo'), context).should == [true, nil]
+ end
+ end
+ end
+ describe "str('foo').absent? (negative lookahead)" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = str('foo').absent?
+ end
+
+ it "should inspect as !'foo'" do
+ parslet.inspect.should == "!'foo'"
+ end
+ context "when fed 'bar'" do
+ it "should parse" do
+ parslet.apply(src('bar'), context).should == [true, nil]
+ end
+ it "should not change input position" do
+ source = src('bar')
+ parslet.apply(source, context)
+ source.pos.charpos.should == 0
+ end
+ end
+ context "when fed 'foo'" do
+ it "should not parse" do
+ lambda { parslet.parse('foo') }.should not_parse
+ end
+ end
+ end
+ describe "non greedy matcher combined with greedy matcher (possible loop)" do
+ attr_reader :parslet
+ before(:each) do
+ # repeat will always succeed, since it has a minimum of 0. It will not
+ # modify input position in that case. absent? will, depending on
+ # implementation, match as much as possible and call its inner element
+ # again. This leads to an infinite loop. This example tests for the
+ # absence of that loop.
+ @parslet = str('foo').repeat.maybe
+ end
+
+ it "should not loop infinitely" do
+ lambda {
+ timeout(1) { parslet.parse('bar') }
+ }.should raise_error(Parslet::ParseFailed)
+ end
+ end
+ describe "any" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = any
+ end
+
+ it "should match" do
+ parslet.parse('.')
+ end
+ it "should consume one char" do
+ source = src('foo')
+ parslet.apply(source, context)
+ source.pos.charpos.should == 1
+ end
+ end
+ describe "eof behaviour" do
+ context "when the pattern just doesn't consume the input" do
+ let (:parslet) { any }
+
+ it "should fail the parse" do
+ cause = catch_failed_parse { parslet.parse('..') }
+ cause.to_s.should == "Don't know what to do with \".\" at line 1 char 2."
+ end
+ end
+ context "when the pattern doesn't match the input" do
+ let (:parslet) { (str('a')).repeat(1) }
+ attr_reader :exception
+ before(:each) do
+ begin
+ parslet.parse('a.')
+ rescue => @exception
+ end
+ end
+
+ it "raises Parslet::ParseFailed" do
+ # ParseFailed here, because the input doesn't match the parser grammar.
+ exception.should be_kind_of(Parslet::ParseFailed)
+ end
+ it "has the correct error message" do
+ exception.message.should == \
+ "Extra input after last repetition at line 1 char 2."
+ end
+ end
+ end
+
+ describe "<- #as(name)" do
+ context "str('foo').as(:bar)" do
+ it "should return :bar => 'foo'" do
+ str('foo').as(:bar).parse('foo').should == { :bar => 'foo' }
+ end
+ end
+ context "match('[abc]').as(:name)" do
+ it "should return :name => 'b'" do
+ match('[abc]').as(:name).parse('b').should == { :name => 'b' }
+ end
+ end
+ context "match('[abc]').repeat.as(:name)" do
+ it "should return collated result ('abc')" do
+ match('[abc]').repeat.as(:name).
+ parse('abc').should == { :name => 'abc' }
+ end
+ end
+ context "(str('a').as(:a) >> str('b').as(:b)).as(:c)" do
+ it "should return a hash of hashes" do
+ (str('a').as(:a) >> str('b').as(:b)).as(:c).
+ parse('ab').should == {
+ :c => {
+ :a => 'a',
+ :b => 'b'
+ }
+ }
+ end
+ end
+ context "(str('a').as(:a) >> str('ignore') >> str('b').as(:b))" do
+ it "should correctly flatten (leaving out 'ignore')" do
+ (str('a').as(:a) >> str('ignore') >> str('b').as(:b)).
+ parse('aignoreb').should ==
+ {
+ :a => 'a',
+ :b => 'b'
+ }
+ end
+ end
+
+ context "(str('a') >> str('ignore') >> str('b')) (no .as(...))" do
+ it "should return simply the original string" do
+ (str('a') >> str('ignore') >> str('b')).
+ parse('aignoreb').should == 'aignoreb'
+ end
+ end
+ context "str('a').as(:a) >> str('b').as(:a)" do
+ attr_reader :parslet
+ before(:each) do
+ @parslet = str('a').as(:a) >> str('b').as(:a)
+ end
+
+ it "should issue a warning that a key is being overwritten in merge" do
+ flexmock(parslet).
+ should_receive(:warn).once
+ parslet.parse('ab').should == { :a => 'b' }
+ end
+ it "should return :a => 'b'" do
+ flexmock(parslet).
+ should_receive(:warn)
+
+ parslet.parse('ab').should == { :a => 'b' }
+ end
+ end
+ context "str('a').absent?" do
+ it "should return something in merge, even though it is nil" do
+ (str('a').absent? >> str('b').as(:b)).
+ parse('b').should == {:b => 'b'}
+ end
+ end
+ context "str('a').as(:a).repeat" do
+ it "should return an array of subtrees" do
+ str('a').as(:a).repeat.
+ parse('aa').should == [{:a=>'a'}, {:a=>'a'}]
+ end
+ end
+ end
+ describe "<- #flatten(val)" do
+ def call(val)
+ dummy = str('a')
+ flexmock(dummy, :warn => nil)
+ dummy.flatten(val)
+ end
+
+ [
+ # In absence of named subtrees: ----------------------------------------
+ # Sequence or Repetition
+ [ [:sequence, 'a', 'b'], 'ab' ],
+ [ [:repetition, 'a', 'a'], 'aa' ],
+
+ # Nested inside another node
+ [ [:sequence, [:sequence, 'a', 'b']], 'ab' ],
+ # Combined with lookahead (nil)
+ [ [:sequence, nil, 'a'], 'a' ],
+
+ # Including named subtrees ---------------------------------------------
+ # Atom: A named subtree
+ [ {:a=>'a'}, {:a=>'a'} ],
+ # Composition of subtrees
+ [ [:sequence, {:a=>'a'},{:b=>'b'}], {:a=>'a',:b=>'b'} ],
+ # Mixed subtrees :sequence of :repetition yields []
+ [ [:sequence, [:repetition, {:a => 'a'}], {:a => 'a'} ], [{:a=>'a'}, {:a=>'a'}]],
+ [ [:sequence, {:a => 'a'},[:repetition, {:a => 'a'}] ], [{:a=>'a'}, {:a=>'a'}]],
+ [ [:sequence, [:repetition, {:a => 'a'}],[:repetition, {:a => 'a'}] ], [{:a=>'a'}, {:a=>'a'}]],
+ # Repetition
+ [ [:repetition, [:repetition, {:a=>'a'}], [:repetition, {:a=>'a'}]],
+ [{:a => 'a'}, {:a => 'a'}]],
+ [ [:repetition, {:a=>'a'}, 'a', {:a=>'a'}], [{:a=>'a'}, {:a=>'a'}]],
+ [ [:repetition, {:a=>'a'}, [:repetition, {:b=>'b'}]], [{:a=>'a'}] ],
+
+ # Some random samples --------------------------------------------------
+ [ [:sequence, {:a => :b, :b => :c}], {:a=>:b, :b=>:c} ],
+ [ [:sequence, {:a => :b}, 'a', {:c=>:d}], {:a => :b, :c=>:d} ],
+ [ [:repetition, {:a => :b}, 'a', {:c=>:d}], [{:a => :b}, {:c=>:d}] ],
+ [ [:sequence, {:a => :b}, {:a=>:d}], {:a => :d} ],
+ [ [:sequence, {:a=>:b}, [:sequence, [:sequence, "\n", nil]]], {:a=>:b} ],
+ [ [:sequence, nil, " "], ' ' ],
+ ].each do |input, output|
+ it "should transform #{input.inspect} to #{output.inspect}" do
+ call(input).should == output
+ end
+ end
+ end
+
+ describe "combinations thereof (regression)" do
+ success=[
+ [(str('a').repeat >> str('b').repeat), 'aaabbb']
+ ].each do |(parslet, input)|
+ describe "#{parslet.inspect} applied to #{input.inspect}" do
+ it "should parse successfully" do
+ parslet.parse(input)
+ end
+ end
+ end
+
+ inspection=[
+ [str('a'), "'a'" ],
+ [(str('a') | str('b')).maybe, "('a' / 'b')?" ],
+ [(str('a') >> str('b')).maybe, "('a' 'b')?" ],
+ [str('a').maybe.maybe, "'a'??" ],
+ [(str('a')>>str('b')).maybe.maybe, "('a' 'b')??" ],
+ [(str('a') >> (str('b') | str('c'))), "'a' ('b' / 'c')"],
+
+ [str('a') >> str('b').repeat, "'a' 'b'{0, }" ],
+ [(str('a')>>str('b')).repeat, "('a' 'b'){0, }" ]
+ ].each do |(parslet, inspect_output)|
+ context "regression for #{parslet.inspect}" do
+ it "should inspect correctly as #{inspect_output}" do
+ parslet.inspect.should == inspect_output
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/convenience_spec.rb b/spec/parslet/convenience_spec.rb
new file mode 100644
index 0000000..8ba95ec
--- /dev/null
+++ b/spec/parslet/convenience_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe 'parslet/convenience' do
+ require 'parslet/convenience'
+ include Parslet
+
+ class FooParser < Parslet::Parser
+ rule(:foo) { str('foo') }
+ root(:foo)
+ end
+
+ describe 'parse_with_debug' do
+ let(:parser) { flexmock FooParser.new }
+ context 'internal' do
+ before(:each) do
+ # Suppress output.
+ #
+ parser.should_receive(:puts)
+ end
+ it 'should exist' do
+ lambda { parser.parse_with_debug('anything') }.should_not raise_error
+ end
+ it 'should catch ParseFailed exceptions' do
+ lambda { parser.parse_with_debug('bar') }.should_not raise_error
+ end
+ it 'should parse correct input like #parse' do
+ lambda { parser.parse_with_debug('foo') }.should_not raise_error
+ end
+ end
+ context 'output' do
+ it 'should puts once for tree output' do
+ parser.should_receive(:puts).once
+
+ parser.parse_with_debug('incorrect')
+ end
+ it "should puts once for the error on unconsumed input" do
+ parser.should_receive(:puts).once
+
+ parser.parse_with_debug('foobar')
+ end
+ end
+
+ it "should work for all parslets" do
+ str('foo').parse_with_debug('foo')
+ match['bar'].parse_with_debug('a')
+ end
+ end
+end
diff --git a/spec/parslet/error_reporter/contextual_spec.rb b/spec/parslet/error_reporter/contextual_spec.rb
new file mode 100644
index 0000000..c7320d5
--- /dev/null
+++ b/spec/parslet/error_reporter/contextual_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Parslet::ErrorReporter::Contextual do
+
+ let(:reporter) { described_class.new }
+ let(:fake_source) { flexmock('source') }
+ let(:fake_atom) { flexmock('atom') }
+ let(:fake_cause) { flexmock('cause') }
+
+ describe '#err' do
+ before(:each) { fake_source.should_receive(
+ :pos => 13,
+ :line_and_column => [1,1]) }
+
+ it "returns the deepest cause" do
+ flexmock(reporter).
+ should_receive(:deepest).and_return(:deepest)
+ reporter.err('parslet', fake_source, 'message').
+ should == :deepest
+ end
+ end
+ describe '#err_at' do
+ before(:each) { fake_source.should_receive(
+ :pos => 13,
+ :line_and_column => [1,1]) }
+
+ it "returns the deepest cause" do
+ flexmock(reporter).
+ should_receive(:deepest).and_return(:deepest)
+ reporter.err('parslet', fake_source, 'message', 13).
+ should == :deepest
+ end
+ end
+ describe '#deepest(cause)' do
+ def fake_cause(pos=13, children=nil)
+ flexmock('cause' + pos.to_s, :pos => pos, :children => children)
+ end
+
+ context "when there is no deepest cause yet" do
+ let(:cause) { fake_cause }
+ it "returns the given cause" do
+ reporter.deepest(cause).should == cause
+ end
+ end
+ context "when the previous cause is deeper (no relationship)" do
+ let(:previous) { fake_cause }
+ before(:each) {
+ reporter.deepest(previous) }
+
+ it "returns the previous cause" do
+ reporter.deepest(fake_cause(12)).
+ should == previous
+ end
+ end
+ context "when the previous cause is deeper (child)" do
+ let(:previous) { fake_cause }
+ before(:each) {
+ reporter.deepest(previous) }
+
+ it "returns the given cause" do
+ given = fake_cause(12, [previous])
+ reporter.deepest(given).should == given
+ end
+ end
+ context "when the previous cause is shallower" do
+ before(:each) {
+ reporter.deepest(fake_cause) }
+
+ it "stores the cause as deepest" do
+ deeper = fake_cause(14)
+ reporter.deepest(deeper)
+ reporter.deepest_cause.should == deeper
+ end
+ end
+ end
+ describe '#reset' do
+ before(:each) { fake_source.should_receive(
+ :pos => Parslet::Position.new('source', 13),
+ :line_and_column => [1,1]) }
+
+ it "resets deepest cause on success of sibling expression" do
+ flexmock(reporter).
+ should_receive(:deepest).and_return(:deepest)
+ reporter.err('parslet', fake_source, 'message').
+ should == :deepest
+ flexmock(reporter).
+ should_receive(:reset).once
+ reporter.succ(fake_source)
+ end
+ end
+
+ describe 'label' do
+ before(:each) { fake_source.should_receive(
+ :pos => Parslet::Position.new('source', 13),
+ :line_and_column => [1,1]) }
+
+ it "sets label if atom has one" do
+ fake_atom.should_receive(:label).once.and_return('label')
+ fake_cause.should_receive(:set_label).once
+ flexmock(reporter).
+ should_receive(:deepest).and_return(fake_cause)
+ reporter.err(fake_atom, fake_source, 'message').
+ should == fake_cause
+ end
+
+ it 'does not set label if atom does not have one' do
+ flexmock(reporter).
+ should_receive(:deepest).and_return(:deepest)
+ fake_atom.should_receive(:update_label).never
+ reporter.err(fake_atom, fake_source, 'message').
+ should == :deepest
+ end
+ end
+
+end
diff --git a/spec/parslet/error_reporter/deepest_spec.rb b/spec/parslet/error_reporter/deepest_spec.rb
new file mode 100644
index 0000000..fbf957e
--- /dev/null
+++ b/spec/parslet/error_reporter/deepest_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Parslet::ErrorReporter::Deepest do
+ let(:reporter) { described_class.new }
+ let(:fake_source) { flexmock('source') }
+
+ describe '#err' do
+ before(:each) { fake_source.should_receive(
+ :pos => 13,
+ :line_and_column => [1,1]) }
+
+ it "returns the deepest cause" do
+ flexmock(reporter).
+ should_receive(:deepest).and_return(:deepest)
+ reporter.err('parslet', fake_source, 'message').
+ should == :deepest
+ end
+ end
+ describe '#err_at' do
+ before(:each) { fake_source.should_receive(
+ :pos => 13,
+ :line_and_column => [1,1]) }
+
+ it "returns the deepest cause" do
+ flexmock(reporter).
+ should_receive(:deepest).and_return(:deepest)
+ reporter.err('parslet', fake_source, 'message', 13).
+ should == :deepest
+ end
+ end
+ describe '#deepest(cause)' do
+ def fake_cause(pos=13, children=nil)
+ flexmock('cause' + pos.to_s, :pos => pos, :children => children)
+ end
+
+ context "when there is no deepest cause yet" do
+ let(:cause) { fake_cause }
+ it "returns the given cause" do
+ reporter.deepest(cause).should == cause
+ end
+ end
+ context "when the previous cause is deeper (no relationship)" do
+ let(:previous) { fake_cause }
+ before(:each) {
+ reporter.deepest(previous) }
+
+ it "returns the previous cause" do
+ reporter.deepest(fake_cause(12)).
+ should == previous
+ end
+ end
+ context "when the previous cause is deeper (child)" do
+ let(:previous) { fake_cause }
+ before(:each) {
+ reporter.deepest(previous) }
+
+ it "returns the given cause" do
+ given = fake_cause(12, [previous])
+ reporter.deepest(given).should == given
+ end
+ end
+ context "when the previous cause is shallower" do
+ before(:each) {
+ reporter.deepest(fake_cause) }
+
+ it "stores the cause as deepest" do
+ deeper = fake_cause(14)
+ reporter.deepest(deeper)
+ reporter.deepest_cause.should == deeper
+ end
+ end
+ end
+end
diff --git a/spec/parslet/error_reporter/tree_spec.rb b/spec/parslet/error_reporter/tree_spec.rb
new file mode 100644
index 0000000..a8b08ab
--- /dev/null
+++ b/spec/parslet/error_reporter/tree_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+require 'parslet/error_reporter'
+
+describe Parslet::ErrorReporter::Tree do
+
+end
\ No newline at end of file
diff --git a/spec/parslet/export_spec.rb b/spec/parslet/export_spec.rb
new file mode 100644
index 0000000..8f12152
--- /dev/null
+++ b/spec/parslet/export_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Parslet::Parser, "exporting to other lingos" do
+ class MiniLisp < Parslet::Parser
+ root :expression
+ rule(:expression) {
+ space? >> str('(') >> space? >> body >> str(')')
+ }
+
+ rule(:body) {
+ (expression | identifier | float | integer | string).repeat.as(:exp)
+ }
+
+ rule(:space) {
+ match('\s').repeat(1)
+ }
+ rule(:space?) {
+ space.maybe
+ }
+
+ rule(:identifier) {
+ (match('[a-zA-Z=*]') >> match('[a-zA-Z=*_]').repeat).as(:identifier) >> space?
+ }
+
+ rule(:float) {
+ (
+ integer >> (
+ str('.') >> match('[0-9]').repeat(1) |
+ str('e') >> match('[0-9]').repeat(1)
+ ).as(:e)
+ ).as(:float) >> space?
+ }
+
+ rule(:integer) {
+ ((str('+') | str('-')).maybe >> match("[0-9]").repeat(1)).as(:integer) >> space?
+ }
+
+ rule(:string) {
+ str('"') >> (
+ str('\\') >> any |
+ str('"').absent? >> any
+ ).repeat.as(:string) >> str('"') >> space?
+ }
+ end
+
+ # I only update the files once I've verified the new syntax to work with
+ # the respective tools. This is more an acceptance test than a real spec.
+
+ describe "<- #to_citrus" do
+ let(:citrus) { File.read(
+ File.join(File.dirname(__FILE__), 'minilisp.citrus'))
+ }
+ it "should be valid citrus syntax" do
+ # puts MiniLisp.new.to_citrus
+ MiniLisp.new.to_citrus.should == citrus
+ end
+ end
+ describe "<- #to_treetop" do
+ let(:treetop) { File.read(
+ File.join(File.dirname(__FILE__), 'minilisp.tt'))
+ }
+ it "should be valid treetop syntax" do
+ # puts MiniLisp.new.to_treetop
+ MiniLisp.new.to_treetop.should == treetop
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/expression/treetop_spec.rb b/spec/parslet/expression/treetop_spec.rb
new file mode 100644
index 0000000..0c0340f
--- /dev/null
+++ b/spec/parslet/expression/treetop_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+require 'parslet'
+
+describe Parslet::Expression::Treetop do
+ include Parslet
+
+ describe "positive samples" do
+ [ # pattern # input
+ "'abc'", 'abc',
+ "...", 'abc',
+ "[1-4]", '3',
+
+ "'abc'?", 'abc',
+ "'abc'?", '',
+
+ "('abc')", 'abc',
+
+ "'a' 'b'", 'ab',
+ "'a' ('b')", 'ab',
+
+ "'a' / 'b'", 'a',
+ "'a' / 'b'", 'b',
+
+ "'a'*", 'aaa',
+ "'a'*", '',
+
+ "'a'+", 'aa',
+ "'a'+", 'a',
+
+ "'a'{1,2}", 'a',
+ "'a'{1,2}", 'aa',
+
+ "'a'{1,}", 'a',
+ "'a'{1,}", 'aa',
+
+ "'a'{,2}", '',
+ "'a'{,2}", 'a',
+ "'a'{,2}", 'aa',
+ ].each_slice(2) do |pattern, input|
+ context "exp(#{pattern.inspect})" do
+ let(:parslet) { exp(pattern) }
+ subject { parslet }
+ it { should parse(input) }
+ context "string representation" do
+ subject { exp(parslet.to_s) }
+ it { should parse(input, :trace => true) }
+ end
+ end
+ end
+ end
+ describe "negative samples" do
+ [ # pattern # input
+ "'abc'", 'cba',
+ "[1-4]", '5',
+
+ "'a' / 'b'", 'c',
+
+ "'a'+", '',
+
+ "'a'{1,2}", '',
+ "'a'{1,2}", 'aaa',
+
+ "'a'{1,}", '',
+
+ "'a'{,2}", 'aaa',
+ ].each_slice(2) do |pattern, input|
+ context "exp(#{pattern.inspect})" do
+ subject { exp(pattern) }
+ it { should_not parse(input) }
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/minilisp.citrus b/spec/parslet/minilisp.citrus
new file mode 100644
index 0000000..fea3d2e
--- /dev/null
+++ b/spec/parslet/minilisp.citrus
@@ -0,0 +1,29 @@
+grammar MiniLisp
+ rule root
+ (expression)
+ end
+ rule expression
+ ((space_p) "(" (space_p) (body) ")")
+ end
+ rule space_p
+ (space)0*1
+ end
+ rule body
+ ((expression) | (identifier) | (float) | (integer) | (string))0*
+ end
+ rule space
+ \s1*
+ end
+ rule identifier
+ (([a-zA-Z=*] [a-zA-Z=*_]0*) (space_p))
+ end
+ rule float
+ (((integer) (("." [0-9]1*) | ("e" [0-9]1*))) (space_p))
+ end
+ rule integer
+ ((("+" | "-")0*1 [0-9]1*) (space_p))
+ end
+ rule string
+ ("\"" (("\\" .) | (!"\"" .))0* "\"" (space_p))
+ end
+end
diff --git a/spec/parslet/minilisp.tt b/spec/parslet/minilisp.tt
new file mode 100644
index 0000000..e0ada6d
--- /dev/null
+++ b/spec/parslet/minilisp.tt
@@ -0,0 +1,29 @@
+grammar MiniLisp
+ rule root
+ (expression)
+ end
+ rule expression
+ ((space_p) "(" (space_p) (body) ")")
+ end
+ rule space_p
+ (space)0..1
+ end
+ rule body
+ ((expression) / (identifier) / (float) / (integer) / (string))0..
+ end
+ rule space
+ \s1..
+ end
+ rule identifier
+ (([a-zA-Z=*] [a-zA-Z=*_]0..) (space_p))
+ end
+ rule float
+ (((integer) (("." [0-9]1..) / ("e" [0-9]1..))) (space_p))
+ end
+ rule integer
+ ((("+" / "-")0..1 [0-9]1..) (space_p))
+ end
+ rule string
+ ("\"" (("\\" .) / (!"\"" .))0.. "\"" (space_p))
+ end
+end
diff --git a/spec/parslet/parser_spec.rb b/spec/parslet/parser_spec.rb
new file mode 100644
index 0000000..c7e67b8
--- /dev/null
+++ b/spec/parslet/parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Parslet::Parser do
+ include Parslet
+ class FooParser < Parslet::Parser
+ rule(:foo) { str('foo') }
+ root(:foo)
+ end
+
+ describe "<- .root" do
+ parser = Class.new(Parslet::Parser)
+ parser.root :root_parslet
+
+ it "should have defined a 'root' method, returning the root" do
+ parser_instance = parser.new
+ flexmock(parser_instance).should_receive(:root_parslet => :answer)
+
+ parser_instance.root.should == :answer
+ end
+ end
+ it "should parse 'foo'" do
+ FooParser.new.parse('foo').should == 'foo'
+ end
+ context "composition" do
+ let(:parser) { FooParser.new }
+ it "should allow concatenation" do
+ composite = parser >> str('bar')
+ composite.should parse('foobar')
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/parslet_spec.rb b/spec/parslet/parslet_spec.rb
new file mode 100644
index 0000000..aea9ec4
--- /dev/null
+++ b/spec/parslet/parslet_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Parslet do
+ include Parslet
+
+ describe Parslet::ParseFailed do
+ it "should be caught by an empty rescue" do
+ begin
+ raise Parslet::ParseFailed
+ rescue
+ # Success! Ignore this.
+ end
+ end
+ end
+ describe "<- .rule" do
+ # Rules define methods. This can be easily tested by defining them right
+ # here.
+ context "empty rule" do
+ rule(:empty) { }
+
+ it "should raise a NotImplementedError" do
+ lambda {
+ empty.parslet
+ }.should raise_error(NotImplementedError)
+ end
+ end
+
+ context "containing 'any'" do
+ rule(:any_rule) { any }
+ subject { any_rule }
+
+ it { should be_a Parslet::Atoms::Entity }
+ it "should memoize the returned instance" do
+ any_rule.object_id.should == any_rule.object_id
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/pattern_spec.rb b/spec/parslet/pattern_spec.rb
new file mode 100644
index 0000000..c7df7f4
--- /dev/null
+++ b/spec/parslet/pattern_spec.rb
@@ -0,0 +1,272 @@
+require 'spec_helper'
+
+require 'parslet'
+
+describe Parslet::Pattern do
+ include Parslet
+
+ # These two factory methods help make the specs more robust to interface
+ # changes. They also help to label trees (t) and patterns (p).
+ def p(pattern)
+ Parslet::Pattern.new(pattern)
+ end
+ def t(obj)
+ obj
+ end
+
+ # Tries to match pattern to the tree, and verifies the bindings hash. Don't
+ # use this for new examples.
+ #
+ RSpec::Matchers.define :match_with_bind do |pattern, exp_bindings|
+ unless respond_to?(:failure_message)
+ alias_method :failure_message_for_should, :failure_message
+ end
+
+ failure_message do |tree|
+ "expected #{pattern.inspect} to match #{tree.inspect}, but didn't. (block wasn't called or not correctly)"
+ end
+ match do |tree|
+ bindings = Parslet::Pattern.new(pattern).match(tree)
+ bindings && bindings == exp_bindings
+ end
+ end
+
+ # This is the more modern version of verifying a match: (uses 'exp'
+ # implicitly). Checks for a match of pattern in +exp+ and yields the
+ # matched variables.
+ #
+ def with_match_locals(pattern, &block)
+ bindings = p(pattern).match(exp)
+ bindings.should_not be_nil
+
+ block.call(bindings) if block
+ end
+
+ # Can't use #match here, so I went to the Thesaurus.
+ #
+ RSpec::Matchers.define :detect do |pattern|
+ match do |tree|
+ bindings = Parslet::Pattern.new(pattern).match(tree)
+
+ bindings ? true : false
+ end
+ end
+
+ describe "<- #match" do
+ context "injecting bindings" do
+ let(:pattern) { p(simple(:x)) }
+
+ it "should not modify the original bindings hash" do
+ h = {}
+ b=pattern.match('a', h)
+ h.size.should == 0
+ b.size.should == 1
+ end
+ it "should return nil when no match succeeds" do
+ pattern.match([], :foo => :bar).should be_nil
+ end
+ context "when matching simple(:x) against 'a'" do
+ let(:bindings) { pattern.match(t('a'), :foo => :bar) }
+
+ before(:each) { bindings.should_not be_nil }
+ it "should return the injected bindings" do
+ bindings[:foo].should == :bar
+ end
+ it "should return the new bindings" do
+ bindings[:x].should == 'a'
+ end
+ end
+ end
+ context "simple strings" do
+ let(:exp) { 'aaaa' }
+
+ it "should match simple strings" do
+ exp.should match_with_bind(simple(:x), :x => 'aaaa')
+ end
+ end
+ context "simple hash {:a => 'b'}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t(:a => 'b')
+ end
+
+ it "should not match {:a => simple(:x), :b => simple(:y)}" do
+ exp.should_not detect(:a => simple(:x), :b => simple(:y))
+ end
+ it "should match {:a => simple(:x)}, binding 'x' to the first argument" do
+ exp.should match_with_bind({:a => simple(:x)}, :x => 'b')
+ end
+ it "should match {:a => 'b'} with no binds" do
+ exp.should match_with_bind({:a => 'b'}, {})
+ end
+ end
+ context "a more complex hash {:a => {:b => 'c'}}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t(:a => {:b => 'c'})
+ end
+
+ it "should match wholly with {:a => {:b => simple(:x)}}" do
+ exp.should match_with_bind({:a => {:b => simple(:x)}}, :x => 'c')
+ end
+ it "should match wholly with {:a => subtree(:t)}" do
+ with_match_locals(:a => subtree(:t)) do |dict|
+ dict[:t].should == {:b => 'c'}
+ end
+ end
+ it "should not bind subtrees to variables in {:a => simple(:x)}" do
+ p(:a => simple(:x)).should_not detect(exp)
+ end
+ end
+ context "a more complex hash {:a => 'a', :b => 'b'}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t({:a => 'a', :b => 'b'})
+ end
+
+ it "should not match partially" do
+ Parslet::Pattern.new(:a => simple(:x)).match(exp).should be_nil
+ end
+ it "should match completely" do
+ exp.should match_with_bind({:a => simple(:x), :b => simple(:y)},
+ :x => 'a',
+ :y => 'b')
+ end
+ end
+ context "an array of 'a', 'b', 'c'" do
+ let(:exp) { ['a', 'b', 'c'] }
+
+ it "should match all elements at once" do
+ exp.should match_with_bind(
+ [simple(:x), simple(:y), simple(:z)],
+ :x => 'a', :y => 'b', :z => 'c')
+ end
+ end
+ context "{:a => 'a', :b => 'b'}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t(:a => 'a', :b => 'b')
+ end
+
+ it "should match both elements simple(:x), simple(:y)" do
+ exp.should match_with_bind(
+ {:a => simple(:x), :b => simple(:y)},
+ :x => 'a', :y => 'b')
+ end
+ it "should not match a constrained match (simple(:x) != simple(:y))" do
+ exp.should_not detect({:a => simple(:x), :b => simple(:x)})
+ end
+ end
+ context "{:a => 'a', :b => 'a'}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t(:a => 'a', :b => 'a')
+ end
+
+ it "should match constrained pattern" do
+ exp.should match_with_bind(
+ {:a => simple(:x), :b => simple(:x)},
+ :x => 'a')
+ end
+ end
+ context "{:sub1 => {:a => 'a'}, :sub2 => {:a => 'a'}}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t({
+ :sub1 => {:a => 'a'},
+ :sub2 => {:a => 'a'}
+ })
+ end
+
+ it "should verify constraints over several subtrees" do
+ exp.should match_with_bind({
+ :sub1 => {:a => simple(:x)},
+ :sub2 => {:a => simple(:x)}
+ }, :x => 'a')
+ end
+ it "should return both bind variables simple(:x), simple(:y)" do
+ exp.should match_with_bind({
+ :sub1 => {:a => simple(:x)},
+ :sub2 => {:a => simple(:y)}
+ }, :x => 'a', :y => 'a')
+ end
+ end
+ context "{:sub1 => {:a => 'a'}, :sub2 => {:a => 'b'}}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t({
+ :sub1 => {:a => 'a'},
+ :sub2 => {:a => 'b'}
+ })
+ end
+
+ it "should verify constraints over several subtrees" do
+ exp.should_not match_with_bind({
+ :sub1 => {:a => simple(:x)},
+ :sub2 => {:a => simple(:x)}
+ }, :x => 'a')
+ end
+ it "should return both bind variables simple(:x), simple(:y)" do
+ exp.should match_with_bind({
+ :sub1 => {:a => simple(:x)},
+ :sub2 => {:a => simple(:y)}
+ }, :x => 'a', :y => 'b')
+ end
+ end
+ context "[{:a => 'x'}, {:a => 'y'}]" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t([{:a => 'x'}, {:a => 'y'}])
+ end
+
+ it "should not match sequence(:x) (as a whole)" do
+ exp.should_not detect(sequence(:x))
+ end
+ end
+ context "['x', 'y', 'z']" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t(['x', 'y', 'z'])
+ end
+
+ it "should match [simple(:x), simple(:y), simple(:z)]" do
+ with_match_locals([simple(:x), simple(:y), simple(:z)]) do |dict|
+ dict[:x].should == 'x'
+ dict[:y].should == 'y'
+ dict[:z].should == 'z'
+ end
+ end
+ it "should match %w(x y z)" do
+ exp.should match_with_bind(%w(x y z), { })
+ end
+ it "should not match [simple(:x), simple(:y), simple(:x)]" do
+ exp.should_not detect([simple(:x), simple(:y), simple(:x)])
+ end
+ it "should not match [simple(:x), simple(:y)]" do
+ exp.should_not detect([simple(:x), simple(:y), simple(:x)])
+ end
+ it "should match sequence(:x) (as array)" do
+ exp.should match_with_bind(sequence(:x), :x => ['x', 'y', 'z'])
+ end
+ end
+ context "{:a => [1,2,3]}" do
+ attr_reader :exp
+ before(:each) do
+ @exp = t(:a => [1,2,3])
+ end
+
+ it "should match :a => sequence(:x) (binding x to the whole array)" do
+ exp.should match_with_bind({:a => sequence(:x)}, {:x => [1,2,3]})
+ end
+ end
+ context "with differently ordered hashes" do
+ it "should still match" do
+ t(:a => 'a', :b => 'b').should detect(:a => 'a', :b => 'b')
+ t(:a => 'a', :b => 'b').should detect(:b => 'b', :a => 'a')
+
+ t(:b => 'b', :a => 'a').should detect(:b => 'b', :a => 'a')
+ t(:b => 'b', :a => 'a').should detect(:a => 'a', :b => 'b')
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/position_spec.rb b/spec/parslet/position_spec.rb
new file mode 100644
index 0000000..99b7a4b
--- /dev/null
+++ b/spec/parslet/position_spec.rb
@@ -0,0 +1,14 @@
+# Encoding: UTF-8
+
+require 'spec_helper'
+
+describe Parslet::Position do
+ slet(:position) { described_class.new('öäüö', 4) }
+
+ it 'should have a charpos of 2' do
+ position.charpos.should == 2
+ end
+ it 'should have a bytepos of 4' do
+ position.bytepos.should == 4
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/rig/rspec_spec.rb b/spec/parslet/rig/rspec_spec.rb
new file mode 100644
index 0000000..4174502
--- /dev/null
+++ b/spec/parslet/rig/rspec_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+require 'parslet/rig/rspec'
+
+describe 'rspec integration' do
+ include Parslet
+ subject { str('example') }
+
+ it { should parse('example') }
+ it { should_not parse('foo') }
+ it { should parse('example').as('example') }
+ it { should_not parse('foo').as('example') }
+ it { should_not parse('example').as('foo') }
+
+ it { str('foo').as(:bar).should parse('foo').as({:bar => 'foo'}) }
+ it { str('foo').as(:bar).should_not parse('foo').as({:b => 'f'}) }
+
+ it 'accepts a block to assert more specific details about the parsing output' do
+ str('foo').as(:bar).should(parse('foo').as { |output|
+ output.should have_key(:bar)
+ output.values.first.should == 'foo'
+ })
+ end
+
+ # Uncomment to test error messages manually:
+ # it { str('foo').should parse('foo', :trace => true).as('bar') }
+ # it { str('foo').should parse('food', :trace => true) }
+ # it { str('foo').should_not parse('foo', :trace => true).as('foo') }
+ # it { str('foo').should_not parse('foo', :trace => true) }
+ # it 'accepts a block to assert more specific details about the parsing output' do
+ # str('foo').as(:bar).should(parse('foo', :trace => true).as { |output|
+ # output.should_not have_key(:bar)
+ # })
+ # end
+
+end
+
+describe 'rspec3 syntax' do
+ include Parslet
+
+ let(:s) { str('example') }
+
+ it { expect(s).to parse('example') }
+ it { expect(s).not_to parse('foo') }
+ it { expect(s).to parse('example').as('example') }
+ it { expect(s).not_to parse('foo').as('example') }
+
+ it { expect(s).not_to parse('example').as('foo') }
+
+ # Uncomment to test error messages manually:
+ # it { expect(str('foo')).to parse('foo', :trace => true).as('bar') }
+ # it { expect(str('foo')).to parse('food', :trace => true) }
+ # it { expect(str('foo')).not_to parse('foo', :trace => true).as('foo') }
+ # it { expect(str('foo')).not_to parse('foo', :trace => true) }
+end
diff --git a/spec/parslet/scope_spec.rb b/spec/parslet/scope_spec.rb
new file mode 100644
index 0000000..398c3be
--- /dev/null
+++ b/spec/parslet/scope_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Parslet::Scope do
+ let(:scope) { described_class.new }
+
+ describe 'simple store/retrieve' do
+ before(:each) { scope[:foo] = :bar }
+ it "allows storing objects" do
+ scope[:obj] = 42
+ end
+ it "raises on access of empty slots" do
+ expect {
+ scope[:empty]
+ }.to raise_error(Parslet::Scope::NotFound)
+ end
+ it "allows retrieval of stored values" do
+ scope[:foo].should == :bar
+ end
+ end
+
+ describe 'scoping' do
+ before(:each) { scope[:depth] = 1 }
+ before(:each) { scope.push }
+
+ let(:depth) { scope[:depth] }
+ subject { depth }
+
+ it { should == 1 }
+ describe 'after a push' do
+ before(:each) { scope.push }
+ it { should == 1 }
+
+ describe 'and reassign' do
+ before(:each) { scope[:depth] = 2 }
+
+ it { should == 2 }
+
+ describe 'and a pop' do
+ before(:each) { scope.pop }
+ it { should == 1 }
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/slice_spec.rb b/spec/parslet/slice_spec.rb
new file mode 100644
index 0000000..f6f8bd2
--- /dev/null
+++ b/spec/parslet/slice_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+describe Parslet::Slice do
+ def cslice string, offset, cache=nil
+ described_class.new(
+ Parslet::Position.new(string, offset),
+ string, cache)
+ end
+
+ describe "construction" do
+ it "should construct from an offset and a string" do
+ cslice('foobar', 40)
+ end
+ end
+ context "('foobar', 40, 'foobar')" do
+ let(:slice) { cslice('foobar', 40) }
+ describe "comparison" do
+ it "should be equal to other slices with the same attributes" do
+ other = cslice('foobar', 40)
+ slice.should == other
+ other.should == slice
+ end
+ it "should be equal to other slices (offset is irrelevant for comparison)" do
+ other = cslice('foobar', 41)
+ slice.should == other
+ other.should == slice
+ end
+ it "should be equal to a string with the same content" do
+ slice.should == 'foobar'
+ end
+ it "should be equal to a string (inversed operands)" do
+ 'foobar'.should == slice
+ end
+ it "should not be equal to a string" do
+ slice.should_not equal('foobar')
+ end
+ it "should not be eql to a string" do
+ slice.should_not eql('foobar')
+ end
+ it "should not hash to the same number" do
+ slice.hash.should_not == 'foobar'.hash
+ end
+ end
+ describe "offset" do
+ it "should return the associated offset" do
+ slice.offset.should == 6
+ end
+ it "should fail to return a line and column" do
+ lambda {
+ slice.line_and_column
+ }.should raise_error(ArgumentError)
+ end
+
+ context "when constructed with a source" do
+ let(:slice) { cslice(
+ 'foobar', 40,
+ flexmock(:cache, :line_and_column => [13, 14])) }
+ it "should return proper line and column" do
+ slice.line_and_column.should == [13, 14]
+ end
+ end
+ end
+ describe "string methods" do
+ describe "matching" do
+ it "should match as a string would" do
+ slice.should match(/bar/)
+ slice.should match(/foo/)
+
+ md = slice.match(/f(o)o/)
+ md.captures.first.should == 'o'
+ end
+ end
+ describe "<- #size" do
+ subject { slice.size }
+ it { should == 6 }
+ end
+ describe "<- #length" do
+ subject { slice.length }
+ it { should == 6 }
+ end
+ describe "<- #+" do
+ let(:other) { cslice('baz', 10) }
+ subject { slice + other }
+
+ it "should concat like string does" do
+ subject.size.should == 9
+ subject.should == 'foobarbaz'
+ subject.offset.should == 6
+ end
+ end
+ end
+ describe "conversion" do
+ describe "<- #to_slice" do
+ it "should return self" do
+ slice.to_slice.should eq(slice)
+ end
+ end
+ describe "<- #to_sym" do
+ it "should return :foobar" do
+ slice.to_sym.should == :foobar
+ end
+ end
+ describe "cast to Float" do
+ it "should return a float" do
+ Float(cslice('1.345', 11)).should == 1.345
+ end
+ end
+ describe "cast to Integer" do
+ it "should cast to integer as a string would" do
+ s = cslice('1234', 40)
+ Integer(s).should == 1234
+ s.to_i.should == 1234
+ end
+ it "should fail when Integer would fail on a string" do
+ lambda { Integer(slice) }.should raise_error
+ end
+ it "should turn into zero when a string would" do
+ slice.to_i.should == 0
+ end
+ end
+ end
+ describe "inspection and string conversion" do
+ describe "#inspect" do
+ subject { slice.inspect }
+ it { should == '"foobar"@6' }
+ end
+ describe "#to_s" do
+ subject { slice.to_s }
+ it { should == 'foobar' }
+ end
+ end
+ describe "serializability" do
+ it "should serialize" do
+ Marshal.dump(slice)
+ end
+ context "when storing a line cache" do
+ let(:slice) { cslice('foobar', 40, Parslet::Source::LineCache.new()) }
+ it "should serialize" do
+ Marshal.dump(slice)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/parslet/source/line_cache_spec.rb b/spec/parslet/source/line_cache_spec.rb
new file mode 100644
index 0000000..b1fa978
--- /dev/null
+++ b/spec/parslet/source/line_cache_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Parslet::Source::RangeSearch do
+ describe "<- #lbound" do
+ context "for a simple array" do
+ let(:ary) { [10, 20, 30, 40, 50] }
+ before(:each) { ary.extend Parslet::Source::RangeSearch }
+
+ it "should return correct answers for numbers not in the array" do
+ ary.lbound(5).should == 0
+ ary.lbound(15).should == 1
+ ary.lbound(25).should == 2
+ ary.lbound(35).should == 3
+ ary.lbound(45).should == 4
+ end
+ it "should return correct answers for numbers in the array" do
+ ary.lbound(10).should == 1
+ ary.lbound(20).should == 2
+ ary.lbound(30).should == 3
+ ary.lbound(40).should == 4
+ end
+ it "should cover right edge case" do
+ ary.lbound(50).should be_nil
+ ary.lbound(51).should be_nil
+ end
+ it "should cover left edge case" do
+ ary.lbound(0).should == 0
+ end
+ end
+ context "for an empty array" do
+ let(:ary) { [] }
+ before(:each) { ary.extend Parslet::Source::RangeSearch }
+
+ it "should return nil" do
+ ary.lbound(1).should be_nil
+ end
+ end
+ end
+end
+
+describe Parslet::Source::LineCache do
+ describe "<- scan_for_line_endings" do
+ context "calculating the line_and_columns" do
+ let(:str) { "foo\nbar\nbazd" }
+
+ it "should return the first line if we have no line ends" do
+ subject.scan_for_line_endings(0, nil)
+ subject.line_and_column(3).should == [1, 4]
+
+ subject.scan_for_line_endings(0, "")
+ subject.line_and_column(5).should == [1, 6]
+ end
+
+ it "should find the right line starting from pos 0" do
+ subject.scan_for_line_endings(0, str)
+ subject.line_and_column(5).should == [2, 2]
+ subject.line_and_column(9).should == [3, 2]
+ end
+
+ it "should find the right line starting from pos 5" do
+ subject.scan_for_line_endings(5, str)
+ subject.line_and_column(11).should == [2, 3]
+ end
+
+ it "should find the right line if scannning the string multiple times" do
+ subject.scan_for_line_endings(0, str)
+ subject.scan_for_line_endings(0, "#{str}\nthe quick\nbrown fox")
+ subject.line_and_column(10).should == [3,3]
+ subject.line_and_column(24).should == [5,2]
+ end
+ end
+ end
+end
+
diff --git a/spec/parslet/source_spec.rb b/spec/parslet/source_spec.rb
new file mode 100644
index 0000000..3480078
--- /dev/null
+++ b/spec/parslet/source_spec.rb
@@ -0,0 +1,168 @@
+# Encoding: UTF-8
+
+require 'spec_helper'
+
+describe Parslet::Source do
+ describe "using simple input" do
+ let(:str) { "a"*100 + "\n" + "a"*100 + "\n" }
+ let(:source) { described_class.new(str) }
+
+ describe "<- #read(n)" do
+ it "should not raise error when the return value is nil" do
+ described_class.new('').consume(1)
+ end
+ it "should return 100 'a's when reading 100 chars" do
+ source.consume(100).should == 'a'*100
+ end
+ end
+ describe "<- #chars_left" do
+ subject { source.chars_left }
+
+ it { should == 202 }
+ context "after depleting the source" do
+ before(:each) { source.consume(10000) }
+
+ it { should == 0 }
+ end
+ end
+ describe "<- #pos" do
+ subject { source.pos.charpos }
+
+ it { should == 0 }
+ context "after reading a few bytes" do
+ it "should still be correct" do
+ pos = 0
+ 10.times do
+ pos += (n = rand(10)+1)
+ source.consume(n)
+
+ source.pos.charpos.should == pos
+ end
+ end
+ end
+ end
+ describe "<- #pos=(n)" do
+ subject { source.pos.charpos }
+ 10.times do
+ pos = rand(200)
+ context "setting position #{pos}" do
+ before(:each) { source.bytepos = pos }
+
+ it { should == pos }
+ end
+ end
+ end
+ describe '#chars_until' do
+ it 'should return 100 chars before line end' do
+ source.chars_until("\n").should == 100
+ end
+ end
+ describe "<- #column & #line" do
+ subject { source.line_and_column }
+
+ it { should == [1,1] }
+
+ context "on the first line" do
+ it "should increase column with every read" do
+ 10.times do |i|
+ source.line_and_column.last.should == 1+i
+ source.consume(1)
+ end
+ end
+ end
+ context "on the second line" do
+ before(:each) { source.consume(101) }
+ it { should == [2, 1]}
+ end
+ context "after reading everything" do
+ before(:each) { source.consume(10000) }
+
+ context "when seeking to 9" do
+ before(:each) { source.bytepos = 9 }
+ it { should == [1, 10] }
+ end
+ context "when seeking to 100" do
+ before(:each) { source.bytepos = 100 }
+ it { should == [1, 101] }
+ end
+ context "when seeking to 101" do
+ before(:each) { source.bytepos = 101 }
+ it { should == [2, 1] }
+ end
+ context "when seeking to 102" do
+ before(:each) { source.bytepos = 102 }
+ it { should == [2, 2] }
+ end
+ context "when seeking beyond eof" do
+ it "should not throw an error" do
+ source.bytepos = 1000
+ end
+ end
+ end
+ context "reading char by char, storing the results" do
+ attr_reader :results
+ before(:each) {
+ @results = {}
+ while source.chars_left>0
+ pos = source.pos.charpos
+ @results[pos] = source.line_and_column
+ source.consume(1)
+ end
+
+ @results.entries.size.should == 202
+ @results
+ }
+
+ context "when using pos argument" do
+ it "should return the same results" do
+ results.each do |pos, result|
+ source.line_and_column(pos).should == result
+ end
+ end
+ end
+ it "should give the same results when seeking" do
+ results.each do |pos, result|
+ source.bytepos = pos
+ source.line_and_column.should == result
+ end
+ end
+ it "should give the same results when reading" do
+ cur = source.bytepos = 0
+ while source.chars_left>0
+ source.line_and_column.should == results[cur]
+ cur += 1
+ source.consume(1)
+ end
+ end
+ end
+ end
+
+ end
+
+ describe "reading encoded input" do
+ let(:source) { described_class.new("éö変わる") }
+
+ def r str
+ Regexp.new(Regexp.escape(str))
+ end
+
+ it "should read characters, not bytes" do
+ source.should match(r("é"))
+ source.consume(1)
+ source.pos.charpos.should == 1
+ source.bytepos.should == 2
+
+ source.should match(r("ö"))
+ source.consume(1)
+ source.pos.charpos.should == 2
+ source.bytepos.should == 4
+
+ source.should match(r("変"))
+ source.consume(1)
+
+ source.consume(2)
+ source.chars_left.should == 0
+ source.chars_left.should == 0
+ end
+ end
+end
diff --git a/spec/parslet/transform/context_spec.rb b/spec/parslet/transform/context_spec.rb
new file mode 100644
index 0000000..d6f6ddb
--- /dev/null
+++ b/spec/parslet/transform/context_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Parslet::Context do
+ def context(*args)
+ described_class.new(*args)
+ end
+
+ it "binds hash keys as variable like things" do
+ context(:a => 'value').instance_eval { a }.
+ should == 'value'
+ end
+ describe 'when a method in BlankSlate is inherited from the environment somehow' do
+ before(:each) { BlankSlate.send(:define_method, :a) { 'c' } }
+ after(:each) { BlankSlate.send(:undef_method, :a) }
+
+ it "masks what is already on blank slate" do
+ context(:a => 'b').instance_eval { a }.
+ should == 'b'
+ end
+ end
+ it "should not reveal define_singleton_method for all users of blankslate, just for us" do
+ expect {
+ BlankSlate.new.instance_eval {
+ define_singleton_method(:foo) { 'foo' }
+ }
+ }.to raise_error(NoMethodError)
+ end
+ it "one contexts variables aren't the next ones" do
+ ca = context(:a => 'b')
+ cb = context(:b => 'c')
+
+ ca.methods.should_not include(:b)
+ cb.methods.should_not include(:a)
+ end
+end
\ No newline at end of file
diff --git a/spec/parslet/transform_spec.rb b/spec/parslet/transform_spec.rb
new file mode 100644
index 0000000..297b757
--- /dev/null
+++ b/spec/parslet/transform_spec.rb
@@ -0,0 +1,165 @@
+require 'spec_helper'
+
+require 'parslet'
+
+describe Parslet::Transform do
+ include Parslet
+
+ let(:transform) { Parslet::Transform.new }
+ attr_reader :transform
+ before(:each) do
+ @transform = Parslet::Transform.new
+ end
+
+ class A < Struct.new(:elt); end
+ class B < Struct.new(:elt); end
+ class C < Struct.new(:elt); end
+ class Bi < Struct.new(:a, :b); end
+
+ describe "delayed construction" do
+ context "given simple(:x) => A.new(x)" do
+ before(:each) do
+ transform.rule(simple(:x)) { |d| A.new(d[:x]) }
+ end
+
+ it "should transform 'a' into A.new('a')" do
+ transform.apply('a').should == A.new('a')
+ end
+ it "should transform ['a', 'b'] into [A.new('a'), A.new('b')]" do
+ transform.apply(['a', 'b']).should ==
+ [A.new('a'), A.new('b')]
+ end
+ end
+ context "given rules on {:a => simple(:x)} and {:b => :_x}" do
+ before(:each) do
+ transform.rule(:a => simple(:x)) { |d| A.new(d[:x]) }
+ transform.rule(:b => simple(:x)) { |d| B.new(d[:x]) }
+ end
+
+ it "should transform {:d=>{:b=>'c'}} into d => B('c')" do
+ transform.apply({:d=>{:b=>'c'}}).should == {:d => B.new('c')}
+ end
+ it "should transform {:a=>{:b=>'c'}} into A(B('c'))" do
+ transform.apply({:a=>{:b=>'c'}}).should == A.new(B.new('c'))
+ end
+ end
+ describe "pulling out subbranches" do
+ before(:each) do
+ transform.rule(:a => {:b => simple(:x)}, :d => {:e => simple(:y)}) { |d|
+ Bi.new(*d.values_at(:x, :y))
+ }
+ end
+
+ it "should yield Bi.new('c', 'f')" do
+ transform.apply(:a => {:b => 'c'}, :d => {:e => 'f'}).should ==
+ Bi.new('c', 'f')
+ end
+ end
+ end
+ describe "dsl construction" do
+ let(:transform) { Parslet::Transform.new do
+ rule(simple(:x)) { A.new(x) }
+ end
+ }
+
+ it "should still evaluate rules correctly" do
+ transform.apply('a').should == A.new('a')
+ end
+ end
+ describe "class construction" do
+ class OptimusPrime < Parslet::Transform
+ rule(:a => simple(:x)) { A.new(x) }
+ rule(:b => simple(:x)) { B.new(x) }
+ end
+ let(:transform) { OptimusPrime.new }
+
+ it "should evaluate rules" do
+ transform.apply(:a => 'a').should == A.new('a')
+ end
+
+ context "with inheritance" do
+ class OptimusPrimeJunior < OptimusPrime
+ rule(:b => simple(:x)) { B.new(x.upcase) }
+ rule(:c => simple(:x)) { C.new(x) }
+ end
+ let(:transform) { OptimusPrimeJunior.new }
+
+ it "should inherit rules from its parent" do
+ transform.apply(:a => 'a').should == A.new('a')
+ end
+
+ it "should be able to override rules from its parent" do
+ transform.apply(:b => 'b').should == B.new('B')
+ end
+
+ it "should be able to define new rules" do
+ transform.apply(:c => 'c').should == C.new('c')
+ end
+ end
+ end
+ describe "<- #call_on_match" do
+ let(:bindings) { { :foo => 'test' } }
+ context "when given a block of arity 1" do
+ it "should call the block" do
+ called = false
+ transform.call_on_match(bindings, lambda do |dict|
+ called = true
+ end)
+
+ called.should == true
+ end
+ it "should yield the bindings" do
+ transform.call_on_match(bindings, lambda do |dict|
+ dict.should == bindings
+ end)
+ end
+ it "should execute in the current context" do
+ foo = 'test'
+ transform.call_on_match(bindings, lambda do |dict|
+ foo.should == 'test'
+ end)
+ end
+ end
+ context "when given a block of arity 0" do
+ it "should call the block" do
+ called = false
+ transform.call_on_match(bindings, proc do
+ called = true
+ end)
+
+ called.should == true
+ end
+ it "should have bindings as local variables" do
+ transform.call_on_match(bindings, proc do
+ foo.should == 'test'
+ end)
+ end
+ it "should execute in its own context" do
+ @bar = 'test'
+ transform.call_on_match(bindings, proc do
+ @bar.should_not == 'test'
+ end)
+ end
+ end
+ end
+
+ context "various transformations (regression)" do
+ context "hashes" do
+ it "are matched completely" do
+ transform.rule(:a => simple(:x)) { fail }
+ transform.apply(:a => 'a', :b => 'b')
+ end
+ end
+ end
+
+ context "when not using the bindings as hash, but as local variables" do
+ it "should access the variables" do
+ transform.rule(simple(:x)) { A.new(x) }
+ transform.apply('a').should == A.new('a')
+ end
+ it "should allow context as local variable" do
+ transform.rule(simple(:x)) { foo }
+ transform.apply('a', :foo => 'bar').should == 'bar'
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..ee7d528
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,38 @@
+
+require 'parslet'
+
+require 'parslet/rig/rspec'
+require 'parslet/atoms/visitor'
+require 'parslet/export'
+
+RSpec.configure do |config|
+ config.mock_with :flexmock
+
+ begin
+ # Here's to the worst idea ever, rspec. This is why we'll be leaving you soon.
+ config.expect_with :rspec do |c|
+ c.syntax = [:should, :expect]
+ end
+ rescue NoMethodError
+ # If the feature is missing, ignore it.
+ end
+
+ # Exclude other ruby versions by giving :ruby => 1.8 or :ruby => 1.9
+ #
+ config.filter_run_excluding :ruby => lambda { |version|
+ RUBY_VERSION.to_s !~ /^#{Regexp.escape(version.to_s)}/
+ }
+end
+
+def catch_failed_parse
+ begin
+ yield
+ rescue Parslet::ParseFailed => exception
+ end
+ exception.cause
+end
+
+def slet name, &block
+ let(name, &block)
+ subject(&block)
+end
\ No newline at end of file
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-parslet.git
More information about the Pkg-ruby-extras-commits
mailing list