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