hammer_builder 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # HammerBuilder
2
2
 
3
3
  [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
4
- is a xhtml5 builder written in Ruby 1.9.2. It does not introduce anything special, you just
4
+ is a xhtml5 builder written in and for Ruby 1.9.2. It does not introduce anything special, you just
5
5
  use Ruby to get your xhtml. [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
6
6
  has been written with three objectives:
7
7
 
@@ -16,6 +16,7 @@ has been written with three objectives:
16
16
  * Yardoc: [http://hammer.pitr.ch/hammer-builder/](http://hammer.pitr.ch/hammer-builder/)
17
17
  * Issues: [https://github.com/ruby-hammer/hammer-builder/issues](https://github.com/ruby-hammer/hammer-builder/issues)
18
18
  * Changelog: [http://hammer.pitr.ch/hammer-builder/file.CHANGELOG.html](http://hammer.pitr.ch/hammer-builder/file.CHANGELOG.html)
19
+ * Gem: [https://rubygems.org/gems/hammer_builder](https://rubygems.org/gems/hammer_builder)
19
20
 
20
21
  ## Syntax
21
22
 
@@ -1,6 +1,7 @@
1
1
  require 'cgi'
2
2
  require 'active_support/core_ext/class/inheritable_attributes'
3
3
  require 'active_support/core_ext/string/inflections'
4
+ require 'hammer_builder/dynamic_classes'
4
5
 
5
6
  module HammerBuilder
6
7
  EXTRA_ATTRIBUTES = {
@@ -133,7 +134,7 @@ module HammerBuilder
133
134
  ]
134
135
 
135
136
  DOUBLE_TAGS = [
136
- 'a', 'abbr', 'article', 'aside', 'audio', 'address',
137
+ 'a', 'abbr', 'article', 'aside', 'audio', 'address',
137
138
  'b', 'bdo', 'blockquote', 'body', 'button',
138
139
  'canvas', 'caption', 'cite', 'code', 'colgroup', 'command',
139
140
  'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt',
@@ -207,64 +208,6 @@ module HammerBuilder
207
208
  end
208
209
  end
209
210
 
210
- module RedefinableClassTree
211
- # defines new class
212
- # @param [Symbol] class_name
213
- # @param [Symbol] superclass_name e.g. :AbstractEmptyTag
214
- # @yield definition block which is evaluated inside the new class, doing so defining the class's methods etc.
215
- def define_class(class_name, superclass_name = nil, &definition)
216
- class_name = class_name(class_name)
217
- superclass_name = class_name(superclass_name) if superclass_name
218
-
219
- raise "class: '#{class_name}' already defined" if respond_to? method_class(class_name)
220
-
221
- define_singleton_method method_class(class_name) do |builder|
222
- builder.instance_variable_get("@#{method_class(class_name)}") || begin
223
- klass = builder.send(method_class_definition(class_name), builder)
224
- builder.const_set class_name, klass
225
- builder.instance_variable_set("@#{method_class(class_name)}", klass)
226
- end
227
- end
228
-
229
- define_singleton_method method_class_definition(class_name) do |builder|
230
- superclass = if superclass_name
231
- builder.send method_class(superclass_name), builder
232
- else
233
- Object
234
- end
235
- Class.new(superclass, &definition)
236
- end
237
- end
238
-
239
- # extends existing class
240
- # @param [Symbol] class_name
241
- # @yield definition block which is evaluated inside the new class, doing so extending the class's methods etc.
242
- def extend_class(class_name, &definition)
243
- raise "class: '#{class_name}' not defined" unless respond_to? method_class(class_name)
244
-
245
- define_singleton_method method_class_definition(class_name) do |builder|
246
- ancestor = super(builder)
247
- count = 1; count += 1 while builder.const_defined? "#{class_name}Super#{count}"
248
- builder.const_set "#{class_name}Super#{count}", ancestor
249
- Class.new(ancestor, &definition)
250
- end
251
- end
252
-
253
- private
254
-
255
- def class_name(klass)
256
- klass.to_s.camelize
257
- end
258
-
259
- def method_class(klass)
260
- "#{klass.to_s.underscore}_class"
261
- end
262
-
263
- def method_class_definition(klass)
264
- "#{method_class(klass)}_definition"
265
- end
266
- end
267
-
268
211
  # Creating builder instances is expensive, therefore you can use Pool to go around that
269
212
  module Pool
270
213
  def self.included(base)
@@ -275,7 +218,7 @@ module HammerBuilder
275
218
  module ClassMethods
276
219
  # This the preferred way of getting new Builder. If you forget to release it, it does not matter -
277
220
  # builder gets GCed after you lose reference
278
- # @return [Standard, Formated]
221
+ # @return [Standard, Formated]
279
222
  def get
280
223
  mutex.synchronize do
281
224
  if free_builders.empty?
@@ -321,7 +264,7 @@ module HammerBuilder
321
264
 
322
265
  # Abstract implementation of Builder
323
266
  class Abstract
324
- extend RedefinableClassTree
267
+ extend DynamicClasses
325
268
  include Pool
326
269
 
327
270
  # << faster then +
@@ -330,142 +273,146 @@ module HammerBuilder
330
273
  # class_eval faster then define_method
331
274
  # beware of strings in methods -> creates a lot of garbage
332
275
 
333
- define_class :AbstractTag do
334
- def initialize(builder)
335
- @builder = builder
336
- @output = builder.instance_eval { @output }
337
- @stack = builder.instance_eval { @stack }
338
- @classes = []
339
- set_tag
340
- end
276
+ dc do
341
277
 
342
- def open(attributes = nil)
343
- @output << LT << @tag
344
- @builder.current = self
345
- attributes(attributes)
346
- default
347
- self
348
- end
278
+ define :AbstractTag do
279
+ def initialize(builder)
280
+ @builder = builder
281
+ @output = builder.instance_eval { @output }
282
+ @stack = builder.instance_eval { @stack }
283
+ @classes = []
284
+ set_tag
285
+ end
349
286
 
350
- # @example
351
- # div.attributes :id => 'id' # => <div id="id"></div>
352
- def attributes(attrs)
353
- return self unless attrs
354
- attrs.each do |attr, value|
355
- __send__(attr, *value)
287
+ def open(attributes = nil)
288
+ @output << LT << @tag
289
+ @builder.current = self
290
+ attributes(attributes)
291
+ default
292
+ self
356
293
  end
357
- self
358
- end
359
294
 
360
- # @example
361
- # div.attribute :id, 'id' # => <div id="id"></div>
362
- def attribute(attribute, content)
363
- @output << SPACE << attribute.to_s << EQL_QUOTE << CGI.escapeHTML(content.to_s) << QUOTE
364
- end
295
+ # @example
296
+ # div.attributes :id => 'id' # => <div id="id"></div>
297
+ def attributes(attrs)
298
+ return self unless attrs
299
+ attrs.each do |attr, value|
300
+ __send__(attr, *value)
301
+ end
302
+ self
303
+ end
365
304
 
366
- alias_method(:rclass, :class)
305
+ # @example
306
+ # div.attribute :id, 'id' # => <div id="id"></div>
307
+ # @deprecated Please use {#attributes} instead
308
+ def attribute(attribute, content) # TODO lose the method in 0.2
309
+ warn ("method #attribute is deprecated use #attributes instead, called from:#{caller[0]}" )
310
+ @output << SPACE << attribute.to_s << EQL_QUOTE << CGI.escapeHTML(content.to_s) << QUOTE
311
+ end
367
312
 
368
- class_inheritable_array :_attributes, :instance_writer => false, :instance_reader => false
313
+ alias_method(:rclass, :class)
369
314
 
370
- def self.attributes
371
- self._attributes
372
- end
315
+ class_inheritable_array :_attributes, :instance_writer => false, :instance_reader => false
373
316
 
374
- # allows data-* attributes
375
- def method_missing(method, *args, &block)
376
- if method.to_s =~ /data_([a-z_]+)/
377
- self.rclass.attributes = [method.to_s]
378
- self.send method, *args, &block
379
- else
380
- super
317
+ def self.attributes
318
+ self._attributes
381
319
  end
382
- end
383
-
384
- protected
385
320
 
386
- # sets the right tag in descendants
387
- def self.set_tag(tag)
388
321
  class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
389
- def set_tag
390
- @tag = '#{tag}'.freeze
322
+ # allows data-* attributes
323
+ def method_missing(method, *args, &block)
324
+ if method.to_s =~ /data_([a-z_]+)/
325
+ self.rclass.attributes = [method.to_s]
326
+ self.send method, *args, &block
327
+ else
328
+ super
391
329
  end
330
+ end
392
331
  RUBYCODE
393
- end
394
332
 
395
- # this method is called on each tag opening, useful for default attributes
396
- # @example html tag uses this to add xmlns attr.
397
- # html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
398
- def default
399
- end
333
+ protected
400
334
 
401
- # defines dynamically methods for attributes
402
- def self.define_attributes
403
- attributes.each do |attr|
404
- next if instance_methods.include?(attr.to_sym)
335
+ # sets the right tag in descendants
336
+ def self.set_tag(tag)
405
337
  class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
338
+ def set_tag
339
+ @tag = '#{tag}'.freeze
340
+ end
341
+ RUBYCODE
342
+ end
343
+
344
+ set_tag 'abstract'
345
+
346
+ # this method is called on each tag opening, useful for default attributes
347
+ # @example html tag uses this to add xmlns attr.
348
+ # html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
349
+ def default
350
+ end
351
+
352
+ # defines dynamically methods for attributes
353
+ def self.define_attributes
354
+ attributes.each do |attr|
355
+ next if instance_methods.include?(attr.to_sym)
356
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
406
357
  def #{attr}(content)
407
358
  @output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
408
359
  self
409
360
  end
410
- RUBYCODE
361
+ RUBYCODE
362
+ end
363
+ define_attribute_constants
411
364
  end
412
- define_attribute_constants
413
- end
414
365
 
415
- # defines constant strings not to make garbage
416
- def self.define_attribute_constants
417
- attributes.each do |attr|
418
- const = "attr_#{attr}".upcase
419
- HammerBuilder.const_set const, " #{attr.gsub('_', '-')}=\"".freeze unless HammerBuilder.const_defined?(const)
366
+ # defines constant strings not to make garbage
367
+ def self.define_attribute_constants
368
+ attributes.each do |attr|
369
+ const = "attr_#{attr}".upcase
370
+ HammerBuilder.const_set const, " #{attr.gsub('_', '-')}=\"".freeze unless HammerBuilder.const_defined?(const)
371
+ end
420
372
  end
421
- end
422
-
423
- # adds attribute to class, triggers dynamical creation of needed instance methods etc.
424
- def self.attributes=(attributes)
425
- self._attributes = attributes
426
- define_attributes
427
- end
428
373
 
429
- # flushes classes to output
430
- def flush_classes
431
- unless @classes.empty?
432
- @output << ATTR_CLASS << CGI.escapeHTML(@classes.join(SPACE)) << QUOTE
433
- @classes.clear
374
+ # adds attribute to class, triggers dynamical creation of needed instance methods etc.
375
+ def self.attributes=(attributes)
376
+ self._attributes = attributes
377
+ define_attributes
434
378
  end
435
- end
436
379
 
437
- def set_tag
438
- @tag = 'abstract'
439
- end
380
+ # flushes classes to output
381
+ def flush_classes
382
+ unless @classes.empty?
383
+ @output << ATTR_CLASS << CGI.escapeHTML(@classes.join(SPACE)) << QUOTE
384
+ @classes.clear
385
+ end
386
+ end
440
387
 
441
- public
388
+ public
442
389
 
443
- # global HTML5 attributes
444
- self.attributes = GLOBAL_ATTRIBUTES
390
+ # global HTML5 attributes
391
+ self.attributes = GLOBAL_ATTRIBUTES
445
392
 
446
- alias :[] :id
393
+ alias :[] :id
447
394
 
448
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
395
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
449
396
  def class(*classes)
450
397
  @classes.push(*classes)
451
398
  self
452
399
  end
453
- RUBYCODE
454
- end
400
+ RUBYCODE
401
+ end
455
402
 
456
- define_class :AbstractEmptyTag, :AbstractTag do
457
- def flush
458
- flush_classes
459
- @output << SLASH_GT
460
- nil
403
+ define :AbstractEmptyTag, :AbstractTag do
404
+ def flush
405
+ flush_classes
406
+ @output << SLASH_GT
407
+ nil
408
+ end
461
409
  end
462
- end
463
410
 
464
- define_class :AbstractDoubleTag, :AbstractTag do
465
- # defined by class_eval because there is a super calling, causing error:
466
- # super from singleton method that is defined to multiple classes is not supported;
467
- # this will be fixed in 1.9.3 or later (NotImplementedError)
468
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
411
+ define :AbstractDoubleTag, :AbstractTag do
412
+ # defined by class_eval because there is a super calling, causing error:
413
+ # super from singleton method that is defined to multiple classes is not supported;
414
+ # this will be fixed in 1.9.3 or later (NotImplementedError)
415
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
469
416
  def initialize(builder)
470
417
  super
471
418
  @content = nil
@@ -484,61 +431,62 @@ module HammerBuilder
484
431
  self
485
432
  end
486
433
  end
487
- RUBYCODE
434
+ RUBYCODE
488
435
 
489
- def flush
490
- flush_classes
491
- @output << GT
492
- @output << CGI.escapeHTML(@content) if @content
493
- @output << SLASH_LT << @stack.pop << GT
494
- @content = nil
495
- end
436
+ def flush
437
+ flush_classes
438
+ @output << GT
439
+ @output << CGI.escapeHTML(@content) if @content
440
+ @output << SLASH_LT << @stack.pop << GT
441
+ @content = nil
442
+ end
496
443
 
497
- # sets content of the double tag
498
- def content(content)
499
- @content = content.to_s
500
- self
501
- end
444
+ # sets content of the double tag
445
+ def content(content)
446
+ @content = content.to_s
447
+ self
448
+ end
502
449
 
503
- # renders content of the double tag with block
504
- def with
505
- flush_classes
506
- @output << GT
507
- @content = nil
508
- @builder.current = nil
509
- yield
510
- # if (content = yield).is_a?(String)
511
- # @output << CGI.escapeHTML(content)
512
- # end
513
- @builder.flush
514
- @output << SLASH_LT << @stack.pop << GT
515
- nil
516
- end
450
+ # renders content of the double tag with block
451
+ def with
452
+ flush_classes
453
+ @output << GT
454
+ @content = nil
455
+ @builder.current = nil
456
+ yield
457
+ # if (content = yield).is_a?(String)
458
+ # @output << CGI.escapeHTML(content)
459
+ # end
460
+ @builder.flush
461
+ @output << SLASH_LT << @stack.pop << GT
462
+ nil
463
+ end
517
464
 
518
- protected
465
+ protected
519
466
 
520
- def self.define_attributes
521
- attributes.each do |attr|
522
- next if instance_methods(false).include?(attr.to_sym)
523
- if instance_methods.include?(attr.to_sym)
524
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
467
+ def self.define_attributes
468
+ attributes.each do |attr|
469
+ next if instance_methods(false).include?(attr.to_sym)
470
+ if instance_methods.include?(attr.to_sym)
471
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
525
472
  def #{attr}(*args, &block)
526
473
  super(*args, &nil)
527
474
  return with(&block) if block
528
475
  self
529
476
  end
530
- RUBYCODE
531
- else
532
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
477
+ RUBYCODE
478
+ else
479
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
533
480
  def #{attr}(content, &block)
534
481
  @output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
535
482
  return with(&block) if block
536
483
  self
537
484
  end
538
- RUBYCODE
485
+ RUBYCODE
486
+ end
539
487
  end
488
+ define_attribute_constants
540
489
  end
541
- define_attribute_constants
542
490
  end
543
491
  end
544
492
 
@@ -549,6 +497,7 @@ module HammerBuilder
549
497
 
550
498
  # defines instance method for +tag+ in builder
551
499
  def self.define_tag(tag)
500
+ tag = tag.to_s
552
501
  class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
553
502
  def #{tag}(*args, &block)
554
503
  flush
@@ -568,7 +517,7 @@ module HammerBuilder
568
517
  @current = nil
569
518
  # tag classes initialization
570
519
  tags.values.each do |klass|
571
- instance_variable_set(:"@#{klass}", self.class.send("#{klass}_class", self.class).new(self))
520
+ instance_variable_set(:"@#{klass}", self.class.dc[klass.camelize.to_sym].new(self))
572
521
  end
573
522
  end
574
523
 
@@ -631,6 +580,13 @@ module HammerBuilder
631
580
  self
632
581
  end
633
582
 
583
+ def set_variables(instance_variables)
584
+ instance_variables.each {|name,value| instance_variable_set("@#{name}", value) }
585
+ yield
586
+ instance_variables.each {|name,_| remove_instance_variable("@#{name}") }
587
+ self
588
+ end
589
+
634
590
  # @return [String] output
635
591
  def to_xhtml()
636
592
  flush
@@ -655,66 +611,86 @@ module HammerBuilder
655
611
  # Builder implementation without formating (one line)
656
612
  class Standard < Abstract
657
613
 
658
- (DOUBLE_TAGS - ['html']).each do |tag|
659
- define_class tag.camelize , :AbstractDoubleTag do
660
- set_tag tag
661
- self.attributes = EXTRA_ATTRIBUTES[tag]
614
+ dc do
615
+ (DOUBLE_TAGS - ['html']).each do |tag|
616
+ define tag.camelize.to_sym , :AbstractDoubleTag do
617
+ set_tag tag
618
+ self.attributes = EXTRA_ATTRIBUTES[tag]
619
+ end
620
+
621
+ base.define_tag(tag)
662
622
  end
663
623
 
664
- define_tag(tag)
665
- end
624
+ define :Html, :AbstractDoubleTag do
625
+ set_tag 'html'
626
+ self.attributes = ['xmlns'] + EXTRA_ATTRIBUTES['html']
666
627
 
667
- define_class :Html, :AbstractDoubleTag do
668
- set_tag 'html'
669
- self.attributes = ['xmlns'] + EXTRA_ATTRIBUTES['html']
628
+ def default
629
+ xmlns('http://www.w3.org/1999/xhtml')
630
+ end
631
+ end
632
+ base.define_tag('html')
633
+
634
+ EMPTY_TAGS.each do |tag|
635
+ define tag.camelize.to_sym, :AbstractEmptyTag do
636
+ set_tag tag
637
+ self.attributes = EXTRA_ATTRIBUTES[tag]
638
+ end
670
639
 
671
- def default
672
- xmlns('http://www.w3.org/1999/xhtml')
640
+ base.define_tag(tag)
673
641
  end
674
642
  end
675
643
 
676
- define_tag('html')
677
-
678
644
  def js(js , options = {})
645
+ flush
679
646
  script({:type => "text/javascript"}.merge(options)) { cdata js }
680
647
  end
681
648
 
682
- EMPTY_TAGS.each do |tag|
683
- define_class tag.camelize, :AbstractEmptyTag do
684
- set_tag tag
685
- self.attributes = EXTRA_ATTRIBUTES[tag]
649
+ def join(collection, glue, &it)
650
+ flush
651
+ glue_block = if glue.is_a? String
652
+ lambda { text glue }
653
+ else
654
+ glue
655
+ end
656
+
657
+ collection.each_with_index do |obj, i|
658
+ glue_block.call() if i > 0
659
+ it.call(obj)
686
660
  end
687
-
688
- define_tag(tag)
689
661
  end
662
+
690
663
  end
691
664
 
692
665
  # Builder implementation with formating (indented by ' ')
693
666
  # Slow down is less then 1%
694
667
  class Formated < Standard
695
- extend_class :AbstractTag do
696
- def open(attributes = nil)
697
- @output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << LT << @tag
698
- @builder.current = self
699
- attributes(attributes)
700
- default
701
- self
668
+
669
+ dc do
670
+ extend :AbstractTag do
671
+ def open(attributes = nil)
672
+ @output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << LT << @tag
673
+ @builder.current = self
674
+ attributes(attributes)
675
+ default
676
+ self
677
+ end
702
678
  end
703
- end
704
679
 
705
- extend_class :AbstractDoubleTag do
706
- def with
707
- flush_classes
708
- @output << GT
709
- @content = nil
710
- @builder.current = nil
711
- yield
712
- # if (content = yield).is_a?(String)
713
- # @output << CGI.escapeHTML(content)
714
- # end
715
- @builder.flush
716
- @output << NEWLINE << SPACES.fetch(@stack.size-1, SPACE) << SLASH_LT << @stack.pop << GT
717
- nil
680
+ extend :AbstractDoubleTag do
681
+ def with
682
+ flush_classes
683
+ @output << GT
684
+ @content = nil
685
+ @builder.current = nil
686
+ yield
687
+ # if (content = yield).is_a?(String)
688
+ # @output << CGI.escapeHTML(content)
689
+ # end
690
+ @builder.flush
691
+ @output << NEWLINE << SPACES.fetch(@stack.size-1, SPACE) << SLASH_LT << @stack.pop << GT
692
+ nil
693
+ end
718
694
  end
719
695
  end
720
696
 
@@ -0,0 +1,205 @@
1
+ require 'active_support/core_ext/object/try'
2
+
3
+ # When extended into a class it enables easy defining and extending classes in extended class.
4
+ #
5
+ # class A
6
+ # extend DynamicClasses
7
+ # dc do
8
+ # define :A do
9
+ # def to_s
10
+ # 'a'
11
+ # end
12
+ # end
13
+ # define :B, :A do
14
+ # class_eval <<-RUBYCODE, __FILE__, __LINE__+1
15
+ # def to_s
16
+ # super + 'b'
17
+ # end
18
+ # RUBYCODE
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # class B < A
24
+ # end
25
+ #
26
+ # class C < A
27
+ # dc do
28
+ # extend :A do
29
+ # def to_s
30
+ # 'aa'
31
+ # end
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # puts A.dc[:A] # => #<Class:0x00000001d449b8(A.dc[:A])>
37
+ # puts B.dc[:A] # => #<Class:0x00000001d42398(B.dc[:A])>
38
+ # puts B.dc[:A].new # => a
39
+ # puts B.dc[:B].new # => ab
40
+ # puts C.dc[:B].new # => aab
41
+ #
42
+ # Last example is the most interesting. It prints 'aab' not 'ab' because of the extension in class C. Class :B has
43
+ # as ancestor extended class :A from C therefore the two 'a'.
44
+ module DynamicClasses
45
+
46
+ # Adds ability to describe itself when class is defined without constant
47
+ module Describable
48
+ def self.included(base)
49
+ base.singleton_class.send :alias_method, :original_to_s, :to_s
50
+ base.extend ClassMethods
51
+ end
52
+
53
+ module ClassMethods
54
+ # sets +description+
55
+ # @param [String] description
56
+ def _description=(description)
57
+ @_description = description
58
+ end
59
+
60
+ def to_s
61
+ super.gsub(/>$/, "(#{@_description})>")
62
+ end
63
+ end
64
+
65
+ def to_s
66
+ klass = respond_to?(:rclass) ? self.rclass : self.class
67
+ super.gsub(klass.original_to_s, klass.to_s)
68
+ end
69
+ end
70
+
71
+ class DescribableClass
72
+ include Describable
73
+ end
74
+
75
+ ClassDefinition = Struct.new(:name, :base, :superclass_or_name, :definition)
76
+ ClassExtension = Struct.new(:name, :base, :definition)
77
+
78
+ class Classes
79
+ attr_reader :base, :class_definitions, :classes, :class_extensions
80
+
81
+ def initialize(base)
82
+ raise unless base.is_a? Class
83
+ @base = base
84
+ @class_definitions = {}
85
+ @class_extensions = {}
86
+ @classes = {}
87
+ end
88
+
89
+ # define a class
90
+ # @param [Symbol] name
91
+ # @param [Symbol, Class, nil] superclass_or_name
92
+ # when Symbol then dynamic class is found
93
+ # when Class then this class is used
94
+ # when nil then Object is used
95
+ # @yield definition block is evaluated inside the class defining it
96
+ def define(name, superclass_or_name = nil, &definition)
97
+ raise ArgumentError, "name is not a Symbol" unless name.is_a?(Symbol)
98
+ unless superclass_or_name.is_a?(Symbol) || superclass_or_name.is_a?(Class) || superclass_or_name.nil?
99
+ raise ArgumentError, "superclass_or_name is not a Symbol, Class or nil"
100
+ end
101
+ raise ArgumentError, "definition is nil" unless definition
102
+ raise ArgumentError, "Class #{name} already defined" if class_definition(name)
103
+ @class_definitions[name] = ClassDefinition.new(name, base, superclass_or_name, definition)
104
+ end
105
+
106
+ # extends already defined class by adding a child,
107
+ # @param [Symbol] name
108
+ # @yield definition block is evaluated inside the class extending it
109
+ def extend(name, &definition)
110
+ raise ArgumentError, "name is not a Symbol" unless name.is_a?(Symbol)
111
+ raise ArgumentError, "definition is nil" unless definition
112
+ raise ArgumentError, "Class #{name} not defined" unless class_definition(name)
113
+ @class_extensions[name] = ClassExtension.new(name, base, definition)
114
+ end
115
+
116
+ # triggers loading of all defined classes
117
+ def load!
118
+ class_names.each {|name| self[name] }
119
+ end
120
+
121
+ # @return [Class] defined class
122
+ def [](name)
123
+ return @classes[name] if @classes[name]
124
+ return nil unless klass_definition = class_definition(name)
125
+
126
+ superclass = case klass_definition.superclass_or_name
127
+ when Symbol then self[klass_definition.superclass_or_name]
128
+ when Class then
129
+ klass = Class.new(klass_definition.superclass_or_name)
130
+ klass.send :include, Describable
131
+ klass._description = "Describable#{klass_definition.superclass_or_name}"
132
+ klass
133
+ when nil then DescribableClass
134
+ end
135
+ klass = Class.new(superclass, &klass_definition.definition)
136
+ klass._description = "#{base}.dc[:#{klass_definition.name}]"
137
+
138
+ class_extensions(name).each do |klass_extension|
139
+ klass = Class.new(klass, &klass_extension.definition)
140
+ klass._description = "#{base}.dc[:#{klass_extension.name}]"
141
+ end
142
+
143
+ @classes[name] = klass
144
+ end
145
+
146
+ private
147
+
148
+ def class_names
149
+ ancestors.map(&:class_definitions).map(&:keys).flatten
150
+ end
151
+
152
+ def class_definition(name)
153
+ @class_definitions[name] || ancestor.try(:class_definition, name)
154
+ end
155
+
156
+ def class_extensions(name)
157
+ ( [*ancestor.try(:class_extensions, name)] + [@class_extensions[name]] ).compact
158
+ end
159
+
160
+ def ancestors
161
+ ( [self] + [*ancestor.try(:ancestors)] ).compact
162
+ end
163
+
164
+ def ancestor
165
+ @base.superclass.dynamic_classes if @base.superclass.kind_of?(DynamicClasses)
166
+ end
167
+ end
168
+
169
+ # hook to create Classes instance
170
+ def self.extended(base)
171
+ base.send :create_dynamic_classes
172
+ super
173
+ end
174
+
175
+ # hook to create Classes instance in descendants
176
+ def inherited(base)
177
+ base.send :create_dynamic_classes
178
+ super
179
+ end
180
+
181
+ # call this to get access to Classes instance to define/extend classes inside +definition+
182
+ # calls Classes#load! to preload defined classes
183
+ # @yield [Proc, nil] definition
184
+ # a Proc enables writing class definitions/extensions
185
+ # @return [Classes] when definition is nil
186
+ def dynamic_classes(&definition)
187
+ if definition
188
+ @dynamic_classes.instance_eval &definition
189
+ @dynamic_classes.load!
190
+ nil
191
+ else
192
+ @dynamic_classes
193
+ end
194
+ end
195
+
196
+ alias :dc :dynamic_classes
197
+
198
+ private
199
+
200
+ def create_dynamic_classes
201
+ @dynamic_classes = Classes.new(self)
202
+ end
203
+ end
204
+
205
+
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 0
9
- version: 0.1.0
8
+ - 1
9
+ version: 0.1.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Petr Chalupa
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-05-11 00:00:00 +02:00
17
+ date: 2011-05-16 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -102,6 +102,7 @@ extra_rdoc_files:
102
102
  - README.md
103
103
  files:
104
104
  - lib/hammer_builder.rb
105
+ - lib/hammer_builder/dynamic_classes.rb
105
106
  - LICENSE
106
107
  - README.md
107
108
  - spec/hammer_builder_spec.rb