rom-rails 0.3.0.beta1 → 0.3.0.rc1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/lib/generators/rom/form/templates/edit_form.rb.erb +1 -1
- data/lib/generators/rom/form/templates/new_form.rb.erb +1 -1
- data/lib/generators/rom/mapper/templates/mapper.rb.erb +2 -2
- data/lib/rom/model.rb +1 -1
- data/lib/rom/rails/controller_extension.rb +0 -35
- data/lib/rom/rails/model/attributes.rb +133 -0
- data/lib/rom/rails/model/form.rb +96 -5
- data/lib/rom/rails/model/form/class_interface.rb +401 -0
- data/lib/rom/rails/model/validator.rb +62 -15
- data/lib/rom/rails/model/validator/uniqueness_validator.rb +37 -1
- data/lib/rom/rails/version.rb +1 -1
- data/rom-rails.gemspec +2 -2
- data/spec/dummy/app/controllers/users_controller.rb +7 -8
- data/spec/dummy/app/forms/new_user_form.rb +2 -2
- data/spec/dummy/app/forms/update_user_form.rb +2 -2
- data/spec/dummy/spec/integration/{user_params_spec.rb → user_attributes_spec.rb} +11 -11
- data/spec/dummy/spec/integration/user_model_mapping_spec.rb +1 -1
- data/spec/lib/generators/form_generator_spec.rb +2 -2
- data/spec/lib/generators/mapper_generator_spec.rb +2 -2
- data/spec/unit/form_spec.rb +20 -16
- data/spec/unit/validator_spec.rb +9 -9
- metadata +11 -16
- data/lib/rom/rails/model/form/dsl.rb +0 -173
- data/lib/rom/rails/model/params.rb +0 -72
- data/spec/dummy/spec/controllers/users_controller_spec.rb +0 -29
- data/spec/unit/params_spec.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f534a7630fb69accef529f372f2b211f35c7985
|
4
|
+
data.tar.gz: c530fd02f02046a61e6f86173f8ba28ef6820474
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e70baa4ab07025be46b4f6ff1e19fad7830150298fa90de2e8fcd5f87cba06967057d48e3b3c6b6ecdd35ef6305cf9770db853aa0c24f8f49150f2a6725b902
|
7
|
+
data.tar.gz: 6b31f9a2aff08f7a4747bb045061f5f3658bfeff2d204abc0981ebea5d4a27094798b7bacc2ebf126690669b7ea40517f140b6341e28f1bdbec6647f2c5b5d5a
|
data/CHANGELOG.md
CHANGED
@@ -5,9 +5,9 @@
|
|
5
5
|
* `ROM::Model::Form` for modeling and setting up web-forms (solnic + cflipse)
|
6
6
|
* Support for timestamps attributes in Form objects (kchien)
|
7
7
|
|
8
|
-
|
9
8
|
### Changed
|
10
9
|
|
10
|
+
* [BREAKING] Model::Params renamed to Model::Attributes (solnic)
|
11
11
|
* Improved initialization process which works with AR-style configurations (aflatter)
|
12
12
|
* Allow setup using a configuration block from railtie (aflatter)
|
13
13
|
|
data/lib/rom/model.rb
CHANGED
@@ -3,44 +3,9 @@ module ROM
|
|
3
3
|
RelationParamsMissingError = Class.new(StandardError)
|
4
4
|
|
5
5
|
module ControllerExtension
|
6
|
-
def self.included(klass)
|
7
|
-
klass.extend(ClassExtensions)
|
8
|
-
end
|
9
|
-
|
10
6
|
def rom
|
11
7
|
ROM.env
|
12
8
|
end
|
13
|
-
|
14
|
-
module ClassExtensions
|
15
|
-
def relation(path, options)
|
16
|
-
root, method = path.split('.').map(&:to_sym)
|
17
|
-
|
18
|
-
name = options.fetch(:as) { root }
|
19
|
-
requires = Array(options.fetch(:requires) { [] })
|
20
|
-
|
21
|
-
before_filter(options.except(:as, :requires)) do
|
22
|
-
args = params.values_at(*requires)
|
23
|
-
|
24
|
-
if requires.any? && args.none?
|
25
|
-
raise RelationParamsMissingError
|
26
|
-
else
|
27
|
-
relation =
|
28
|
-
if args.any?
|
29
|
-
rom.read(root).send(method, *args)
|
30
|
-
else
|
31
|
-
rom.read(root).send(method)
|
32
|
-
end
|
33
|
-
|
34
|
-
instance_variable_set("@#{name}", relation.to_a)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
unless respond_to?(name)
|
39
|
-
attr_reader name
|
40
|
-
helper_method name
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
9
|
end
|
45
10
|
end
|
46
11
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
require 'active_model/conversion'
|
3
|
+
|
4
|
+
module ROM
|
5
|
+
module Model
|
6
|
+
# Mixin for validatable and coercible parameters
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# class UserAttributes
|
11
|
+
# include ROM::Model::Attributes
|
12
|
+
#
|
13
|
+
# attribute :email, String
|
14
|
+
# attribute :age, Integer
|
15
|
+
#
|
16
|
+
# validates :email, :age, presence: true
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# user_attrs = UserAttributes.new(email: '', age: '18')
|
20
|
+
#
|
21
|
+
# user_attrs.email # => ''
|
22
|
+
# user_attrs.age # => 18
|
23
|
+
#
|
24
|
+
# user_attrs.valid? # => false
|
25
|
+
# user_attrs.errors # => #<ActiveModel::Errors:0x007fd2423fadb0 ...>
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
module Attributes
|
29
|
+
VirtusModel = Virtus.model(nullify_blank: true)
|
30
|
+
|
31
|
+
# Inclusion hook used to extend a class with required interfaces
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
def self.included(base)
|
35
|
+
base.class_eval do
|
36
|
+
include VirtusModel
|
37
|
+
include ActiveModel::Conversion
|
38
|
+
end
|
39
|
+
base.extend(ClassMethods)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return model name for the attributes class
|
43
|
+
#
|
44
|
+
# The model name object is configurable using `set_model_name` macro
|
45
|
+
#
|
46
|
+
# @see ClassMethods#set_model_name
|
47
|
+
#
|
48
|
+
# @return [ActiveModel::Name]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def model_name
|
52
|
+
self.class.model_name
|
53
|
+
end
|
54
|
+
|
55
|
+
# Class extensions for an attributes class
|
56
|
+
#
|
57
|
+
# @api public
|
58
|
+
module ClassMethods
|
59
|
+
# Default timestamp attribute names used by `timestamps` method
|
60
|
+
DEFAULT_TIMESTAMPS = [:created_at, :updated_at].freeze
|
61
|
+
|
62
|
+
# Process input and return attributes instance
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# class UserAttributes
|
66
|
+
# include ROM::Model::Attributes
|
67
|
+
#
|
68
|
+
# attribute :name, String
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# UserAttributes[name: 'Jane']
|
72
|
+
#
|
73
|
+
# @param [Hash,#to_hash] input The input params
|
74
|
+
#
|
75
|
+
# @return [Attributes]
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
def [](input)
|
79
|
+
input.is_a?(self) ? input : new(input)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Macro for defining ActiveModel::Name object on the attributes class
|
83
|
+
#
|
84
|
+
# This is essential for rails helpers to work properly when generating
|
85
|
+
# form input names etc.
|
86
|
+
#
|
87
|
+
# @example
|
88
|
+
# class UserAttributes
|
89
|
+
# include ROM::Model::Attributes
|
90
|
+
#
|
91
|
+
# set_model_name 'User'
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# @return [undefined]
|
95
|
+
#
|
96
|
+
# @api public
|
97
|
+
def set_model_name(name)
|
98
|
+
class_eval <<-RUBY
|
99
|
+
def self.model_name
|
100
|
+
@model_name ||= ActiveModel::Name.new(self, nil, #{name.inspect})
|
101
|
+
end
|
102
|
+
RUBY
|
103
|
+
end
|
104
|
+
|
105
|
+
# Shortcut for defining timestamp attributes like created_at etc.
|
106
|
+
#
|
107
|
+
# @example
|
108
|
+
# class NewPostAttributes
|
109
|
+
# include ROM::Model::Attributes
|
110
|
+
#
|
111
|
+
# # provide name(s) explicitly
|
112
|
+
# timestamps :published_at
|
113
|
+
#
|
114
|
+
# # defaults to :created_at, :updated_at without args
|
115
|
+
# timestamps
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# @api public
|
119
|
+
def timestamps(*attrs)
|
120
|
+
if attrs.empty?
|
121
|
+
DEFAULT_TIMESTAMPS.each do |t|
|
122
|
+
attribute t, DateTime, default: proc { DateTime.now }
|
123
|
+
end
|
124
|
+
else
|
125
|
+
attrs.each do |attr|
|
126
|
+
attribute attr, DateTime, default: proc { DateTime.now }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/rom/rails/model/form.rb
CHANGED
@@ -1,24 +1,85 @@
|
|
1
|
-
require 'rom/rails/model/form/
|
1
|
+
require 'rom/rails/model/form/class_interface'
|
2
2
|
|
3
3
|
module ROM
|
4
4
|
module Model
|
5
|
+
# Abstract form class
|
6
|
+
#
|
7
|
+
# Form objects in ROM are your top-level interface to persist data in the
|
8
|
+
# database. They combine many features that you know from ActiveRecord:
|
9
|
+
#
|
10
|
+
# * params processing with sanitization and coercion
|
11
|
+
# * attribute validations
|
12
|
+
# * persisting data in the database
|
13
|
+
#
|
14
|
+
# The major difference is that a ROM form object separates those
|
15
|
+
# responsibilities - a ROM form class has its own Attributes, Validator and
|
16
|
+
# ROM commands that are accessible within its instance.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# class UserForm < ROM::Model::Form
|
20
|
+
# commands users: :create
|
21
|
+
#
|
22
|
+
# input do
|
23
|
+
# set_model_name 'User'
|
24
|
+
#
|
25
|
+
# attribute :name, String
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# validations do
|
29
|
+
# validates :name, presence: true
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# class CreateUserForm < UserForm
|
34
|
+
# attributes.timestamps :created_at
|
35
|
+
#
|
36
|
+
# def commit!
|
37
|
+
# users.try { users.create.call(attributes) }
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# # then in your controller
|
42
|
+
# CreateUserForm.build(params[:user]).save
|
43
|
+
#
|
44
|
+
# @api public
|
5
45
|
class Form
|
6
46
|
include Equalizer.new(:params, :model, :result)
|
7
47
|
|
8
48
|
extend ROM::ClassMacros
|
9
|
-
extend Form::
|
49
|
+
extend Form::ClassInterface
|
10
50
|
|
11
51
|
defines :relation
|
12
52
|
|
13
|
-
|
53
|
+
# Return raw params received from the request
|
54
|
+
#
|
55
|
+
# @return [Object]
|
56
|
+
#
|
57
|
+
# @api public
|
58
|
+
attr_reader :params
|
59
|
+
|
60
|
+
# Return model instance representing an ActiveModel object that will be
|
61
|
+
# persisted or updated
|
62
|
+
#
|
63
|
+
# @return [Object]
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
attr_reader :model
|
67
|
+
|
68
|
+
# Return the result of commit!
|
69
|
+
#
|
70
|
+
# @return [Object]
|
71
|
+
#
|
72
|
+
# @api public
|
73
|
+
attr_reader :result
|
14
74
|
|
15
75
|
delegate :model_name, :persisted?, :to_key, to: :model
|
16
76
|
alias_method :to_model, :model
|
17
77
|
|
18
78
|
class << self
|
19
|
-
delegate :model_name, to: :
|
79
|
+
delegate :model_name, to: :attributes
|
20
80
|
end
|
21
81
|
|
82
|
+
# @api private
|
22
83
|
def initialize(params = {}, options = {})
|
23
84
|
@params = params
|
24
85
|
@model = self.class.model.new(params.merge(options.slice(*self.class.key)))
|
@@ -27,19 +88,37 @@ module ROM
|
|
27
88
|
options.each { |key, value| instance_variable_set("@#{key}", value) }
|
28
89
|
end
|
29
90
|
|
91
|
+
# A specialized form object must implement this method
|
92
|
+
#
|
93
|
+
# @abstract
|
94
|
+
#
|
95
|
+
# @api public
|
30
96
|
def commit!
|
31
97
|
raise NotImplementedError, "#{self.class}#commit! must be implemented"
|
32
98
|
end
|
33
99
|
|
100
|
+
# Save a form by calling commit! and memoizing result
|
101
|
+
#
|
102
|
+
# @return [self]
|
103
|
+
#
|
104
|
+
# @api public
|
34
105
|
def save(*args)
|
35
106
|
@result = commit!(*args)
|
36
107
|
self
|
37
108
|
end
|
38
109
|
|
110
|
+
# Return whether commit was successful
|
111
|
+
#
|
112
|
+
# @return [TrueClass,FalseClass]
|
113
|
+
#
|
114
|
+
# @api public
|
39
115
|
def success?
|
40
116
|
errors.nil? || !errors.any?
|
41
117
|
end
|
42
118
|
|
119
|
+
# Trigger validation and store errors (if any)
|
120
|
+
#
|
121
|
+
# @api public
|
43
122
|
def validate!
|
44
123
|
validator = self.class::Validator.new(attributes)
|
45
124
|
validator.validate
|
@@ -47,10 +126,22 @@ module ROM
|
|
47
126
|
@errors = validator.errors
|
48
127
|
end
|
49
128
|
|
129
|
+
# Sanitize and coerce input params
|
130
|
+
#
|
131
|
+
# This can also set default values
|
132
|
+
#
|
133
|
+
# @return [Model::Attributes]
|
134
|
+
#
|
135
|
+
# @api public
|
50
136
|
def attributes
|
51
|
-
self.class.
|
137
|
+
self.class.attributes[params]
|
52
138
|
end
|
53
139
|
|
140
|
+
# Return errors
|
141
|
+
#
|
142
|
+
# @return [ActiveModel::Errors]
|
143
|
+
#
|
144
|
+
# @api public
|
54
145
|
def errors
|
55
146
|
(result && result.error) || @errors
|
56
147
|
end
|
@@ -0,0 +1,401 @@
|
|
1
|
+
module ROM
|
2
|
+
module Model
|
3
|
+
class Form
|
4
|
+
module ClassInterface
|
5
|
+
# Return param handler class
|
6
|
+
#
|
7
|
+
# This class is used to process input params coming from a request and
|
8
|
+
# it's being created using `input` API
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# class MyForm < ROM::Model::Form
|
13
|
+
# input do
|
14
|
+
# attribute :name, String
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# MyForm.attributes # => MyForm::Attributes
|
19
|
+
#
|
20
|
+
# # process input params
|
21
|
+
# attributes = MyForm.attributes[name: 'Jane']
|
22
|
+
#
|
23
|
+
# @return [Class]
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
attr_reader :attributes
|
27
|
+
|
28
|
+
# Return attributes validator
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# class MyForm < ROM::Model::Form
|
32
|
+
# input do
|
33
|
+
# attribute :name, String
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# validations do
|
37
|
+
# validates :name, presence: true
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# attributes = MyForm.attributes[name: nil]
|
42
|
+
# MyForm::Validator.call(attributes) # raises validation error
|
43
|
+
#
|
44
|
+
# @return [Class]
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
attr_reader :validator
|
48
|
+
|
49
|
+
# Return model class
|
50
|
+
#
|
51
|
+
# @return [Class]
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
attr_reader :model
|
55
|
+
|
56
|
+
# relation => command name mapping used to generate commands automatically
|
57
|
+
#
|
58
|
+
# @return [Hash]
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
attr_reader :self_commands
|
62
|
+
|
63
|
+
# A list of relation names for which commands should be injected from
|
64
|
+
# the rom env automatically.
|
65
|
+
#
|
66
|
+
# This is used only when a given form re-uses existing commands
|
67
|
+
#
|
68
|
+
# @return [Hash]
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
attr_reader :injectible_commands
|
72
|
+
|
73
|
+
# input block stored to be used in inherited hook
|
74
|
+
#
|
75
|
+
# @return [Proc]
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
attr_reader :input_block
|
79
|
+
|
80
|
+
# validation block stored to be used in inherited hook
|
81
|
+
#
|
82
|
+
# @return [Proc]
|
83
|
+
#
|
84
|
+
# @api private
|
85
|
+
attr_reader :validations_block
|
86
|
+
|
87
|
+
# Copy input attributes, validator and model to the descendant
|
88
|
+
#
|
89
|
+
# @api private
|
90
|
+
def inherited(klass)
|
91
|
+
klass.inject_commands_for(*injectible_commands) if injectible_commands
|
92
|
+
klass.commands(*self_commands) if self_commands
|
93
|
+
klass.input(readers: false, &input_block) if input_block
|
94
|
+
klass.validations(&validations_block) if validations_block
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set key for the model that is handled by a form object
|
99
|
+
#
|
100
|
+
# This defaults to [:id]
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# class MyForm < ROM::Model::Form
|
104
|
+
# key [:user_id]
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# @return [Array<Symbol>]
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def key(*keys)
|
111
|
+
if keys.any? && !@key
|
112
|
+
@key = keys
|
113
|
+
attr_reader(*keys)
|
114
|
+
elsif !@key
|
115
|
+
@key = [:id]
|
116
|
+
attr_reader :id
|
117
|
+
elsif keys.any?
|
118
|
+
@key = keys
|
119
|
+
end
|
120
|
+
@key
|
121
|
+
end
|
122
|
+
|
123
|
+
# Specify what commands should be generated for a form object
|
124
|
+
#
|
125
|
+
# @example
|
126
|
+
# class MyForm < ROM::Model::Form
|
127
|
+
# commands users: :create
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# @param [Hash] relation => command name map
|
131
|
+
#
|
132
|
+
# @return [self]
|
133
|
+
#
|
134
|
+
# @api public
|
135
|
+
def commands(names)
|
136
|
+
names.each { |relation, _action| attr_reader(relation) }
|
137
|
+
@self_commands = names
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
# Specify input params handler class
|
142
|
+
#
|
143
|
+
# This uses Virtus DSL
|
144
|
+
#
|
145
|
+
# @example
|
146
|
+
# class MyForm < ROM::Model::Form
|
147
|
+
# input do
|
148
|
+
# set_model_name 'User'
|
149
|
+
#
|
150
|
+
# attribute :name, String
|
151
|
+
# attribute :age, Integer
|
152
|
+
# end
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# MyForm.build(name: 'Jane', age: 21).attributes
|
156
|
+
# # => #<MyForm::Attributes:0x007f821f863d48 @name="Jane", @age=21>
|
157
|
+
#
|
158
|
+
# @return [self]
|
159
|
+
#
|
160
|
+
# @api public
|
161
|
+
def input(options = {}, &block)
|
162
|
+
readers = options.fetch(:readers) { true }
|
163
|
+
define_attributes!(block)
|
164
|
+
define_attribute_readers! if readers
|
165
|
+
define_model!
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
# Specify attribute validator class
|
170
|
+
#
|
171
|
+
# This uses ActiveModel::Validations DSL
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# class MyForm < ROM::Model::Form
|
175
|
+
# input do
|
176
|
+
# set_model_name 'User'
|
177
|
+
#
|
178
|
+
# attribute :name, String
|
179
|
+
# attribute :age, Integer
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# validations do
|
183
|
+
# validates :name, :age, presence: true
|
184
|
+
# end
|
185
|
+
# end
|
186
|
+
#
|
187
|
+
# form = MyForm.build(name: 'Jane', age: nil)
|
188
|
+
# # => #<MyForm::Attributes:0x007f821f863d48 @name="Jane", @age=21>
|
189
|
+
# form.validate! # raises
|
190
|
+
#
|
191
|
+
# @return [self]
|
192
|
+
#
|
193
|
+
# @api public
|
194
|
+
def validations(&block)
|
195
|
+
define_validator!(block)
|
196
|
+
self
|
197
|
+
end
|
198
|
+
|
199
|
+
# Inject specific commands from the rom env
|
200
|
+
#
|
201
|
+
# This can be used when the env has re-usable commands
|
202
|
+
#
|
203
|
+
# @example
|
204
|
+
# class MyForm < ROM::Model::Form
|
205
|
+
# inject_commands_for :users
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# @api public
|
209
|
+
def inject_commands_for(*names)
|
210
|
+
@injectible_commands = names
|
211
|
+
names.each { |name| attr_reader(name) }
|
212
|
+
self
|
213
|
+
end
|
214
|
+
|
215
|
+
# Build a form object using input params and options
|
216
|
+
#
|
217
|
+
# @example
|
218
|
+
# class MyForm < ROM::Model::Form
|
219
|
+
# input do
|
220
|
+
# set_model_name 'User'
|
221
|
+
#
|
222
|
+
# attribute :name, String
|
223
|
+
# attribute :age, Integer
|
224
|
+
# end
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# # form for a new object
|
228
|
+
# form = MyForm.build(name: 'Jane')
|
229
|
+
#
|
230
|
+
# # form for a persisted object
|
231
|
+
# form = MyForm.build({ name: 'Jane' }, id: 1)
|
232
|
+
#
|
233
|
+
# @return [Model::Form]
|
234
|
+
#
|
235
|
+
# @api public
|
236
|
+
def build(input = {}, options = {})
|
237
|
+
new(input, options.merge(command_registry))
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
# @return [Hash<Symbol=>ROM::CommandRegistry>]
|
243
|
+
#
|
244
|
+
# @api private
|
245
|
+
def command_registry
|
246
|
+
@command_registry ||= setup_command_registry
|
247
|
+
end
|
248
|
+
|
249
|
+
# Create attribute handler class
|
250
|
+
#
|
251
|
+
# @return [Class]
|
252
|
+
#
|
253
|
+
# @api private
|
254
|
+
def define_attributes!(block)
|
255
|
+
@input_block = block
|
256
|
+
@attributes = ClassBuilder.new(name: "#{name}::Attributes", parent: Object).call { |klass|
|
257
|
+
klass.send(:include, ROM::Model::Attributes)
|
258
|
+
}
|
259
|
+
@attributes.class_eval(&block)
|
260
|
+
const_set(:Attributes, @attributes)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Define attribute readers for the form
|
264
|
+
#
|
265
|
+
# This is very unfortunate but rails `form_for` and friends require
|
266
|
+
# the object to provide attribute values, hence we need to expose those
|
267
|
+
# using the form object itself.
|
268
|
+
#
|
269
|
+
# @return [Class]
|
270
|
+
#
|
271
|
+
# @api private
|
272
|
+
def define_attribute_readers!
|
273
|
+
@attributes.attribute_set.each do |attribute|
|
274
|
+
if public_instance_methods.include?(attribute.name)
|
275
|
+
raise(
|
276
|
+
ArgumentError,
|
277
|
+
"#{attribute.name} attribute is in conflict with #{self}##{attribute.name}"
|
278
|
+
)
|
279
|
+
end
|
280
|
+
|
281
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
282
|
+
def #{attribute.name}
|
283
|
+
attributes[:#{attribute.name}]
|
284
|
+
end
|
285
|
+
RUBY
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Create model class
|
290
|
+
#
|
291
|
+
# Model instance represents an entity that will be persisted or was
|
292
|
+
# already persisted and will be updated.
|
293
|
+
#
|
294
|
+
# This object is returned via `Form#to_model` which rails uses internally
|
295
|
+
# in many places to figure out what to do.
|
296
|
+
#
|
297
|
+
# Model object provides two crucial pieces of information: whether or not
|
298
|
+
# something was persisted and its primary key value
|
299
|
+
#
|
300
|
+
# @return [Class]
|
301
|
+
#
|
302
|
+
# @api private
|
303
|
+
def define_model!
|
304
|
+
@model = ClassBuilder.new(name: "#{name}::Model", parent: @attributes).call { |klass|
|
305
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
306
|
+
def persisted?
|
307
|
+
to_key.any?
|
308
|
+
end
|
309
|
+
|
310
|
+
def to_key
|
311
|
+
to_h.values_at(#{key.map(&:inspect).join(', ')}).compact
|
312
|
+
end
|
313
|
+
RUBY
|
314
|
+
}
|
315
|
+
key.each { |name| @model.attribute(name) }
|
316
|
+
const_set(:Model, @model)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Define attribute validator class
|
320
|
+
#
|
321
|
+
# @return [Class]
|
322
|
+
#
|
323
|
+
# @api private
|
324
|
+
def define_validator!(block)
|
325
|
+
@validations_block = block
|
326
|
+
@validator = ClassBuilder.new(name: "#{name}::Validator", parent: Object).call { |klass|
|
327
|
+
klass.send(:include, ROM::Model::Validator)
|
328
|
+
}
|
329
|
+
@validator.class_eval(&block)
|
330
|
+
const_set(:Validator, @validator)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Shortcut to global ROM env
|
334
|
+
#
|
335
|
+
# @return [ROM::Env]
|
336
|
+
#
|
337
|
+
# @api private
|
338
|
+
def rom
|
339
|
+
ROM.env
|
340
|
+
end
|
341
|
+
|
342
|
+
# Return identifier of the default adapter
|
343
|
+
#
|
344
|
+
# TODO: we need an interface for that in ROM
|
345
|
+
#
|
346
|
+
# @return [Symbol]
|
347
|
+
#
|
348
|
+
# @api private
|
349
|
+
def adapter
|
350
|
+
ROM.adapters.keys.first
|
351
|
+
end
|
352
|
+
|
353
|
+
# Generate a command registry hash which will be auto-injected to a form
|
354
|
+
# object.
|
355
|
+
#
|
356
|
+
# @return [Hash<Symbol=>ROM::CommandRegistry>]
|
357
|
+
#
|
358
|
+
# @api private
|
359
|
+
def setup_command_registry
|
360
|
+
commands = {}
|
361
|
+
|
362
|
+
if self_commands
|
363
|
+
self_commands.each do |rel_name, name|
|
364
|
+
command = build_command(name, rel_name)
|
365
|
+
commands[rel_name] = CommandRegistry.new(name => command)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
if injectible_commands
|
370
|
+
injectible_commands.each do |relation|
|
371
|
+
commands[relation] = rom.command(relation)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
commands
|
376
|
+
end
|
377
|
+
|
378
|
+
# Build a command object with a specific name
|
379
|
+
#
|
380
|
+
# @param [Symbol] name The name of the command
|
381
|
+
# @param [Symbol] rel_name The name of the command's relation
|
382
|
+
#
|
383
|
+
# @return [ROM::Command]
|
384
|
+
#
|
385
|
+
# @api private
|
386
|
+
def build_command(name, rel_name)
|
387
|
+
klass = Command.build_class(name, rel_name, adapter: adapter)
|
388
|
+
|
389
|
+
klass.result :one
|
390
|
+
klass.validator @validator
|
391
|
+
|
392
|
+
relation = rom.relations[rel_name]
|
393
|
+
repository = rom.repositories[relation.repository]
|
394
|
+
repository.extend_command_class(klass, relation.dataset)
|
395
|
+
|
396
|
+
klass.build(relation)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|