icss 0.1.3 → 0.3.2

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 (98) hide show
  1. data/.watchr +35 -3
  2. data/CHANGELOG.md +38 -0
  3. data/Gemfile +19 -14
  4. data/README.md +296 -0
  5. data/Rakefile +2 -6
  6. data/TODO.md +13 -0
  7. data/VERSION +1 -1
  8. data/examples/avro_examples/complicated.icss.yaml +14 -13
  9. data/examples/bnc.icss.yaml +70 -0
  10. data/examples/chronic.icss.yaml +3 -3
  11. data/examples/license.icss.yaml +7 -0
  12. data/examples/source1.icss.yaml +4 -0
  13. data/examples/source2.icss.yaml +4 -0
  14. data/examples/test_icss.yaml +67 -0
  15. data/icss.gemspec +103 -43
  16. data/lib/icss.rb +37 -15
  17. data/lib/icss/core_types.rb +19 -0
  18. data/lib/icss/error.rb +4 -0
  19. data/{init.rb → lib/icss/init.rb} +0 -0
  20. data/lib/icss/message.rb +124 -66
  21. data/lib/icss/message/message_sample.rb +144 -0
  22. data/lib/icss/protocol.rb +184 -131
  23. data/lib/icss/protocol/code_asset.rb +18 -0
  24. data/lib/icss/protocol/data_asset.rb +23 -0
  25. data/lib/icss/protocol/license.rb +41 -0
  26. data/lib/icss/protocol/source.rb +37 -0
  27. data/lib/icss/protocol/target.rb +68 -0
  28. data/lib/icss/receiver_model.rb +24 -0
  29. data/lib/icss/receiver_model/active_model_shim.rb +36 -0
  30. data/lib/icss/receiver_model/acts_as_catalog.rb +170 -0
  31. data/lib/icss/receiver_model/acts_as_hash.rb +177 -0
  32. data/lib/icss/receiver_model/acts_as_loadable.rb +47 -0
  33. data/lib/icss/receiver_model/acts_as_tuple.rb +100 -0
  34. data/lib/icss/receiver_model/locale/en.yml +27 -0
  35. data/lib/icss/receiver_model/to_geo_json.rb +19 -0
  36. data/lib/icss/receiver_model/tree_merge.rb +34 -0
  37. data/lib/icss/receiver_model/validations.rb +31 -0
  38. data/lib/icss/serialization.rb +51 -0
  39. data/lib/icss/serialization/zaml.rb +443 -0
  40. data/lib/icss/type.rb +148 -501
  41. data/lib/icss/type/base_type.rb +0 -0
  42. data/lib/icss/type/named_type.rb +184 -0
  43. data/lib/icss/type/record_field.rb +77 -0
  44. data/lib/icss/type/record_model.rb +49 -0
  45. data/lib/icss/type/record_schema.rb +54 -0
  46. data/lib/icss/type/record_type.rb +325 -0
  47. data/lib/icss/type/simple_types.rb +72 -0
  48. data/lib/icss/type/structured_schema.rb +288 -0
  49. data/lib/icss/type/type_factory.rb +144 -0
  50. data/lib/icss/type/union_schema.rb +41 -0
  51. data/lib/icss/view_helper.rb +56 -19
  52. data/notes/named_array.md +32 -0
  53. data/notes/on_include_vs_extend_etc.rb +176 -0
  54. data/notes/technical_details.md +278 -0
  55. data/spec/core_types_spec.rb +119 -0
  56. data/spec/fixtures/zaml_complex_hash.yaml +35 -0
  57. data/spec/icss_spec.rb +86 -23
  58. data/spec/message/message_sample_spec.rb +4 -0
  59. data/spec/message_spec.rb +139 -0
  60. data/spec/protocol/license_spec.rb +67 -0
  61. data/spec/protocol/protocol_catalog_spec.rb +48 -0
  62. data/spec/protocol/protocol_validations_spec.rb +176 -0
  63. data/spec/protocol/source_spec.rb +65 -0
  64. data/spec/protocol_spec.rb +91 -37
  65. data/spec/receiver_model_spec.rb +111 -0
  66. data/spec/serialization/zaml_spec.rb +81 -0
  67. data/spec/serialization/zaml_test.rb +473 -0
  68. data/spec/serialization_spec.rb +63 -0
  69. data/spec/spec_helper.rb +24 -7
  70. data/spec/support/icss_test_helper.rb +67 -0
  71. data/spec/support/load_example_protocols.rb +17 -0
  72. data/spec/type/base_type_spec.rb +0 -0
  73. data/spec/type/named_type_spec.rb +75 -0
  74. data/spec/type/record_field_spec.rb +44 -0
  75. data/spec/type/record_model_spec.rb +206 -0
  76. data/spec/type/record_schema_spec.rb +161 -0
  77. data/spec/type/record_type_spec.rb +155 -0
  78. data/spec/type/simple_types_spec.rb +121 -0
  79. data/spec/type/structured_schema_spec.rb +300 -0
  80. data/spec/type/type_catalog_spec.rb +44 -0
  81. data/spec/type/type_factory_spec.rb +93 -0
  82. data/spec/type/union_schema_spec.rb +0 -0
  83. data/spec/type_spec.rb +63 -0
  84. metadata +205 -144
  85. data/CHANGELOG.textile +0 -9
  86. data/Gemfile.lock +0 -40
  87. data/README.textile +0 -29
  88. data/lib/icss/brevity.rb +0 -136
  89. data/lib/icss/code_asset.rb +0 -16
  90. data/lib/icss/core_ext.rb +0 -9
  91. data/lib/icss/data_asset.rb +0 -22
  92. data/lib/icss/old.rb +0 -96
  93. data/lib/icss/protocol_set.rb +0 -48
  94. data/lib/icss/sample_message_call.rb +0 -142
  95. data/lib/icss/target.rb +0 -72
  96. data/lib/icss/type/factory.rb +0 -196
  97. data/lib/icss/validations.rb +0 -16
  98. data/spec/validations_spec.rb +0 -171
File without changes
@@ -0,0 +1,184 @@
1
+ module Icss
2
+ module Meta
3
+
4
+ #
5
+ # Record, Error, Enum and Fixed are named types. Each has a fullname that is
6
+ # composed of two parts; a name and a namespace. Equality of names is
7
+ # defined on the fullname.
8
+ #
9
+ # The name portion of a fullname, and record field names, must:
10
+ #
11
+ # * start with [A-Za-z_]
12
+ # * subsequently contain only [A-Za-z0-9_]
13
+ # * A namespace is a dot-separated sequence of such names.
14
+ #
15
+ # References to previously defined names are as in the latter two cases above:
16
+ # if they contain a dot they are a fullname, if they do not contain a dot, the
17
+ # namespace is the namespace of the enclosing definition.
18
+ #
19
+ # Simple type names have no namespace and their names may not be defined in
20
+ # any namespace. A schema may only contain multiple definitions of a
21
+ # fullname if the definitions are equivalent.
22
+ #
23
+ module NamedType
24
+ def doc() "" end
25
+ def doc=(str)
26
+ singleton_class.class_eval do
27
+ remove_possible_method(:doc)
28
+ define_method(:doc){ str }
29
+ end
30
+ end
31
+ #
32
+ def fullname
33
+ ::Icss::Meta::Type.fullname_for(self.name)
34
+ end
35
+ def basename
36
+ @basename ||= fullname.to_s.gsub(/.*[\.]/, "")
37
+ end
38
+ def namespace
39
+ @namespace ||= fullname.to_s.include?('.') ? fullname.to_s.gsub(/\.[^\.]+\z/so, '') : ''
40
+ end
41
+ def pathname
42
+ fullname.to_s.gsub(/\./, '/')
43
+ end
44
+ #
45
+ def to_schema
46
+ if respond_to?(:_schema) then return _schema.to_hash ; end
47
+ {
48
+ :name => fullname,
49
+ # :namespace => namespace,
50
+ :doc => doc,
51
+ }.compact_blank
52
+ end
53
+
54
+ def is_core?
55
+ respond_to?(:_schema) && _schema.is_core?
56
+ end
57
+
58
+ # ---------------------------------------------------------------------------
59
+ #
60
+ # Type Factory methods
61
+ #
62
+
63
+ #
64
+ # Returns the metamodel -- a module extending the type, on which all the
65
+ # accessors and receive methods are inscribed. (This allows you to call
66
+ # +super()+ from within receive_foo)
67
+ #
68
+ def metamodel
69
+ return @metamodel if @metamodel
70
+ @metamodel = Icss::Meta::NamedType.get_meta_module(self.to_s)
71
+ self.class_eval{ include(@metamodel) }
72
+ @metamodel
73
+ end
74
+
75
+ #
76
+ # Manufactures klass and metamodel
77
+ #
78
+ # for type science.astronomy.ufo_sighting, we synthesize
79
+ # * a module, ::Icss::Meta::Science::Astronomy::UfoSightingType
80
+ # * a class, ::Icss::Science::Astronomy::UfoSighting
81
+ #
82
+ # If no superklass is given, Icss::Entity is used.
83
+ def self.make(fullname, superklass)
84
+ klass = get_model_klass(fullname, superklass)
85
+ metamodel = get_meta_module(klass.to_s)
86
+ klass.class_eval{ extend(::Icss::Meta::NamedType) }
87
+ klass.class_eval{ include(metamodel) }
88
+ [klass, metamodel]
89
+ end
90
+
91
+ protected
92
+
93
+ def define_metamodel_method(meth_name, visibility=:public, &blk)
94
+ metamodel.class_eval do
95
+ define_method(meth_name, &blk) unless method_defined?(meth_name)
96
+ case visibility
97
+ when :protected then protected meth_name
98
+ when :private then private meth_name
99
+ when :public then public meth_name
100
+ else raise ArgumentError, "visibility must be :public, :private or :protected"
101
+ end
102
+ end
103
+ end
104
+
105
+ # Returns the klass for the given scope and name, starting with '::Icss'
106
+ # and creating all necessary parents along the way. Note that if the given
107
+ # class or its parent scopes already exist, they're trusted to be correct
108
+ # -- we don't do any error checking as to their type or superclass.
109
+ #
110
+ # @example
111
+ # Icss::Meta::Type.get_model_klass('this.that.the_other')
112
+ # # Icss::This::That::TheOther
113
+ #
114
+ # @param scope_names [Array of String]
115
+ # @param superklass [Class] - the superclass to use if the class doesn't exist.
116
+ def self.get_model_klass(fn, superklass)
117
+ fullname = Icss::Meta::Type.fullname_for(fn)
118
+ return Class.new(superklass) if fullname.nil?
119
+ #
120
+ scope_names = scope_names_for(fullname)
121
+ klass_name = scope_names.pop
122
+ parent_module = get_nested_module(%w[Icss] + scope_names)
123
+
124
+ # const_defined?(klass, inherit), const_get(klass, inherit)
125
+ # inherit = false makes these methods be scoped to parent_module instead of universally
126
+ if parent_module.const_defined?(klass_name, false)
127
+ klass = parent_module.const_get(klass_name, false)
128
+ # #{superklass.object_id}: #{klass.ancestors.map{|o| [o, o.object_id] }}
129
+ unless klass.ancestors.include?(superklass)
130
+ warn "+++++++++++++++++++++++++++++++ Superclass and is_a? mismatch for #{klass.inspect} (doesn't inherit from #{superklass.inspect})"
131
+ # p [klass_name, klass, Icss::Thing.object_id, ::Icss::Thing.object_id, Icss.const_get(:Thing).object_id,]
132
+ # p klass .ancestors.flatten.map{|t| [t, t.object_id]}
133
+ # p Icss::Thing.ancestors.flatten.map{|t| [t, t.object_id]}
134
+ end
135
+ klass
136
+ else
137
+ parent_module.const_set(klass_name, Class.new(superklass))
138
+ end
139
+ end
140
+
141
+ # Returns the meta-module for the given scope and name, starting with
142
+ # '::Icss::Meta' and creating all necessary parents along the way.
143
+ # @example
144
+ # Icss::Meta::TypeFactory.get_meta_module(["This", "That"], "TheOther")
145
+ # # Icss::Meta::This::That::TheOtherModel
146
+ def self.get_meta_module(fullname)
147
+ fullname = Icss::Meta::Type.fullname_for(fullname)
148
+ return Module.new if fullname.nil?
149
+ #
150
+ scope_names = scope_names_for(fullname)
151
+ scope_names[-1] += "Model"
152
+ get_nested_module(%w[Icss Meta] + scope_names)
153
+ end
154
+
155
+ # Turns a dotted namespace.name into camelized rubylike names for a class
156
+ # @example
157
+ # scope_names_for('this.that.the_other')
158
+ # # ["This", "That", "TheOther"]
159
+ def self.scope_names_for(fullname)
160
+ fullname.split('.').map(&:camelize)
161
+ end
162
+
163
+ # Returns a module for the given scope names, rooted always at Object (so
164
+ # implicity with '::').
165
+ # @example
166
+ # get_nested_module(["This", "That", "TheOther"])
167
+ # # This::That::TheOther
168
+ def self.get_nested_module(scope_names)
169
+ scope_names.inject(Object) do |parent_module, module_name|
170
+
171
+ # const_defined?(klass, inherit), const_get(klass, inherit)
172
+ # inherit = false makes these methods be scoped to parent_module instead of universally
173
+ if parent_module.const_defined?(module_name, false)
174
+ parent_module.const_get(module_name, false)
175
+ else
176
+ parent_module.const_set(module_name.to_sym, Module.new)
177
+ end
178
+ end
179
+ end
180
+
181
+ end
182
+
183
+ end
184
+ end
@@ -0,0 +1,77 @@
1
+ module Icss
2
+ module Meta
3
+
4
+ class RecordField
5
+ include Icss::Meta::RecordModel
6
+ include Icss::ReceiverModel::ActsAsHash
7
+ include Gorillib::Hashlike
8
+ include Gorillib::Hashlike::Keys
9
+ include Icss::ReceiverModel
10
+ include Icss::ReceiverModel::ActiveModelShim
11
+ remove_possible_method(:type)
12
+
13
+ ALLOWED_ORDERS = %w[ascending descending ignore].freeze unless defined?(ALLOWED_ORDERS)
14
+
15
+ field :name, Symbol, :required => true
16
+ field :type, Icss::Meta::TypeFactory, :required => true
17
+ field :doc, String
18
+ field :default, Icss::Meta::IdenticalFactory
19
+ field :replace, Hash
20
+ field :required, Boolean
21
+ field :aliases, Array, :items => Symbol
22
+ field :order, String, :validates => { :inclusion => { :in => ALLOWED_ORDERS } }
23
+ field :accessor, Symbol
24
+ field :receiver, Symbol
25
+ field :validates, Hash
26
+ rcvr_remaining :_extra_params
27
+ attr_accessor :parent
28
+
29
+ # FIXME: cruft
30
+ field :indexed_on, Symbol
31
+ field :identifier, Boolean, :doc => 'indicates the field is suitable for use'
32
+
33
+ after_receive(:warnings) do |hsh|
34
+ warn "Extra params given to field #{self}: #{_extra_params.inspect}" if _extra_params.present?
35
+ warn "Validation failed for field #{self}: #{errors.inspect}" if respond_to?(:errors) && (not valid?)
36
+ end
37
+
38
+ # track recursion of type references
39
+ after_receive(:am_i_a_reference) do |hsh|
40
+ hsh = hsh.symbolize_keys
41
+ nonreference_klasses = [::Icss::Meta::ArrayType, ::Icss::Meta::HashType, ::Icss::Meta::EnumType, ::Icss::Meta::FixedType]
42
+
43
+ @is_reference = (hsh[:type].is_a?(String) || hsh[:type].is_a?(Symbol) || (hsh[:type].is_a?(Class) && nonreference_klasses.none?{|klass| hsh[:type].is_a?(klass) }))
44
+ end
45
+
46
+ # is the field a reference to a named type (true), or an inline schema (false)?
47
+ def is_reference?() @is_reference ; end
48
+
49
+ def to_hash()
50
+ hsh = super
51
+ hsh = hsh.merge({ :type => (is_reference? ? type.fullname : Type.schema_for(type)) })
52
+ hsh.delete(:_extra_params)
53
+ hsh
54
+ end
55
+ def to_schema
56
+ to_hash
57
+ end
58
+
59
+ # Hack hack -- makes it so fields go thru the receiver door when merged in RecordType
60
+ def merge(hsh)
61
+ dup.receive!(hsh)
62
+ end
63
+
64
+ # Order is defined by the avro spec
65
+ def order() @order || 'ascending' ; end
66
+ def order_direction() case order when 'ascending' then 1 when 'descending' then -1 else 0 ; end ; end
67
+ end
68
+
69
+ module RecordType
70
+ protected
71
+ # create new field_schema as a RecordField object, not Hash
72
+ def make_field_schema
73
+ RecordField.new
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,49 @@
1
+ module Icss
2
+ module Meta
3
+
4
+ module RecordModel
5
+ def self.included(base)
6
+ base.extend(Icss::Meta::RecordType)
7
+ base.metamodel
8
+ end
9
+
10
+ #
11
+ # modify object in place with new typecast values.
12
+ #
13
+ def receive!(hsh={})
14
+ raise ArgumentError, "Can't receive (it isn't hashlike): {#{hsh.inspect}}" unless hsh.respond_to?(:[]) && hsh.respond_to?(:has_key?)
15
+ self.class.send(:_rcvr_methods).each do |attr, meth|
16
+ if hsh.has_key?(attr) then val = hsh[attr]
17
+ elsif hsh.has_key?(attr.to_s) then val = hsh[attr.to_s]
18
+ else next ; end
19
+ self.send(meth, val)
20
+ end
21
+ run_after_receivers(hsh)
22
+ self
23
+ end
24
+
25
+ # true if the attr is a receiver variable and it has been set
26
+ def attr_set?(attr)
27
+ self.class.has_field?(attr) && self.instance_variable_defined?("@#{attr}")
28
+ end
29
+
30
+ def unset!(attr)
31
+ self.send(:remove_instance_variable, "@#{attr}") if self.instance_variable_defined?("@#{attr}")
32
+ end
33
+ protected :unset!
34
+
35
+ protected
36
+
37
+ def run_after_receivers(hsh)
38
+ self.class.after_receivers.each do |after_hook_name, after_hook|
39
+ self.instance_exec(hsh, &after_hook)
40
+ end
41
+ end
42
+
43
+ def _set_field_val(field_name, val)
44
+ self.instance_variable_set("@#{field_name}", val)
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,54 @@
1
+ module Icss
2
+ module Meta
3
+
4
+ class RecordSchema < SimpleSchema
5
+ include Icss::Meta::RecordModel
6
+ include Icss::ReceiverModel::ActsAsHash
7
+ include Gorillib::Hashlike
8
+ include Gorillib::Hashlike::Keys
9
+ #
10
+ field :_domain_id_field, String, :default => 'name'
11
+ field :_doc_hints, Hash, :default => {}
12
+ field :fields, Array, :default => []
13
+ #
14
+
15
+ def type() :record ; end
16
+
17
+ def to_hash
18
+ {
19
+ :name => basename,
20
+ :namespace => namespace,
21
+ :type => type,
22
+ :is_a => (respond_to?(:is_a) ? is_a : []),
23
+ :doc => doc,
24
+ :fields => fields.map(&:to_schema),
25
+ }.compact_blank
26
+ end
27
+
28
+ def model_klass
29
+ return @model_klass if @model_klass
30
+ super
31
+ @model_klass.class_eval{ include(::Icss::Meta::RecordModel)}
32
+ self.fields.each do |field_schema|
33
+ @model_klass.field(field_schema[:name], field_schema[:type], field_schema)
34
+ end
35
+ @model_klass
36
+ end
37
+
38
+ def receive_fields(flds)
39
+ super(flds.map(&:symbolize_keys!))
40
+ end
41
+
42
+ def attrs_to_inscribe
43
+ [ :doc, :fullname, :is_a, :_domain_id_field, :_doc_hints ]
44
+ end
45
+ end
46
+
47
+ class ErrorSchema
48
+ include Icss::Meta::RecordType
49
+ end
50
+ module ErrorModel
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,325 @@
1
+ require 'icss/type/type_factory'
2
+
3
+ module Icss
4
+ module Meta
5
+
6
+ #
7
+ # RecordType -- class methods for a RecordModel
8
+ #
9
+ # Endows the model class with
10
+ # * klass.field -- adds a field
11
+ # * klass.fields -- list of record_fields
12
+ #
13
+ # * klass.after_receive(&blk) -- blocks to execute after .receive is called
14
+ # * klass.after_receivers -- list of after_receivers
15
+ # * klass.metamodel -- overlay module that actually carries the model's instance methods
16
+ # * klass.to_schema --
17
+ module RecordType
18
+ include Icss::Meta::NamedType
19
+
20
+ #
21
+ # Returns a new instance with the given hash used to set all rcvrs.
22
+ #
23
+ # All args up to the last one are passed to the initializer.
24
+ # The last arg must be a hash -- its attributes are set on the newly-created object
25
+ #
26
+ # @param hsh [Hash] attr-value pairs to set on the newly created object.
27
+ # @param *args [Array] arguments to pass to the constructor
28
+ # @return [Object] a new instance
29
+ def receive *args
30
+ hsh = args.pop
31
+ raise ArgumentError, "#{self} can't receive '#{hsh.inspect}' (it isn't hashlike)" unless hsh.respond_to?(:[]) && hsh.respond_to?(:has_key?)
32
+ obj = self.new(*args)
33
+ obj.receive!(hsh)
34
+ end
35
+
36
+ #
37
+ # Describes a field in a Record object.
38
+ #
39
+ # Each field has the following attributes:
40
+ #
41
+ # @param [Symbol] name -- a string providing the name of the field
42
+ # (required)
43
+ #
44
+ # @param [Class, Icss::Meta::Type] type a schema, or a string or symbol
45
+ # naming a record definition (required)
46
+ #
47
+ # avro type json type ruby type kind example
48
+ # --------- --------- ---------- -------- ---------
49
+ # null null NilClass simple nil
50
+ # boolean boolean Boolean simple true
51
+ # int,long integer Integer simple 1
52
+ # float,double number Float simple 1.1
53
+ # bytes string String simple "\u00FF"
54
+ # string string String simple "foo"
55
+ # record object RecordModel named {"a": 1}
56
+ # enum string Enum named "FOO"
57
+ # array array Array container [1]
58
+ # map object Hash container { "a": 1 }
59
+ # fixed string String container "\u00ff"
60
+ # union object XxxFactory union
61
+ # time string Time simple "2011-01-02T03:04:05Z"
62
+ #
63
+ # @option schema [String] :doc -- description of field for users (optional)
64
+ #
65
+ # @option schema [Object] :default -- a default value for this field, used
66
+ # when reading instances that lack this field (optional).
67
+ # Permitted values depend on the field's schema type, according to the
68
+ # table below. Default values for union fields correspond to the first
69
+ # schema in the union. Default values for bytes and fixed fields are
70
+ # JSON strings, where Unicode code points 0-255 are mapped to unsigned
71
+ # 8-bit byte values 0-255.
72
+ #
73
+ # @option schema [String] :order -- specifies how this field impacts sort
74
+ # ordering of this record (optional).
75
+ # Valid values are "ascending" (the default), "descending", or
76
+ # "ignore". For more details on how this is used, see the the sort
77
+ # order section below.
78
+ #
79
+ # @option schema [Boolean] :required -- same as :validates => :presence
80
+ #
81
+ # @option schema [Hash] :validates -- sends the validation on to
82
+ # Icss::Type::Validations. Uses syntax parallel to ActiveModel's:
83
+ #
84
+ # @option schema [Symbol] :accessor -- with +:none+, no accessor is
85
+ # created. With +:protected+, +:private+, or +:public+, applies
86
+ # corresponding access rule.
87
+ #
88
+ # :presence => true
89
+ # :uniqueness => true
90
+ # :numericality => true
91
+ # :length => { :minimum => 0, maximum => 2000 }
92
+ # :format => { :with => /.*/ }
93
+ # :inclusion => { :in => [1,2,3] }
94
+ # :exclusion => { :in => [1,2,3] }
95
+ #
96
+ def field(field_name, type, schema={})
97
+ field_name = field_name.to_sym
98
+ #
99
+ schema = add_field_schema(field_name, type, schema)
100
+ add_field_accessor(field_name, schema)
101
+ rcvr(field_name, schema)
102
+ add_validator(field_name) if respond_to?(:add_validator)
103
+ end
104
+
105
+ def field_names
106
+ all_f = []
107
+ call_ancestor_chain(:field_names){|anc_f| all_f = all_f | anc_f }
108
+ all_f
109
+ end
110
+
111
+ def fields
112
+ field_schemas.values # _at(*field_names)
113
+ end
114
+
115
+ def field_named(fn)
116
+ fn = fn.to_sym
117
+ ancestors.each{|anc| hsh = anc.instance_variable_get('@field_schemas') or next ; return(hsh[fn]) if hsh[fn] }
118
+ nil
119
+ end
120
+
121
+ def has_field?(fn)
122
+ !! field_schemas.has_key?(fn.to_sym)
123
+ end
124
+
125
+ #
126
+ # define a receiver attribute.
127
+ #
128
+ # @param [Symbol] field_name - name of the receiver property
129
+ # @param [Class] type - a
130
+ #
131
+ # @option [Object] :default - After any receive! operation, attribute is set to this value unless attr_set? is true
132
+ # @option [Class] :items - For collections (Array, Hash, etc), the type of the collection's items
133
+ #
134
+ def rcvr(field_name, schema={})
135
+ return if schema[:receiver] == :none
136
+ klass = schema[:type]
137
+ define_metamodel_method("receive_#{field_name}") do |val|
138
+ _set_field_val(field_name, klass.receive(val))
139
+ end
140
+ _register_rcvr_for(field_name, "receive_#{field_name}")
141
+ add_after_receivers(field_name)
142
+ end
143
+
144
+ def rcvr_alias(fake_attr, field_name)
145
+ _register_rcvr_for(fake_attr, "receive_#{field_name}")
146
+ end
147
+
148
+ #
149
+ def to_schema
150
+ {
151
+ :name => fullname,
152
+ :namespace => namespace,
153
+ :type => :record,
154
+ :is_a => (respond_to?(:is_a) ? is_a : []),
155
+ :doc => doc,
156
+ :fields => fields.map(&:to_hash),
157
+ }.compact_blank
158
+ end
159
+
160
+ #
161
+ # Defines a receiver for attributes sent to receive! that are
162
+ # * not defined as receivers
163
+ # * field's name does not start with '_'
164
+ #
165
+ # @example
166
+ # class Foo ; include RecordModel
167
+ # field :bob, String
168
+ # rcvr_remaining :other_params
169
+ # end
170
+ # foo_obj = Foo.receive(:bob => 'hi, bob", :joe => 'hi, joe')
171
+ # # => <Foo @bob='hi, bob' @other_params={ :joe => 'hi, joe' }>
172
+ def rcvr_remaining(field_name, schema={})
173
+ schema[:values] ||= Object
174
+ field(field_name, Hash, schema)
175
+ after_receive(:rcvr_remaining) do |hsh|
176
+ remaining_vals_hsh = hsh.reject{|k,v| (self.class.has_field?(k)) || (k.to_s =~ /^_/) }
177
+ self.send("receive_#{field_name}", remaining_vals_hsh)
178
+ end
179
+ add_after_receivers(field_name)
180
+ end
181
+
182
+ # make a block to run after each time .receive! is invoked
183
+ def after_receive(after_hook_name, &after_hook)
184
+ @after_receivers ||= {}
185
+ @after_receivers[after_hook_name] = after_hook
186
+ end
187
+
188
+ # after_receive blocks for self and all ancestors
189
+ def after_receivers
190
+ all_f = {} # @after_receivers || {}
191
+ call_ancestor_chain(:after_receivers){|anc_f| all_f.merge!(anc_f) }
192
+ all_f
193
+ end
194
+
195
+ protected
196
+
197
+ #
198
+ # yield, in turn, the given instance variable from each ancestor having
199
+ # that variable. Ancestors are called from parent to great-grandparent
200
+ #
201
+ # So you're asking yourself "Self, why not just call .super?
202
+ #
203
+ # Consider:
204
+ #
205
+ # class Base
206
+ # extend(Icss::Meta::RecordType)
207
+ # field :smurfiness, Integer
208
+ # end
209
+ # class Poppa < Base
210
+ # field :height, Integer
211
+ # end
212
+ #
213
+ # Poppa.field_names calls Icss::Meta::RecordType --
214
+ # it's the first member of its inheritance chain to define the method.
215
+ # We want it to do so for each ancestor that has added fields.
216
+ def call_ancestor_chain(attr)
217
+ # if caller.length > 200
218
+ # puts "\n\n********\n#{self}\n#{attr}\n#{ancestors.inspect}"
219
+ # puts caller.grep(%r{lib/icss})
220
+ # end
221
+ ivar = "@#{attr}"
222
+ self.ancestors.reverse.each do |ancestor|
223
+ yield(ancestor.instance_variable_get(ivar)) if ancestor.instance_variable_defined?(ivar)
224
+ end
225
+ end
226
+
227
+ def make_field_schema
228
+ Hash.new
229
+ end
230
+
231
+ def field_schemas
232
+ all_f = {}
233
+ call_ancestor_chain(:field_schemas){|anc_f| all_f.merge!(anc_f) }
234
+ all_f
235
+ end
236
+
237
+ # register the field schema internally.
238
+ # To preserve field order for 1.8.7, we track field names as an array
239
+ def add_field_schema(name, type, schema)
240
+ @field_names ||= [] ; @field_schemas ||= {}
241
+ @field_names = @field_names | [name]
242
+ schema = schema.symbolize_keys.merge({ :name => name })
243
+ #
244
+ # FIXME: this is terrible, especially given what's in TypeFactory anyway.
245
+ #
246
+ schema[:type] =
247
+ case
248
+ when type == Hash && schema.has_key?(:values) then Icss::Meta::TypeFactory.receive({ :type => :hash, :values => schema.delete(:values)})
249
+ when type == Array && schema.has_key?(:items) then Icss::Meta::TypeFactory.receive({ :type => :array, :items => schema.delete(:items) })
250
+ when type == Hash then IdenticalHashFactory
251
+ when type == Array then IdenticalArrayFactory
252
+ else
253
+ Icss::Meta::TypeFactory.receive(type)
254
+ end
255
+ fld = (field_schemas[name] || make_field_schema).merge(schema)
256
+ fld[:parent] = self
257
+ @field_schemas[name] = fld
258
+ end
259
+
260
+ def add_field_accessor(field_name, schema)
261
+ visibility = schema[:accessor] || :public
262
+ reader_meth = field_name ; writer_meth = "#{field_name}=" ; attr_name = "@#{field_name}"
263
+ unless (visibility == :none)
264
+ define_metamodel_method(reader_meth, visibility){ instance_variable_get(attr_name) }
265
+ define_metamodel_method(writer_meth, visibility){|v| instance_variable_set(attr_name, v) }
266
+ end
267
+ end
268
+
269
+ def _rcvr_methods
270
+ all_f = {}
271
+ call_ancestor_chain(:_rcvr_methods){|anc_f| all_f.merge!(anc_f) }
272
+ all_f
273
+ end
274
+
275
+ def _register_rcvr_for(attr_name, rcvr_meth)
276
+ @_rcvr_methods ||= {}
277
+ @_rcvr_methods[attr_name.to_sym] = rcvr_meth.to_sym
278
+ end
279
+
280
+ # Adds after_receivers to implement some of the options to .field
281
+ #
282
+ # @option schema [Object] :default -- if field is unset by time of
283
+ # after_receive, the field will be set to a copy of this value
284
+ #
285
+ # @option schema [Hash] :replace -- if value is in the hash
286
+ # class Foo < Icss::Thing
287
+ # field :temperature, Integer, :replace => { 9999 => nil }
288
+ # end
289
+ # f = Foo.receive({:temperature => 9999})
290
+ # # #<Foo:0x10156c820 @temperature=nil>
291
+ #
292
+ def add_after_receivers(field_name)
293
+ schema = field_named(field_name)
294
+ set_field_default(field_name, schema[:default]) if schema.has_key?(:default)
295
+ if schema.has_key?(:replace)
296
+ repl = schema[:replace]
297
+ after_receive(:"replace_#{field_name}") do
298
+ val = self.send(field_name)
299
+ if repl.has_key?(val)
300
+ self._set_field_val(field_name, repl[val])
301
+ end
302
+ end
303
+ end
304
+ # super(field_name) if defined?(super)
305
+ end
306
+
307
+
308
+ def set_field_default(field_name, default_val=nil, &blk)
309
+ blk = default_val if default_val.is_a?(Proc)
310
+ if blk
311
+ after_receive(:"default_#{field_name}") do
312
+ val = instance_exec(&blk)
313
+ self._set_field_val(field_name, val) unless attr_set?(field_name)
314
+ end
315
+ else
316
+ after_receive(:"default_#{field_name}") do
317
+ val = default_val.try_dup
318
+ self._set_field_val(field_name, val) unless attr_set?(field_name)
319
+ end
320
+ end
321
+ end
322
+
323
+ end # RecordType
324
+ end
325
+ end