succubus 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ _A random generator based on a generalised Backus-Naur Form grammar_
2
+
3
+ [![Build Status](https://travis-ci.org/asilano/succubus.png?branch=master)](https://travis-ci.org/asilano/succubus)
4
+
5
+ **Succubus** is a generator which takes stochastic paths through a
6
+ generalised Backus-Naur Form grammar to produce random text. For instance, the following:
7
+
8
+ grammar = Succubus::Grammar.new do
9
+ add_rule :base, "I have a <colour> <pet>"
10
+ add_rule :colour, "black", "brown", "white"
11
+ add_rule :pet, "cat", "dog", "rabbit"
12
+ end
13
+ puts grammar.execute :base
14
+
15
+ ...might output `I have a black dog`. Or `I have a brown rabbit`. Or any of the other 7 possible combinations of colour and animal.
16
+
17
+ See the `examples/` folder for more sample generators!
18
+
19
+ Installation
20
+ ============
21
+ Succubus is a RubyGem. That means it's really easy to install. Simply run:
22
+
23
+ gem install succubus
24
+
25
+ and you're done.
26
+
27
+ Compatability
28
+ =============
29
+ Succubus is tested against Ruby 1.9.3 and Ruby 2.0 (by Travis - see the little widget up to check we're still passing).
30
+ I know of no reason it shouldn't work with Ruby 1.8.7 or 1.9.2 - I just haven't got a test environment that works there yet!
31
+
32
+ Usage
33
+ =====
34
+ Everything you need to make and run your own generators is defined in the `Succubus::Grammar` class.
35
+
36
+ To get started:
37
+
38
+ 1. Require in Succubus:
39
+
40
+ require 'succubus'
41
+
42
+ 2. Create a new instance of `Succubus::Grammar`, passing it a block:
43
+
44
+ require 'succubus'
45
+ grammar = Succubus::Grammar.new do
46
+ # See step 3 for what goes here
47
+ end
48
+
49
+ 3. Call `add_rule` within the block. `add_rule` takes a symbol which names the rule, followed by one or more
50
+ strings. When the rule is invoked during grammar execution, exactly one of the strings will be chosen and
51
+ included in the result text. Text in each string is treated literally, except for instances of `<foo>`, which
52
+ instead paste in the result of invoking the rule `:foo`. Rules can be nested as far as you like!
53
+
54
+ require 'succubus'
55
+ grammar = Succubus::Grammar.new do
56
+ add_rule :silly_name, "<title> <adjective><noun>"
57
+ add_rule :title, "Mr.", "Mrs.", "Professor", "Little Miss"
58
+ add_rule :adjective "<smelladj>", "<colour>", "Smarty"
59
+ add_rule :smelladj, "Smelly", "Poopy", "Floral"
60
+ add_rule :colour, "Green", "Purple"
61
+ add_rule :noun, "pants", "brain", "banana", "nose"
62
+ end
63
+
64
+ 4. Call `execute(<rule>)` on your grammar, where `<rule>` is the symbol naming the top-level rule you want to invoke:
65
+
66
+ require 'succubus'
67
+ grammar = Succubus::Grammar.new do
68
+ add_rule :silly_name, "<title> <adjective><noun>"
69
+ add_rule :title, "Mr.", "Mrs.", "Professor", "Little Miss"
70
+ add_rule :adjective, "<smelladj>", "<colour>", "Smarty"
71
+ add_rule :smelladj, "Smelly", "Poopy", "Floral"
72
+ add_rule :colour, "Green", "Purple"
73
+ add_rule :noun, "pants", "brain", "banana", "nose"
74
+ end
75
+
76
+ puts grammar.execute(:silly_name)
77
+ # Professor Purplepants
78
+
79
+ About that name...?
80
+ ===================
81
+ Oh, why **Succubus**? Well, Backus-Naur is usually used to describe or verify legal sentences;
82
+ what you have here is something that uses Backup-Naur to _generate_ sentences. Which is kinda backwards.
83
+ "Backus", backwards, is "Sukcab"; it's just a short hop from there to Succubus.
84
+
85
+ I did consider "Bacchus" - a sort of drunken Backus - but the gem name was taken.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+ require 'bundler/gem_tasks'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = FileList['test/**/*_test.rb', 'spec/**/*_spec.rb']
6
+ t.warning = true
7
+ end
8
+
9
+ task :default => :test
data/lib/succubus.rb ADDED
@@ -0,0 +1,77 @@
1
+ module Succubus
2
+ class ParseError < RuntimeError
3
+ attr_accessor :errors
4
+ end
5
+
6
+ class ExecuteError < RuntimeError
7
+ attr_accessor :errors, :partial
8
+ end
9
+
10
+ class Grammar
11
+ def initialize(&block)
12
+ @rules = {}
13
+ @errors = []
14
+ define_singleton_method(:create, block)
15
+ create
16
+ class << self ; undef_method :create ; end
17
+
18
+ unless @errors.empty?
19
+ pe = ParseError.new("Errors found parsing")
20
+ pe.errors = @errors
21
+ raise pe
22
+ end
23
+ end
24
+
25
+ def add_rule(name, *choices, &block)
26
+ name = name.to_sym
27
+ @errors << "Duplicate rule definition: #{name}" if @rules.include? name
28
+ @rules[name] = choices
29
+ end
30
+
31
+ def execute(start)
32
+ gen = Generator.new(@rules)
33
+ gen.run(start)
34
+
35
+ unless gen.errors.empty?
36
+ ee = ExecuteError.new("Errors found executing")
37
+ ee.errors = gen.errors
38
+ ee.partial = gen.result
39
+ raise ee
40
+ end
41
+
42
+ gen.result
43
+ end
44
+ end
45
+
46
+ class Generator
47
+ attr_reader :result
48
+ attr_reader :errors
49
+
50
+ def initialize(rules)
51
+ @result = ""
52
+ @rules = rules
53
+ @errors = []
54
+ end
55
+
56
+ def run(rule)
57
+ @result = invoke(rule)
58
+ end
59
+
60
+ def invoke(rule)
61
+ unless @rules.include? rule
62
+ @errors << "No such rule: #{rule}"
63
+ return "!!#{rule}!!"
64
+ end
65
+
66
+ local_res = ""
67
+ @rules[rule].sample.scan(/(.*?(?<!\\)(?:\\\\)*)(<.*?>|$)/) do |match|
68
+ local_res << match[0]
69
+ unless match[1].empty?
70
+ local_res << invoke(match[1].match(/<(.*?)>/)[1].to_sym)
71
+ end
72
+ end
73
+
74
+ return local_res
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module Succubus
2
+ VERSION = "0.0.0"
3
+ end
@@ -0,0 +1,91 @@
1
+ require 'minitest/autorun'
2
+ require File.dirname(__FILE__) + '/../test/support/fixed_random'
3
+ require 'succubus'
4
+
5
+ describe Succubus::Grammar do
6
+ describe "creating grammar" do
7
+ it "should accept valid grammar" do
8
+ Succubus::Grammar.new do
9
+ add_rule :base, "I have a <colour> <pet>"
10
+ add_rule :colour, "black", "white"
11
+ add_rule :pet, "cat", "dog"
12
+ end
13
+ end
14
+
15
+ it "should reject invalid grammar" do
16
+ proc do
17
+ Succubus::Grammar.new do
18
+ add_rule :base, "I have a <colour> <pet>"
19
+ add_rule :colour, "black", "white"
20
+ add_rule :colour, "cat", "dog"
21
+ end
22
+ end.must_raise Succubus::ParseError
23
+ end
24
+ end
25
+
26
+ describe "trivial grammar" do
27
+ before do
28
+ @grammar = Succubus::Grammar.new do
29
+ add_rule :base, "Just a single fixed string"
30
+ end
31
+ end
32
+
33
+ it "should always produce the fixed string" do
34
+ 1000.times { @grammar.execute(:base).must_equal "Just a single fixed string" }
35
+ end
36
+ end
37
+
38
+ describe "simple grammar" do
39
+ before do
40
+ @grammar = Succubus::Grammar.new do
41
+ add_rule :base, "I have a <colour> <pet>", "<pet>s look best in <colour>"
42
+ add_rule :colour, "black", "white"
43
+ add_rule :pet, "cat", "dog"
44
+ add_rule :unreachable, "No-one likes animals anyway"
45
+ end
46
+ end
47
+
48
+ it "should always produce a valid string" do
49
+ 1000.times { @grammar.execute(:base).must_match(/(I have a (black|white) (cat|dog))|((cat|dog)s look best in (black|white))/) }
50
+ end
51
+
52
+ it "should produce a known string given pre-chosen randomness" do
53
+ expect_sample ["I have a <colour> <pet>", "<pet>s look best in <colour>"], "<pet>s look best in <colour>"
54
+ expect_sample ["cat", "dog"], "cat"
55
+ expect_sample ["black", "white"], "black"
56
+
57
+ @grammar.execute(:base).must_equal "cats look best in black"
58
+
59
+ samples_must_be_used
60
+ end
61
+ end
62
+
63
+ describe "failure cases" do
64
+ it "should fail to parse duplicate rules" do
65
+ bad_parse = proc do
66
+ Succubus::Grammar.new do
67
+ add_rule :base, "I have a <colour> <pet>"
68
+ add_rule :colour, "black", "white"
69
+ add_rule :colour, "cat", "dog"
70
+ add_rule :base, "are belong to us"
71
+ end
72
+ end
73
+
74
+ ex = bad_parse.must_raise Succubus::ParseError
75
+ ex.errors.must_have_same_elements_as ["Duplicate rule definition: colour", "Duplicate rule definition: base"]
76
+ end
77
+
78
+ it "should fail to execute when rules are missing" do
79
+ grammar = Succubus::Grammar.new do
80
+ add_rule :base, "I have a <size> <colour> <pet>"
81
+ add_rule :colour, "black", "white"
82
+ end
83
+
84
+ expect_sample %w<black white>, "black"
85
+
86
+ ex = proc {grammar.execute(:base)}.must_raise Succubus::ExecuteError
87
+ ex.errors.must_have_same_elements_as ["No such rule: size", "No such rule: pet"]
88
+ ex.partial.must_equal "I have a !!size!! black !!pet!!"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__) + '/subset_asserts'
2
+
3
+ module MiniTest::Assertions
4
+ @@queued_samples = []
5
+
6
+ def self.queued_samples; @@queued_samples; end
7
+ def self.next_sample; @@queued_samples.shift; end
8
+
9
+ def expect_sample(options, *choice)
10
+ assert_subset choice, options, "Can't queue #{options.inspect}.sample => #{choice}; choice not in options"
11
+ @@queued_samples << {:options => options, :choice => choice}
12
+ end
13
+
14
+ def samples_must_be_used
15
+ assert_empty @@queued_samples, "Expected to have used all queued samples: #{@@queued_samples.length} left"
16
+ end
17
+ end
18
+
19
+ unless Array.method_defined? :sample_with_predefined_values
20
+ class Array
21
+ def sample_with_predefined_values(n = nil, &block)
22
+ queued = MiniTest::Assertions.queued_samples
23
+ if !queued.empty? && queued[0][:options] == self && queued[0][:choice].length == (n || 1)
24
+ expected = MiniTest::Assertions.next_sample
25
+ ret = expected[:choice]
26
+ ret = ret[0] if n.nil?
27
+ return ret
28
+ else
29
+ if n
30
+ sample_without_predefined_values(n, &block)
31
+ else
32
+ sample_without_predefined_values(&block)
33
+ end
34
+ end
35
+ end
36
+ alias_method :sample_without_predefined_values, :sample
37
+ alias_method :sample, :sample_with_predefined_values
38
+
39
+ end
40
+ end
@@ -0,0 +1,49 @@
1
+ module MiniTest::Assertions
2
+ def assert_subset(subset, superset, msg = nil)
3
+ sub = subset.to_a.dup
4
+ supe = superset.to_a.dup
5
+ msg ||= "#{sub.inspect} is not a subset of #{supe.inspect}"
6
+ failed = false
7
+ sub.each do |elem|
8
+ if supe.index(elem)
9
+ supe.delete_at(supe.index(elem) || li.length)
10
+ else
11
+ failed = true
12
+ break
13
+ end
14
+ end
15
+
16
+ assert(!failed, msg)
17
+ end
18
+
19
+ def assert_disjoint(left, right, msg = nil)
20
+ left_a = left.to_a.dup
21
+ right_a = right.to_a.dup
22
+ msg ||= "#{left_a.inspect} and #{right_a.inspect} are not disjoint"
23
+ failed = false
24
+ left_a.each do |elem|
25
+ if right_a.index(elem)
26
+ failed = true
27
+ break
28
+ end
29
+ end
30
+
31
+ assert(!failed, msg)
32
+ end
33
+
34
+ def assert_same_elements(a1, a2, msg = nil)
35
+ [:select, :inject, :size].each do |m|
36
+ [a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a.inspect} is an array? It doesn't respond to #{m}.") }
37
+ end
38
+
39
+ assert a1h = a1.inject({}) { |h,e| h[e] = a1.select { |i| i == e }.size; h }
40
+ assert a2h = a2.inject({}) { |h,e| h[e] = a2.select { |i| i == e }.size; h }
41
+
42
+ assert_equal(a1h, a2h, msg)
43
+ end
44
+ end
45
+
46
+ module MiniTest::Expectations
47
+ infect_an_assertion :assert_subset, :must_be_subset_of
48
+ infect_an_assertion :assert_same_elements, :must_have_same_elements_as
49
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: succubus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Chris Howlett
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: minitest
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: ! " Succubus is a generator which takes stochastic paths through a
47
+ \n generalised Backus-Naur Form grammar to produce random text.\n\n See the
48
+ examples for details.\n"
49
+ email:
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/succubus/version.rb
55
+ - lib/succubus.rb
56
+ - Rakefile
57
+ - README.md
58
+ - test/support/fixed_random.rb
59
+ - test/support/subset_asserts.rb
60
+ - spec/grammar_spec.rb
61
+ homepage: https://github.com/asilano/succubus
62
+ licenses: []
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.24
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: A random generator based on a generalised Backus-Naur Form grammar
85
+ test_files:
86
+ - test/support/fixed_random.rb
87
+ - test/support/subset_asserts.rb
88
+ - spec/grammar_spec.rb