gorillib-model 0.0.1

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