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
@@ -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