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