doodle 0.2.2 → 0.2.3

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 (48) hide show
  1. data/History.txt +24 -0
  2. data/Manifest.txt +26 -1
  3. data/README.txt +9 -8
  4. data/lib/doodle.rb +43 -1496
  5. data/lib/doodle/app.rb +6 -0
  6. data/lib/doodle/attribute.rb +165 -0
  7. data/lib/doodle/base.rb +180 -0
  8. data/lib/doodle/collector-1.9.rb +72 -0
  9. data/lib/doodle/collector.rb +191 -0
  10. data/lib/doodle/comparable.rb +8 -0
  11. data/lib/doodle/conversion.rb +80 -0
  12. data/lib/doodle/core.rb +42 -0
  13. data/lib/doodle/datatype-holder.rb +39 -0
  14. data/lib/doodle/debug.rb +20 -0
  15. data/lib/doodle/deferred.rb +13 -0
  16. data/lib/doodle/equality.rb +21 -0
  17. data/lib/doodle/exceptions.rb +29 -0
  18. data/lib/doodle/factory.rb +91 -0
  19. data/lib/doodle/getter-setter.rb +154 -0
  20. data/lib/doodle/info.rb +298 -0
  21. data/lib/doodle/inherit.rb +40 -0
  22. data/lib/doodle/json.rb +38 -0
  23. data/lib/doodle/marshal.rb +16 -0
  24. data/lib/doodle/normalized_array.rb +512 -0
  25. data/lib/doodle/normalized_hash.rb +356 -0
  26. data/lib/doodle/ordered-hash.rb +8 -0
  27. data/lib/doodle/singleton.rb +23 -0
  28. data/lib/doodle/smoke-and-mirrors.rb +23 -0
  29. data/lib/doodle/to_hash.rb +17 -0
  30. data/lib/doodle/utils.rb +173 -11
  31. data/lib/doodle/validation.rb +122 -0
  32. data/lib/doodle/version.rb +1 -1
  33. data/lib/molic_orderedhash.rb +24 -10
  34. data/spec/assigned_spec.rb +45 -0
  35. data/spec/attributes_spec.rb +7 -7
  36. data/spec/collector_spec.rb +100 -13
  37. data/spec/doodle_context_spec.rb +5 -5
  38. data/spec/from_spec.rb +43 -3
  39. data/spec/json_spec.rb +232 -0
  40. data/spec/member_init_spec.rb +11 -11
  41. data/spec/modules_spec.rb +4 -4
  42. data/spec/multi_collector_spec.rb +91 -0
  43. data/spec/must_spec.rb +32 -0
  44. data/spec/spec_helper.rb +14 -4
  45. data/spec/specialized_attribute_class_spec.rb +2 -2
  46. data/spec/typed_collector_spec.rb +57 -0
  47. data/spec/xml_spec.rb +8 -8
  48. metadata +33 -3
@@ -0,0 +1,8 @@
1
+ class Doodle
2
+ # doodles are compared (sorted) on values
3
+ module Comparable
4
+ def <=>(o)
5
+ doodle.values <=> o.doodle.values
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,80 @@
1
+ class Doodle
2
+ module ConversionHelper
3
+ # if block passed, define a conversion from class
4
+ # if no args, apply conversion to arguments
5
+ def from(*args, &block)
6
+ Doodle::Debug.d { [self, args, block]}
7
+ #p [:from, self, args]
8
+ if block_given?
9
+ # set the rule for each arg given
10
+ args.each do |arg|
11
+ __doodle__.local_conversions[arg] = block
12
+ end
13
+ else
14
+ convert(self, *args)
15
+ end
16
+ end
17
+
18
+ # convert a value according to conversion rules
19
+ # FIXME: move
20
+ def convert(owner, *args)
21
+ #pp( { :convert => 1, :owner => owner, :args => args, :conversions => __doodle__.conversions } )
22
+ begin
23
+ args = args.map do |value|
24
+ #!p [:convert, 2, value]
25
+ if (converter = __doodle__.conversions[value.class])
26
+ #p [:convert, 3, value, self, caller]
27
+ value = converter[value]
28
+ #!p [:convert, 4, value]
29
+ else
30
+ #!p [:convert, 5, value]
31
+ # try to find nearest ancestor
32
+ this_ancestors = value.class.ancestors
33
+ #!p [:convert, 6, this_ancestors]
34
+ matches = this_ancestors & __doodle__.conversions.keys
35
+ #!p [:convert, 7, matches]
36
+ indexed_matches = matches.map{ |x| this_ancestors.index(x)}
37
+ #!p [:convert, 8, indexed_matches]
38
+ if indexed_matches.size > 0
39
+ #!p [:convert, 9]
40
+ converter_class = this_ancestors[indexed_matches.min]
41
+ #!p [:convert, 10, converter_class]
42
+ if converter = __doodle__.conversions[converter_class]
43
+ #!p [:convert, 11, converter]
44
+ value = converter[value]
45
+ #!p [:convert, 12, value]
46
+ end
47
+ else
48
+ #!p [:convert, 13, :kind, kind, name, value]
49
+ mappable_kinds = kind.select{ |x| x <= Doodle::Core }
50
+ #!p [:convert, 13.1, :kind, kind, mappable_kinds]
51
+ if mappable_kinds.size > 0
52
+ mappable_kinds.each do |mappable_kind|
53
+ #!p [:convert, 14, :kind_is_a_doodle, value.class, mappable_kind, mappable_kind.doodle.conversions, args]
54
+ if converter = mappable_kind.doodle.conversions[value.class]
55
+ #!p [:convert, 15, value, mappable_kind, args]
56
+ value = converter[value]
57
+ break
58
+ else
59
+ #!p [:convert, 16, :no_conversion_for, value.class]
60
+ end
61
+ end
62
+ else
63
+ #!p [:convert, 17, :kind_has_no_conversions]
64
+ end
65
+ end
66
+ end
67
+ #!p [:convert, 18, value]
68
+ value
69
+ end
70
+ rescue Exception => e
71
+ owner.__doodle__.handle_error name, ConversionError, "#{e.message}", Doodle::Utils.doodle_caller
72
+ end
73
+ if args.size > 1
74
+ args
75
+ else
76
+ args.first
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,42 @@
1
+ class Doodle
2
+ module ClassMethods
3
+ # provide somewhere to hold thread-specific context information
4
+ # (I'm claiming the :doodle_xxx namespace)
5
+ def context
6
+ Thread.current[:doodle_context] ||= []
7
+ end
8
+ def parent
9
+ context[-1]
10
+ end
11
+ end
12
+
13
+ extend ClassMethods
14
+
15
+ # Place to hold refs to built-in classes that need special handling
16
+ module BuiltIns
17
+ BUILTINS = [String, Hash, Array]
18
+ end
19
+
20
+ # Include Doodle::Core if you want to derive from another class
21
+ # but still get Doodle goodness in your class (including Factory
22
+ # methods).
23
+ module Core
24
+ module ModuleMethods
25
+ def included(other)
26
+ super
27
+ other.module_eval {
28
+ # FIXME: this is getting a bit arbitrary
29
+ include Equality
30
+ include Comparable
31
+ include Inherited
32
+ inherit BaseMethods
33
+ }
34
+ end
35
+ end
36
+ extend ModuleMethods
37
+ end
38
+
39
+ include Core
40
+
41
+ end
42
+
@@ -0,0 +1,39 @@
1
+ class Doodle
2
+ # implements the #doodle directive
3
+ class DataTypeHolder
4
+ attr_accessor :klass
5
+
6
+ def initialize(klass, &block)
7
+ @klass = klass
8
+ instance_eval(&block) if block_given?
9
+ end
10
+
11
+ def define(name, params, block, type_params, &type_block)
12
+ @klass.class_eval {
13
+ td = has(name, type_params.merge(params), &type_block)
14
+ td.instance_eval(&block) if block
15
+ td
16
+ }
17
+ end
18
+
19
+ def has(*args, &block)
20
+ @klass.class_eval { has(*args, &block) }
21
+ end
22
+
23
+ def must(*args, &block)
24
+ @klass.class_eval { must(*args, &block) }
25
+ end
26
+
27
+ def from(*args, &block)
28
+ @klass.class_eval { from(*args, &block) }
29
+ end
30
+
31
+ def arg_order(*args, &block)
32
+ @klass.class_eval { arg_order(*args, &block) }
33
+ end
34
+
35
+ def doc(*args, &block)
36
+ @klass.class_eval { doc(*args, &block) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ class Doodle
2
+ # debugging utilities
3
+ module Debug
4
+ class << self
5
+ # Robert Klemme, (ruby-talk 205150), (ruby-talk 205950)
6
+ def calling_method(level = 1)
7
+ caller[level] =~ /`([^']*)'/ and $1
8
+ end
9
+
10
+ def this_method
11
+ calling_method
12
+ end
13
+
14
+ # output result of block if ENV['DEBUG_DOODLE'] set
15
+ def d(&block)
16
+ puts(calling_method + ": " + block.call.inspect) if ENV['DEBUG_DOODLE']
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ class Doodle
2
+ # save a block for later execution
3
+ class DeferredBlock
4
+ attr_accessor :block
5
+ def initialize(arg_block = nil, &block)
6
+ arg_block = block if block_given?
7
+ @block = arg_block
8
+ end
9
+ def call(*a, &b)
10
+ block.call(*a, &b)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ class Doodle
2
+ # two doodles of the same class with the same attribute values are
3
+ # considered equal
4
+ module Equality
5
+ def eql?(o)
6
+ # p [:comparing, self.class, o.class, self.class == o.class]
7
+ # p [:values, self.doodle.values, o.doodle.values, self.doodle.values == o.doodle.values]
8
+ # p [:attributes, doodle.attributes.map { |k, a| [k, send(k).==(o.send(k))] }]
9
+ res = self.class == o.class &&
10
+ #self.doodle.values == o.doodle.values
11
+ # short circuit comparison
12
+ doodle.attributes.all? { |k, a| send(k).==(o.send(k)) }
13
+ # p [:res, res]
14
+ res
15
+ end
16
+ def ==(o)
17
+ eql?(o)
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,29 @@
1
+ class Doodle
2
+ # error handling
3
+ @@raise_exception_on_error = true
4
+ def self.raise_exception_on_error
5
+ @@raise_exception_on_error
6
+ end
7
+ def self.raise_exception_on_error=(tf)
8
+ @@raise_exception_on_error = tf
9
+ end
10
+
11
+ # internal error raised when a default was expected but not found
12
+ class NoDefaultError < Exception
13
+ end
14
+ # raised when a validation rule returns false
15
+ class ValidationError < Exception
16
+ end
17
+ # raised when an unknown parameter is passed to initialize
18
+ class UnknownAttributeError < Exception
19
+ end
20
+ # raised when a conversion fails
21
+ class ConversionError < Exception
22
+ end
23
+ # raised when arg_order called with incorrect arguments
24
+ class InvalidOrderError < Exception
25
+ end
26
+ # raised when try to set a readonly attribute after initialization
27
+ class ReadOnlyError < Exception
28
+ end
29
+ end
@@ -0,0 +1,91 @@
1
+ class Doodle
2
+ # A factory function is a function that has the same name as
3
+ # a class which acts just like class.new. For example:
4
+ # Cat(:name => 'Ren')
5
+ # is the same as:
6
+ # Cat.new(:name => 'Ren')
7
+ # As the notion of a factory function is somewhat contentious [xref
8
+ # ruby-talk], you need to explicitly ask for them by including Factory
9
+ # in your base class:
10
+ # class Animal < Doodle
11
+ # include Factory
12
+ # end
13
+ # class Dog < Animal
14
+ # end
15
+ # stimpy = Dog(:name => 'Stimpy')
16
+ # etc.
17
+ module Factory
18
+ RX_IDENTIFIER = /^[A-Za-z_][A-Za-z_0-9]+\??$/
19
+ module ClassMethods
20
+ # create a factory function in appropriate module for the specified class
21
+ def factory(konst)
22
+ #p [:factory, :ancestors, konst, konst.ancestors]
23
+ #p [:factory, :lookup, Module.nesting]
24
+ name = konst.to_s
25
+ #p [:factory, :name, name]
26
+ anon_class = false
27
+ if name =~ /#<Class:0x[a-fA-F0-9]+>::/
28
+ #p [:factory_anon_class, name]
29
+ anon_class = true
30
+ end
31
+ names = name.split(/::/)
32
+ name = names.pop
33
+ # TODO: the code below is almost the same - refactor
34
+ #p [:factory, :names, names, name]
35
+ if names.empty? && !anon_class
36
+ #p [:factory, :top_level_class]
37
+ # top level class - should be available to all
38
+ parent_class = Object
39
+ method_defined = begin
40
+ method(name)
41
+ true
42
+ rescue Object
43
+ false
44
+ end
45
+
46
+ if name =~ Factory::RX_IDENTIFIER && !method_defined && !parent_class.respond_to?(name) && !eval("respond_to?(:#{name})", TOPLEVEL_BINDING)
47
+ eval("def #{ name }(*args, &block); ::#{name}.new(*args, &block); end", ::TOPLEVEL_BINDING, __FILE__, __LINE__)
48
+ end
49
+ else
50
+ #p [:factory, :other_level_class]
51
+ parent_class = Object
52
+ if !anon_class
53
+ parent_class = names.inject(parent_class) {|c, n| c.const_get(n)}
54
+ #p [:factory, :parent_class, parent_class]
55
+ if name =~ Factory::RX_IDENTIFIER && !parent_class.respond_to?(name)
56
+ # FIXME: find out why define_method version not working
57
+ parent_class.module_eval("def self.#{name}(*args, &block); #{name}.new(*args, &block); end", __FILE__, __LINE__)
58
+ end
59
+ else
60
+ # NOTE: ruby 1.9.1 specific
61
+ parent_class_name = names.join('::')
62
+ #p [:factory, :parent_class_name, parent_class_name]
63
+ #p [:parent_class_name, parent_class_name]
64
+ # FIXME: this is truly horrible...
65
+ hex_object_id = parent_class_name.match(/:(0x[a-zA-Z0-9]+)/)[1]
66
+ oid = hex_object_id.to_i(16) >> 1
67
+ # p [:object_id, oid, hex_object_id, hex_object_id.to_i(16) >> 1]
68
+ parent_class = ObjectSpace._id2ref(oid)
69
+
70
+ #p [:parent_object_id, parent_class.object_id, names, parent_class, parent_class_name, parent_class.name]
71
+ # p [:names, :oid, "%x" % (oid << 1), :konst, konst, :pc, parent_class, :names, names, :self, self]
72
+ if name =~ Factory::RX_IDENTIFIER && !parent_class.respond_to?(name) && parent_class.const_defined?(name)
73
+ #p [:context, context]
74
+ parent_class.module_eval("def #{name}(*args, &block); #{name}.new(*args, &block); end", __FILE__, __LINE__)
75
+ end
76
+ end
77
+ # TODO: check how many times this is being called
78
+ end
79
+ end
80
+
81
+ # inherit the factory function capability
82
+ def included(other)
83
+ #p [:included, other]
84
+ super
85
+ # make +factory+ method available
86
+ factory other
87
+ end
88
+ end
89
+ extend ClassMethods
90
+ end
91
+ end
@@ -0,0 +1,154 @@
1
+ class Doodle
2
+ module GetterSetter
3
+ # either get an attribute value (if no args given) or set it
4
+ # (using args and/or block)
5
+ # FIXME: move
6
+ def getter_setter(name, *args, &block)
7
+ #p [:getter_setter, name]
8
+ name = name.to_sym
9
+ if block_given? || args.size > 0
10
+ #!p [:getter_setter, :setter, name, *args]
11
+ _setter(name, *args, &block)
12
+ else
13
+ #!p [:getter_setter, :getter, name]
14
+ _getter(name)
15
+ end
16
+ end
17
+ private :getter_setter
18
+
19
+ # get an attribute by name - return default if not otherwise defined
20
+ # FIXME: init deferred blocks are not getting resolved in all cases
21
+ def _getter(name, &block)
22
+ begin
23
+ #p [:_getter, name]
24
+ ivar = "@#{name}"
25
+ if instance_variable_defined?(ivar)
26
+ #p [:_getter, :instance_variable_defined, name, ivar, instance_variable_get(ivar)]
27
+ instance_variable_get(ivar)
28
+ else
29
+ # handle default
30
+ # Note: use :init => value to cover cases where defaults don't work
31
+ # (e.g. arrays that disappear when you go out of scope)
32
+ att = __doodle__.lookup_attribute(name)
33
+ # special case for class/singleton :init
34
+ if att && att.optional?
35
+ optional_value = att.init_defined? ? att.init : att.default
36
+ #p [:optional_value, optional_value]
37
+ case optional_value
38
+ when DeferredBlock
39
+ #p [:deferred_block]
40
+ v = instance_eval(&optional_value.block)
41
+ when Proc
42
+ v = instance_eval(&optional_value)
43
+ else
44
+ v = optional_value
45
+ end
46
+ if att.init_defined?
47
+ _setter(name, v)
48
+ end
49
+ v
50
+ else
51
+ # This is an internal error (i.e. shouldn't happen)
52
+ __doodle__.handle_error name, NoDefaultError, "'#{name}' has no default defined", Doodle::Utils.doodle_caller
53
+ end
54
+ end
55
+ rescue Object => e
56
+ __doodle__.handle_error name, e, e.to_s, Doodle::Utils.doodle_caller
57
+ end
58
+ end
59
+ private :_getter
60
+
61
+ def after_update(params)
62
+ end
63
+
64
+ # set an instance variable by symbolic name and call after_update if changed
65
+ def ivar_set(name, *args)
66
+ ivar = "@#{name}"
67
+ if instance_variable_defined?(ivar)
68
+ old_value = instance_variable_get(ivar)
69
+ else
70
+ old_value = nil
71
+ end
72
+ instance_variable_set(ivar, *args)
73
+ new_value = instance_variable_get(ivar)
74
+ if new_value != old_value
75
+ #pp [Doodle, :after_update, { :instance => self, :name => name, :old_value => old_value, :new_value => new_value }]
76
+ after_update :instance => self, :name => name, :old_value => old_value, :new_value => new_value
77
+ end
78
+ end
79
+ private :ivar_set
80
+
81
+ # set an attribute by name - apply validation if defined
82
+ # FIXME: move
83
+ def _setter(name, *args, &block)
84
+ ##DBG: Doodle::Debug.d { [:_setter, name, args] }
85
+ #p [:_setter, name, *args]
86
+ att = __doodle__.lookup_attribute(name)
87
+ if att && __doodle__.validation_on && att.readonly
88
+ raise Doodle::ReadOnlyError, "Trying to set a readonly attribute: #{att.name}", Doodle::Utils.doodle_caller
89
+ end
90
+ if block_given?
91
+ # if a class has been defined, let's assume it can take a
92
+ # block initializer (test that it's a Doodle or Proc)
93
+ if att.kind && !att.abstract && klass = att.kind.first
94
+ if [Doodle, Proc].any?{ |c| klass <= c }
95
+ # p [:_setter, '# 1 converting arg to value with kind ' + klass.to_s]
96
+ args = [klass.new(*args, &block)]
97
+ else
98
+ __doodle__.handle_error att.name, ArgumentError, "#{klass} #{att.name} does not take a block initializer", Doodle::Utils.doodle_caller
99
+ end
100
+ else
101
+ # this is used by init do ... block
102
+ args.unshift(DeferredBlock.new(block))
103
+ end
104
+ end
105
+ if att # = __doodle__.lookup_attribute(name)
106
+ if att.kind && !att.abstract && klass = att.kind.first
107
+ if !args.first.kind_of?(klass) && [Doodle].any?{ |c| klass <= c }
108
+ #p [:_setter, "#2 converting arg #{att.name} to value with kind #{klass.to_s}"]
109
+ #p [:_setter, args]
110
+ begin
111
+ args = [klass.new(*args, &block)]
112
+ rescue Object => e
113
+ __doodle__.handle_error att.name, e.class, e.to_s, Doodle::Utils.doodle_caller
114
+ end
115
+ end
116
+ end
117
+ #p [:_setter, :got_att1, name, ivar, *args]
118
+ v = ivar_set(name, att.validate(self, *args))
119
+
120
+ #p [:_setter, :got_att2, name, ivar, :value, v]
121
+ #v = instance_variable_set(ivar, *args)
122
+ else
123
+ #p [:_setter, :no_att, name, *args]
124
+ ##DBG: Doodle::Debug.d { [:_setter, "no attribute"] }
125
+ v = ivar_set(name, *args)
126
+ end
127
+ validate!(false)
128
+ v
129
+ end
130
+ private :_setter
131
+
132
+ # define a getter_setter
133
+ # fixme: move
134
+ def define_getter_setter(name, params = { }, &block)
135
+ # need to use string eval because passing block
136
+ sc_eval "def #{name}(*args, &block); getter_setter(:#{name}, *args, &block); end", __FILE__, __LINE__
137
+ sc_eval "def #{name}=(*args, &block); _setter(:#{name}, *args); end", __FILE__, __LINE__
138
+
139
+ # this is how it should be done (in 1.9)
140
+ # module_eval {
141
+ # define_method name do |*args, &block|
142
+ # getter_setter(name.to_sym, *args, &block)
143
+ # end
144
+ # define_method "#{name}=" do |*args, &block|
145
+ # _setter(name.to_sym, *args, &block)
146
+ # end
147
+ # }
148
+ end
149
+ private :define_getter_setter
150
+
151
+
152
+
153
+ end
154
+ end