doodle 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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