gorillib-model 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +12 -0
- data/README.md +21 -0
- data/Rakefile +15 -0
- data/gorillib-model.gemspec +27 -0
- data/lib/gorillib/builder.rb +239 -0
- data/lib/gorillib/core_ext/datetime.rb +23 -0
- data/lib/gorillib/core_ext/exception.rb +153 -0
- data/lib/gorillib/core_ext/module.rb +10 -0
- data/lib/gorillib/core_ext/object.rb +14 -0
- data/lib/gorillib/model/base.rb +273 -0
- data/lib/gorillib/model/collection/model_collection.rb +157 -0
- data/lib/gorillib/model/collection.rb +200 -0
- data/lib/gorillib/model/defaults.rb +115 -0
- data/lib/gorillib/model/errors.rb +24 -0
- data/lib/gorillib/model/factories.rb +555 -0
- data/lib/gorillib/model/field.rb +168 -0
- data/lib/gorillib/model/lint.rb +24 -0
- data/lib/gorillib/model/named_schema.rb +53 -0
- data/lib/gorillib/model/positional_fields.rb +35 -0
- data/lib/gorillib/model/schema_magic.rb +163 -0
- data/lib/gorillib/model/serialization/csv.rb +60 -0
- data/lib/gorillib/model/serialization/json.rb +44 -0
- data/lib/gorillib/model/serialization/lines.rb +30 -0
- data/lib/gorillib/model/serialization/to_wire.rb +54 -0
- data/lib/gorillib/model/serialization/tsv.rb +53 -0
- data/lib/gorillib/model/serialization.rb +41 -0
- data/lib/gorillib/model/type/extended.rb +83 -0
- data/lib/gorillib/model/type/ip_address.rb +153 -0
- data/lib/gorillib/model/type/url.rb +11 -0
- data/lib/gorillib/model/validate.rb +22 -0
- data/lib/gorillib/model/version.rb +5 -0
- data/lib/gorillib/model.rb +34 -0
- data/spec/builder_spec.rb +193 -0
- data/spec/core_ext/datetime_spec.rb +41 -0
- data/spec/core_ext/exception.rb +98 -0
- data/spec/core_ext/object.rb +45 -0
- data/spec/model/collection_spec.rb +290 -0
- data/spec/model/defaults_spec.rb +104 -0
- data/spec/model/factories_spec.rb +323 -0
- data/spec/model/lint_spec.rb +28 -0
- data/spec/model/serialization/csv_spec.rb +30 -0
- data/spec/model/serialization/tsv_spec.rb +28 -0
- data/spec/model/serialization_spec.rb +41 -0
- data/spec/model/type/extended_spec.rb +166 -0
- data/spec/model/type/ip_address_spec.rb +141 -0
- data/spec/model_spec.rb +261 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/capture_output.rb +28 -0
- data/spec/support/nuke_constants.rb +9 -0
- data/spec/support/shared_context_for_builders.rb +59 -0
- data/spec/support/shared_context_for_models.rb +55 -0
- data/spec/support/shared_examples_for_factories.rb +71 -0
- data/spec/support/shared_examples_for_model_fields.rb +62 -0
- data/spec/support/shared_examples_for_models.rb +87 -0
- metadata +193 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Represents a field for reflection
|
5
|
+
#
|
6
|
+
# @example Usage
|
7
|
+
# Gorillib::Model::Field.new(:name => 'problems', type => Integer, :doc => 'Count of problems')
|
8
|
+
#
|
9
|
+
#
|
10
|
+
class Field
|
11
|
+
include Gorillib::Model
|
12
|
+
remove_possible_method(:type)
|
13
|
+
|
14
|
+
# [Gorillib::Model] Model owning this field
|
15
|
+
attr_reader :model
|
16
|
+
|
17
|
+
# [Hash] all options passed to the field not recognized by one of its own current fields
|
18
|
+
attr_reader :_extra_attributes
|
19
|
+
|
20
|
+
# Note: `Gorillib::Model::Field` is assembled in two pieces, so that it
|
21
|
+
# can behave as a model itself. Defining `name` here, along with some
|
22
|
+
# fudge in #initialize, provides enough functionality to bootstrap.
|
23
|
+
# The fields are then defined properly at the end of the file.
|
24
|
+
|
25
|
+
attr_reader :name
|
26
|
+
attr_reader :type
|
27
|
+
|
28
|
+
class_attribute :visibilities, :instance_writer => false
|
29
|
+
self.visibilities = { :reader => :public, :writer => :public, :receiver => :public, :tester => false }
|
30
|
+
|
31
|
+
# @param [#to_sym] name Field name
|
32
|
+
# @param [#receive] type Factory for field values. To accept any object as-is, specify `Object` as the type.
|
33
|
+
# @param [Gorillib::Model] model Field's owner
|
34
|
+
# @param [Hash{Symbol => Object}] options Extended attributes
|
35
|
+
# @option options [String] doc Description of the field's purpose
|
36
|
+
# @option options [true, false, :public, :protected, :private] :reader Visibility for the reader (`#foo`) method; `false` means don't create one.
|
37
|
+
# @option options [true, false, :public, :protected, :private] :writer Visibility for the writer (`#foo=`) method; `false` means don't create one.
|
38
|
+
# @option options [true, false, :public, :protected, :private] :receiver Visibility for the receiver (`#receive_foo`) method; `false` means don't create one.
|
39
|
+
#
|
40
|
+
def initialize(model, name, type, options={})
|
41
|
+
Validate.identifier!(name)
|
42
|
+
type_opts = options.extract!(:blankish, :empty_product, :items, :keys, :of)
|
43
|
+
type_opts[:items] = type_opts.delete(:of) if type_opts.has_key?(:of)
|
44
|
+
#
|
45
|
+
@model = model
|
46
|
+
@name = name.to_sym
|
47
|
+
@type = Gorillib::Factory.factory_for(type, type_opts)
|
48
|
+
default_visibilities = visibilities
|
49
|
+
@visibilities = default_visibilities.merge( options.extract!(*default_visibilities.keys).compact )
|
50
|
+
@doc = options.delete(:name){ "#{name} field" }
|
51
|
+
receive!(options)
|
52
|
+
end
|
53
|
+
|
54
|
+
# __________________________________________________________________________
|
55
|
+
|
56
|
+
# @return [String] the field name
|
57
|
+
def to_s
|
58
|
+
name.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [String] Human-readable presentation of the field definition
|
62
|
+
def inspect
|
63
|
+
args = [name.inspect, type.to_s, attributes.reject{|k,v| k =~ /^(name|type)$/}.inspect]
|
64
|
+
"field(#{args.join(",")})"
|
65
|
+
end
|
66
|
+
def inspect_compact
|
67
|
+
"field(#{name})"
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_hash
|
71
|
+
attributes.merge!(@visibility).merge!(@_extra_attributes)
|
72
|
+
end
|
73
|
+
|
74
|
+
def ==(val)
|
75
|
+
super && (val._extra_attributes == self._extra_attributes) && (val.model == self.model)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.receive(hsh)
|
79
|
+
name = hsh.fetch(:name)
|
80
|
+
type = hsh.fetch(:type)
|
81
|
+
model = hsh.fetch(:model)
|
82
|
+
new(model, name, type, hsh)
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# returns the visibility
|
87
|
+
#
|
88
|
+
# @example reader is protected, no writer:
|
89
|
+
# Foo.field :granuloxity, :reader => :protected, :writer => false
|
90
|
+
#
|
91
|
+
def visibility(meth_type)
|
92
|
+
Validate.included_in!("method type", meth_type, @visibilities.keys)
|
93
|
+
@visibilities[meth_type]
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
#
|
99
|
+
#
|
100
|
+
#
|
101
|
+
def inscribe_methods(model)
|
102
|
+
model.__send__(:define_attribute_reader, self.name, self.type, visibility(:reader))
|
103
|
+
model.__send__(:define_attribute_writer, self.name, self.type, visibility(:writer))
|
104
|
+
model.__send__(:define_attribute_tester, self.name, self.type, visibility(:tester))
|
105
|
+
model.__send__(:define_attribute_receiver, self.name, self.type, visibility(:receiver))
|
106
|
+
end
|
107
|
+
|
108
|
+
public
|
109
|
+
|
110
|
+
#
|
111
|
+
# Now we can construct the actual fields.
|
112
|
+
#
|
113
|
+
|
114
|
+
field :position, Integer, :tester => true, :doc => "Indicates this is a positional initialization arg -- you can pass it as a plain value in the given slot to #initialize"
|
115
|
+
|
116
|
+
# Name of this field. Must start with `[A-Za-z_]` and subsequently contain only `[A-Za-z0-9_]` (required)
|
117
|
+
# @macro [attach] field
|
118
|
+
# @attribute $1
|
119
|
+
# @return [$2] the $1 field $*
|
120
|
+
field :name, String, position: 0, writer: false, doc: "The field name. Must start with `[A-Za-z_]` and subsequently contain only `[A-Za-z0-9_]` (required)"
|
121
|
+
|
122
|
+
field :type, Class, position: 1, doc: "Factory to generate field's values"
|
123
|
+
|
124
|
+
field :doc, String, doc: "Field's description"
|
125
|
+
|
126
|
+
# remove the attr_reader method (needed for scaffolding), leaving the meta_module method to remain
|
127
|
+
remove_possible_method(:name)
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
class SimpleCollectionField < Gorillib::Model::Field
|
133
|
+
field :item_type, Class, default: Whatever, doc: "Factory for collection items"
|
134
|
+
# field :collection_attrs, Hash, default: Hash.new, doc: "Extra attributes to pass to the collection on creation -- eg. key_method"
|
135
|
+
|
136
|
+
def initialize(model, name, type, options={})
|
137
|
+
super
|
138
|
+
collection_type = self.type
|
139
|
+
item_type = self.item_type
|
140
|
+
key_method = options[:key_method] if options[:key_method]
|
141
|
+
raise "Please supply an item type for #{self.inspect} -- eg 'collection #{name.inspect}, of: FooClass'" unless item_type
|
142
|
+
self.default ||= ->{ collection_type.new(item_type: item_type, belongs_to: self, key_method: key_method) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def inscribe_methods(model)
|
146
|
+
super
|
147
|
+
model.__send__(:define_collection_receiver, self)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# * aliases
|
155
|
+
# * order
|
156
|
+
# * dirty
|
157
|
+
# * lazy
|
158
|
+
# * mass assignable
|
159
|
+
# * identifier / index
|
160
|
+
# * hook
|
161
|
+
# * validates / required
|
162
|
+
# - presence => true
|
163
|
+
# - uniqueness => true
|
164
|
+
# - numericality => true # also :==, :>, :>=, :<, :<=, :odd?, :even?, :equal_to, :less_than, etc
|
165
|
+
# - length => { :< => 7 } # also :==, :>=, :<=, :is, :minimum, :maximum
|
166
|
+
# - format => { :with => /.*/ }
|
167
|
+
# - inclusion => { :in => [1,2,3] }
|
168
|
+
# - exclusion => { :in => [1,2,3] }
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
#
|
4
|
+
# A set of guards for good behavior:
|
5
|
+
#
|
6
|
+
# * checks that fields given to read_attribute, write_attribute, etc are defined
|
7
|
+
#
|
8
|
+
module Lint
|
9
|
+
def read_attribute(field_name, *) check_field(field_name) ; super ; end
|
10
|
+
def write_attribute(field_name, *) check_field(field_name) ; super ; end
|
11
|
+
def unset_attribute(field_name, *) check_field(field_name) ; super ; end
|
12
|
+
def attribute_set?(field_name, *) check_field(field_name) ; super ; end
|
13
|
+
|
14
|
+
protected
|
15
|
+
# @return [true] if the field exists
|
16
|
+
# @raise [UnknownFieldError] if the field is missing
|
17
|
+
def check_field(field_name)
|
18
|
+
return true if self.class.has_field?(field_name)
|
19
|
+
raise UnknownFieldError, "unknown field: #{field_name} for #{self}"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
module NamedSchema
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
#
|
8
|
+
# Returns the meta_module -- a module extending the type, on which all the
|
9
|
+
# model methods are inscribed. This allows you to override the model methods
|
10
|
+
# and call +super()+ to get the generic behavior.
|
11
|
+
#
|
12
|
+
# The meta_module is named for the including class, but with 'Meta::'
|
13
|
+
# prepended and 'Type' appended -- so Geo::Place has meta_module
|
14
|
+
# "Meta::Geo::PlaceType"
|
15
|
+
#
|
16
|
+
def meta_module
|
17
|
+
return @_meta_module if defined?(@_meta_module)
|
18
|
+
if self.name
|
19
|
+
@_meta_module = ::Gorillib::Model::NamedSchema.get_nested_module("Meta::#{self.name}Type")
|
20
|
+
else
|
21
|
+
@_meta_module = Module.new
|
22
|
+
end
|
23
|
+
self.class_eval{ include(@_meta_module) }
|
24
|
+
@_meta_module
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_meta_module_method(method_name, visibility=:public, &block)
|
28
|
+
if (visibility == false) then return ; end
|
29
|
+
if (visibility == true) then visibility = :public ; end
|
30
|
+
Validate.included_in!("visibility", visibility, [:public, :private, :protected])
|
31
|
+
meta_module.module_eval{ define_method(method_name, &block) }
|
32
|
+
meta_module.module_eval "#{visibility} :#{method_name}", __FILE__, __LINE__
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a module for the given names, rooted at Object (so
|
36
|
+
# implicity with '::').
|
37
|
+
# @example
|
38
|
+
# get_nested_module(["This", "That", "TheOther"])
|
39
|
+
# # This::That::TheOther
|
40
|
+
def self.get_nested_module(name)
|
41
|
+
name.split('::').inject(Object) do |parent_module, module_name|
|
42
|
+
# inherit = false makes these methods be scoped to parent_module instead of universally
|
43
|
+
if parent_module.const_defined?(module_name, false)
|
44
|
+
parent_module.const_get(module_name, false)
|
45
|
+
else
|
46
|
+
parent_module.const_set(module_name.to_sym, Module.new)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
|
4
|
+
#
|
5
|
+
# give each field the next position in order
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Foo
|
9
|
+
# include Gorillib::Model
|
10
|
+
# field :bob, String # not positional
|
11
|
+
# field :zoe, String, position: 0 # positional 0 (explicit)
|
12
|
+
# end
|
13
|
+
# class Subby < Foo
|
14
|
+
# include Gorillib::Model::PositionalFields
|
15
|
+
# field :wun, String # positional 1
|
16
|
+
# end
|
17
|
+
# Foo.field :nope, String # not positional
|
18
|
+
# Subby.field :toofer, String # positional 2
|
19
|
+
#
|
20
|
+
# @note: make sure you're keeping positionals straight in super classes, or
|
21
|
+
# in anything added after this.
|
22
|
+
#
|
23
|
+
module PositionalFields
|
24
|
+
extend ActiveSupport::Concern
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def field(*args)
|
28
|
+
options = args.extract_options!
|
29
|
+
super(*args, {position: positionals.count}.merge(options))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
|
6
|
+
# Defines a new field
|
7
|
+
#
|
8
|
+
# For each field that is defined, a getter and setter will be added as
|
9
|
+
# an instance method to the model. An Field instance will be added to
|
10
|
+
# result of the fields class method.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# field :height, Integer
|
14
|
+
#
|
15
|
+
# @param [Symbol] field_name The field name. Must start with `[A-Za-z_]` and subsequently contain only `[A-Za-z0-9_]`
|
16
|
+
# @param [Class] type The field's type (required)
|
17
|
+
# @option options [String] doc Documentation string for the field (optional)
|
18
|
+
# @option options [Proc, Object] default Default value, or proc that instance can evaluate to find default value
|
19
|
+
#
|
20
|
+
# @return Gorillib::Model::Field
|
21
|
+
def field(field_name, type, options={})
|
22
|
+
options = options.symbolize_keys
|
23
|
+
field_type = options.delete(:field_type){ ::Gorillib::Model::Field }
|
24
|
+
fld = field_type.new(self, field_name, type, options)
|
25
|
+
@_own_fields[fld.name] = fld
|
26
|
+
_reset_descendant_fields
|
27
|
+
fld.send(:inscribe_methods, self)
|
28
|
+
fld
|
29
|
+
end
|
30
|
+
|
31
|
+
def collection(field_name, collection_type, options={})
|
32
|
+
options[:item_type] = options[:of] if options.has_key?(:of)
|
33
|
+
field(field_name, collection_type, {
|
34
|
+
field_type: ::Gorillib::Model::SimpleCollectionField}.merge(options))
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [{Symbol => Gorillib::Model::Field}]
|
38
|
+
def fields
|
39
|
+
return @_fields if defined?(@_fields)
|
40
|
+
@_fields = ancestors.reverse.inject({}){|acc, ancestor| acc.merge!(ancestor.try(:_own_fields) || {}) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [true, false] true if the field is defined on this class
|
44
|
+
def has_field?(field_name)
|
45
|
+
fields.has_key?(field_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Array<Symbol>] The attribute names
|
49
|
+
def field_names
|
50
|
+
@_field_names ||= fields.keys
|
51
|
+
end
|
52
|
+
|
53
|
+
def positionals
|
54
|
+
@_positionals ||= assemble_positionals
|
55
|
+
end
|
56
|
+
|
57
|
+
def assemble_positionals
|
58
|
+
positionals = fields.values.keep_if{|fld| fld.position? }.sort_by!{|fld| fld.position }
|
59
|
+
return [] if positionals.empty?
|
60
|
+
if (positionals.map(&:position) != (0..positionals.length-1).to_a) then raise ConflictingPositionError, "field positions #{positionals.map(&:position).join(",")} for #{positionals.map(&:name).join(",")} aren't in strict minimal order" ; end
|
61
|
+
positionals.map!(&:name)
|
62
|
+
end
|
63
|
+
|
64
|
+
# turn model constructor args (`*positional_args, {attrs}`) into a combined
|
65
|
+
# attrs hash. positional_args are mapped to the set of attribute names in
|
66
|
+
# order -- by default, the class' field names.
|
67
|
+
#
|
68
|
+
# Notes:
|
69
|
+
#
|
70
|
+
# * Positional args always clobber elements of the attribute hash.
|
71
|
+
# * Nil positional args are treated as present-and-nil (this might change).
|
72
|
+
# * Raises an error if positional args
|
73
|
+
#
|
74
|
+
# @param [Array[Symbol]] args list of attributes, in order, to map.
|
75
|
+
#
|
76
|
+
# @return [Hash] a combined, reconciled hash of attributes to set
|
77
|
+
def attrs_hash_from_args(args)
|
78
|
+
attrs = args.extract_options!
|
79
|
+
if args.present?
|
80
|
+
ArgumentError.check_arity!(args, 0..positionals.length){ "extracting args #{args} for #{self}" }
|
81
|
+
positionals_to_map = positionals[0..(args.length-1)]
|
82
|
+
attrs = attrs.merge(Hash[positionals_to_map.zip(args)])
|
83
|
+
end
|
84
|
+
attrs
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
attr_reader :_own_fields
|
90
|
+
|
91
|
+
# Ensure that classes inherit all their parents' fields, even if fields
|
92
|
+
# are added after the child class is defined.
|
93
|
+
def _reset_descendant_fields
|
94
|
+
ObjectSpace.each_object(::Class) do |klass|
|
95
|
+
klass.__send__(:remove_instance_variable, '@_fields') if (klass <= self) && klass.instance_variable_defined?('@_fields')
|
96
|
+
klass.__send__(:remove_instance_variable, '@_field_names') if (klass <= self) && klass.instance_variable_defined?('@_field_names')
|
97
|
+
klass.__send__(:remove_instance_variable, '@_positionals') if (klass <= self) && klass.instance_variable_defined?('@_positionals')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# define the reader method `#foo` for a field named `:foo`
|
102
|
+
def define_attribute_reader(field_name, field_type, visibility)
|
103
|
+
define_meta_module_method(field_name, visibility) do
|
104
|
+
begin
|
105
|
+
read_attribute(field_name)
|
106
|
+
rescue StandardError => err ; err.polish("#{self.class}.#{field_name}") rescue nil ; raise ; end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# define the writer method `#foo=` for a field named `:foo`
|
111
|
+
def define_attribute_writer(field_name, field_type, visibility)
|
112
|
+
define_meta_module_method("#{field_name}=", visibility) do |val|
|
113
|
+
write_attribute(field_name, val)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# define the present method `#foo?` for a field named `:foo`
|
118
|
+
def define_attribute_tester(field_name, field_type, visibility)
|
119
|
+
field = fields[field_name]
|
120
|
+
define_meta_module_method("#{field_name}?", visibility) do
|
121
|
+
attribute_set?(field_name) || field.has_default?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def define_attribute_receiver(field_name, field_type, visibility)
|
126
|
+
# @param [Object] val the raw value to type-convert and adopt
|
127
|
+
# @return [Object] the attribute's new value
|
128
|
+
define_meta_module_method("receive_#{field_name}", visibility) do |val|
|
129
|
+
begin
|
130
|
+
val = field_type.receive(val)
|
131
|
+
write_attribute(field_name, val)
|
132
|
+
rescue StandardError => err ; err.polish("#{self.class}.#{field_name} type #{type} on #{val}") rescue nil ; raise ; end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# Collection receiver --
|
138
|
+
#
|
139
|
+
def define_collection_receiver(field)
|
140
|
+
collection_field_name = field.name; collection_type = field.type
|
141
|
+
# @param [Array[Object],Hash[Object]] the collection to merge
|
142
|
+
# @return [Gorillib::Collection] the updated collection
|
143
|
+
define_meta_module_method("receive_#{collection_field_name}", true) do |coll, &block|
|
144
|
+
begin
|
145
|
+
if collection_type.native?(coll)
|
146
|
+
write_attribute(collection_field_name, coll)
|
147
|
+
else
|
148
|
+
read_attribute(collection_field_name).receive!(coll, &block)
|
149
|
+
end
|
150
|
+
rescue StandardError => err ; err.polish("#{self.class} #{collection_field_name} collection on #{args}'") rescue nil ; raise ; end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def inherited(base)
|
155
|
+
base.instance_eval do
|
156
|
+
self.meta_module
|
157
|
+
@_own_fields ||= {}
|
158
|
+
end
|
159
|
+
super
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Gorillib
|
4
|
+
module Model
|
5
|
+
|
6
|
+
module LoadFromCsv
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
included do |base|
|
9
|
+
# Options that will be passed to CSV. Be careful to modify with assignment (`+=`) and not in-place (`<<`)
|
10
|
+
base.class_attribute :csv_options
|
11
|
+
base.csv_options = Hash.new
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# Iterate a block over each line of a CSV file
|
17
|
+
#
|
18
|
+
# @raise [Gorillib::Model::RawDataMismatchError] if a line has too many or too few fields
|
19
|
+
# @yield an object instantiated from each line in the file.
|
20
|
+
def each_in_csv(filename, options={})
|
21
|
+
filename = Pathname.new(filename)
|
22
|
+
options = csv_options.merge(options)
|
23
|
+
#
|
24
|
+
pop_headers = options.delete(:pop_headers)
|
25
|
+
num_fields = options.delete(:num_fields){ (fields.length .. fields.length) }
|
26
|
+
raise ArgumentError, "The :headers option to CSV changes its internal behavior; use 'pop_headers: true' to ignore the first line" if options[:headers]
|
27
|
+
#
|
28
|
+
CSV.open(filename, options) do |csv_file|
|
29
|
+
csv_file.shift if pop_headers
|
30
|
+
csv_file.each do |tuple|
|
31
|
+
next if tuple.empty?
|
32
|
+
unless num_fields.include?(tuple.length) then raise Gorillib::Model::RawDataMismatchError, "yark, spurious fields: #{tuple.inspect}" ; end
|
33
|
+
yield from_tuple(*tuple)
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# With a block, calls block on each object in turn (and returns nil)
|
40
|
+
#
|
41
|
+
# With no block, accumulates all the instances into the array it
|
42
|
+
# returns. As opposed to the with-a-block case, the memory footprint of
|
43
|
+
# this increases as the filesize does, so use caution with large files.
|
44
|
+
#
|
45
|
+
# @return with a block, returns nil; with no block, an array of this class' instances
|
46
|
+
def load_csv(*args)
|
47
|
+
if block_given?
|
48
|
+
each_in_csv(*args, &Proc.new)
|
49
|
+
else
|
50
|
+
objs = []
|
51
|
+
each_in_csv(*args){|obj| objs << obj }
|
52
|
+
objs
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Gorillib
|
4
|
+
module Model
|
5
|
+
|
6
|
+
module LoadFromJson
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include LoadLines
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Iterate a block over each line of a file having JSON records, one per
|
13
|
+
# line, in a big stack
|
14
|
+
#
|
15
|
+
# @yield an object instantiated from each line in the file.
|
16
|
+
def _each_from_json(filename, options={})
|
17
|
+
_each_raw_line(filename, options) do |line|
|
18
|
+
hsh = MultiJson.load(line)
|
19
|
+
yield receive(hsh)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# With a block, calls block on each object in turn (and returns nil)
|
24
|
+
#
|
25
|
+
# With no block, accumulates all the instances into the array it
|
26
|
+
# returns. As opposed to the with-a-block case, the memory footprint of
|
27
|
+
# this increases as the filesize does, so use caution with large files.
|
28
|
+
#
|
29
|
+
# @return with a block, returns nil; with no block, an array of this class' instances
|
30
|
+
def load_json(*args)
|
31
|
+
if block_given?
|
32
|
+
_each_from_json(*args, &Proc.new)
|
33
|
+
else
|
34
|
+
objs = []
|
35
|
+
_each_from_json(*args){|obj| objs << obj }
|
36
|
+
objs
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module LoadLines
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
# Iterate a block over each line of a file
|
10
|
+
# @yield each line in the file.
|
11
|
+
def _each_raw_line(filename, options={})
|
12
|
+
filename = Pathname.new(filename)
|
13
|
+
#
|
14
|
+
pop_headers = options.delete(:pop_headers)
|
15
|
+
#
|
16
|
+
File.open(filename) do |file|
|
17
|
+
file.readline if pop_headers
|
18
|
+
file.each do |line|
|
19
|
+
line.chomp! ; next if line.empty?
|
20
|
+
yield line
|
21
|
+
end
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Gorillib
|
2
|
+
|
3
|
+
module Hashlike
|
4
|
+
module Serialization
|
5
|
+
#
|
6
|
+
# Returns a hash with each key set to its associated value
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# my_hshlike = MyHashlike.new
|
10
|
+
# my_hshlike[:a] = 100; my_hshlike[:b] = 200
|
11
|
+
# my_hshlike.to_hash # => { :a => 100, :b => 200 }
|
12
|
+
#
|
13
|
+
# @return [Hash] a new Hash instance, with each key set to its associated value.
|
14
|
+
#
|
15
|
+
def to_wire(options={})
|
16
|
+
{}.tap do |hsh|
|
17
|
+
each do |attr,val|
|
18
|
+
hsh[attr] =
|
19
|
+
case
|
20
|
+
when val.respond_to?(:to_wire) then val.to_wire(options)
|
21
|
+
when val.respond_to?(:to_hash) then val.to_hash
|
22
|
+
else val ; end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Gorillib::Hashlike
|
31
|
+
include ::Gorillib::Hashlike::Serialization
|
32
|
+
end
|
33
|
+
|
34
|
+
class ::Array
|
35
|
+
def to_wire(options={})
|
36
|
+
map{|item| item.respond_to?(:to_wire) ? item.to_wire : item }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class ::Hash
|
41
|
+
include ::Gorillib::Hashlike::Serialization
|
42
|
+
end
|
43
|
+
|
44
|
+
class ::Time
|
45
|
+
def to_wire(options={})
|
46
|
+
self.iso8601
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ::NilClass
|
51
|
+
def to_wire(options={})
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module LoadFromTsv
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include LoadLines
|
7
|
+
|
8
|
+
included do |base|
|
9
|
+
# Options that will be passed to CSV. Be careful to modify with assignment (`+=`) and not in-place (`<<`)
|
10
|
+
base.class_attribute :tsv_options
|
11
|
+
base.tsv_options = Hash.new
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# Iterate a block over each line of a TSV file
|
17
|
+
#
|
18
|
+
# @raise [Gorillib::Model::RawDataMismatchError] if a line has too many or too few fields
|
19
|
+
# @yield an object instantiated from each line in the file.
|
20
|
+
def _each_from_tsv(filename, options={})
|
21
|
+
options = tsv_options.merge(options)
|
22
|
+
num_fields = options.delete(:num_fields){ (fields.length .. fields.length) }
|
23
|
+
max_fields = num_fields.max # need to make sure "1\t2\t\t\t" becomes ["1","2","","",""]
|
24
|
+
#
|
25
|
+
_each_raw_line(filename, options) do |line|
|
26
|
+
tuple = line.split("\t", max_fields)
|
27
|
+
unless num_fields.include?(tuple.length) then raise Gorillib::Model::RawDataMismatchError, "yark, spurious fields: #{tuple.inspect}" ; end
|
28
|
+
yield from_tuple(*tuple)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# With a block, calls block on each object in turn (and returns nil)
|
33
|
+
#
|
34
|
+
# With no block, accumulates all the instances into the array it
|
35
|
+
# returns. As opposed to the with-a-block case, the memory footprint of
|
36
|
+
# this increases as the filesize does, so use caution with large files.
|
37
|
+
#
|
38
|
+
# @return with a block, returns nil; with no block, an array of this class' instances
|
39
|
+
def load_tsv(*args)
|
40
|
+
if block_given?
|
41
|
+
_each_from_tsv(*args, &Proc.new)
|
42
|
+
else
|
43
|
+
objs = []
|
44
|
+
_each_from_tsv(*args){|obj| objs << obj }
|
45
|
+
objs
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|