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 +85 -0
- data/Rakefile +9 -0
- data/lib/succubus.rb +77 -0
- data/lib/succubus/version.rb +3 -0
- data/spec/grammar_spec.rb +91 -0
- data/test/support/fixed_random.rb +40 -0
- data/test/support/subset_asserts.rb +49 -0
- metadata +88 -0
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
_A random generator based on a generalised Backus-Naur Form grammar_
|
2
|
+
|
3
|
+
[](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
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,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
|