field_mapper 0.1.0

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.
@@ -0,0 +1,157 @@
1
+ require "csv"
2
+ require "oj"
3
+ require_relative "value"
4
+
5
+ module FieldMapper
6
+ module Standard
7
+ class Field
8
+ include FieldMapper::Marshaller
9
+
10
+ attr_reader(
11
+ :name,
12
+ :type,
13
+ :desc,
14
+ :default,
15
+ :values
16
+ )
17
+
18
+ def initialize(
19
+ name,
20
+ type: nil,
21
+ desc: nil,
22
+ default: nil
23
+ )
24
+ raise TypeNotSpecified.new("type not specified for: #{name}") if type.nil?
25
+ @name = name.to_sym
26
+ @type = type
27
+ @desc= desc
28
+ @default = default
29
+ end
30
+
31
+ def list?
32
+ type.name == "FieldMapper::Types::List"
33
+ end
34
+
35
+ def plat?
36
+ type.name == "FieldMapper::Types::Plat"
37
+ end
38
+
39
+ def plat_field?
40
+ plat? || plat_list?
41
+ end
42
+
43
+ def plat_list?
44
+ list? && type.plat_list?
45
+ end
46
+
47
+ def raw_values
48
+ return nil unless has_values?
49
+ values.map { |v| v.value }
50
+ end
51
+
52
+ # Adds a value to a Field instance.
53
+ # Intended use is from within a Plat class declaration.
54
+ #
55
+ # @example
56
+ # class ExamplePlat < FieldMapper::Standard::Plat
57
+ # field :example do
58
+ # value 1
59
+ # end
60
+ # end
61
+ def value(val)
62
+ @values ||= []
63
+ @values << FieldMapper::Standard::Value.new(val, field: self)
64
+ @values.last
65
+ end
66
+
67
+ # Adds values to a Field instance that are defined in a CSV file.
68
+ #
69
+ # Intended use is from within a Plat class declaration.
70
+ # @example
71
+ # class ExamplePlat < FieldMapper::Standard::Plat
72
+ # field :example do
73
+ # load_values "/path/to/file.csv"
74
+ # end
75
+ # end
76
+ #
77
+ # The format of the CSV file should contain a single column with a header row.
78
+ # @example
79
+ # Name of Field
80
+ # 1
81
+ # 2
82
+ #
83
+ def load_values(path_to_csv)
84
+ CSV.foreach(path_to_csv, :headers => true) do |row|
85
+ value row["standard_value"].to_s.strip
86
+ end
87
+ end
88
+
89
+ def has_values?
90
+ !values.nil?
91
+ end
92
+
93
+ def find_value(value)
94
+ return nil unless has_values?
95
+ values.find { |val| val == value || val.value == value }
96
+ end
97
+
98
+ def cast(value, as_single_value: false)
99
+ value = cast_value(type, value, as_single_value: as_single_value)
100
+ return nil if value.nil? || value.to_s.blank?
101
+ value = clean_value(value) unless as_single_value
102
+ value
103
+ end
104
+
105
+ alias_method :to_s, :name
106
+
107
+ private
108
+
109
+ def cast_value(type, value, as_single_value: false)
110
+ return nil if value.nil?
111
+ case type.name
112
+ when "String" then return value.to_s.strip
113
+ when "FieldMapper::Types::Boolean" then return FieldMapper::Types::Boolean.parse(value)
114
+ when "Time" then return Time.parse(value.to_s) rescue nil # TODO: log error?
115
+ when "Integer" then return value.to_i
116
+ when "Float" then return value.to_f
117
+ when "Money" then return Monetize.parse(value) rescue nil # TODO: log error?
118
+ when "FieldMapper::Types::Plat" then return plat_instance(type, value)
119
+ when "FieldMapper::Types::List" then
120
+ return value if value.is_a?(Array) && value.empty?
121
+ return plat_instance_list(type, value) if type.plat_list?
122
+ return cast_value(type.type, value) if as_single_value
123
+ get_list value
124
+ else
125
+ nil
126
+ end
127
+ end
128
+
129
+ def get_list(value)
130
+ value = unmarshal(value) if value.is_a?(String)
131
+ value.map { |val| cast_value(type.type, val) }
132
+ end
133
+
134
+ def clean_value(value)
135
+ return value unless has_values?
136
+ return value unless type.name == "FieldMapper::Types::List"
137
+ value & raw_values
138
+ end
139
+
140
+ def plat_instance(type, value)
141
+ return value if value.is_a?(FieldMapper::Standard::Plat)
142
+ return value if value.is_a?(Numeric)
143
+ return value.to_i if value.is_a?(String) && value =~ /\A\d+\z/
144
+ return type.type.new(value) if value.is_a?(Hash)
145
+ return type.type.new(unmarshal(value)) if value.is_a?(String)
146
+ nil
147
+ end
148
+
149
+ def plat_instance_list(type, value)
150
+ return value if value.empty?
151
+ value = unmarshal(value) if value.is_a?(String)
152
+ return value.map { |val| plat_instance(type, val) }
153
+ end
154
+
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,279 @@
1
+ require "digest/md5"
2
+ require_relative "../errors"
3
+ require_relative "../name_helper"
4
+ require_relative "../marshaller"
5
+ require_relative "field"
6
+
7
+ module FieldMapper
8
+ module Standard
9
+ class Plat
10
+ include FieldMapper::NameHelper
11
+ include FieldMapper::Marshaller
12
+
13
+ class << self
14
+ include FieldMapper::NameHelper
15
+
16
+ def fields
17
+ @fields ||= HashWithIndifferentAccess.new
18
+ end
19
+
20
+ def field_names
21
+ @field_names ||= {}
22
+ end
23
+
24
+ def field(
25
+ name,
26
+ type: nil,
27
+ desc: nil,
28
+ default: nil,
29
+ &block
30
+ )
31
+ field_names[attr_name(name)] = name
32
+
33
+ field = fields[name] = FieldMapper::Standard::Field.new(
34
+ name,
35
+ type: type,
36
+ desc: desc,
37
+ default: default
38
+ )
39
+
40
+ field.instance_exec(&block) if block_given?
41
+
42
+ define_method(attr_name name) do
43
+ self[name]
44
+ end
45
+
46
+ define_method("#{attr_name name}=") do |value|
47
+ self[name] = value
48
+ end
49
+ end
50
+
51
+ def find_field(field_name)
52
+ fields[field_names[attr_name(field_name)]]
53
+ end
54
+
55
+ def has_plat_fields?
56
+ !plat_fields.empty?
57
+ end
58
+
59
+ def plat_fields
60
+ fields.reduce({}) do |memo, keypair|
61
+ if keypair.last.plat_field?
62
+ memo[keypair.first] = keypair.last
63
+ end
64
+ memo
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ attr_reader :node_id
71
+
72
+ def initialize(params={})
73
+ @node_id = params["_node_id"]
74
+ assign_defaults
75
+ assign_params params
76
+ end
77
+
78
+ def scope
79
+ @scope ||= begin
80
+ scope_name = self.class.name.split("::")[0..-2].join("::")
81
+ if scope_name.empty?
82
+ ::Object
83
+ else
84
+ ::Object.const_get(scope_name)
85
+ end
86
+ end
87
+ end
88
+
89
+ def after_convert(from: nil, to: nil)
90
+ # abstract method to be implemented by subclasses
91
+ end
92
+
93
+ def [](field_name)
94
+ raise FieldNotDefined unless field_exists?(field_name)
95
+ instance_variable_get "@#{attr_name(field_name)}"
96
+ end
97
+
98
+ def []=(field_name, value)
99
+ field = self.class.find_field(field_name)
100
+ raise FieldNotDefined if field.nil?
101
+ instance_variable_set "@#{attr_name(field_name)}", field.cast(value)
102
+ end
103
+
104
+ def to_hash(flatten: false, history: {}, include_meta: true)
105
+ history[object_id] = true
106
+ hash = self.class.fields.values.reduce(HashWithIndifferentAccess.new) do |memo, field|
107
+ name = field.name
108
+ value = instance_variable_get("@#{attr_name(name)}")
109
+
110
+ if value.present?
111
+ case field.type.name
112
+ when "FieldMapper::Types::Plat" then
113
+ if value.is_a? FieldMapper::Standard::Plat
114
+ oid = value.object_id
115
+ if history[oid].nil?
116
+ history[oid] = true
117
+ value = value.to_hash(flatten: flatten, history: history, include_meta: include_meta)
118
+ value = marshal(value) if flatten
119
+ else
120
+ value = oid
121
+ end
122
+ else
123
+ value
124
+ end
125
+ when "FieldMapper::Types::List" then
126
+ if field.plat_list?
127
+ value = value.map do |val|
128
+ if val.is_a? FieldMapper::Standard::Plat
129
+ oid = val.object_id
130
+ if history[oid].nil?
131
+ history[oid] = true
132
+ val.to_hash(flatten: flatten, history: history, include_meta: include_meta)
133
+ else
134
+ oid
135
+ end
136
+ else
137
+ val
138
+ end
139
+ end
140
+ end
141
+ value = marshal(value) if flatten
142
+ when "Money" then
143
+ value = value.format(with_currency: true)
144
+ when "Time" then
145
+ value = value.utc.iso8601
146
+ end
147
+ end
148
+
149
+ memo[name] = value
150
+ memo
151
+ end
152
+
153
+ if include_meta
154
+ hash = {
155
+ _node_id: object_id,
156
+ _flat: flatten
157
+ }.merge(hash)
158
+ end
159
+
160
+ HashWithIndifferentAccess.new(hash)
161
+ end
162
+
163
+ def cache_key
164
+ self.class.name + "-" + Digest::MD5.hexdigest(to_hash.to_s)
165
+ end
166
+
167
+ protected
168
+
169
+ def plat_values
170
+ self.class.plat_fields.values.reduce([]) do |memo, field|
171
+ value = send(attr_name(field.name))
172
+ memo << value if field.plat?
173
+ memo.concat value if field.plat_list? && !value.nil?
174
+ memo
175
+ end
176
+ end
177
+
178
+ def descendant_plats(history: {})
179
+ return {} if history[object_id]
180
+ history[object_id] = true
181
+ plats = {}
182
+ plats[self.node_id || self.object_id] = self
183
+
184
+ plat_values.each do |plat|
185
+ if plat.is_a? FieldMapper::Standard::Plat
186
+ plats[plat.node_id || plat.object_id] = plat
187
+ plats.merge! plat.descendant_plats(history: history)
188
+ end
189
+ end
190
+
191
+ plats
192
+ end
193
+
194
+ def pending_assignments
195
+ @pending_assignments ||= []
196
+ end
197
+
198
+ def all_pending_assignments
199
+ descendant_plats.values.reduce([]) do |memo, plat|
200
+ memo.concat plat.pending_assignments
201
+ end
202
+ end
203
+
204
+ def field_exists?(field_name)
205
+ !!self.class.field_names[attr_name(field_name)]
206
+ end
207
+
208
+ def assign_defaults
209
+ self.class.fields.each do |name, field|
210
+ if !field.default.nil?
211
+ value = field.default
212
+ value = value.clone rescue value
213
+ instance_variable_set "@#{attr_name(field.name)}", field.cast(value)
214
+ end
215
+ end
216
+ end
217
+
218
+ def assign_params(params)
219
+ pending_assignments.clear
220
+ params.each do |name, value|
221
+ field = self.class.fields[name]
222
+ next if field.nil?
223
+ value = field.cast(value)
224
+ next if value.nil?
225
+ next if field.list? && value.compact.empty?
226
+ assign_param name, value
227
+ add_pending_assignment field, value
228
+ end
229
+
230
+ apply_pending_assignments descendant_plats
231
+ end
232
+
233
+ def assign_param(name, value)
234
+ instance_variable_set "@#{attr_name(name)}", value
235
+ end
236
+
237
+ def add_pending_assignment(field, value)
238
+ if field.plat? && value.is_a?(Numeric)
239
+ add_pending_assignment_for_plat field, value
240
+ end
241
+
242
+ if field.plat_list? && !value.nil?
243
+ add_pending_assignment_for_plat_list field, value
244
+ end
245
+ end
246
+
247
+ def add_pending_assignment_for_plat(field, value)
248
+ pending_assignments << lambda do |descendant_plats|
249
+ if value.is_a?(Numeric)
250
+ plat = descendant_plats[value]
251
+ assign_param field.name, plat unless plat.nil?
252
+ end
253
+ end
254
+ end
255
+
256
+ def add_pending_assignment_for_plat_list(field, value)
257
+ pending_assignments << lambda do |descendant_plats|
258
+ list = value.reduce([]) do |memo, val|
259
+ if val.is_a?(Numeric)
260
+ plat = descendant_plats[val]
261
+ memo << (plat ? plat : val)
262
+ else
263
+ memo << val
264
+ end
265
+ memo
266
+ end
267
+ assign_param field.name, list
268
+ end
269
+ end
270
+
271
+ def apply_pending_assignments(descendant_plats)
272
+ all_pending_assignments.each do |assignment|
273
+ assignment.call descendant_plats
274
+ end
275
+ end
276
+
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "../errors"
2
+
3
+ module FieldMapper
4
+ module Standard
5
+ class Value
6
+ attr_reader(
7
+ :value,
8
+ :field
9
+ )
10
+
11
+ def initialize(
12
+ value,
13
+ field: nil
14
+ )
15
+ raise ArgumentError.new("field is required") if
16
+ field.nil?
17
+
18
+ @field = field
19
+ @value = field.cast(value, as_single_value: true)
20
+ end
21
+ end
22
+
23
+ end
24
+ end