icss 0.1.3 → 0.3.2

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