madderlib 0.1.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.
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