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.
Files changed (56) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +12 -0
  3. data/README.md +21 -0
  4. data/Rakefile +15 -0
  5. data/gorillib-model.gemspec +27 -0
  6. data/lib/gorillib/builder.rb +239 -0
  7. data/lib/gorillib/core_ext/datetime.rb +23 -0
  8. data/lib/gorillib/core_ext/exception.rb +153 -0
  9. data/lib/gorillib/core_ext/module.rb +10 -0
  10. data/lib/gorillib/core_ext/object.rb +14 -0
  11. data/lib/gorillib/model/base.rb +273 -0
  12. data/lib/gorillib/model/collection/model_collection.rb +157 -0
  13. data/lib/gorillib/model/collection.rb +200 -0
  14. data/lib/gorillib/model/defaults.rb +115 -0
  15. data/lib/gorillib/model/errors.rb +24 -0
  16. data/lib/gorillib/model/factories.rb +555 -0
  17. data/lib/gorillib/model/field.rb +168 -0
  18. data/lib/gorillib/model/lint.rb +24 -0
  19. data/lib/gorillib/model/named_schema.rb +53 -0
  20. data/lib/gorillib/model/positional_fields.rb +35 -0
  21. data/lib/gorillib/model/schema_magic.rb +163 -0
  22. data/lib/gorillib/model/serialization/csv.rb +60 -0
  23. data/lib/gorillib/model/serialization/json.rb +44 -0
  24. data/lib/gorillib/model/serialization/lines.rb +30 -0
  25. data/lib/gorillib/model/serialization/to_wire.rb +54 -0
  26. data/lib/gorillib/model/serialization/tsv.rb +53 -0
  27. data/lib/gorillib/model/serialization.rb +41 -0
  28. data/lib/gorillib/model/type/extended.rb +83 -0
  29. data/lib/gorillib/model/type/ip_address.rb +153 -0
  30. data/lib/gorillib/model/type/url.rb +11 -0
  31. data/lib/gorillib/model/validate.rb +22 -0
  32. data/lib/gorillib/model/version.rb +5 -0
  33. data/lib/gorillib/model.rb +34 -0
  34. data/spec/builder_spec.rb +193 -0
  35. data/spec/core_ext/datetime_spec.rb +41 -0
  36. data/spec/core_ext/exception.rb +98 -0
  37. data/spec/core_ext/object.rb +45 -0
  38. data/spec/model/collection_spec.rb +290 -0
  39. data/spec/model/defaults_spec.rb +104 -0
  40. data/spec/model/factories_spec.rb +323 -0
  41. data/spec/model/lint_spec.rb +28 -0
  42. data/spec/model/serialization/csv_spec.rb +30 -0
  43. data/spec/model/serialization/tsv_spec.rb +28 -0
  44. data/spec/model/serialization_spec.rb +41 -0
  45. data/spec/model/type/extended_spec.rb +166 -0
  46. data/spec/model/type/ip_address_spec.rb +141 -0
  47. data/spec/model_spec.rb +261 -0
  48. data/spec/spec_helper.rb +15 -0
  49. data/spec/support/capture_output.rb +28 -0
  50. data/spec/support/nuke_constants.rb +9 -0
  51. data/spec/support/shared_context_for_builders.rb +59 -0
  52. data/spec/support/shared_context_for_models.rb +55 -0
  53. data/spec/support/shared_examples_for_factories.rb +71 -0
  54. data/spec/support/shared_examples_for_model_fields.rb +62 -0
  55. data/spec/support/shared_examples_for_models.rb +87 -0
  56. 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