form_obj 0.4.0 → 0.5.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/Rakefile CHANGED
@@ -1,6 +1,13 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rake/testtask"
2
3
  require "rspec/core/rake_task"
3
4
 
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
4
10
  RSpec::Core::RakeTask.new(:spec)
5
11
 
6
12
  task :default => :spec
13
+ # task :default => :test
data/form_obj.gemspec CHANGED
@@ -33,13 +33,14 @@ 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_dependency "typed_array", ">= 1.0.1"
36
+ spec.add_dependency "typed_array", ">= 1.0.2"
37
37
  spec.add_dependency "activemodel", ">= 3.2"
38
38
  spec.add_dependency "activesupport", ">= 3.2"
39
39
 
40
40
  spec.add_development_dependency "bundler", "~> 1.16"
41
41
  spec.add_development_dependency "rake", "~> 10.0"
42
42
  spec.add_development_dependency "rspec", "~> 3.0"
43
+ spec.add_development_dependency "minitest", "~> 5.0"
43
44
  spec.add_development_dependency "actionpack", ">= 3.2"
44
45
  spec.add_development_dependency "appraisal"
45
46
  end
@@ -1,35 +1,35 @@
1
1
  module FormObj
2
2
  class Form < FormObj::Struct
3
3
  class Array < FormObj::Struct::Array
4
- def update_attributes(vals)
5
- ids_exists = []
6
- items_to_add = []
4
+ def persisted?
5
+ all?(&:persisted?)
6
+ end
7
7
 
8
- vals.each do |val|
9
- id = primary_key(val)
10
- item = self.find { |i| i.primary_key == id }
11
- if item
12
- item.update_attributes(val)
13
- ids_exists << id
14
- else
15
- items_to_add << val
16
- end
17
- end
8
+ def mark_as_persisted
9
+ each(&:mark_as_persisted)
10
+ end
18
11
 
19
- ids_to_remove = self.map(&:primary_key) - ids_exists
20
- self.delete_if { |item| ids_to_remove.include? item.primary_key }
12
+ def mark_for_destruction
13
+ each(&:mark_for_destruction)
14
+ end
21
15
 
22
- items_to_add.each do |item|
23
- self.create.update_attributes(item)
24
- end
16
+ private
25
17
 
26
- sort! { |a, b| vals.index { |val| primary_key(val) == a.primary_key } <=> vals.index { |val| primary_key(val) == b.primary_key } }
18
+ # items_to_add - array of hashes of new attribute values
19
+ # items_to_update - hash where key is the item to update and value is a hash of new attribute values
20
+ def items_for_destruction(items_to_add:, items_to_update:)
21
+ items_to_update.select { |item, attr_values| attr_values[:_destroy] }.keys
27
22
  end
28
23
 
29
- private
24
+ # items - array of items to be destroyed
25
+ def destroy_items(items)
26
+ items.each(&:mark_for_destruction)
27
+ end
30
28
 
31
- def primary_key(hash)
32
- hash.key?(item_class.primary_key.to_sym) ? hash[item_class.primary_key.to_sym] : hash[item_class.primary_key.to_s]
29
+ # items - hash where key is the item to update and value is a hash of new attribute values
30
+ # params - additional params for :update_attributes method
31
+ def update_items(items, params)
32
+ items.each_pair { |item, attr_values| item.update_attributes(attr_values, params) unless attr_values.delete(:_destroy) }
33
33
  end
34
34
  end
35
35
  end
@@ -2,17 +2,9 @@ module FormObj
2
2
  class Form < FormObj::Struct
3
3
  class Attribute < FormObj::Struct::Attribute
4
4
  def initialize(name, array: false, class: nil, default: nil, parent:, primary_key: nil, &block)
5
- super(name, array: array, class: binding.local_variable_get(:class), default: default, parent: parent, &block)
5
+ super(name, array: array, class: binding.local_variable_get(:class), default: default, parent: parent, primary_key: primary_key, &block)
6
6
 
7
7
  @nested_class.instance_variable_set(:@model_name, ActiveModel::Name.new(@nested_class, nil, name.to_s)) if !@nested_class && block_given?
8
-
9
- if primary_key
10
- if @nested_class
11
- @nested_class.primary_key = primary_key
12
- else
13
- parent.primary_key = name.to_sym
14
- end
15
- end
16
8
  end
17
9
  end
18
10
  end
@@ -1,13 +1,5 @@
1
1
  module FormObj
2
2
  class Form < FormObj::Struct
3
- class Attributes < FormObj::Struct::Attributes
4
- def find(name)
5
- @items.find { |item| item.name == name.to_sym }
6
- end
7
-
8
- def each(&block)
9
- @items.each(&block)
10
- end
11
- end
3
+ class Attributes < FormObj::Struct::Attributes; end
12
4
  end
13
5
  end
data/lib/form_obj/form.rb CHANGED
@@ -5,8 +5,6 @@ require 'form_obj/form/array'
5
5
  require 'active_model'
6
6
 
7
7
  module FormObj
8
- class UnknownAttributeError < RuntimeError; end
9
-
10
8
  class Form < FormObj::Struct
11
9
  extend ::ActiveModel::Naming
12
10
  extend ::ActiveModel::Translation
@@ -14,8 +12,6 @@ module FormObj
14
12
  include ::ActiveModel::Conversion
15
13
  include ::ActiveModel::Validations
16
14
 
17
- class_attribute :primary_key, instance_predicate: false, instance_reader: false, instance_writer: false
18
- self.primary_key = :id
19
15
  self._attributes = Attributes.new
20
16
 
21
17
  class << self
@@ -36,51 +32,47 @@ module FormObj
36
32
  end
37
33
  end
38
34
 
39
- attr_accessor :persisted
35
+ attr_writer :persisted
40
36
  attr_reader :errors
41
37
 
42
- def initialize()
38
+ def initialize(*args)
43
39
  @errors = ActiveModel::Errors.new(self)
44
40
  @persisted = false
41
+ super
45
42
  end
46
43
 
47
44
  def persisted?
48
- @persisted
45
+ @persisted && self.class._attributes.reduce(true) { |persisted, attr|
46
+ persisted && (!attr.subform? || read_attribute(attr).persisted?)
47
+ }
49
48
  end
50
49
 
51
- def _set_attribute_value(attribute, value)
52
- @persisted = false
53
- super
50
+ def mark_as_persisted
51
+ self.persisted = true
52
+ self.class._attributes.each { |attr|
53
+ read_attribute(attr).mark_as_persisted if attr.subform?
54
+ }
55
+ self
54
56
  end
55
57
 
56
- def primary_key
57
- send(self.class.primary_key)
58
+ def mark_for_destruction
59
+ @marked_for_destruction = true
60
+ self.class._attributes.each { |attr|
61
+ read_attribute(attr).mark_for_destruction if attr.subform?
62
+ }
63
+ self.persisted = false
64
+ self
58
65
  end
59
66
 
60
- def primary_key=(val)
61
- send("#{self.class.primary_key}=", val)
67
+ def marked_for_destruction?
68
+ @marked_for_destruction ||= false
62
69
  end
63
70
 
64
- def update_attributes(new_attrs, raise_if_not_found: true)
65
- new_attrs.each_pair do |new_attr, new_val|
66
- attr = self.class._attributes.find(new_attr)
67
- if attr.nil?
68
- raise UnknownAttributeError.new(new_attr) if raise_if_not_found
69
- else
70
- if attr.subform?
71
- self.send(new_attr).update_attributes(new_val)
72
- else
73
- self.send("#{new_attr}=", new_val)
74
- end
75
- end
76
- end
77
-
78
- self
79
- end
71
+ private
80
72
 
81
- def saved
82
- @persisted = true
83
- self
73
+ def _set_attribute_value(attribute, value)
74
+ @persisted = false unless _get_attribute_value(attribute) === value
75
+ super
84
76
  end
85
77
  end
86
78
  end
@@ -1,15 +1,15 @@
1
1
  module FormObj
2
2
  module ModelMapper
3
3
  class Array < FormObj::Form::Array
4
- def initialize(item_class, model_attribute:)
4
+ def initialize(item_class, model_attribute, *args)
5
5
  @model_attribute = model_attribute
6
- super(item_class)
6
+ super(item_class, *args)
7
7
  end
8
8
 
9
- def load_from_models(models)
9
+ def load_from_models(models, *args)
10
10
  clear
11
- (models[:default] || []).each do |model|
12
- create.load_from_models(models.merge(default: model))
11
+ iterate_through_models_to_load_them(models[:default] || [], *args) do |model|
12
+ build.load_from_models(models.merge(default: model), *args)
13
13
  end
14
14
  self
15
15
  end
@@ -32,7 +32,7 @@ module FormObj
32
32
  delete_models(model_array, ids_to_remove)
33
33
 
34
34
  items_to_add.each do |item|
35
- model_array << model = @model_attribute.create_model # || model_array.create_model
35
+ model_array << model = model_attribute.create_model
36
36
  item.sync_to_models(models.merge(default: model))
37
37
  end
38
38
  end
@@ -61,6 +61,14 @@ module FormObj
61
61
  self.each { |item| models[:default] << item.to_models_hash(models.merge(default: {}))[:default] }
62
62
  models
63
63
  end
64
+
65
+ private
66
+
67
+ attr_reader :model_attribute
68
+
69
+ def iterate_through_models_to_load_them(models, *args, &block)
70
+ models.each { |model| block.call(model) }
71
+ end
64
72
  end
65
73
  end
66
74
  end
@@ -5,8 +5,8 @@ module FormObj
5
5
  class Attribute < FormObj::Form::Attribute
6
6
  attr_reader :model_attribute
7
7
 
8
- def initialize(name, array: false, class: nil, default: nil, model_hash: false, model: :default, model_attribute: nil, model_class: nil, parent:, primary_key: nil, &block)
9
- @model_attribute = ModelAttribute.new(model: model, names: model_attribute, classes: model_class, default_name: name, array: array, hash: model_hash, subform: binding.local_variable_get(:class) || block_given?)
8
+ def initialize(name, array: false, class: nil, default: nil, model_hash: false, model: :default, model_attribute: nil, model_class: nil, model_nesting: true, parent:, primary_key: nil, &block)
9
+ @model_attribute = ModelAttribute.new(model: model, names: model_attribute, classes: model_class, default_name: name, nesting: model_nesting, array: array, hash: model_hash, subform: binding.local_variable_get(:class) || block_given?)
10
10
 
11
11
  if block_given?
12
12
  new_block = Proc.new do
@@ -16,7 +16,6 @@ module FormObj
16
16
  end
17
17
  super(name, array: array, class: binding.local_variable_get(:class), default: default, parent: parent, primary_key: primary_key, &new_block)
18
18
 
19
- @nested_class = Class.new(@nested_class) if binding.local_variable_get(:class)
20
19
  @nested_class.model_hash = model_hash if @nested_class
21
20
  end
22
21
 
@@ -26,8 +25,8 @@ module FormObj
26
25
 
27
26
  private
28
27
 
29
- def create_array
30
- @parent.array_class.new(@nested_class, model_attribute: @model_attribute)
28
+ def create_array(*args)
29
+ @parent.array_class.new(@nested_class, @model_attribute, *args)
31
30
  end
32
31
  end
33
32
  end
@@ -6,9 +6,10 @@ module FormObj
6
6
  class ModelAttribute
7
7
  attr_reader :model
8
8
 
9
- def initialize(names:, classes:, default_name:, array:, hash:, subform:, model:)
9
+ def initialize(names:, classes:, default_name:, array:, hash:, subform:, model:, nesting:)
10
10
  @read_from_model = @write_to_model = !(names === false)
11
11
 
12
+ @nesting = nesting
12
13
  @model = model
13
14
  @array = array
14
15
 
@@ -29,6 +30,10 @@ module FormObj
29
30
  end
30
31
  end
31
32
 
33
+ def names
34
+ @items.map(&:name)
35
+ end
36
+
32
37
  def last_name
33
38
  @items.last.name
34
39
  end
@@ -42,6 +47,10 @@ module FormObj
42
47
  @items.last.create_model
43
48
  end
44
49
 
50
+ def nesting?
51
+ @nesting
52
+ end
53
+
45
54
  def read_from_model?
46
55
  @read_from_model
47
56
  end
@@ -29,12 +29,12 @@ module FormObj
29
29
  end
30
30
  end
31
31
 
32
- def load_from_model(model)
33
- load_from_models(default: model)
32
+ def load_from_model(model, *args)
33
+ load_from_models({ default: model }, *args)
34
34
  end
35
35
 
36
- def load_from_models(models)
37
- self.class._attributes.each { |attribute| load_attribute_from_model(attribute, models) }
36
+ def load_from_models(models, *args)
37
+ self.class._attributes.each { |attribute| load_attribute_from_model(attribute, models, *args) }
38
38
  self.persisted = true
39
39
  self
40
40
  end
@@ -60,24 +60,7 @@ module FormObj
60
60
 
61
61
  def to_models_hash(models = {})
62
62
  self.class._attributes.each do |attribute|
63
- val = if attribute.subform?
64
- if attribute.array?
65
- []
66
- else
67
- attribute.model_attribute.write_to_model? ? {} : (models[attribute.model_attribute.model] ||= {})
68
- end
69
- else
70
- send(attribute.name)
71
- end
72
-
73
- value = if attribute.subform? && !attribute.model_attribute.write_to_model?
74
- attribute.array? ? { self: val } : {}
75
- elsif attribute.subform? || attribute.model_attribute.write_to_model?
76
- attribute.model_attribute.to_model_hash(val)
77
- end
78
-
79
- (models[attribute.model_attribute.model] ||= {}).merge!(value) if attribute.subform? || attribute.model_attribute.write_to_model?
80
- send(attribute.name).to_models_hash(models.merge(default: val)) if attribute.subform?
63
+ attribute_to_models_hash(attribute, models)
81
64
  end
82
65
  models
83
66
  end
@@ -98,28 +81,55 @@ module FormObj
98
81
 
99
82
  private
100
83
 
101
- def load_attribute_from_model(attribute, models)
84
+ def load_attribute_from_model(attribute, models, *args)
85
+ return unless attribute.model_attribute.read_from_model?
86
+
102
87
  if attribute.subform?
103
- if attribute.model_attribute.read_from_model?
104
- self.send(attribute.name).load_from_models(models.merge(default: attribute.model_attribute.read_from_models(models)))
88
+ if attribute.model_attribute.nesting?
89
+ read_attribute(attribute).load_from_models(models.merge(default: attribute.model_attribute.read_from_models(models)), *args)
105
90
  else
106
- self.send(attribute.name).load_from_models(models.merge(default: models[attribute.model_attribute.model]))
91
+ read_attribute(attribute).load_from_models(models.merge(default: models[attribute.model_attribute.model]), *args)
107
92
  end
108
- elsif attribute.model_attribute.read_from_model?
109
- self.send("#{attribute.name}=", attribute.model_attribute.read_from_models(models))
93
+ else
94
+ write_attribute(attribute, attribute.model_attribute.read_from_models(models))
110
95
  end
111
96
  end
112
97
 
113
98
  def sync_attribute_to_model(attribute, models)
99
+ return unless attribute.model_attribute.write_to_model?
100
+
114
101
  if attribute.subform?
115
- if attribute.model_attribute.write_to_model?
116
- self.send(attribute.name).sync_to_models(models.merge(default: attribute.model_attribute.read_from_models(models, create_nested_model_if_nil: true)))
102
+ if attribute.model_attribute.nesting?
103
+ read_attribute(attribute).sync_to_models(models.merge(default: attribute.model_attribute.read_from_models(models, create_nested_model_if_nil: true)))
117
104
  else
118
- self.send(attribute.name).sync_to_models(models.merge(default: models[attribute.model_attribute.model]))
105
+ read_attribute(attribute).sync_to_models(models.merge(default: models[attribute.model_attribute.model]))
119
106
  end
120
- elsif attribute.model_attribute.write_to_model?
121
- attribute.model_attribute.write_to_models(models, self.send(attribute.name))
107
+ else
108
+ attribute.model_attribute.write_to_models(models, read_attribute(attribute))
122
109
  end
123
110
  end
111
+
112
+ def attribute_to_models_hash(attribute, models)
113
+ return unless attribute.model_attribute.write_to_model?
114
+
115
+ val = if attribute.subform?
116
+ if attribute.array?
117
+ []
118
+ else
119
+ attribute.model_attribute.nesting? ? {} : (models[attribute.model_attribute.model] ||= {})
120
+ end
121
+ else
122
+ read_attribute(attribute)
123
+ end
124
+
125
+ value = if attribute.subform? && !attribute.model_attribute.nesting?
126
+ attribute.array? ? { self: val } : {}
127
+ else
128
+ attribute.model_attribute.to_model_hash(val)
129
+ end
130
+
131
+ (models[attribute.model_attribute.model] ||= {}).merge!(value)
132
+ read_attribute(attribute).to_models_hash(models.merge(default: val)) if attribute.subform?
133
+ end
124
134
  end
125
135
  end
@@ -3,19 +3,76 @@ require 'typed_array'
3
3
  module FormObj
4
4
  class Struct
5
5
  class Array < TypedArray
6
- def create
7
- self << (item = create_item)
6
+ def build(hash = nil, raise_if_not_found: true)
7
+ self << (item = build_item(hash, raise_if_not_found: raise_if_not_found))
8
8
  item
9
9
  end
10
10
 
11
+ def create(*args)
12
+ build(*args)
13
+ end
14
+
11
15
  def to_hash
12
16
  self.map(&:to_hash)
13
17
  end
14
18
 
19
+ def update_attributes(vals, raise_if_not_found:)
20
+ items_to_add = []
21
+ items_to_update = {}
22
+
23
+ vals.each do |val|
24
+ val = HashWithIndifferentAccess.new(val)
25
+ item = find_by_primary_key(primary_key(val))
26
+ if item
27
+ items_to_update[item] = val
28
+ else
29
+ items_to_add << val
30
+ end
31
+ end
32
+ items_to_destroy = items_for_destruction(items_to_add: items_to_add, items_to_update: items_to_update)
33
+
34
+ destroy_items(items_to_destroy)
35
+ update_items(items_to_update, raise_if_not_found: raise_if_not_found)
36
+ build_items(items_to_add, raise_if_not_found: raise_if_not_found)
37
+
38
+ sort! { |a, b| (vals.index { |val| primary_key(val) == a.primary_key } || -1) <=> (vals.index { |val| primary_key(val) == b.primary_key } || -1) }
39
+ end
40
+
15
41
  private
16
42
 
17
- def create_item
18
- item_class.new
43
+ def primary_key(hash)
44
+ hash[item_class.primary_key]
45
+ end
46
+
47
+ def find_by_primary_key(id)
48
+ find { |item| item.primary_key == id }
49
+ end
50
+
51
+ def build_item(hash, raise_if_not_found:)
52
+ item_class.new(hash, raise_if_not_found: raise_if_not_found)
53
+ end
54
+
55
+ # items_to_add - array of hashes of new attribute values
56
+ # items_to_update - hash where key is the item to update and value is a hash of new attribute values
57
+ def items_for_destruction(items_to_add:, items_to_update:)
58
+ self - items_to_update.keys
59
+ end
60
+
61
+ # items - array of items to be destroyed
62
+ def destroy_items(items)
63
+ items.each { |item| delete(item) }
64
+ end
65
+
66
+ # items - hash where key is the item to update and value is a hash of new attribute values
67
+ # params - additional params for :update_attributes method
68
+ def update_items(items, params)
69
+ items.each_pair { |item, attr_values| item.update_attributes(attr_values, params) }
70
+ end
71
+
72
+ # items - array of hashes of new attribute values
73
+ # params - additional params for constructor
74
+ def build_items(items, params)
75
+ items.each { |item| self << build_item(item, params) }
19
76
  end
20
77
  end
21
78
  end
@@ -3,16 +3,25 @@ module FormObj
3
3
  class Attribute
4
4
  attr_reader :name
5
5
 
6
- def initialize(name, array: false, class: nil, default: nil, parent:, &block)
6
+ def initialize(name, array: false, class: nil, default: nil, parent:, primary_key: nil, &block)
7
7
  @name = name.to_sym
8
8
  @array = array
9
9
  @default_value = default
10
10
  @parent = parent
11
11
 
12
12
  @nested_class = binding.local_variable_get(:class)
13
+ @nested_class = @nested_class.constantize if @nested_class.is_a? String
13
14
  @nested_class = Class.new(@parent.nested_class, &block) if !@nested_class && block_given?
14
15
 
15
16
  raise ArgumentError.new('Nested structure has to be defined (either with :class parameter or with block) for arrays if :default parameter is not specified') if @array && @nested_class.nil? && @default_value.nil?
17
+
18
+ if primary_key
19
+ if @nested_class
20
+ @nested_class.primary_key = primary_key
21
+ else
22
+ parent.primary_key = name.to_sym
23
+ end
24
+ end
16
25
  end
17
26
 
18
27
  def subform?
@@ -47,22 +56,39 @@ module FormObj
47
56
  end
48
57
  end
49
58
 
50
- elsif @default_value.is_a? Proc
51
- @default_value.call(@parent, self)
52
-
53
59
  else
54
- @default_value
60
+ value = if @default_value.is_a? ::Proc
61
+ @default_value.call(@parent, self)
62
+ else
63
+ @default_value
64
+ end
65
+
66
+ if @nested_class
67
+ if @array
68
+ raise FormObj::WrongDefaultValueClass unless value.is_a? ::Array
69
+ value = create_array(value.map do |val|
70
+ val = create_nested(val) if val.is_a?(::Hash)
71
+ raise FormObj::WrongDefaultValueClass if val.class != @nested_class
72
+ val
73
+ end)
74
+ else
75
+ value = create_nested(value) if value.is_a? ::Hash
76
+ raise FormObj::WrongDefaultValueClass if value.class != @nested_class
77
+ end
78
+ end
79
+
80
+ value
55
81
  end
56
82
  end
57
83
 
58
84
  private
59
85
 
60
- def create_nested
61
- @nested_class.new
86
+ def create_nested(*args)
87
+ @nested_class.new(*args)
62
88
  end
63
89
 
64
- def create_array
65
- @parent.array_class.new(@nested_class)
90
+ def create_array(*args)
91
+ @parent.array_class.new(@nested_class, *args)
66
92
  end
67
93
  end
68
94
  end
@@ -13,9 +13,21 @@ module FormObj
13
13
  end
14
14
  end
15
15
 
16
+ def each(&block)
17
+ @items.each(&block)
18
+ end
19
+
20
+ def find(name)
21
+ @items.find { |item| item.name == name.to_sym }
22
+ end
23
+
16
24
  def map(*args, &block)
17
25
  @items.map(*args, &block)
18
26
  end
27
+
28
+ def reduce(*args, &block)
29
+ @items.reduce(*args, &block)
30
+ end
19
31
  end
20
32
  end
21
33
  end