succubus 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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