Class: MadderLib::Builder

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/madderlib/builder.rb

Overview

Builder

A builder object for MadderLib sentences.

The builder is constructed to include all of the Phrases (rules) for building a sentence.

Traditionally this is done through KernelMethods shortcuts:

builder = madderlib :id do
  say 'through Kernel'
  ...
end

This can also be done through standard construction

builder = MadderLib::Builder.new(:id) do
  say('through construction')
  ...
end

Each time you invoke the builder using one of the following methods, its Phrases are executed from scratch using a new build context:

  • sentence : returns the resulting words as a composite String

  • words : returns a list of all the resulting words

  • each_words : iterates through each of the resulting words

You can clone an existing builder. The resulting object is ‘deeply’ cloned.

You can extend or append additional rules to an existing builder.

builder.extend { say 'something more'; ... }

You can add multiple setup and teardown blocks to the builder, which provide access to the active build context.

All of the other features of the Builder involve management of and dispatching to the current Phrase.

Defined Under Namespace

Classes: DEPENDS, ORDERED

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id = nil, &block) ⇒ Builder

Constructs a new Builder. The id is optional. When a block is provided, it is used to extend the new (empty) Builder

The new Builder is not automatically added to the active Grammar. That is the responsibility of the caller

Examples:

builder = MadderLib::Builder.new
builder.id.should be_nil
builder.should have(0).phrases

builder = MadderLib::Builder.new :id
builder.id.should equal(:id)
builder.should have(0).phrases

builder = MadderLib::Builder.new do
  say 'no id'
end
builder.id.should be_nil
builder.sentence.should eql('no id')

builder = MadderLib::Builder.new :id do
  say {|context| context.builder.id }
end
builder.sentence.should eql('id')


76
77
78
79
80
81
82
83
84
85
# File 'lib/madderlib/builder.rb', line 76

def initialize(id=nil, &block)
	@id = id
	@phrases, @phrase_ids = [], []
	@ordered, @depends = [], []
	@setup, @teardown = [], []
	@meta = {}

	self.extend &block if block_given?
	self
end

Instance Attribute Details

#idObject (readonly)

The (optional) id of the Builder



41
42
43
# File 'lib/madderlib/builder.rb', line 41

def id
  @id
end

#metaObject (readonly)

A Hash of arbitrary meta-data for the Builder. It is reserved for custom developer logic; the Builder doesn’t consider its meta-data



48
49
50
# File 'lib/madderlib/builder.rb', line 48

def meta
  @meta
end

#phrase_idsObject (readonly)

An Array containing the id from each Phrase which has one (they’re optional)



45
46
47
# File 'lib/madderlib/builder.rb', line 45

def phrase_ids
  @phrase_ids
end

#phrasesObject (readonly)

An Array of each Phrase (rule) within the Builder



43
44
45
# File 'lib/madderlib/builder.rb', line 43

def phrases
  @phrases
end

Instance Method Details

#[](k) ⇒ Object

Provides convenient access to the meta Hash.

Examples:

builder = MadderLib::Builder.new do
  meta[:key] = :value
end
builder[:key].should equal(:value)


237
238
239
# File 'lib/madderlib/builder.rb', line 237

def [](k)
	@meta[k]
end

#[]=(k, v) ⇒ Object

Provides convenient access to the meta Hash.

See: []



244
245
246
# File 'lib/madderlib/builder.rb', line 244

def []=(k, v)
	@meta[k] = v
end

#after(ref, id = nil) ⇒ Object

Allocates a phrase which is said after another Phrase. The resulting words are inserted immediately after the referenced Phrase.

This phrase would of course be followed by any subsequent after Phrase referenced against the same id. Think of it as adding layers to an onion.

If the referenced Phrase is never said, due to conditionals / odds / etc, the dependent Phrase will not be said. This of course cascades throughout the dependency chain.

The referenced Phrase must exist, by id. However, that is not checked until execution (eg. not during build). You can test your completed Builder using the validate method.

Examples:

builder = MadderLib::Builder.new do
  an(:always).says 'always'
  a(:sometimes).says('sometimes').if {|context| context.builder[:sometimes] == true }
  after(:always).say 'after-always'
  after(:sometimes, :depends).say 'after-sometimes'
  after(:depends).say 'depends'
end

builder.sentence.should eql('always after-always')

builder[:sometimes] = true

builder.sentence.should eql('always after-always sometimes after-sometimes depends')


433
434
435
# File 'lib/madderlib/builder.rb', line 433

def after(ref, id=nil)
	depends and_then(id), :after, ref
end

#alternately(*args, &block) ⇒ Object Also known as: or

A shorthand method for phrase.alternately . The Phrase#alternately method is invoked against the current Phrase with the arguments provided

An Error will be raised if there is no current Phrase. It’s an easy condition to recover from, but it’s bad use of the syntax.

Examples:

builder = MadderLib::Builder.new do
  says 'word'
  alternately.says :symbol
end
builder.or.say { 'lambda' }

builder.should have(1).phrases
builder.phrase.should have(3).instructions
%w{ word symbol lambda}.include?(builder.sentence).should be_true

Raises:



470
471
472
473
# File 'lib/madderlib/builder.rb', line 470

def alternately(*args, &block)
	raise Error, "there is no active phrase.  start one with 'say'" unless self.phrase
	self.phrase.or *args, &block
end

#an(id) ⇒ Object Also known as: a

Allocates another Phrase, where an id is required. This is semantic sugar

Examples:

builder = MadderLib::Builder.new do
  say 'first'
  a(:second).says 'second'
  an(:other).says 'other'
end

builder.sentence.should eql('first second other')


302
303
304
# File 'lib/madderlib/builder.rb', line 302

def an(id)
	and_then id
end

#and_then(id = nil) ⇒ Object Also known as: also, and, then

Allocates another Phrase. An optional Phrase id can be provided.

Examples:

builder = MadderLib::Builder.new do
  say 'first'
  and_then.say 'and_then'
  also.say 'also'
end
builder.and.say 'and'
builder.then.say 'then'


282
283
284
285
286
# File 'lib/madderlib/builder.rb', line 282

def and_then(id=nil)
	add_id id
	@phrases << Phrase.new(self, id)
	@phrases.last
end

#anytime(id = nil) ⇒ Object Also known as: anywhere

Allocates a Phrase which will be said anywhere. It’s position will be random (though not first or last, except when there is no alternative). The Phrase will only appear once; you may want to make the Phrase recur as well

Examples:

builder = MadderLib::Builder.new do
  say 'top'
  say 'here'
  say 'there'
  say 'bottom'
end
builder.anywhere.say 'anywhere'

words = builder.words
words.should have(5).words
words.find_all {|word| word == 'anywhere'}.should have(1).word

builder.it.recurs(2)

words = builder.words
words.should have(6).words
words.find_all {|word| word == 'anywhere'}.should have(2).word


368
369
370
371
372
# File 'lib/madderlib/builder.rb', line 368

def anytime(id=nil)
	add_id id
	@phrases << AnytimePhrase.new(self, id)
	ordered self.phrase, :anytime
end

#append(&block) ⇒ Object Also known as: extend

Executes the block provided within the context of the Builder instance. This provides easy contextual access to say, or, first, anytime, and all other instance methods.

Examples:

builder = MadderLib::Builder.new { say 'construct' }
builder.append { say 'extended' }
builder.extend { say 'appending' }
builder.words.should eql(%w{ construct extended appending })

Raises:



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/madderlib/builder.rb', line 95

def append(&block)
	raise Error, 'extending block is required' unless block_given?

	#	evaluate in our context
	#	available in scope
	#		closure locals
	#		static methods, if you specify self.class
	#		instance variables and methods
	#	unavailable
	#		any methods in closure scope (sorry, Tennessee)
	self.instance_eval &block
	self
end

#before(ref, id = nil) ⇒ Object

Allocates a phrase which is said before another Phrase. The resulting words are inserted immediately before the referenced Phrase.

This phrase would of course be preceded by any subsequent before Phrase referenced against the same id. Think of it as adding layers to an onion.

If the referenced Phrase is never said, due to conditionals / odds / etc, the dependent Phrase will not be said. This of course cascades throughout the dependency chain.

The referenced Phrase must exist, by id. However, that is not checked until execution (eg. not during build). You can test your completed Builder using the validate method.

Examples:

builder = MadderLib::Builder.new do
  an(:always).says 'always'
  a(:sometimes).says('sometimes').if {|context| context.builder[:sometimes] == true }
  before(:always).say 'before-always'
  before(:sometimes, :depends).say 'before-sometimes'
  before(:depends).say 'depends'
end

builder.sentence.should eql('before-always always')

builder[:sometimes] = true

builder.sentence.should eql('before-always always depends before-sometimes sometimes')


402
403
404
# File 'lib/madderlib/builder.rb', line 402

def before(ref, id=nil)
	depends and_then(id), :before, ref
end

#clone(id = nil) ⇒ Object

Creates a deep-clone of the builder. You can extend the new builder’s Phrases, or add new setup blocks, without impacting the original.

Note that the two builders will share the same original Phrase list. If you modify one of them behind-the-scenes, that change will be shared by both builders.

Examples:

original = MadderLib::Builder.new do
  meta[:meta] = :original
  say 'original'
  and_then(:shared).say('initial').likely(1)
end

cloned = original.clone
cloned[:meta] = :cloned
cloned.extend { say 'cloned' }

done = :original
cloned.setup { done = :cloned }

shared = original.phrases.find {|phrase| phrase.id == :shared }
shared.instructions.first.words << 'added'

original[:meta].should equal(:original)
original.sentence.should eql('original initial added')
done.should equal(:original)

cloned[:meta].should equal(:cloned)
cloned.sentence.should eql('original initial added cloned')
done.should equal(:cloned)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/madderlib/builder.rb', line 140

def clone(id=nil)
	o = super()

	#	deeper copy
	@phrases = @phrases.clone
	@phrase_ids = @phrase_ids.clone
	@ordered = @ordered.clone
	@depends = @depends.clone
	@setup = @setup.clone
	@teardown = @teardown.clone
	@meta = @meta.clone

	#	don't want two of them floating around with the same id
	o.instance_variable_set :@id, id

	#	put it into the grammar
	#		most importantly,
	grammar = Grammar.get_instance
	grammar.add o if grammar.builders.include?(self)
	o
end

#each_word(data = nil) ⇒ Object Also known as: each

Iterates through each of the words resulting from execution of the Builder.

An optional Hash of Context data can be provided. It is merged into Context#data before the Builder is executed



482
483
484
485
# File 'lib/madderlib/builder.rb', line 482

def each_word(data=nil)
	#	from our words
	self.words(data).each {|word| yield word }
end

#first(id = nil) ⇒ Object

Allocates a Phrase which will be said first, relative to any existing Phrases. An optional Phrase id can be provided

This phrase would of course be preceded by any subsequent first Phrase

Examples:

builder = MadderLib::Builder.new do
  say 'something'
  first.say 'say'
end
builder.sentence.should eql('say something')

builder.first.say 'first'
builder.sentence.should eql('first say something')


323
324
325
# File 'lib/madderlib/builder.rb', line 323

def first(id=nil)
	ordered and_then(id), :first
end

#last(id = nil) ⇒ Object Also known as: lastly

Allocates a Phrase which will be said last, relative to any existing Phrases. An optional Phrase id can be provided

This phrase would of course be followed by any subsequent last Phrase

Examples:

builder = MadderLib::Builder.new do
  last.say 'said'
  say 'something'
end
builder.sentence.should eql('something said')

builder.last.say 'last'
builder.sentence.should eql('something said last')


341
342
343
# File 'lib/madderlib/builder.rb', line 341

def last(id=nil)
	ordered and_then(id), :last
end

#phraseObject Also known as: it

Returns the current Phrase.

Examples:

builder = MadderLib::Builder.new do
  say 'yes'
  phrase.if {|context| context.builder[:activated] == true }
  it.repeat(3)
end

builder.should have(1).phrases
builder.phrase.should have(1).instructions

builder.should have(0).words

builder[:activated] = true
builder.sentence.should eql('yes yes yes')


266
267
268
# File 'lib/madderlib/builder.rb', line 266

def phrase
	@phrases.last
end

#say(*args, &block) ⇒ Object Also known as: says

A shorthand method for and_then.say . A new Phrase is allocated (without an id), and then Phrase#say method is invoked with the arguments provided.

Examples:

builder = MadderLib::Builder.new do
  says 'word'
  say :symbol
  say { 'lambda' }
end
builder.should have(3).phrases
builder.sentence.should eql('word symbol lambda')


449
450
451
# File 'lib/madderlib/builder.rb', line 449

def say(*args, &block)
	and_then.say *args, &block
end

#sentence(*args, &block) ⇒ Object Also known as: to_s

Returns the composite sentence resulting from execution of the Builder. It’s really just a shortcut for words.join .

An optional separator String can be provided. The default separator is a single space

An optional Hash of Context data can be provided. It is merged into Context#data before the Phrase rules are executed

An optional block can be provided. It will be invoked before the Phrase rules are executed The block can either take no arguments, or a Context.

Examples:

builder = MadderLib::Builder.new do
  says 'word'
  say :symbol, [:with, :hierarchy]
  say { 'lambda' }
end
builder.sentence.should eql('word symbol with hierarchy lambda')


540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/madderlib/builder.rb', line 540

def sentence(*args, &block)
	#	argument scan
	sep, data = ' ', nil
	args.each do |arg|
		if String === arg
			#	separator
			sep = arg
		elsif Hash === arg
			#	context data
			data = arg
		end
	end

	self.words(data, &block).join(sep)
end

#setup(*args, &block) ⇒ Object

Adds a setup block to the builder.

The block is executed before the builder invokes its Phrases (rules). The block can either take no arguments, or a Context.

Subsequent blocks are executed in the order provided. If you provide :first as an argument, the block will occur prior to any existing blocks. It would of course be preceded by any subsequent block which says that it is :first.

Examples:

builder = MadderLib::Builder.new do
  say {|context| context[:setup] }
end
builder.setup {|context| context[:setup] << 2 }
builder.setup {|context| context[:setup] << 3 }
builder.setup(:first) {|context| context[:setup] = [1] }

builder.sentence.should eql('1 2 3')


182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/madderlib/builder.rb', line 182

def setup(*args, &block)
	Context.validate(block)

	#	ordering
	if args.include?(:first)
		@setup.unshift block
	else
		@setup.push block
	end

	self
end

#teardown(*args, &block) ⇒ Object

Adds a teardown block to the builder.

The block is executed after the builder has invoked its Phrases (rules). The block can either take no arguments, or a Context.

Subsequent blocks are executed in the order provided. If you provide :first as an argument, the block will occur prior to any existing blocks. It would of course be preceded by any subsequent block which says that it is :first.

Examples:

builder = MadderLib::Builder.new do
  say 'teardown'
end
markers = []
builder.teardown {|context| markers << 2 }
builder.teardown {|context| markers << 3 }
builder.teardown(:first) {|context| markers = [1] }

builder.sentence.should eql('teardown')
markers.should eql([1, 2, 3])


215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/madderlib/builder.rb', line 215

def teardown(*args, &block)
	Context.validate(block)

	#	ordering
	if args.include?(:first)
		@teardown.unshift block
	else
		@teardown.push block
	end

	self
end

#to_sequencerObject Also known as: validate

:nodoc:



560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
# File 'lib/madderlib/builder.rb', line 560

def to_sequencer #:nodoc:
	#	general ordering
	sequence = []
	map = {}
	@phrases.each do |phrase|
		sequence << phrase unless self.ordered?(phrase) || self.depends?(phrase)
		map[phrase.id] = phrase if phrase.id
	end

	#	specified ordering
	#	anytimes
	anytimes = []
	@ordered.each do |o|
		case o.type
			when :first
				#	before all other firsts
				sequence.unshift o.phrase
			when :last
				#	after all other lasts
				sequence.push o.phrase

			when :anytime
				#	guarantee valid references
				phrase = o.phrase

				[phrase.before, phrase.after].each do |ref|
					raise Error, "no such phrase : #{ref.inspect}" unless (!ref) || map[ref]
				end
				anytimes << phrase

			else
				raise Error, "unknown ordering : #{o.type.inspect}"
		end
	end

	befores, afters = {}, {}
	@depends.each do |o|
		ref = o.ref
		raise Error, "no such phrase : #{ref.inspect}" unless map[ref]
		case o.type
			when :before
				phrases = befores[ref]
				befores[ref] = (phrases = []) unless phrases
				#	before all other befores
				phrases.unshift o.phrase
			when :after
				phrases = afters[ref]
				afters[ref] = (phrases = []) unless phrases
				#	after all other afters
				phrases.push o.phrase
			else
				raise Error, "unknown dependency : #{o.type.inspect}"
		end
	end

	Sequencer.new(self, sequence, map.keys, {
		:anytime => anytimes, :before => befores, :after => afters,
		:setup => @setup, :teardown => @teardown
	})
end

#words(data = nil, &block) ⇒ Object Also known as: to_a

Returns the array of words resulting from execution of the Builder.

An optional Hash of Context data can be provided. It is merged into Context#data before the Phrase rules are executed

An optional block can be provided. It will be invoked before the Phrase rules are executed The block can either take no arguments, or a Context.

All Phrase rules are applied. Each word in the Array is a String. The resulting Array is flattened (vs. any Array hierarchies in the ruleset)

Examples:

builder = MadderLib::Builder.new do
  says 'word'
  say :symbol, [:with, :hierarchy]
  say { 'lambda' }
end
builder.words.should eql(%w{ word symbol with hierarchy lambda })


508
509
510
511
512
513
514
515
516
517
# File 'lib/madderlib/builder.rb', line 508

def words(data=nil, &block)
	#	words from a sequencer
	#		pass on the context data
	#		pass on the block, to pull in the context
	#	a new Sequencer each time
	#	TODO: optimize
	#		dirty flag is hard since phrases is exposed
	#		hashsum?  clone of last known phrases PLUS dirty flag?
	self.to_sequencer.words data, &block
end