form_obj 0.1.0 → 0.2.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.
data/form_obj.gemspec CHANGED
@@ -33,8 +33,9 @@ well as serialized to a hash which reflects a model. ActiveModel::Errors could b
33
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
34
  spec.require_paths = ["lib"]
35
35
 
36
- spec.add_runtime_dependency "typed_array", ">= 1.0.0"
37
- spec.add_runtime_dependency "activemodel", ">= 3.2"
36
+ spec.add_dependency "tree_struct", ">= 1.0.2"
37
+ spec.add_dependency "activemodel", ">= 3.2"
38
+ spec.add_dependency "activesupport", ">= 3.2"
38
39
 
39
40
  spec.add_development_dependency "bundler", "~> 1.16"
40
41
  spec.add_development_dependency "rake", "~> 10.0"
@@ -1,28 +1,13 @@
1
- require 'typed_array'
2
-
3
- class FormObj
4
- class Array < TypedArray
5
- def initialize(klass, hash: false, model_class: nil)
6
- @item_hash = hash
7
- @model_class = model_class
8
- super(klass)
9
- end
10
-
11
- def item_hash?
12
- @item_hash
13
- end
14
-
15
- def create
16
- self << (item = item_class.new({}, hash: @item_hash))
17
- item
18
- end
1
+ require 'tree_struct'
19
2
 
3
+ module FormObj
4
+ class Array < ::TreeStruct::Array
20
5
  def update_attributes(vals)
21
6
  ids_exists = []
22
7
  items_to_add = []
23
8
 
24
- vals.map(&:with_indifferent_access).each do |val|
25
- id = val[item_class.primary_key]
9
+ vals.each do |val|
10
+ id = primary_key(val)
26
11
  item = self.find { |i| i.primary_key == id }
27
12
  if item
28
13
  item.update_attributes(val)
@@ -39,58 +24,13 @@ class FormObj
39
24
  self.create.update_attributes(item)
40
25
  end
41
26
 
42
- sort! { |a, b| vals.index { |val| val[item_class.primary_key] == a.primary_key } <=> vals.index { |val| val[item_class.primary_key] == b.primary_key } }
27
+ sort! { |a, b| vals.index { |val| primary_key(val) == a.primary_key } <=> vals.index { |val| primary_key(val) == b.primary_key } }
43
28
  end
44
29
 
45
- def save_to_models(models)
46
- model_primary_key = self.item_class.model_primary_key
47
- default_models = models[:default]
48
- ids_exists = []
49
- items_to_add = []
50
-
51
- self.each do |item|
52
- id = item.primary_key
53
- model = if default_models.respond_to?("find_by_#{model_primary_key}")
54
- default_models.send("find_by_#{model_primary_key}", id)
55
- elsif @item_hash
56
- default_models.find { |m| (m.key?(model_primary_key.to_sym) ? m[model_primary_key.to_sym] : m[model_primary_key.to_s]) == id }
57
- else
58
- default_models.find { |m| m.send(model_primary_key) == id }
59
- end
60
- if model
61
- item.save_to_models(models.merge(default: model))
62
- ids_exists << id
63
- else
64
- items_to_add << item
65
- end
66
- end
67
-
68
- ids_to_remove = if @item_hash
69
- default_models.map { |m| m.key?(model_primary_key.to_sym) ? m[model_primary_key.to_sym] : m[model_primary_key.to_s] }
70
- else
71
- default_models.map(&(model_primary_key.to_sym))
72
- end - ids_exists
73
- if default_models.respond_to?(:destroy_all)
74
- default_models.destroy_all(model_primary_key => ids_to_remove)
75
- elsif @item_hash
76
- default_models.delete_if { |m| ids_to_remove.include? (m.key?(model_primary_key.to_sym) ? m[model_primary_key.to_sym] : m[model_primary_key.to_s]) }
77
- else
78
- default_models.delete_if { |m| ids_to_remove.include? m.send(model_primary_key) }
79
- end
80
-
81
- items_to_add.each do |item|
82
- default_models << model = (@model_class.is_a?(String) ? @model_class.constantize : @model_class).new
83
- item.save_to_models(models.merge(default: model))
84
- end
85
- end
86
-
87
- def to_hash
88
- self.map(&:to_hash)
89
- end
30
+ private
90
31
 
91
- def export_to_model_hash(models)
92
- self.each { |item| models[:default] << item.export_to_model_hash(models.merge(default: {}))[:default] }
93
- models
32
+ def primary_key(hash)
33
+ hash.key?(item_class.primary_key.to_sym) ? hash[item_class.primary_key.to_sym] : hash[item_class.primary_key.to_s]
94
34
  end
95
35
  end
96
36
  end
@@ -1,25 +1,19 @@
1
- class FormObj
2
- class Attribute
3
- attr_reader :name, :subform, :model, :model_attributes, :model_class
1
+ require 'tree_struct'
4
2
 
5
- def initialize(name, subform = false, model: :default, model_attribute: nil, model_class: nil, hash: false, array: false)
6
- @subform = subform
7
- @array = array
8
- @hash = hash
3
+ module FormObj
4
+ class Attribute < ::TreeStruct::Attribute
5
+ def initialize(name, array: false, class: nil, default: nil, parent:, primary_key: nil, &block)
6
+ super(name, array: array, class: binding.local_variable_get(:class), default: default, parent: parent, &block)
9
7
 
10
- @model_attributes = model_attribute === false ? [] : (model_attribute || name).to_s.split('.')
11
- @name = name.to_s.start_with?(':') ? name.to_s[1..-1] : name.to_s
8
+ @nested_class.instance_variable_set(:@model_name, ActiveModel::Name.new(@nested_class, nil, name.to_s)) if !@nested_class && block_given?
12
9
 
13
- @model = model
14
- @model_class = model_class.is_a?(Enumerable) ? model_class : [model_class || (hash ? Hash : name.to_s.camelize)]
15
- end
16
-
17
- def hash?
18
- @hash
19
- end
20
-
21
- def array?
22
- @array
10
+ if primary_key
11
+ if @nested_class
12
+ @nested_class.primary_key = primary_key
13
+ else
14
+ parent.primary_key = name.to_sym
15
+ end
16
+ end
23
17
  end
24
18
  end
25
19
  end
@@ -0,0 +1,13 @@
1
+ require 'tree_struct'
2
+
3
+ module FormObj
4
+ class Attributes < ::TreeStruct::Attributes
5
+ def find(name)
6
+ @items.find { |item| item.name == name.to_sym }
7
+ end
8
+
9
+ def each(&block)
10
+ @items.each(&block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,92 @@
1
+ require 'tree_struct'
2
+ require 'form_obj/attributes'
3
+ require 'form_obj/attribute'
4
+ require 'form_obj/array'
5
+ require 'active_model'
6
+
7
+ module FormObj
8
+ class UnknownAttributeError < RuntimeError; end
9
+
10
+ class Form < ::TreeStruct
11
+ extend ::ActiveModel::Naming
12
+ extend ::ActiveModel::Translation
13
+
14
+ include ::ActiveModel::Conversion
15
+ include ::ActiveModel::Validations
16
+
17
+ private
18
+ self._attributes = Attributes.new
19
+
20
+ public
21
+
22
+ def self.array_class
23
+ Array
24
+ end
25
+
26
+ def self.nested_class
27
+ ::TreeStruct
28
+ end
29
+
30
+ def self.attribute_class
31
+ Attribute
32
+ end
33
+
34
+ attr_accessor :persisted
35
+ attr_reader :errors
36
+
37
+ class_attribute :primary_key, instance_predicate: false, instance_reader: false, instance_writer: false
38
+ self.primary_key = :id
39
+
40
+ def self.nested_class
41
+ ::FormObj::Form
42
+ end
43
+
44
+ def self.model_name
45
+ @model_name || super
46
+ end
47
+
48
+ def initialize()
49
+ @errors = ActiveModel::Errors.new(self)
50
+ @persisted = false
51
+ end
52
+
53
+ def persisted?
54
+ @persisted
55
+ end
56
+
57
+ def _set_attribute_value(attribute, value)
58
+ @persisted = false
59
+ super
60
+ end
61
+
62
+ def primary_key
63
+ send(self.class.primary_key)
64
+ end
65
+
66
+ def primary_key=(val)
67
+ send("#{self.class.primary_key}=", val)
68
+ end
69
+
70
+ def update_attributes(new_attrs, raise_if_not_found: true)
71
+ new_attrs.each_pair do |new_attr, new_val|
72
+ attr = self.class._attributes.find(new_attr)
73
+ if attr.nil?
74
+ raise UnknownAttributeError.new(new_attr) if raise_if_not_found
75
+ else
76
+ if attr.subform?
77
+ self.send(new_attr).update_attributes(new_val)
78
+ else
79
+ self.send("#{new_attr}=", new_val)
80
+ end
81
+ end
82
+ end
83
+
84
+ self
85
+ end
86
+
87
+ def saved
88
+ @persisted = true
89
+ self
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,66 @@
1
+ module FormObj
2
+ module Mappable
3
+ class Array < FormObj::Array
4
+ def initialize(item_class, model_attribute:)
5
+ @model_attribute = model_attribute
6
+ super(item_class)
7
+ end
8
+
9
+ def load_from_models(models)
10
+ clear
11
+ (models[:default] || []).each do |model|
12
+ create.load_from_models(models.merge(default: model))
13
+ end
14
+ self
15
+ end
16
+
17
+ def save_to_models(models)
18
+ model_array = models[:default]
19
+ ids_exists = []
20
+ items_to_add = []
21
+
22
+ self.each do |item|
23
+ if model = find_model(model_array, id = item.primary_key)
24
+ item.save_to_models(models.merge(default: model))
25
+ ids_exists << id
26
+ else
27
+ items_to_add << item
28
+ end
29
+ end
30
+
31
+ ids_to_remove = model_array.map { |m| model_primary_key.read_from_model(m) } - ids_exists
32
+ delete_models(model_array, ids_to_remove)
33
+
34
+ items_to_add.each do |item|
35
+ model_array << model = @model_attribute.create_model # || model_array.create_model
36
+ item.save_to_models(models.merge(default: model))
37
+ end
38
+ end
39
+
40
+ def find_model(model_array, id)
41
+ if model_array.respond_to?("find_by_#{model_primary_key.name}")
42
+ model_array.send("find_by_#{model_primary_key.name}", id)
43
+ else
44
+ model_array.find { |m| model_primary_key.read_from_model(m) == id }
45
+ end
46
+ end
47
+
48
+ def delete_models(model_array, ids_to_delete)
49
+ if model_array.respond_to?(:destroy_all)
50
+ model_array.destroy_all(model_primary_key.name => ids_to_delete)
51
+ else
52
+ model_array.delete_if { |m| ids_to_delete.include?(model_primary_key.read_from_model(m)) }
53
+ end
54
+ end
55
+
56
+ def model_primary_key
57
+ self.item_class.model_primary_key
58
+ end
59
+
60
+ def to_models_hash(models)
61
+ self.each { |item| models[:default] << item.to_models_hash(models.merge(default: {}))[:default] }
62
+ models
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,35 @@
1
+ require 'form_obj/mappable/model_attribute'
2
+
3
+ module FormObj
4
+ module Mappable
5
+ class Attribute < FormObj::Attribute
6
+ attr_reader :model_attribute
7
+
8
+ def initialize(name, array: false, class: nil, default: nil, hash: false, model: :default, model_attribute: nil, model_class: nil, parent:, primary_key: nil, &block)
9
+ @hash = hash
10
+ @model_attribute = ModelAttribute.new(model: model, names: model_attribute, classes: model_class, default_name: name, array: array, hash: hash, subform: binding.local_variable_get(:class) || block_given?)
11
+
12
+ if block_given?
13
+ new_block = Proc.new do
14
+ include FormObj::Mappable
15
+ class_eval &block
16
+ end
17
+ end
18
+ super(name, array: array, class: binding.local_variable_get(:class), default: default, parent: parent, primary_key: primary_key, &new_block)
19
+
20
+ @nested_class = Class.new(@nested_class) if binding.local_variable_get(:class)
21
+ @nested_class.hash = hash if @nested_class
22
+ end
23
+
24
+ def array?
25
+ @array
26
+ end
27
+
28
+ private
29
+
30
+ def create_array
31
+ @parent.array_class.new(@nested_class, model_attribute: @model_attribute)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,77 @@
1
+ require 'active_support/inflector'
2
+
3
+ module FormObj
4
+ module Mappable
5
+ class ModelAttribute
6
+ private
7
+
8
+ class Item
9
+ attr_accessor :hash
10
+ attr_reader :hash_item, :name
11
+
12
+ def initialize(name:, klass:, hash:, array:)
13
+ @array = array
14
+ @hash = hash
15
+ @hash_item = name[0] == ':'
16
+ @name = (name[0] == ':' ? name[1..-1] : name).to_sym
17
+ @klass = klass || @name.to_s.classify
18
+ end
19
+
20
+ def hash_item=(value)
21
+ @hash_item ||= value
22
+ end
23
+
24
+ def create_model
25
+ if @hash
26
+ {}
27
+ else
28
+ (@klass.is_a?(String) ? @klass.constantize : @klass).try(:new)
29
+ end
30
+ end
31
+
32
+ def create_array
33
+ []
34
+ end
35
+
36
+ def read_from_model(model, create_nested_model_if_nil: false)
37
+ return nil if model.nil?
38
+
39
+ result = if @hash_item
40
+ model[hash_attribute_name(model, @name)]
41
+ else
42
+ model.send(@name)
43
+ end
44
+
45
+ if result.nil? && create_nested_model_if_nil
46
+ result = @array ? create_array : create_model
47
+ write_to_model(model, result)
48
+ end
49
+
50
+ result
51
+ end
52
+
53
+ def write_to_model(model, value)
54
+ if @hash_item
55
+ model[hash_attribute_name(model, @name)] = value
56
+ else
57
+ model.send("#{@name}=", value)
58
+ end
59
+ end
60
+
61
+ def to_hash(value)
62
+ { @name => value }
63
+ end
64
+
65
+ def read_errors_from_model(model)
66
+ @hash_item ? [] : model.errors[@name]
67
+ end
68
+
69
+ private
70
+
71
+ def hash_attribute_name(model, name)
72
+ model.key?(name.to_s) && !model.key?(name.to_sym) ? name.to_s : name.to_sym
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,93 @@
1
+ require 'active_support/inflector'
2
+ require 'form_obj/mappable/model_attribute/item'
3
+
4
+ module FormObj
5
+ module Mappable
6
+ class ModelAttribute
7
+ attr_reader :model
8
+
9
+ def initialize(names:, classes:, default_name:, array:, hash:, subform:, model:)
10
+ @read_from_model = @write_to_model = !(names === false)
11
+
12
+ @model = model
13
+ @array = array
14
+
15
+ names = (names || default_name).to_s.split('.') unless names.is_a? ::Enumerable
16
+ classes = classes.nil? ? [] : [classes] unless classes.is_a? ::Enumerable
17
+
18
+ if classes.size > 0
19
+ if (subform && (names.size != classes.size)) || (!subform && (names.size != classes.size + 1))
20
+ raise "Since the :model_attribute size is #{names.size} the :model_class size should be #{names.size - subform ? 0 : 1} in terms of nested items but it was #{classes.size}" unless names.size == classes.size
21
+ end
22
+ end
23
+
24
+ @items = names.zip(classes, [hash], names[0..-2].map{nil} + [array]).map { |item| Item.new(name: item[0], klass: item[1], hash: item[2], array: item[3]) }
25
+
26
+ @items.inject do |prev, item|
27
+ prev.hash = true if item.hash_item
28
+ item
29
+ end
30
+ end
31
+
32
+ def last_name
33
+ @items.last.name
34
+ end
35
+
36
+ def hash_item=(value)
37
+ @items[0].hash_item = value
38
+ end
39
+
40
+ def create_model
41
+ raise 'Creation available only for array attributes' unless @array
42
+ @items.last.create_model
43
+ end
44
+
45
+ def read_from_model?
46
+ @read_from_model
47
+ end
48
+
49
+ def read_from_model(model, create_nested_model_if_nil: false)
50
+ @items.reduce(model) { |last_model, item| item.read_from_model(last_model, create_nested_model_if_nil: create_nested_model_if_nil) }
51
+ end
52
+
53
+ def read_from_models(models, create_nested_model_if_nil: false)
54
+ read_from_model(models[@model], create_nested_model_if_nil: create_nested_model_if_nil)
55
+ end
56
+
57
+ def write_to_model?
58
+ @write_to_model
59
+ end
60
+
61
+ def write_to_model(model, value)
62
+ model = @items[0..-2].reduce(model) { |last_model, item| item.read_from_model(last_model, create_nested_model_if_nil: true) } if @items.size > 1
63
+ @items.last.write_to_model(model, value)
64
+ end
65
+
66
+ def write_to_models(models, value)
67
+ write_to_model(models[@model], value)
68
+ end
69
+
70
+ def validate_primary_key!
71
+ if @items.size > 1
72
+ raise PrimaryKeyMappingError.new('Primary key should not be mapped to nested model')
73
+ elsif @items.size == 0
74
+ raise PrimaryKeyMappingError.new('Primary key should not be mapped to non-mapped attribute')
75
+ end
76
+ end
77
+
78
+ def to_model_hash(value)
79
+ @items.reverse.reduce(value) { |value, item| item.to_hash(value) }
80
+ end
81
+
82
+ def read_errors_from_model(model)
83
+ @items.last.try(:read_errors_from_model,
84
+ @items[0..-2].reduce(model) { |last_model, item| item.read_from_model(last_model, create_nested_model_if_nil: false) }
85
+ )
86
+ end
87
+
88
+ def read_errors_from_models(models)
89
+ read_errors_from_model(models[@model])
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,17 @@
1
+ module FormObj
2
+ module Mappable
3
+ class ModelPrimaryKey
4
+ def initialize(model_attribute)
5
+ @model_attribute = model_attribute
6
+ end
7
+
8
+ def name
9
+ @model_attribute.last_name
10
+ end
11
+
12
+ def read_from_model(model)
13
+ @model_attribute.read_from_model(model, create_nested_model_if_nil: false)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,124 @@
1
+ require 'form_obj/mappable/attribute'
2
+ require 'form_obj/mappable/array'
3
+ require 'form_obj/mappable/model_primary_key'
4
+
5
+ module FormObj
6
+ module Mappable
7
+ class PrimaryKeyMappingError < RuntimeError; end
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def attribute_class
15
+ Mappable::Attribute
16
+ end
17
+
18
+ def array_class
19
+ Mappable::Array
20
+ end
21
+
22
+ def hash=(value)
23
+ _attributes.each { |attribute| attribute.model_attribute.hash_item = value }
24
+ end
25
+
26
+ def model_primary_key
27
+ Mappable::ModelPrimaryKey.new(self._attributes.find(self.primary_key).model_attribute)
28
+ end
29
+ end
30
+
31
+ def load_from_model(model)
32
+ load_from_models(default: model)
33
+ end
34
+
35
+ def load_from_models(models)
36
+ self.class._attributes.each { |attribute| load_attribute_from_model(attribute, models) }
37
+ self.persisted = true
38
+ self
39
+ end
40
+
41
+ def save_to_model(model)
42
+ save_to_models(default: model)
43
+ end
44
+
45
+ def save_to_models(models)
46
+ self.class._attributes.each { |attribute | save_attribute_to_model(attribute, models) }
47
+ self.persisted = true
48
+ self
49
+ end
50
+
51
+ def primary_key=(val)
52
+ self.class._attributes.find(self.class.primary_key).validate_primary_key!
53
+ super
54
+ end
55
+
56
+ def to_model_hash(model = :default)
57
+ to_models_hash[model]
58
+ end
59
+
60
+ def to_models_hash(models = {})
61
+ self.class._attributes.each do |attribute|
62
+ val = if attribute.subform?
63
+ if attribute.array?
64
+ []
65
+ else
66
+ attribute.model_attribute.write_to_model? ? {} : (models[attribute.model_attribute.model] ||= {})
67
+ end
68
+ else
69
+ send(attribute.name)
70
+ end
71
+
72
+ value = if attribute.subform? && !attribute.model_attribute.write_to_model?
73
+ attribute.array? ? { self: val } : {}
74
+ elsif attribute.subform? || attribute.model_attribute.write_to_model?
75
+ attribute.model_attribute.to_model_hash(val)
76
+ end
77
+
78
+ (models[attribute.model_attribute.model] ||= {}).merge!(value) if attribute.subform? || attribute.model_attribute.write_to_model?
79
+ send(attribute.name).to_models_hash(models.merge(default: val)) if attribute.subform?
80
+ end
81
+ models
82
+ end
83
+
84
+ def copy_errors_from_model(model)
85
+ copy_errors_from_models(default: model)
86
+ end
87
+
88
+ def copy_errors_from_models(models)
89
+ self.class._attributes.each do |attribute|
90
+ if attribute.subform?
91
+ elsif attribute.model_attribute.write_to_model? # Use :write_to_model? instead of :read_to_model? because validation errors appears after writing to model
92
+ @errors[attribute.name].push(*attribute.model_attribute.read_errors_from_models(models))
93
+ end
94
+ end
95
+ self
96
+ end
97
+
98
+ private
99
+
100
+ def load_attribute_from_model(attribute, models)
101
+ if attribute.subform?
102
+ if attribute.model_attribute.read_from_model?
103
+ self.send(attribute.name).load_from_models(models.merge(default: attribute.model_attribute.read_from_models(models)))
104
+ else
105
+ self.send(attribute.name).load_from_models(models.merge(default: models[attribute.model_attribute.model]))
106
+ end
107
+ elsif attribute.model_attribute.read_from_model?
108
+ self.send("#{attribute.name}=", attribute.model_attribute.read_from_models(models))
109
+ end
110
+ end
111
+
112
+ def save_attribute_to_model(attribute, models)
113
+ if attribute.subform?
114
+ if attribute.model_attribute.write_to_model?
115
+ self.send(attribute.name).save_to_models(models.merge(default: attribute.model_attribute.read_from_models(models, create_nested_model_if_nil: true)))
116
+ else
117
+ self.send(attribute.name).save_to_models(models.merge(default: models[attribute.model_attribute.model]))
118
+ end
119
+ elsif attribute.model_attribute.write_to_model?
120
+ attribute.model_attribute.write_to_models(models, self.send(attribute.name))
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,3 +1,3 @@
1
- class FormObj
2
- VERSION = "0.1.0"
1
+ module FormObj
2
+ VERSION = "0.2.0"
3
3
  end