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
@@ -0,0 +1,56 @@
1
+ module MadderLib
2
+ module Conditional
3
+ module Registry #:nodoc: all
4
+
5
+ module Static
6
+ # registers a preparation closure for the container
7
+ def add_prepare(&block)
8
+ conditional_prepares << block
9
+ end
10
+
11
+ def conditional_prepares
12
+ @conditional_prepares ||= []
13
+ end
14
+
15
+ # registers a test closure for the container
16
+ def add_test(&block)
17
+ raise Error, 'block required' unless block_given?
18
+ conditional_tests << block
19
+ end
20
+
21
+ def conditional_tests
22
+ @conditional_tests ||= []
23
+ end
24
+ end
25
+
26
+
27
+
28
+ module Instance
29
+ # called once per execution
30
+ def prepare(context)
31
+ # execute all of our registered preparation blocks
32
+ self.class.conditional_prepares.each do |block|
33
+ (block.arity == 1 ? block.call(self) : block.call(self, context))
34
+ end
35
+
36
+ if self.methods.include?('instructions')
37
+ # prepare each instruction
38
+ self.instructions.each {|instruction| instruction.prepare(context) }
39
+ end
40
+ end
41
+
42
+ # returns true if the owner should be used
43
+ def test(context)
44
+ # find the first failing test closure
45
+ # it'd be nil if they all pass
46
+ failed = self.class.conditional_tests.find do |block|
47
+ # first failure stops us
48
+ (! (block.arity == 1 ? block.call(self) : block.call(self, context)))
49
+ end
50
+
51
+ failed.nil?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,130 @@
1
+ module MadderLib
2
+ module Conditional
3
+ #= Recur
4
+ #
5
+ #Introduces support for repeating usage of a Phrase
6
+ module Repeat
7
+
8
+ #= Recur::Phrase
9
+ #
10
+ #Introduces support for repeating usage of a Phrase.
11
+ #The Phrase itself is largely uninvolved; it simply uses the repeat logic of its Instruction
12
+ #
13
+ #See: Recur::Instruction
14
+ module Phrase
15
+ #Adds repetition logic to the current Instruction
16
+ #
17
+ #See: Instruction#repeat
18
+ def repeat(*args, &block)
19
+ self.instruction.repeat *args, &block
20
+ end
21
+ alias :repeats :repeat
22
+ alias :repeating :repeat
23
+ alias :times :repeat
24
+ alias :while :repeat
25
+ end
26
+
27
+
28
+
29
+ #= Recur::Instruction
30
+ #
31
+ #Introduces support for repeating usage of a Phrase.
32
+ #The Instruction will simply call its own Instruction#speak method until the repetition is over.
33
+ #The sum and total of all those calls becomes the Instruction's resulting words.
34
+ #Note that this is not simply a blind duplication of the results of the first call to speak
35
+ #
36
+ #See: Recur::Phrase
37
+ module Instruction
38
+ def self.included(target) #:nodoc:
39
+ # this method won't exist until inclusion
40
+ # can't mess with it until that point
41
+ # moreover, can't call the method 'speak'
42
+ # won't overwrite an existing method
43
+ # so, we do aliasing to swap
44
+ target.class_eval %q{
45
+ alias :pre_repeat_speak :speak
46
+ alias :speak :repeat_speak
47
+ }
48
+ end
49
+
50
+
51
+
52
+ def repeat_speak(context) #:nodoc:
53
+ # no repetition may be requested
54
+ return pre_repeat_speak(context) unless @repeat_tester
55
+
56
+ # keep speaking until we're told to stop
57
+ composite, count = [], 0
58
+
59
+ loop do
60
+ break unless @repeat_tester.invoke(count, context)
61
+
62
+ words = pre_repeat_speak(context)
63
+ break if words.empty?
64
+
65
+ composite << words
66
+ count += 1
67
+ end
68
+
69
+ # as if we said it all at once
70
+ composite.flatten
71
+ end
72
+
73
+
74
+
75
+ #Specifies the repetition of this Phrase
76
+ #
77
+ #If provided, the arguments should contain:
78
+ #* a numeric value, which becomes the count
79
+ #* a Range, or two numerics (which define a Range), from which the count is chosen randomly
80
+ #* a Proc / lambda / block / closure, which returns false when the repetition should stop. \
81
+ #The block can either take (a) no arguments, (b) the repetition count, or; (c) the count <i>and</i> a Context.
82
+ #
83
+ #A repetition count of 0 will exclude the Phrase from the Builder result
84
+ #
85
+ #A repetition always ends when any Instruction returns an empty set of words.
86
+ #Processing will skip to the next Phrase, even if it could repeat again.
87
+ #This is due to the fact that Instruction#speak is called each time, which could provide different results
88
+ #
89
+ #Examples:
90
+ # builder = madderlib do
91
+ # say(:twice).times(2)
92
+ # say(:couple).repeats(1, 2)
93
+ # say(:thrice).while {|count| count < 3 }
94
+ # end
95
+ #
96
+ # words = builder.words
97
+ # words.find_all {|word| word == 'twice' }.should have(2).items
98
+ # words.find_all {|word| word == 'thrice' }.should have(3).items
99
+ # count = words.find_all {|word| word == 'couple' }.size
100
+ # (count >= 1 && count <= 2).should be_true
101
+ def repeat(*args, &block)
102
+ # build a tester, set it aside
103
+ @repeat_tester = Helper::TestBlock.new *args, &block
104
+ self
105
+ end
106
+ alias :repeats :repeat
107
+ alias :repeating :repeat
108
+
109
+ #Specifies the repetition of this Phrase using arguments
110
+ #
111
+ #This is syntactic sugar, but also does not accept a block
112
+ #
113
+ #See: repeat
114
+ def times(*args)
115
+ repeat *args
116
+ end
117
+
118
+ #Specifies the repetition of this Phrase using a block
119
+ #
120
+ #This is syntactic sugar, but also does not accept numeric arguments
121
+ #
122
+ #See: repeat
123
+ def while(&block)
124
+ repeat &block
125
+ end
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,140 @@
1
+ module MadderLib
2
+ #= Context
3
+ #
4
+ #A context-holder object for MadderLib sentences.
5
+ #
6
+ #During the execution of a builder, the context is used to retain state for all parties that care about such things.
7
+ #Each execution produces a new context
8
+ #
9
+ #It is a useful tool for providing dynamic logic and data to a completed (eg. 'static') Builder
10
+ class Context
11
+ attr_reader :sequencer #:nodoc:
12
+ #An Array of all Phrases which contributed words.
13
+ #The Phrases are listed in the order that they were executed
14
+ attr_reader :spoken
15
+ #An Array of all Phrases which <i>did not</i> contribute words.
16
+ #A conditional may have failed, or the resulting content was either empty or nil.
17
+ #The Phrases are listed in the order that they were executed
18
+ attr_reader :silent
19
+ #An Array of the ids of all Phrases which contributed words.
20
+ #The ids are listed in the order that their Phrases were executed
21
+ attr_reader :spoken_ids
22
+ #An Array of all Instructions which contributed words, as chosen from their Phrase.
23
+ #The Phrases are listed in the order that they were executed
24
+ attr_reader :instructions
25
+ #A Hash of arbitrary data for the Context.
26
+ #It is reserved for custom developer logic; the Context doesn't consider its data
27
+ attr_reader :data
28
+
29
+ #Constructs a new Context.
30
+ #
31
+ #An optional Sequencer can be provided.
32
+ #The Sequencer is intentionally clouded in mystery, since it fulfils no external purpose.
33
+ #It is optional only for mock testing; it is <i>required</i> for Builder execution.
34
+ def initialize(sequencer=nil)
35
+ @sequencer = sequencer
36
+ @spoken, @silent, @spoken_ids = [], [], []
37
+ @instructions, @contexts = [], []
38
+ @state, @data = {}, {}
39
+ end
40
+
41
+ #Returns the Builder associated with the Context, via its Sequencer
42
+ def builder
43
+ @sequencer.builder
44
+ end
45
+
46
+ #Returns a Hash associated with the key provided.
47
+ #The value returned will not be nil
48
+ #
49
+ #This Hash can be used to store state data through the lifecycle of the Context.
50
+ #
51
+ #Examples:
52
+ # context = MadderLib::Context.new
53
+ # state = context.state(:state)
54
+ # state.should_not be_nil
55
+ #
56
+ # state[:key] = :value
57
+ # context.state(:state)[:key].should equal(:value)
58
+ def state(key)
59
+ hash = @state[key]
60
+ @state[key] = hash = {} unless hash
61
+ hash
62
+ end
63
+
64
+ #Provides convenient access to the data Hash.
65
+ #
66
+ #Examples:
67
+ # context = MadderLib::Context.new
68
+ # context.data[:key] = :value
69
+ #
70
+ # context[:key].should equal(:value)
71
+ def [](k)
72
+ @data[k]
73
+ end
74
+ def []=(k, v)
75
+ @data[k] = v
76
+ end
77
+
78
+ #Returns a list of all sub-contexts which were generated during Builder execution.
79
+ #These would come from any Builders that were executed as children of their parent Builder
80
+ #The list will <i>not</i> include self, only its children (etc.)
81
+ #
82
+ #The sub-contexts will be returned as an Array, and so on down the Context hierarchy.
83
+ #If <code>:flat</code> is passed as an argument, the Array returned will contain a flattened hierarchy
84
+ def contexts(mode=nil)
85
+ mode ||= :flat
86
+
87
+ if mode == :flat
88
+ queue, ctxs = @contexts.clone, []
89
+ while (ctx = queue.shift)
90
+ # myself
91
+ ctxs << ctx
92
+ # all my children
93
+ queue += ctx.contexts
94
+ end
95
+
96
+ ctxs
97
+ else
98
+ # only the ones for our immediate children
99
+ @contexts
100
+ end
101
+ end
102
+
103
+ #Adds a sub-context to the Context hierarchy
104
+ def add_context(context)
105
+ @contexts << context
106
+ end
107
+
108
+
109
+
110
+ def freeze #:nodoc:
111
+ super
112
+
113
+ # just like clone, we have to do this deeply!
114
+ [
115
+ @sequencer,
116
+ @spoken, @silent, @spoken_ids,
117
+ @instructions, @contexts,
118
+ @state, @data,
119
+ ].each {|o| o.freeze }
120
+ end
121
+
122
+
123
+
124
+ class << self
125
+ def validate(block) #:nodoc:
126
+ raise Error, 'block required' unless block
127
+ raise Error, 'block arity should be 0 or 1 (Context)' unless (block.arity < 2)
128
+ end
129
+
130
+ def invoke(block, context) #:nodoc:
131
+ (block.arity == 0 ? block.call : block.call(context))
132
+ end
133
+ end
134
+ end
135
+
136
+ #An immutable empty Context singleton.
137
+ #Beats returning a null
138
+ Context::EMPTY = Context.new
139
+ Context::EMPTY.freeze
140
+ end
@@ -0,0 +1,217 @@
1
+ module MadderLib
2
+ #= Error
3
+ #
4
+ #A Module-specific Exception class
5
+ #
6
+ #--
7
+ # i would have called it MadderLib::Exception
8
+ # except that i don't know how to access Kernel::Exception within the initialize logic
9
+ #++
10
+ class Error < Exception
11
+ #The propagated cause of this Exception, if appropriate
12
+ attr_reader :cause
13
+
14
+ #Constructed with a message and an optional 'causing' Exception.
15
+ #
16
+ #If no message is passed -- eg. only an Exception -- then this Error inherits its message.
17
+ def initialize(message, cause=nil)
18
+ if (Exception === message)
19
+ super message.to_s
20
+ @cause = message
21
+ else
22
+ super message
23
+ @cause = cause
24
+ end
25
+ end
26
+ end
27
+
28
+
29
+
30
+ #= KernelMethods
31
+ #
32
+ #A Module containing MadderLib methods which are injected into the Kernel scope / namespace.
33
+ #Requiring the gem has the side-effect of injecting these methods.
34
+ module KernelMethods
35
+ #A proxy for MadderLib::Builder.new .
36
+ #It returns a constructed Builder.
37
+ #
38
+ #The resulting Builder is automatically added to the active Grammar.
39
+ #The active grammar can be accessed via madderlib_grammar .
40
+ #
41
+ #Please see MadderLib::Builder for extensive examples of how a Builder itself is put to use
42
+ #
43
+ #Examples:
44
+ # builder = madderlib do
45
+ # say 'no id'
46
+ # end
47
+ # madderlib_grammar.builders.include?(builder).should be_true
48
+ # madderlib_grammar.builder_map.values.include?(builder).should_not be_true
49
+ #
50
+ # builder = madderlib :id do
51
+ # say 'has id'
52
+ # end
53
+ # madderlib_grammar.builders.include?(builder).should be_true
54
+ # madderlib_grammar.builder_map.values.include?(builder).should be_true
55
+ def madderlib(*args, &block)
56
+ builder = Builder.new *args
57
+ madderlib_grammar.add builder
58
+
59
+ builder.extend &block
60
+ end
61
+
62
+ #A proxy for MadderLib::Grammar.get_instance .
63
+ #It returns the active Grammar
64
+ #
65
+ #See: madderlib
66
+ def madderlib_grammar
67
+ # the current instance we're working with
68
+ Grammar.get_instance
69
+ end
70
+ end
71
+
72
+
73
+
74
+ #= Grammar
75
+ #
76
+ #A class for registering MadderLib Builders.
77
+ #
78
+ #It is intended to help de-couple Ruby scripts which generate Builders from those which use them.
79
+ class Grammar
80
+ class << self
81
+ #Constructs a new Grammar instance
82
+ #
83
+ #The new instance becomes the active Grammar, as accessible from get_instance
84
+ #
85
+ #Examples:
86
+ # current = MadderLib::Grammar.new_instance
87
+ # current.should have(0).builders
88
+ # current.should equal(MadderLib::Grammar.get_instance)
89
+ #
90
+ # one = madderlib { say 'one' }
91
+ # current.should have(1).builders
92
+ # current.builders.include?(one).should be_true
93
+ #
94
+ # fresh = MadderLib::Grammar.new_instance
95
+ # fresh.should equal(MadderLib::Grammar.get_instance)
96
+ #
97
+ # two = madderlib { say 'two' }
98
+ # fresh.should have(1).builders
99
+ # fresh.builders.include?(two).should be_true
100
+ #
101
+ # current.should_not equal(MadderLib::Grammar.get_instance)
102
+ # current.builders.include?(two).should_not be_true
103
+ def new_instance
104
+ @instance = self.new
105
+ end
106
+
107
+ #Returns the active Grammar instance
108
+ #
109
+ #If no such Grammar exists, a new one is created
110
+ #
111
+ #See: new_instance
112
+ def get_instance
113
+ @instance ||= new_instance
114
+ end
115
+ end
116
+
117
+ #An Array of all Builders in the Grammar
118
+ attr_reader :builders
119
+ #A Hash of all the Builders in the Grammar which have an id
120
+ attr_reader :builder_map
121
+
122
+ #Constructs a new Grammar
123
+ def initialize
124
+ @builders = []
125
+ @builder_map = {}
126
+
127
+ # randomness
128
+ srand(Time.now.to_i)
129
+ end
130
+
131
+ #Adds a Builder to the Grammar.
132
+ #
133
+ #How this is done depends on the arguments passed
134
+ #* if an existing Builder is provided, it is added to the Grammar (as-is)
135
+ #* if nothing is provided, then a new Builder is constructed (without any id)
136
+ #* otherwise, any argument passed is treated as the id for a newly-constructed Builder
137
+ #
138
+ #If a block is provided, and a Builder is constructed, that block is leveraged <i>a la</i> Builder#extend
139
+ #
140
+ #Examples:
141
+ # grammar = MadderLib::Grammar.new_instance
142
+ #
143
+ # builder = madderlib { say 'exists' }
144
+ # x = grammar.add(builder)
145
+ # x.should equal(builder)
146
+ # grammar.should have(1).builders
147
+ # grammar.builder_map.should have(0).keys
148
+ #
149
+ # builder = grammar.add { say 'no id' }
150
+ # grammar.should have(2).builders
151
+ # grammar.builder_map.should have(0).keys
152
+ # builder.sentence.should eql('no id')
153
+ #
154
+ # builder = grammar << :id
155
+ # grammar.should have(3).builders
156
+ # grammar.builder_map.values.include?(builder).should be_true
157
+ # builder.sentence.should eql('')
158
+ def add(*args, &block)
159
+ builder = args.first
160
+
161
+ case builder
162
+ when Builder
163
+ # leave it alone
164
+ when nil
165
+ # new, with block dispatched
166
+ builder = Builder.new &block
167
+ else
168
+ # new, assume the arg is an ID
169
+ builder = Builder.new args.first, &block
170
+ end
171
+
172
+ unless @builders.include?(builder)
173
+ @builders << builder
174
+
175
+ # an id is not required
176
+ id = builder.id
177
+ (@builder_map[id] = builder) if id
178
+ end
179
+
180
+ builder
181
+ end
182
+ alias :<< :add
183
+
184
+ #Provides convenient access to the Builder map (builder_map).
185
+ #
186
+ #Examples:
187
+ # grammar = MadderLib::Grammar.new_instance
188
+ #
189
+ # builder = grammar.add(:id) { say 'has id' }
190
+ # grammar[:id].should equal(builder)
191
+ def [](key)
192
+ @builder_map[key]
193
+ end
194
+
195
+ #Freezes, and closes / completes, the current Grammar
196
+ #
197
+ #The Grammar becomes immutable.
198
+ #As a side-effect, if it is the current Grammar, a new_instance is created and used from this point forwards
199
+ def freeze
200
+ super
201
+
202
+ # deep freeze
203
+ [@builders, @builder_map].each {|o| o.freeze }
204
+
205
+ if self.class.get_instance == self
206
+ # we can no longer be the current Grammar
207
+ self.class.new_instance
208
+ end
209
+ end
210
+ alias :close :freeze
211
+ alias :complete :freeze
212
+ end
213
+ end
214
+
215
+
216
+ # inject into the Kernel
217
+ include MadderLib::KernelMethods