madderlib 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +22 -0
  3. data/README.rdoc +173 -0
  4. data/Rakefile +134 -0
  5. data/lib/madderlib.rb +27 -0
  6. data/lib/madderlib/builder.rb +659 -0
  7. data/lib/madderlib/conditional/allowed.rb +144 -0
  8. data/lib/madderlib/conditional/helper.rb +135 -0
  9. data/lib/madderlib/conditional/likely.rb +162 -0
  10. data/lib/madderlib/conditional/recur.rb +103 -0
  11. data/lib/madderlib/conditional/registry.rb +56 -0
  12. data/lib/madderlib/conditional/repeat.rb +130 -0
  13. data/lib/madderlib/context.rb +140 -0
  14. data/lib/madderlib/core.rb +217 -0
  15. data/lib/madderlib/extensions.rb +40 -0
  16. data/lib/madderlib/instruction.rb +171 -0
  17. data/lib/madderlib/phrase.rb +284 -0
  18. data/lib/madderlib/sequencer.rb +337 -0
  19. data/madderlib.gemspec +72 -0
  20. data/spec/benchmark_spec.rb +69 -0
  21. data/spec/builder_spec.rb +321 -0
  22. data/spec/builder_to_other_spec.rb +47 -0
  23. data/spec/builder_to_sequencer_spec.rb +388 -0
  24. data/spec/conditional_allowed_spec.rb +130 -0
  25. data/spec/conditional_helper_spec.rb +131 -0
  26. data/spec/conditional_likely_spec.rb +138 -0
  27. data/spec/conditional_recur_spec.rb +102 -0
  28. data/spec/conditional_registry_spec.rb +94 -0
  29. data/spec/conditional_repeat_spec.rb +88 -0
  30. data/spec/doc_spec.rb +550 -0
  31. data/spec/error_spec.rb +33 -0
  32. data/spec/examples_spec.rb +151 -0
  33. data/spec/extensions_spec.rb +47 -0
  34. data/spec/grammar_spec.rb +101 -0
  35. data/spec/instruction_spec.rb +133 -0
  36. data/spec/kernel_spec.rb +58 -0
  37. data/spec/phrase_spec.rb +7 -0
  38. data/spec/sequencer_spec.rb +317 -0
  39. data/spec/spec_helper.rb +54 -0
  40. metadata +98 -0
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ = 0.1.0
2
+
3
+ * Initial gem release of 0.1 codebase.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 Dan Foley / CantRemember.com
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,173 @@
1
+ = MadderLib
2
+
3
+ A Sentence-Building DSL for the easily amused.
4
+
5
+ == Overview
6
+
7
+ This is a library for describing the logic for building a sentence.
8
+ Or, more specifically, for building an Array of Strings.
9
+
10
+ The steps involved are to:
11
+
12
+ * create a MadderLib::Builder
13
+ * define the MadderLib::Phrase objects which will comprise the sentence
14
+ * define one or more MadderLib::Instruction objects for each Phrase (eg. its word(s))
15
+ * execute the Builder to produce the result
16
+
17
+ A simple example would be:
18
+
19
+ require 'rubygems'
20
+ require 'madderlib'
21
+
22
+ puts madderlib {
23
+ say 'hello,'
24
+ say('welcome to').or.say("you're viewing")
25
+ say('the README.doc')
26
+ }.sentence
27
+
28
+ Once constructed, the Builder contains all of the rules necessary to build and re-build its sentence.
29
+ Within the builder, its Phrase and Instruction set can define:
30
+
31
+ * the ordering of the Phrase
32
+ * conditional interdependence of Phrases (eg. only say <i>X</i> if <i>Y</i> has already been said)
33
+ * conditions for including / excluding a given Instruction (eg. only if a customer has enrolled in <i>X</i>)
34
+ * the likelihood (odds) for choosing one Instruction over another within a given Phrase
35
+ * how often an Instruction will repeat, if chosen
36
+
37
+ And other variations along that line.
38
+
39
+ Some of the more useful aspects of this library are:
40
+
41
+ * the descriptive Builder syntax itself
42
+ * a Builder, once defined, can be cloned and/or extended, allowing for re-use and re-purposing
43
+ * extensive support for Procs / lambdas to allow for dynamic rules and content, vs. having to alter Builder logic
44
+
45
+ == MadderLib::KernelMethods
46
+
47
+ A small set of methods are injected into the Kernel namespace / scope for easy creation of a Builder.
48
+
49
+ The most commonly used method would be madderlib; it initiates a Builder
50
+
51
+ == MadderLib::Builder
52
+
53
+ The Builder and its methods provides access to all of the core capabilities of the library.
54
+ Examples are provided in the documentation.
55
+ Some good starters are:
56
+
57
+ * Builder#append (extend)
58
+ * Builder#clone
59
+ * Builder#phrase (it)
60
+ * Builder#and_then (and, then, also)
61
+ * Builder#first
62
+ * Builder#before
63
+ * Builder#alternately (or)
64
+ * Builder#sentence (to_s)
65
+
66
+ == MadderLib::Phrase and MadderLib::Instruction
67
+
68
+ Each Phrase is a segement of the sentence, usually only a word or more.
69
+ A Phrase is comprised of at least one Instruction, and perhaps more if there are multiple options.
70
+ By and large, you won't even notice them as being separate from the Builder.
71
+
72
+ Some good examples are:
73
+
74
+ * Phrase#alternately
75
+ * AnytimePhrase#before
76
+ * Instruction#speak
77
+
78
+ == MadderLib::Conditional
79
+
80
+ These are additional aspects to the library which support various features.
81
+ It's easiest to reference them by example:
82
+
83
+ * Conditional::Allowed::Instruction#assuming (if)
84
+ * Conditional::Likely::Instruction#likely (weight, odds)
85
+ * Conditional::Repeat::Instruction#repeat
86
+ * Conditional::Recur::Phrase#recur
87
+
88
+ == Examples
89
+
90
+ This example comes from Snake 'n' Bacon (http://wiki.cantremember.com/Twitter/SnakeAndBacon):
91
+
92
+ builder = madderlib do
93
+ say "i'm"
94
+
95
+ and_then
96
+ ['sometimes', 'always', nil].each {|word| alternately.say(word) }
97
+
98
+ say('quite').or(2).say('rather').or(2).say('so').or.nothing
99
+
100
+ say 'salty'
101
+ ['smoky', 'peppery'].each {|word| alternately(2).say(word) }
102
+ end
103
+ builder.validate
104
+
105
+ 5.times { puts builder.sentence }
106
+
107
+ This is one of the simpler Builders from The Conet Project (http://wiki.cantremember.com/Twitter/Conet):
108
+
109
+ builder = madderlib do
110
+ meta[:audio] = [
111
+ 'http://www.archive.org/download/ird059/tcp_d1_06_the_lincolnshire_poacher_mi5_irdial.mp3',
112
+ 'http://www.archive.org/download/ird059/tcp_d3_02_iran_iraq_jamming_efficacy_testting_irdial.mp3',
113
+ ]
114
+
115
+ digits = lambda do |len|
116
+ s = rand(10 ** len).to_s
117
+ s = ('0' * (len - s.size)) + s
118
+ s
119
+ end
120
+
121
+ say 'Lincolnshire Poacher'
122
+ say { digits.call(5) }.repeat(10)
123
+
124
+ say('~').repeat(6)
125
+
126
+ 200.times do
127
+ say { s = digits.call(5); [s, s] }
128
+ end
129
+
130
+ say('~').repeat(6)
131
+
132
+ say 'Lincolnshire Poacher'
133
+ end
134
+ builder.validate
135
+
136
+ 5.times { puts builder.sentence }
137
+
138
+ Here is a rather boring example, yet which is a bit more practical:
139
+
140
+ user = Struct.new(:name)
141
+
142
+ builder = madderlib do
143
+ setup {|context| context[:hour] ||= Time.new.hour }
144
+ a(:morning).says('top of the morning,').if {|c| Range.new(8, 12).include?(c[:hour]) }
145
+ say('good afternoon,').if {|c| Range.new(12, 17).include?(c[:hour]) }
146
+ say("g'night").if {|c| Range.new(19, 24).include?(c[:hour]) }
147
+ say {|c| c[:user].name + '.' }
148
+ end
149
+
150
+ puts builder.sentence {|c| c[:user] = user.new('joe')}
151
+
152
+ puts builder.sentence {|c|
153
+ c[:user] = user.new('fred')
154
+ c[:hour] = 13
155
+ }
156
+
157
+ extended = builder.clone.extend { say('have a nice day!').if(:morning) }
158
+ puts extended.sentence {|c|
159
+ c[:user] = user.new('charlie')
160
+ c[:hour] = 8
161
+ }
162
+
163
+ == Contributing
164
+
165
+ === Issue Tracking and Feature Requests
166
+
167
+ * http://madderlib.rubyforge.org
168
+
169
+ == Community
170
+
171
+ === Wiki
172
+
173
+ * http://wiki.cantremember.com/MadderLib
data/Rakefile ADDED
@@ -0,0 +1,134 @@
1
+ require 'rubygems'
2
+ require 'rake/clean'
3
+ require 'fileutils'
4
+
5
+ task :default => :test
6
+
7
+ # SPECS ===============================================================
8
+
9
+ desc 'Run specs with story style output'
10
+ task :spec do
11
+ sh 'spec --format specdoc spec/*_spec.rb'
12
+ end
13
+
14
+ desc 'Run specs with unit test style output'
15
+ task :test => FileList['spec/*_spec.rb'] do |t|
16
+ suite = t.prerequisites.map{|f| "-r#{f.chomp('.rb')}"}.join(' ')
17
+ sh "ruby -Ilib:spec #{suite} -e ''", :verbose => false
18
+ end
19
+
20
+ # PACKAGING ============================================================
21
+
22
+ # Load the gemspec using the same limitations as github
23
+ def spec
24
+ @spec ||=
25
+ begin
26
+ require 'rubygems/specification'
27
+ data = File.read('madderlib.gemspec')
28
+ spec = nil
29
+ # OS X didn't like SAFE = 2
30
+ # (eval):25:in `glob': Insecure operation - glob
31
+ Thread.new { spec = eval("$SAFE = 2\n#{data}") }.join
32
+ spec
33
+ end
34
+ end
35
+
36
+ def package(ext='')
37
+ "dist/madderlib-#{spec.version}" + ext
38
+ end
39
+
40
+ desc 'Build packages'
41
+ task :package => %w[.gem .tar.gz].map {|e| package(e)}
42
+
43
+ desc 'Build and install as local gem'
44
+ task :install => package('.gem') do
45
+ sh "gem install #{package('.gem')}"
46
+ end
47
+
48
+ directory 'dist/'
49
+ CLEAN.include 'dist'
50
+
51
+ file package('.gem') => %w[dist/ madderlib.gemspec] + spec.files do |f|
52
+ sh "gem build madderlib.gemspec"
53
+ mv File.basename(f.name), f.name
54
+ end
55
+
56
+ file package('.tar.gz') => %w[dist/] + spec.files do |f|
57
+ sh "git archive --format=tar HEAD | gzip > #{f.name}"
58
+ end
59
+
60
+ # Rubyforge Release / Publish Tasks ==================================
61
+
62
+ desc 'Publish website to rubyforge'
63
+ task 'publish:doc' => 'doc/api/index.html' do
64
+ sh 'scp -rp doc/* cantremember@rubyforge.org:/var/www/gforge-projects/madderlib/'
65
+ end
66
+
67
+ task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
68
+ sh <<-end
69
+ rubyforge add_release madderlib madderlib #{spec.version} #{package('.gem')} &&
70
+ rubyforge add_file madderlib madderlib #{spec.version} #{package('.tar.gz')}
71
+ end
72
+ end
73
+
74
+ # Website ============================================================
75
+ # Building docs
76
+
77
+ task 'doc' => ['doc:api','doc:site']
78
+
79
+ desc 'Generate RDoc under doc/api'
80
+ task 'doc:api' => ['doc/api/index.html']
81
+
82
+ file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc','CHANGELOG','LICENSE'] do |f|
83
+ rb_files = f.prerequisites
84
+ sh((<<-end).gsub(/\s+/, ' '))
85
+ rdoc --line-numbers --inline-source --title MadderLib --main README.rdoc
86
+ #{rb_files.join(' ')}
87
+ end
88
+ end
89
+ CLEAN.include 'doc/api'
90
+
91
+ def rdoc_to_html(file_name)
92
+ require 'rdoc/markup/to_html'
93
+ rdoc = RDoc::Markup::ToHtml.new
94
+ rdoc.convert(File.read(file_name))
95
+ end
96
+
97
+ def haml(locals={})
98
+ require 'haml'
99
+ template = File.read('doc/template.haml')
100
+ haml = Haml::Engine.new(template, :format => :html4, :attr_wrapper => '"')
101
+ haml.render(Object.new, locals)
102
+ end
103
+
104
+ desc 'Build website HTML and stuff'
105
+ task 'doc:site' => ['doc/index.html']
106
+
107
+ file 'doc/index.html' => %w[README.rdoc doc/template.haml] do |file|
108
+ File.open(file.name, 'w') do |file|
109
+ file << haml(:title => 'MadderLib', :content => rdoc_to_html('README.rdoc'))
110
+ end
111
+ end
112
+ CLEAN.include 'doc/index.html'
113
+
114
+ # Gemspec Helpers ====================================================
115
+
116
+ file 'madderlib.gemspec' => FileList['{lib,spec}/**','Rakefile'] do |f|
117
+ # read spec file and split out manifest section
118
+ spec = File.read(f.name)
119
+ parts = spec.split(/\s+# = MANIFEST =\n/)
120
+ fail 'bad spec' if parts.length != 3
121
+ # determine file list from git ls-files
122
+ files = `git ls-files`.
123
+ split("\n").
124
+ sort.
125
+ reject{ |file| file =~ /^\./ }.
126
+ reject { |file| file =~ /^doc/ }.
127
+ map{ |file| " #{file}" }.
128
+ join("\n")
129
+ # piece file back together and write...
130
+ parts[1] = " s.files = %w[\n#{files}\n ]\n"
131
+ spec = parts.join(" # = MANIFEST =\n")
132
+ File.open(f.name, 'w') { |io| io.write(spec) }
133
+ puts "updated #{f.name}"
134
+ end
data/lib/madderlib.rb ADDED
@@ -0,0 +1,27 @@
1
+ #Requires all the individual components, in the proper sequence.
2
+ #That makes the use of this gem as easy as:
3
+ #
4
+ # require 'madderlib'
5
+ #--
6
+
7
+ # external
8
+ %w{ }.each {|lib| require lib }
9
+
10
+ # internal, and in the proper sequence
11
+ %w{
12
+ madderlib/core
13
+ madderlib/context
14
+ madderlib/conditional/helper
15
+ madderlib/conditional/registry
16
+ madderlib/conditional/allowed
17
+ madderlib/conditional/repeat
18
+ madderlib/conditional/recur
19
+ madderlib/conditional/likely
20
+ madderlib/extensions
21
+ madderlib/instruction
22
+ madderlib/phrase
23
+ madderlib/sequencer
24
+ madderlib/builder
25
+ }.each do |file|
26
+ require File.expand_path(File.join(File.dirname(__FILE__), file))
27
+ end
@@ -0,0 +1,659 @@
1
+ %w{ generator }.each {|lib| require lib }
2
+
3
+
4
+
5
+ module MadderLib
6
+ #= Builder
7
+ #
8
+ #A builder object for MadderLib sentences.
9
+ #
10
+ #The builder is constructed to include all of the Phrases (rules) for building a sentence.
11
+ #
12
+ #Traditionally this is done through KernelMethods shortcuts:
13
+ # builder = madderlib :id do
14
+ # say 'through Kernel'
15
+ # ...
16
+ # end
17
+ #
18
+ #This can also be done through standard construction
19
+ # builder = MadderLib::Builder.new(:id) do
20
+ # say('through construction')
21
+ # ...
22
+ # end
23
+ #
24
+ #Each time you invoke the builder using one of the following methods, its Phrases are executed from scratch using a new build context:
25
+ #* sentence : returns the resulting words as a composite String
26
+ #* words : returns a list of all the resulting words
27
+ #* each_words : iterates through each of the resulting words
28
+ #
29
+ #You can clone an existing builder. The resulting object is 'deeply' cloned.
30
+ #
31
+ #You can extend or append additional rules to an existing builder.
32
+ # builder.extend { say 'something more'; ... }
33
+ #
34
+ #You can add multiple setup and teardown blocks to the builder, which provide access to the active build context.
35
+ #
36
+ #All of the other features of the Builder involve management of and dispatching to the current Phrase.
37
+ class Builder
38
+ include Enumerable
39
+
40
+ #The (optional) id of the Builder
41
+ attr_reader :id
42
+ #An Array of each Phrase (rule) within the Builder
43
+ attr_reader :phrases
44
+ #An Array containing the id from each Phrase which has one (they're optional)
45
+ attr_reader :phrase_ids
46
+ #A Hash of arbitrary meta-data for the Builder.
47
+ #It is reserved for custom developer logic; the Builder doesn't consider its meta-data
48
+ attr_reader :meta
49
+
50
+ #Constructs a new Builder.
51
+ #The id is optional.
52
+ #When a block is provided, it is used to extend the new (empty) Builder
53
+ #
54
+ #The new Builder is not automatically added to the active Grammar.
55
+ #That is the responsibility of the caller
56
+ #
57
+ #Examples:
58
+ # builder = MadderLib::Builder.new
59
+ # builder.id.should be_nil
60
+ # builder.should have(0).phrases
61
+ #
62
+ # builder = MadderLib::Builder.new :id
63
+ # builder.id.should equal(:id)
64
+ # builder.should have(0).phrases
65
+ #
66
+ # builder = MadderLib::Builder.new do
67
+ # say 'no id'
68
+ # end
69
+ # builder.id.should be_nil
70
+ # builder.sentence.should eql('no id')
71
+ #
72
+ # builder = MadderLib::Builder.new :id do
73
+ # say {|context| context.builder.id }
74
+ # end
75
+ # builder.sentence.should eql('id')
76
+ def initialize(id=nil, &block)
77
+ @id = id
78
+ @phrases, @phrase_ids = [], []
79
+ @ordered, @depends = [], []
80
+ @setup, @teardown = [], []
81
+ @meta = {}
82
+
83
+ self.extend &block if block_given?
84
+ self
85
+ end
86
+
87
+ #Executes the block provided within the context of the Builder instance.
88
+ #This provides easy contextual access to say, or, first, anytime, and all other instance methods.
89
+ #
90
+ #Examples:
91
+ # builder = MadderLib::Builder.new { say 'construct' }
92
+ # builder.append { say 'extended' }
93
+ # builder.extend { say 'appending' }
94
+ # builder.words.should eql(%w{ construct extended appending })
95
+ def append(&block)
96
+ raise Error, 'extending block is required' unless block_given?
97
+
98
+ # evaluate in our context
99
+ # available in scope
100
+ # closure locals
101
+ # static methods, if you specify self.class
102
+ # instance variables and methods
103
+ # unavailable
104
+ # any methods in closure scope (sorry, Tennessee)
105
+ self.instance_eval &block
106
+ self
107
+ end
108
+ alias :extend :append
109
+
110
+ #Creates a deep-clone of the builder.
111
+ #You can extend the new builder's Phrases, or add new setup blocks, without impacting the original.
112
+ #
113
+ #Note that the two builders will share the same <i>original</i> Phrase list.
114
+ #If you modify one of them behind-the-scenes, that change will be shared by <i>both</i> builders.
115
+ #
116
+ #Examples:
117
+ # original = MadderLib::Builder.new do
118
+ # meta[:meta] = :original
119
+ # say 'original'
120
+ # and_then(:shared).say('initial').likely(1)
121
+ # end
122
+ #
123
+ # cloned = original.clone
124
+ # cloned[:meta] = :cloned
125
+ # cloned.extend { say 'cloned' }
126
+ #
127
+ # done = :original
128
+ # cloned.setup { done = :cloned }
129
+ #
130
+ # shared = original.phrases.find {|phrase| phrase.id == :shared }
131
+ # shared.instructions.first.words << 'added'
132
+ #
133
+ # original[:meta].should equal(:original)
134
+ # original.sentence.should eql('original initial added')
135
+ # done.should equal(:original)
136
+ #
137
+ # cloned[:meta].should equal(:cloned)
138
+ # cloned.sentence.should eql('original initial added cloned')
139
+ # done.should equal(:cloned)
140
+ def clone(id=nil)
141
+ o = super()
142
+
143
+ # deeper copy
144
+ @phrases = @phrases.clone
145
+ @phrase_ids = @phrase_ids.clone
146
+ @ordered = @ordered.clone
147
+ @depends = @depends.clone
148
+ @setup = @setup.clone
149
+ @teardown = @teardown.clone
150
+ @meta = @meta.clone
151
+
152
+ # don't want two of them floating around with the same id
153
+ o.instance_variable_set :@id, id
154
+
155
+ # put it into the grammar
156
+ # most importantly,
157
+ grammar = Grammar.get_instance
158
+ grammar.add o if grammar.builders.include?(self)
159
+ o
160
+ end
161
+
162
+
163
+
164
+ #Adds a setup block to the builder.
165
+ #
166
+ #The block is executed before the builder invokes its Phrases (rules).
167
+ #The block can either take no arguments, or a Context.
168
+ #
169
+ #Subsequent blocks are executed in the order provided.
170
+ #If you provide <code>:first</code> as an argument, the block will occur prior to any existing blocks.
171
+ #It would of course be preceded by any subsequent block which says that it is <code>:first</code>.
172
+ #
173
+ #Examples:
174
+ # builder = MadderLib::Builder.new do
175
+ # say {|context| context[:setup] }
176
+ # end
177
+ # builder.setup {|context| context[:setup] << 2 }
178
+ # builder.setup {|context| context[:setup] << 3 }
179
+ # builder.setup(:first) {|context| context[:setup] = [1] }
180
+ #
181
+ # builder.sentence.should eql('1 2 3')
182
+ def setup(*args, &block)
183
+ Context.validate(block)
184
+
185
+ # ordering
186
+ if args.include?(:first)
187
+ @setup.unshift block
188
+ else
189
+ @setup.push block
190
+ end
191
+
192
+ self
193
+ end
194
+
195
+ #Adds a teardown block to the builder.
196
+ #
197
+ #The block is executed after the builder has invoked its Phrases (rules).
198
+ #The block can either take no arguments, or a Context.
199
+ #
200
+ #Subsequent blocks are executed in the order provided.
201
+ #If you provide <code>:first</code> as an argument, the block will occur prior to any existing blocks.
202
+ #It would of course be preceded by any subsequent block which says that it is <code>:first</code>.
203
+ #
204
+ #Examples:
205
+ # builder = MadderLib::Builder.new do
206
+ # say 'teardown'
207
+ # end
208
+ # markers = []
209
+ # builder.teardown {|context| markers << 2 }
210
+ # builder.teardown {|context| markers << 3 }
211
+ # builder.teardown(:first) {|context| markers = [1] }
212
+ #
213
+ # builder.sentence.should eql('teardown')
214
+ # markers.should eql([1, 2, 3])
215
+ def teardown(*args, &block)
216
+ Context.validate(block)
217
+
218
+ # ordering
219
+ if args.include?(:first)
220
+ @teardown.unshift block
221
+ else
222
+ @teardown.push block
223
+ end
224
+
225
+ self
226
+ end
227
+
228
+
229
+
230
+ #Provides convenient access to the meta Hash.
231
+ #
232
+ #Examples:
233
+ # builder = MadderLib::Builder.new do
234
+ # meta[:key] = :value
235
+ # end
236
+ # builder[:key].should equal(:value)
237
+ def [](k)
238
+ @meta[k]
239
+ end
240
+
241
+ #Provides convenient access to the meta Hash.
242
+ #
243
+ #See: []
244
+ def []=(k, v)
245
+ @meta[k] = v
246
+ end
247
+
248
+
249
+
250
+ #Returns the current Phrase.
251
+ #
252
+ #Examples:
253
+ # builder = MadderLib::Builder.new do
254
+ # say 'yes'
255
+ # phrase.if {|context| context.builder[:activated] == true }
256
+ # it.repeat(3)
257
+ # end
258
+ #
259
+ # builder.should have(1).phrases
260
+ # builder.phrase.should have(1).instructions
261
+ #
262
+ # builder.should have(0).words
263
+ #
264
+ # builder[:activated] = true
265
+ # builder.sentence.should eql('yes yes yes')
266
+ def phrase
267
+ @phrases.last
268
+ end
269
+ alias :it :phrase
270
+
271
+ #Allocates another Phrase.
272
+ #An optional Phrase id can be provided.
273
+ #
274
+ #Examples:
275
+ # builder = MadderLib::Builder.new do
276
+ # say 'first'
277
+ # and_then.say 'and_then'
278
+ # also.say 'also'
279
+ # end
280
+ # builder.and.say 'and'
281
+ # builder.then.say 'then'
282
+ def and_then(id=nil)
283
+ add_id id
284
+ @phrases << Phrase.new(self, id)
285
+ @phrases.last
286
+ end
287
+ alias :also :and_then
288
+ alias :and :and_then
289
+ alias :then :and_then
290
+
291
+ #Allocates another Phrase, where an id is required.
292
+ #This is semantic sugar
293
+ #
294
+ #Examples:
295
+ # builder = MadderLib::Builder.new do
296
+ # say 'first'
297
+ # a(:second).says 'second'
298
+ # an(:other).says 'other'
299
+ # end
300
+ #
301
+ # builder.sentence.should eql('first second other')
302
+ def an(id)
303
+ and_then id
304
+ end
305
+ alias :a :an
306
+
307
+
308
+
309
+ #Allocates a Phrase which will be said first, relative to any existing Phrases.
310
+ #An optional Phrase id can be provided
311
+ #
312
+ #This phrase would of course be preceded by any subsequent first Phrase
313
+ #
314
+ #Examples:
315
+ # builder = MadderLib::Builder.new do
316
+ # say 'something'
317
+ # first.say 'say'
318
+ # end
319
+ # builder.sentence.should eql('say something')
320
+ #
321
+ # builder.first.say 'first'
322
+ # builder.sentence.should eql('first say something')
323
+ def first(id=nil)
324
+ ordered and_then(id), :first
325
+ end
326
+
327
+ #Allocates a Phrase which will be said last, relative to any existing Phrases.
328
+ #An optional Phrase id can be provided
329
+ #
330
+ #This phrase would of course be followed by any subsequent last Phrase
331
+ #
332
+ #Examples:
333
+ # builder = MadderLib::Builder.new do
334
+ # last.say 'said'
335
+ # say 'something'
336
+ # end
337
+ # builder.sentence.should eql('something said')
338
+ #
339
+ # builder.last.say 'last'
340
+ # builder.sentence.should eql('something said last')
341
+ def last(id=nil)
342
+ ordered and_then(id), :last
343
+ end
344
+ alias :lastly :last
345
+
346
+ #Allocates a Phrase which will be said anywhere.
347
+ #It's position will be random (though not first or last, except when there is no alternative).
348
+ #The Phrase will only appear once; you may want to make the Phrase recur as well
349
+ #
350
+ #Examples:
351
+ # builder = MadderLib::Builder.new do
352
+ # say 'top'
353
+ # say 'here'
354
+ # say 'there'
355
+ # say 'bottom'
356
+ # end
357
+ # builder.anywhere.say 'anywhere'
358
+ #
359
+ # words = builder.words
360
+ # words.should have(5).words
361
+ # words.find_all {|word| word == 'anywhere'}.should have(1).word
362
+ #
363
+ # builder.it.recurs(2)
364
+ #
365
+ # words = builder.words
366
+ # words.should have(6).words
367
+ # words.find_all {|word| word == 'anywhere'}.should have(2).word
368
+ def anytime(id=nil)
369
+ add_id id
370
+ @phrases << AnytimePhrase.new(self, id)
371
+ ordered self.phrase, :anytime
372
+ end
373
+ alias :anywhere :anytime
374
+
375
+ #Allocates a phrase which is said before another Phrase.
376
+ #The resulting words are inserted immediately before the referenced Phrase.
377
+ #
378
+ #This phrase would of course be preceded by any subsequent before Phrase referenced against the same id.
379
+ #Think of it as adding layers to an onion.
380
+ #
381
+ #If the referenced Phrase is never said, due to conditionals / odds / etc, the dependent Phrase will not be said.
382
+ #This of course cascades throughout the dependency chain.
383
+ #
384
+ #The referenced Phrase must exist, by id.
385
+ #However, that is not checked until execution (eg. not during build).
386
+ #You can test your completed Builder using the validate method.
387
+ #
388
+ #Examples:
389
+ # builder = MadderLib::Builder.new do
390
+ # an(:always).says 'always'
391
+ # a(:sometimes).says('sometimes').if {|context| context.builder[:sometimes] == true }
392
+ # before(:always).say 'before-always'
393
+ # before(:sometimes, :depends).say 'before-sometimes'
394
+ # before(:depends).say 'depends'
395
+ # end
396
+ #
397
+ # builder.sentence.should eql('before-always always')
398
+ #
399
+ # builder[:sometimes] = true
400
+ #
401
+ # builder.sentence.should eql('before-always always depends before-sometimes sometimes')
402
+ def before(ref, id=nil)
403
+ depends and_then(id), :before, ref
404
+ end
405
+
406
+ #Allocates a phrase which is said after another Phrase.
407
+ #The resulting words are inserted immediately after the referenced Phrase.
408
+ #
409
+ #This phrase would of course be followed by any subsequent after Phrase referenced against the same id.
410
+ #Think of it as adding layers to an onion.
411
+ #
412
+ #If the referenced Phrase is never said, due to conditionals / odds / etc, the dependent Phrase will not be said.
413
+ #This of course cascades throughout the dependency chain.
414
+ #
415
+ #The referenced Phrase must exist, by id.
416
+ #However, that is not checked until execution (eg. not during build).
417
+ #You can test your completed Builder using the validate method.
418
+ #
419
+ #Examples:
420
+ # builder = MadderLib::Builder.new do
421
+ # an(:always).says 'always'
422
+ # a(:sometimes).says('sometimes').if {|context| context.builder[:sometimes] == true }
423
+ # after(:always).say 'after-always'
424
+ # after(:sometimes, :depends).say 'after-sometimes'
425
+ # after(:depends).say 'depends'
426
+ # end
427
+ #
428
+ # builder.sentence.should eql('always after-always')
429
+ #
430
+ # builder[:sometimes] = true
431
+ #
432
+ # builder.sentence.should eql('always after-always sometimes after-sometimes depends')
433
+ def after(ref, id=nil)
434
+ depends and_then(id), :after, ref
435
+ end
436
+
437
+
438
+ #A shorthand method for and_then.say .
439
+ #A new Phrase is allocated (without an id), and then Phrase#say method is invoked with the arguments provided.
440
+ #
441
+ #Examples:
442
+ # builder = MadderLib::Builder.new do
443
+ # says 'word'
444
+ # say :symbol
445
+ # say { 'lambda' }
446
+ # end
447
+ # builder.should have(3).phrases
448
+ # builder.sentence.should eql('word symbol lambda')
449
+ def say(*args, &block)
450
+ and_then.say *args, &block
451
+ end
452
+ alias :says :say
453
+
454
+ #A shorthand method for phrase.alternately .
455
+ #The Phrase#alternately method is invoked against the current Phrase with the arguments provided
456
+ #
457
+ #An Error will be raised if there is no current Phrase.
458
+ #It's an easy condition to recover from, but it's bad use of the syntax.
459
+ #
460
+ #Examples:
461
+ # builder = MadderLib::Builder.new do
462
+ # says 'word'
463
+ # alternately.says :symbol
464
+ # end
465
+ # builder.or.say { 'lambda' }
466
+ #
467
+ # builder.should have(1).phrases
468
+ # builder.phrase.should have(3).instructions
469
+ # %w{ word symbol lambda}.include?(builder.sentence).should be_true
470
+ def alternately(*args, &block)
471
+ raise Error, "there is no active phrase. start one with 'say'" unless self.phrase
472
+ self.phrase.or *args, &block
473
+ end
474
+ alias :or :alternately
475
+
476
+
477
+
478
+ #Iterates through each of the words resulting from execution of the Builder.
479
+ #
480
+ #An optional Hash of Context data can be provided.
481
+ #It is merged into Context#data before the Builder is executed
482
+ def each_word(data=nil)
483
+ # from our words
484
+ self.words(data).each {|word| yield word }
485
+ end
486
+ alias :each :each_word
487
+
488
+ #Returns the array of words resulting from execution of the Builder.
489
+ #
490
+ #An optional Hash of Context data can be provided.
491
+ #It is merged into Context#data before the Phrase rules are executed
492
+ #
493
+ #An optional block can be provided.
494
+ #It will be invoked before the Phrase rules are executed
495
+ #The block can either take no arguments, or a Context.
496
+ #
497
+ #All Phrase rules are applied.
498
+ #Each word in the Array is a String.
499
+ #The resulting Array is flattened (vs. any Array hierarchies in the ruleset)
500
+ #
501
+ #Examples:
502
+ # builder = MadderLib::Builder.new do
503
+ # says 'word'
504
+ # say :symbol, [:with, :hierarchy]
505
+ # say { 'lambda' }
506
+ # end
507
+ # builder.words.should eql(%w{ word symbol with hierarchy lambda })
508
+ def words(data=nil, &block)
509
+ # words from a sequencer
510
+ # pass on the context data
511
+ # pass on the block, to pull in the context
512
+ # a new Sequencer each time
513
+ # TODO: optimize
514
+ # dirty flag is hard since phrases is exposed
515
+ # hashsum? clone of last known phrases PLUS dirty flag?
516
+ self.to_sequencer.words data, &block
517
+ end
518
+ alias :to_a :words
519
+
520
+ #Returns the composite sentence resulting from execution of the Builder.
521
+ #It's really just a shortcut for words.join .
522
+ #
523
+ #An optional separator String can be provided.
524
+ #The default separator is a single space
525
+ #
526
+ #An optional Hash of Context data can be provided.
527
+ #It is merged into Context#data before the Phrase rules are executed
528
+ #
529
+ #An optional block can be provided.
530
+ #It will be invoked before the Phrase rules are executed
531
+ #The block can either take no arguments, or a Context.
532
+ #
533
+ #Examples:
534
+ # builder = MadderLib::Builder.new do
535
+ # says 'word'
536
+ # say :symbol, [:with, :hierarchy]
537
+ # say { 'lambda' }
538
+ # end
539
+ # builder.sentence.should eql('word symbol with hierarchy lambda')
540
+ def sentence(*args, &block)
541
+ # argument scan
542
+ sep, data = ' ', nil
543
+ args.each do |arg|
544
+ if String === arg
545
+ # separator
546
+ sep = arg
547
+ elsif Hash === arg
548
+ # context data
549
+ data = arg
550
+ end
551
+ end
552
+
553
+ self.words(data, &block).join(sep)
554
+ end
555
+ alias :to_s :sentence
556
+
557
+
558
+
559
+
560
+ def to_sequencer #:nodoc:
561
+ # general ordering
562
+ sequence = []
563
+ map = {}
564
+ @phrases.each do |phrase|
565
+ sequence << phrase unless self.ordered?(phrase) || self.depends?(phrase)
566
+ map[phrase.id] = phrase if phrase.id
567
+ end
568
+
569
+ # specified ordering
570
+ # anytimes
571
+ anytimes = []
572
+ @ordered.each do |o|
573
+ case o.type
574
+ when :first
575
+ # before all other firsts
576
+ sequence.unshift o.phrase
577
+ when :last
578
+ # after all other lasts
579
+ sequence.push o.phrase
580
+
581
+ when :anytime
582
+ # guarantee valid references
583
+ phrase = o.phrase
584
+
585
+ [phrase.before, phrase.after].each do |ref|
586
+ raise Error, "no such phrase : #{ref.inspect}" unless (!ref) || map[ref]
587
+ end
588
+ anytimes << phrase
589
+
590
+ else
591
+ raise Error, "unknown ordering : #{o.type.inspect}"
592
+ end
593
+ end
594
+
595
+ befores, afters = {}, {}
596
+ @depends.each do |o|
597
+ ref = o.ref
598
+ raise Error, "no such phrase : #{ref.inspect}" unless map[ref]
599
+ case o.type
600
+ when :before
601
+ phrases = befores[ref]
602
+ befores[ref] = (phrases = []) unless phrases
603
+ # before all other befores
604
+ phrases.unshift o.phrase
605
+ when :after
606
+ phrases = afters[ref]
607
+ afters[ref] = (phrases = []) unless phrases
608
+ # after all other afters
609
+ phrases.push o.phrase
610
+ else
611
+ raise Error, "unknown dependency : #{o.type.inspect}"
612
+ end
613
+ end
614
+
615
+ Sequencer.new(self, sequence, map.keys, {
616
+ :anytime => anytimes, :before => befores, :after => afters,
617
+ :setup => @setup, :teardown => @teardown
618
+ })
619
+ end
620
+
621
+ #Validates the Phrase rules in the Builder.
622
+ #If there are any execution-time semantic issues, it will raise the applicable Error
623
+ alias :validate :to_sequencer
624
+
625
+
626
+
627
+ protected
628
+
629
+ def add_id(id) #:nodoc:
630
+ if id
631
+ raise Error, "id already exists : #{id.inspect}" if @phrase_ids.include?(id)
632
+ @phrase_ids << id
633
+ end
634
+ end
635
+
636
+ ORDERED = Struct.new(:phrase, :type) #:nodoc:
637
+ def ordered(phrase, type) #:nodoc:
638
+ # simple tuple, but with order retained
639
+ (@ordered ||= []) << ORDERED.new(phrase, type)
640
+ phrase
641
+ end
642
+
643
+ def ordered?(phrase) #:nodoc:
644
+ !! @ordered.find {|o| o.phrase == phrase }
645
+ end
646
+
647
+ DEPENDS = Struct.new(:phrase, :type, :ref) #:nodoc:
648
+ def depends(phrase, type, ref) #:nodoc:
649
+ # simple tuple, but with order retained
650
+ (@depends ||= []) << DEPENDS.new(phrase, type, ref)
651
+ phrase
652
+ end
653
+
654
+ def depends?(phrase) #:nodoc:
655
+ !! @depends.find {|o| o.phrase == phrase }
656
+ end
657
+ end
658
+
659
+ end