operations 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +14 -3
- data/CHANGELOG.md +10 -1
- data/README.md +93 -29
- data/lib/operations/command.rb +21 -37
- data/lib/operations/contract/messages_resolver.rb +1 -1
- data/lib/operations/form/attribute.rb +6 -5
- data/lib/operations/form/base.rb +264 -0
- data/lib/operations/form/builder.rb +34 -37
- data/lib/operations/form/deprecated_legacy_model_map_implementation.rb +31 -0
- data/lib/operations/form.rb +62 -160
- data/lib/operations/inspect.rb +33 -0
- data/lib/operations/result.rb +9 -18
- data/lib/operations/version.rb +1 -1
- data/lib/operations.rb +5 -2
- data/operations.gemspec +7 -7
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37feed93ae3f90b8e08f2ab960faf4195c8891e6bc4d60a78d03666c5c035de3
|
4
|
+
data.tar.gz: 29b2bad869d32e4bf22f188eb4c57a199300a315431f373d5cf988f15f63ab8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 186e9327e813bbf7ddd5d5aea52b5bf00cc7a62a9c8b235b7fd25f36585a2468192922df3ff9435d7320b164807e31acf73523eaf9b106c67de6f2f27da19400
|
7
|
+
data.tar.gz: 43d61db8dbe71a4f9269755a6fc73c46214ba3baddeed80e650a7ae2ced43c08fb361dd19cef005954504970c06bfa573a1a55f760e7ac711c68c19f4e34601b
|
data/.rubocop_todo.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on
|
3
|
+
# on 2024-05-05 08:51:48 UTC using RuboCop version 1.59.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
@@ -19,12 +19,23 @@ Gemspec/DevelopmentDependencies:
|
|
19
19
|
Metrics/AbcSize:
|
20
20
|
Max: 28
|
21
21
|
|
22
|
-
# Offense count:
|
22
|
+
# Offense count: 1
|
23
23
|
# Configuration parameters: CountComments, CountAsOne.
|
24
24
|
Metrics/ClassLength:
|
25
|
-
Max:
|
25
|
+
Max: 145
|
26
26
|
|
27
27
|
# Offense count: 1
|
28
|
+
# Configuration parameters: CountComments, CountAsOne.
|
29
|
+
Metrics/ModuleLength:
|
30
|
+
Max: 142
|
31
|
+
|
32
|
+
# Offense count: 2
|
28
33
|
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
29
34
|
Metrics/ParameterLists:
|
30
35
|
Max: 7
|
36
|
+
|
37
|
+
# Offense count: 1
|
38
|
+
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
39
|
+
RSpec/VerifiedDoubles:
|
40
|
+
Exclude:
|
41
|
+
- 'spec/operations/form_spec.rb'
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [0.
|
3
|
+
## [0.7.0](https://github.com/BookingSync/operations/tree/main)
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Implement new forms system detaching it from operations [\#47](https://github.com/BookingSync/operations/pull/47) ([pyromaniac](https://github.com/pyromaniac))
|
8
|
+
|
9
|
+
### Improvements
|
10
|
+
|
11
|
+
- Better inspect for all the opjects [\#45](https://github.com/BookingSync/operations/pull/45) ([pyromaniac](https://github.com/pyromaniac))
|
12
|
+
- Use #to_hash instead of #as_json [\#44](https://github.com/BookingSync/operations/pull/44) ([pyromaniac](https://github.com/pyromaniac))
|
4
13
|
|
5
14
|
### Fixes
|
6
15
|
|
data/README.md
CHANGED
@@ -262,7 +262,7 @@ end
|
|
262
262
|
|
263
263
|
The operation body can be any callable object (respond to the `call` method), even a lambda. But it is always better to define it as a class since there might be additional instance methods and [dependency injections](#dependency-injection).
|
264
264
|
|
265
|
-
In any event, the operation body should return a Dry::
|
265
|
+
In any event, the operation body should return a Dry::Monads::Result instance. In case of a Failure, it will be converted into an `Operation::Result#error` and in case of Success(), its content will be merged into the operation context.
|
266
266
|
|
267
267
|
**Important:** since the Success result payload is merged inside of a context, it is supposed to be a hash.
|
268
268
|
|
@@ -305,7 +305,7 @@ end
|
|
305
305
|
|
306
306
|
Dependency injection can be used to provide IO clients with the operation. It could be DB repositories or API clients. The best way is to use Dry::Initializer for it since it provides the ability to define acceptable types.
|
307
307
|
|
308
|
-
If you still prefer to use ActiveRecord, it is worth creating a wrapper around it providing Dry::
|
308
|
+
If you still prefer to use ActiveRecord, it is worth creating a wrapper around it providing Dry::Monads-compatible interfaces.
|
309
309
|
|
310
310
|
```ruby
|
311
311
|
class ActiveRecordRepository
|
@@ -858,31 +858,38 @@ While we normally recommend using frontend-backend separation, it is still possi
|
|
858
858
|
```ruby
|
859
859
|
class PostsController < ApplicationController
|
860
860
|
def edit
|
861
|
-
@
|
862
|
-
{ post_id: params[:id] },
|
863
|
-
current_user: current_user
|
864
|
-
)
|
861
|
+
@post_update_form = Post::Update.default_form.build(params, current_user: current_user)
|
865
862
|
|
866
|
-
respond_with @
|
863
|
+
respond_with @post_update_form
|
867
864
|
end
|
868
865
|
|
869
866
|
def update
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
867
|
+
@post_update_form = Post::Update.default_form.persist(params, current_user: current_user)
|
868
|
+
|
869
|
+
respond_with @post_update_form, location: edit_post_url(@post_update_form.operation_result.context[:post])
|
870
|
+
end
|
871
|
+
end
|
872
|
+
```
|
873
|
+
|
874
|
+
Where the form class is defined this way:
|
875
|
+
|
876
|
+
```ruby
|
877
|
+
class Post::Update
|
878
|
+
def self.default
|
879
|
+
@default ||= Operations::Command.new(...)
|
880
|
+
end
|
875
881
|
|
876
|
-
|
882
|
+
def self.default_form
|
883
|
+
@default_form ||= Operations::Form.new(default)
|
877
884
|
end
|
878
885
|
end
|
879
886
|
```
|
880
887
|
|
881
|
-
|
888
|
+
Then, the form can be used as any other form object:
|
882
889
|
|
883
890
|
```erb
|
884
891
|
# views/posts/edit.html.erb
|
885
|
-
<%= form_for @
|
892
|
+
<%= form_for @post_update_form, url: post_url(@post_update_form.operation_result.context[:post]) do |f| %>
|
886
893
|
<%= f.input :title %>
|
887
894
|
<%= f.text_area :body %>
|
888
895
|
<% end %>
|
@@ -892,16 +899,16 @@ In cases when we need to populate the form data, it is possible to pass `form_hy
|
|
892
899
|
|
893
900
|
```ruby
|
894
901
|
class Post::Update
|
895
|
-
def self.
|
896
|
-
@
|
897
|
-
|
898
|
-
|
902
|
+
def self.default_form
|
903
|
+
@default_form ||= Operations::Form.new(
|
904
|
+
default,
|
905
|
+
hydrator: Post::Update::Hydrator.new
|
899
906
|
)
|
900
907
|
end
|
901
908
|
end
|
902
909
|
|
903
910
|
class Post::Update::Hydrator
|
904
|
-
def call(form_class, params, post:, **)
|
911
|
+
def call(form_class, params, post:, **_context)
|
905
912
|
value_attributes = form_class.attributes.keys - %i[post_id]
|
906
913
|
data = value_attributes.index_with { |name| post.public_send(name) }
|
907
914
|
|
@@ -912,23 +919,80 @@ end
|
|
912
919
|
|
913
920
|
The general idea here is to figure out attributes we have in the contract (those attributes are also defined automatically in a generated form class) and then fetch those attributes from the model and merge them with the params provided within the request.
|
914
921
|
|
915
|
-
Also, in the case of, say, [simple_form](https://github.com/heartcombo/simple_form), we need to provide additional attributes information, like data type. It is possible to do this with
|
922
|
+
Also, in the case of, say, [simple_form](https://github.com/heartcombo/simple_form), we need to provide additional attributes information, like data type. It is possible to do this with `model_map:` option:
|
916
923
|
|
917
924
|
```ruby
|
918
925
|
class Post::Update
|
919
|
-
def self.
|
920
|
-
@
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
[%r{.+}] => "Post"
|
925
|
-
}
|
926
|
+
def self.default_form
|
927
|
+
@default_form ||= Operations::Form.new(
|
928
|
+
default,
|
929
|
+
model_map: Post::Update::ModelMap.new,
|
930
|
+
hydrator: Post::Update::Hydrator.new
|
926
931
|
)
|
927
932
|
end
|
928
933
|
end
|
934
|
+
|
935
|
+
class Post::Update::ModelMap
|
936
|
+
def call(_path)
|
937
|
+
Post
|
938
|
+
end
|
939
|
+
end
|
929
940
|
```
|
930
941
|
|
931
|
-
|
942
|
+
In forms, params input is already transformed to extract the nested data with the form name. `form_for @post_update_form` will generate the form that send params nested under the `params[:post_update_form]` key. By default operation forms extract this form data and send it to the operation at the top level, so `{ id: 42, post_update_form: { title: "Post Title" } }` params will be sent to the operation as `{ id: 42, title: "Post Title" }`. Strong params are also accepted by the form, though they are being converted with `to_unsafe_hash`.
|
943
|
+
|
944
|
+
It is possible to add more params transfomations to the form in cases when operation contract is different from the params structure:
|
945
|
+
|
946
|
+
```ruby
|
947
|
+
class Post::Update
|
948
|
+
def self.default_form
|
949
|
+
@default_form ||= Operations::Form.new(
|
950
|
+
default,
|
951
|
+
model_name: "post_update_form", # form name can be customized
|
952
|
+
params_transformations: [
|
953
|
+
ParamsMap.new(id: :post_id),
|
954
|
+
NestedAttributes.new(:sections)
|
955
|
+
]
|
956
|
+
)
|
957
|
+
end
|
958
|
+
|
959
|
+
contract do
|
960
|
+
required(:post_id).filled(:integer)
|
961
|
+
optional(:title).filled(:string)
|
962
|
+
optional(:sections).array(:hash) do
|
963
|
+
optional(:id).filled(:integer)
|
964
|
+
optional(:content).filled(:string)
|
965
|
+
optional(:_destroy).filled(:bool)
|
966
|
+
end
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
# This will transform `{ id: 42, title: "Post Title" }` params to `{ post_id: 42, title: "Post Title" }`
|
971
|
+
class ParamsMap
|
972
|
+
extend Dry::Initializer
|
973
|
+
|
974
|
+
param :params_map
|
975
|
+
|
976
|
+
def call(_form_class, params, **_context)
|
977
|
+
params.transform_keys { |key| params_map[key] || key }
|
978
|
+
end
|
979
|
+
end
|
980
|
+
|
981
|
+
# And this will transform nested attributes hash from the form to an array acceptable by the operation:
|
982
|
+
# from
|
983
|
+
# `{ id: 42, sections_attributes: { '0' => { id: 1, content: "First paragraph" }, 'new' => { content: 'New Paragraph' } } }`
|
984
|
+
# into
|
985
|
+
# `{ id: 42, sections: [{ id: 1, content: "First paragraph" }, { content: 'New Paragraph' }] }`
|
986
|
+
class NestedAttributes
|
987
|
+
extend Dry::Initializer
|
988
|
+
|
989
|
+
param :name, Types::Coercible::Symbol
|
990
|
+
|
991
|
+
def call(_form_class, params, **_context)
|
992
|
+
params[name] = params[:"#{name}_attrbutes"].values
|
993
|
+
end
|
994
|
+
end
|
995
|
+
```
|
932
996
|
|
933
997
|
## Development
|
934
998
|
|
data/lib/operations/command.rb
CHANGED
@@ -125,15 +125,14 @@ require "operations/components/on_failure"
|
|
125
125
|
# which contains all the artifacts and the information about the errors
|
126
126
|
# should they ever happen.
|
127
127
|
class Operations::Command
|
128
|
-
UNDEFINED = Object.new.freeze
|
129
|
-
EMPTY_HASH = {}.freeze
|
130
128
|
COMPONENTS = %i[contract policies idempotency preconditions operation on_success on_failure].freeze
|
131
129
|
FORM_HYDRATOR = ->(_form_class, params, **_context) { params }
|
132
130
|
|
131
|
+
extend Dry::Initializer
|
132
|
+
include Dry::Core::Constants
|
133
133
|
include Dry::Monads[:result]
|
134
134
|
include Dry::Monads::Do.for(:call_monad, :callable_monad, :validate_monad, :execute_operation)
|
135
135
|
include Dry::Equalizer(*COMPONENTS)
|
136
|
-
extend Dry::Initializer
|
137
136
|
|
138
137
|
# Provides message and meaningful sentry context for failed operations
|
139
138
|
class OperationFailed < StandardError
|
@@ -158,17 +157,14 @@ class Operations::Command
|
|
158
157
|
option :preconditions, Operations::Types::Array.of(Operations::Types.Interface(:call)), default: -> { [] }
|
159
158
|
option :on_success, Operations::Types::Array.of(Operations::Types.Interface(:call)), default: -> { [] }
|
160
159
|
option :on_failure, Operations::Types::Array.of(Operations::Types.Interface(:call)), default: -> { [] }
|
161
|
-
option :form_model_map, Operations::
|
162
|
-
|
163
|
-
|
164
|
-
),
|
165
|
-
Operations::Types::String
|
166
|
-
), default: proc { {} }
|
167
|
-
option :form_base, Operations::Types::Class, default: proc { ::Operations::Form }
|
168
|
-
option :form_class, Operations::Types::Class.optional, default: proc {}
|
160
|
+
option :form_model_map, Operations::Form::DeprecatedLegacyModelMapImplementation::TYPE, default: proc { {} }
|
161
|
+
option :form_base, Operations::Types::Class, default: proc { ::Operations::Form::Base }
|
162
|
+
option :form_class, Operations::Types::Class.optional, default: proc {}, reader: false
|
169
163
|
option :form_hydrator, Operations::Types.Interface(:call), default: proc { FORM_HYDRATOR }
|
170
164
|
option :configuration, Operations::Configuration, default: proc { Operations.default_config }
|
171
165
|
|
166
|
+
include Operations::Inspect.new(dry_initializer.attributes(self).keys)
|
167
|
+
|
172
168
|
# A short-cut to initialize operation by convention:
|
173
169
|
#
|
174
170
|
# Namespace::OperationName - operation
|
@@ -194,16 +190,15 @@ class Operations::Command
|
|
194
190
|
end
|
195
191
|
|
196
192
|
def initialize(
|
197
|
-
operation, policy:
|
193
|
+
operation, policy: Undefined, policies: [Undefined],
|
198
194
|
precondition: nil, preconditions: [], after: [], **options
|
199
195
|
)
|
200
196
|
policies_sum = Array.wrap(policy) + policies
|
201
|
-
result_policies = policies_sum - [
|
197
|
+
result_policies = policies_sum - [Undefined] unless policies_sum == [Undefined, Undefined]
|
202
198
|
options[:policies] = result_policies if result_policies
|
203
199
|
|
204
200
|
preconditions.push(precondition) if precondition.present?
|
205
201
|
super(operation, preconditions: preconditions, on_success: after, **options)
|
206
|
-
@form_class ||= build_form_class
|
207
202
|
end
|
208
203
|
|
209
204
|
# Instantiates a new command with the given fields updated.
|
@@ -276,32 +271,21 @@ class Operations::Command
|
|
276
271
|
validate(*args, **kwargs).success?
|
277
272
|
end
|
278
273
|
|
279
|
-
def
|
280
|
-
attributes = self.class.dry_initializer.attributes(self)
|
281
|
-
|
282
|
-
pp.object_group(self) do
|
283
|
-
pp.seplist(attributes.keys, -> { pp.text "," }) do |name|
|
284
|
-
pp.breakable " "
|
285
|
-
pp.group(1) do
|
286
|
-
pp.text name.to_s
|
287
|
-
pp.text " = "
|
288
|
-
pp.pp send(name)
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
def as_json(*)
|
274
|
+
def to_hash
|
295
275
|
{
|
296
|
-
**
|
297
|
-
**
|
298
|
-
configuration: configuration
|
276
|
+
**main_components_to_hash,
|
277
|
+
**form_components_to_hash,
|
278
|
+
configuration: configuration
|
299
279
|
}
|
300
280
|
end
|
301
281
|
|
282
|
+
def form_class
|
283
|
+
@form_class ||= build_form_class
|
284
|
+
end
|
285
|
+
|
302
286
|
private
|
303
287
|
|
304
|
-
def
|
288
|
+
def main_components_to_hash
|
305
289
|
{
|
306
290
|
operation: operation.class.name,
|
307
291
|
contract: contract.class.name,
|
@@ -313,7 +297,7 @@ class Operations::Command
|
|
313
297
|
}
|
314
298
|
end
|
315
299
|
|
316
|
-
def
|
300
|
+
def form_components_to_hash
|
317
301
|
{
|
318
302
|
form_model_map: form_model_map,
|
319
303
|
form_base: form_base.name,
|
@@ -401,9 +385,9 @@ class Operations::Command
|
|
401
385
|
.new(base_class: form_base)
|
402
386
|
.build(
|
403
387
|
key_map: contract.class.schema.key_map,
|
388
|
+
model_map: Operations::Form::DeprecatedLegacyModelMapImplementation.new(form_model_map),
|
404
389
|
namespace: operation.class,
|
405
|
-
class_name: form_class_name
|
406
|
-
model_map: form_model_map
|
390
|
+
class_name: form_class_name
|
407
391
|
)
|
408
392
|
end
|
409
393
|
|
@@ -6,6 +6,6 @@ class Operations::Contract::MessagesResolver < Dry::Validation::Messages::Resolv
|
|
6
6
|
def call(message:, meta: Dry::Schema::EMPTY_HASH, **rest)
|
7
7
|
meta = meta.merge(code: message) if message.is_a?(Symbol)
|
8
8
|
|
9
|
-
super
|
9
|
+
super
|
10
10
|
end
|
11
11
|
end
|
@@ -5,14 +5,15 @@
|
|
5
5
|
# legacy UI.
|
6
6
|
class Operations::Form::Attribute
|
7
7
|
extend Dry::Initializer
|
8
|
-
include Dry::Equalizer(:name, :collection, :
|
8
|
+
include Dry::Equalizer(:name, :collection, :model_name, :form)
|
9
|
+
include Operations::Inspect.new(:name, :collection, :model_name, :form)
|
9
10
|
|
10
11
|
param :name, type: Operations::Types::Coercible::Symbol
|
11
|
-
option :collection, type: Operations::Types::Bool,
|
12
|
-
option :form, type: Operations::Types::Class, optional: true
|
12
|
+
option :collection, type: Operations::Types::Bool, default: proc { false }
|
13
13
|
option :model_name,
|
14
|
-
type: Operations::Types::String | Operations::Types.Instance(Class).constrained(lt: ActiveRecord::Base),
|
15
|
-
|
14
|
+
type: (Operations::Types::String | Operations::Types.Instance(Class).constrained(lt: ActiveRecord::Base)).optional,
|
15
|
+
default: proc {}
|
16
|
+
option :form, type: Operations::Types::Class.optional, default: proc {}
|
16
17
|
|
17
18
|
def model_type
|
18
19
|
@model_type ||= owning_model.type_for_attribute(string_name) if model_name
|
@@ -0,0 +1,264 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This class implements Rails form object compatibility layer
|
4
|
+
# It is possible to configure form object attributes automatically
|
5
|
+
# basing on Dry Schema user {Operations::Form::Builder}
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# class AuthorForm < Operations::Form::Base
|
9
|
+
# attribute :name
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# class PostForm < Operations::Form::Base
|
13
|
+
# attribute :title
|
14
|
+
# attribute :tags, collection: true
|
15
|
+
# attribute :author, form: AuthorForm
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# PostForm.new({ tags: ["foobar"], author: { name: "Batman" } })
|
19
|
+
# # => #<PostForm attributes={:title=>nil, :tags=>["foobar"], :author=>#<AuthorForm attributes={:name=>"Batman"}>}>
|
20
|
+
#
|
21
|
+
# @see Operations::Form::Builder
|
22
|
+
class Operations::Form::Base
|
23
|
+
BUILD_ASSOCIATION_PREFIX = "build_"
|
24
|
+
NESTED_ATTRIBUTES_SUFFIX = "_attributes="
|
25
|
+
|
26
|
+
# :nodoc:
|
27
|
+
module ClassMethods
|
28
|
+
def self.extended(base)
|
29
|
+
base.singleton_class.include Operations::Inspect.new(:attributes)
|
30
|
+
|
31
|
+
base.extend Dry::Initializer
|
32
|
+
base.include Dry::Equalizer(:attributes, :errors)
|
33
|
+
base.include Operations::Inspect.new(:attributes, :errors)
|
34
|
+
|
35
|
+
base.param :data,
|
36
|
+
type: Operations::Types::Hash.map(Operations::Types::Symbol, Operations::Types::Any),
|
37
|
+
default: proc { {} },
|
38
|
+
reader: :private
|
39
|
+
base.option :messages,
|
40
|
+
type: Operations::Types::Hash.map(
|
41
|
+
Operations::Types::Nil | Operations::Types::Coercible::Symbol,
|
42
|
+
Operations::Types::Any
|
43
|
+
),
|
44
|
+
default: proc { {} },
|
45
|
+
reader: :private
|
46
|
+
base.option :operation_result, default: proc {}
|
47
|
+
|
48
|
+
base.class_attribute :attributes, instance_accessor: false, default: {}
|
49
|
+
base.class_attribute :primary_key, instance_accessor: false, default: :id
|
50
|
+
|
51
|
+
base.define_method :initialize do |*args, **kwargs|
|
52
|
+
args.empty? && kwargs.present? ? super(kwargs, **{}) : super(*args, **kwargs)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def attribute(name, **options)
|
57
|
+
attribute = Operations::Form::Attribute.new(name, **options)
|
58
|
+
|
59
|
+
self.attributes = attributes.merge(
|
60
|
+
attribute.name => attribute
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def human_attribute_name(name, options = {})
|
65
|
+
if attributes[name.to_sym]
|
66
|
+
attributes[name.to_sym].model_human_name(options)
|
67
|
+
else
|
68
|
+
name.to_s.humanize
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def validators_on(name)
|
73
|
+
attributes[name.to_sym]&.model_validators || []
|
74
|
+
end
|
75
|
+
|
76
|
+
def model_name
|
77
|
+
@model_name ||= ActiveModel::Name.new(self)
|
78
|
+
end
|
79
|
+
|
80
|
+
def reflect_on_association(...); end
|
81
|
+
end
|
82
|
+
|
83
|
+
# :nodoc:
|
84
|
+
module InstanceMethods
|
85
|
+
def type_for_attribute(name)
|
86
|
+
self.class.attributes[name.to_sym].model_type
|
87
|
+
end
|
88
|
+
|
89
|
+
def localized_attr_name_for(name, locale)
|
90
|
+
self.class.attributes[name.to_sym].model_localized_attr_name(locale)
|
91
|
+
end
|
92
|
+
|
93
|
+
def has_attribute?(name) # rubocop:disable Naming/PredicateName
|
94
|
+
self.class.attributes.key?(name.to_sym)
|
95
|
+
end
|
96
|
+
|
97
|
+
def attributes
|
98
|
+
self.class.attributes.keys.to_h do |name|
|
99
|
+
[name, read_attribute(name)]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def assigned_attributes
|
104
|
+
(self.class.attributes.keys & data.keys).to_h do |name|
|
105
|
+
[name, read_attribute(name)]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# For now we gracefully return nil for unknown methods
|
110
|
+
def method_missing(name, *args, **kwargs)
|
111
|
+
build_attribute_name = build_attribute_name(name)
|
112
|
+
build_attribute = self.class.attributes[build_attribute_name]
|
113
|
+
plural_build_attribute = self.class.attributes[build_attribute_name.to_s.pluralize.to_sym]
|
114
|
+
|
115
|
+
if has_attribute?(name)
|
116
|
+
read_attribute(name)
|
117
|
+
elsif build_attribute&.form
|
118
|
+
build_attribute.form.new(*args, **kwargs)
|
119
|
+
elsif plural_build_attribute&.form
|
120
|
+
plural_build_attribute.form.new(*args, **kwargs)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def respond_to_missing?(name, *)
|
125
|
+
has_attribute?(name) ||
|
126
|
+
build_nested_form?(build_attribute_name(name)) ||
|
127
|
+
self.class.attributes[nested_attribute_name(name)]&.form
|
128
|
+
end
|
129
|
+
|
130
|
+
def model_name
|
131
|
+
self.class.model_name
|
132
|
+
end
|
133
|
+
|
134
|
+
def persisted?
|
135
|
+
!has_attribute?(self.class.primary_key) || read_attribute(self.class.primary_key).present?
|
136
|
+
end
|
137
|
+
|
138
|
+
def new_record?
|
139
|
+
!persisted?
|
140
|
+
end
|
141
|
+
|
142
|
+
def _destroy
|
143
|
+
Operations::Types::Params::Bool.call(read_attribute(:_destroy)) { false }
|
144
|
+
end
|
145
|
+
alias_method :marked_for_destruction?, :_destroy
|
146
|
+
|
147
|
+
# Probably can be always nil, it is used in automated URL derival.
|
148
|
+
# We can make it work later but it will require additional concepts.
|
149
|
+
def to_key
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def errors
|
154
|
+
@errors ||= ActiveModel::Errors.new(self).tap do |errors|
|
155
|
+
self.class.attributes.each do |name, attribute|
|
156
|
+
add_messages(errors, name, messages[name])
|
157
|
+
add_messages_to_collection(errors, name, messages[name]) if attribute.collection
|
158
|
+
end
|
159
|
+
|
160
|
+
add_messages(errors, :base, messages[nil])
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def valid?
|
165
|
+
errors.empty?
|
166
|
+
end
|
167
|
+
|
168
|
+
def read_attribute(name)
|
169
|
+
cached_attribute(name) do |value, attribute|
|
170
|
+
if attribute.collection && attribute.form
|
171
|
+
wrap_collection([name], value, attribute.form)
|
172
|
+
elsif attribute.form
|
173
|
+
wrap_object([name], value, attribute.form)
|
174
|
+
elsif attribute.collection
|
175
|
+
value.nil? ? [] : value
|
176
|
+
else
|
177
|
+
value
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
alias_method :read_attribute_for_validation, :read_attribute
|
182
|
+
|
183
|
+
def to_hash
|
184
|
+
{
|
185
|
+
attributes: attributes,
|
186
|
+
errors: errors
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def add_messages(errors, key, messages)
|
193
|
+
return unless messages.is_a?(Array)
|
194
|
+
|
195
|
+
messages.each do |message|
|
196
|
+
message = message[:text] if message.is_a?(Hash) && message.key?(:text)
|
197
|
+
errors.add(key, message)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def add_messages_to_collection(errors, key, messages)
|
202
|
+
return unless messages.is_a?(Hash)
|
203
|
+
|
204
|
+
read_attribute(key).size.times do |i|
|
205
|
+
add_messages(errors, "#{key}[#{i}]", messages[i])
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def cached_attribute(name)
|
210
|
+
name = name.to_sym
|
211
|
+
return unless self.class.attributes.key?(name)
|
212
|
+
|
213
|
+
nested_name = :"#{name}_attributes"
|
214
|
+
value = data.key?(nested_name) ? data[nested_name] : data[name]
|
215
|
+
|
216
|
+
(@attributes_cache ||= {})[name] ||= yield(value, self.class.attributes[name])
|
217
|
+
end
|
218
|
+
|
219
|
+
def wrap_collection(path, collection, form)
|
220
|
+
collection = [] if collection.nil?
|
221
|
+
|
222
|
+
case collection
|
223
|
+
when Hash
|
224
|
+
collection.values.map.with_index do |data, i|
|
225
|
+
wrap_object(path + [i], data, form)
|
226
|
+
end
|
227
|
+
when Array
|
228
|
+
collection.map.with_index do |data, i|
|
229
|
+
wrap_object(path + [i], data, form)
|
230
|
+
end
|
231
|
+
else
|
232
|
+
collection
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def wrap_object(path, data, form)
|
237
|
+
data = {} if data.nil?
|
238
|
+
|
239
|
+
if data.is_a?(Hash)
|
240
|
+
nested_messages = messages.dig(*path)
|
241
|
+
nested_messages = {} unless nested_messages.is_a?(Hash)
|
242
|
+
form.new(data, messages: nested_messages)
|
243
|
+
else
|
244
|
+
data
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def build_attribute_name(name)
|
249
|
+
name.to_s.delete_prefix(BUILD_ASSOCIATION_PREFIX).to_sym if name.to_s.start_with?(BUILD_ASSOCIATION_PREFIX)
|
250
|
+
end
|
251
|
+
|
252
|
+
def nested_attribute_name(name)
|
253
|
+
name.to_s.delete_suffix(NESTED_ATTRIBUTES_SUFFIX).to_sym if name.to_s.end_with?(NESTED_ATTRIBUTES_SUFFIX)
|
254
|
+
end
|
255
|
+
|
256
|
+
def build_nested_form?(name)
|
257
|
+
!!(self.class.attributes[name]&.form ||
|
258
|
+
self.class.attributes[name.to_s.pluralize.to_sym]&.form)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
extend ClassMethods
|
263
|
+
include InstanceMethods
|
264
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Traverses the passed {Dry::Schema::KeyMap} and generates
|
4
|
-
# {Operations::Form} classes on the fly. Handles nested structures.
|
4
|
+
# {Operations::Form::Base} classes on the fly. Handles nested structures.
|
5
5
|
#
|
6
|
-
# @see Operations::Form
|
6
|
+
# @see Operations::Form::Base
|
7
7
|
class Operations::Form::Builder
|
8
8
|
extend Dry::Initializer
|
9
9
|
|
@@ -11,38 +11,49 @@ class Operations::Form::Builder
|
|
11
11
|
|
12
12
|
option :base_class, Operations::Types::Instance(Class)
|
13
13
|
|
14
|
-
def build(key_map:,
|
14
|
+
def build(key_map:, model_map:, namespace: nil, class_name: nil, model_name: nil)
|
15
15
|
return namespace.const_get(class_name) if namespace && class_name && namespace.const_defined?(class_name)
|
16
16
|
|
17
|
-
traverse(key_map, namespace, class_name,
|
17
|
+
traverse(key_map, model_map, namespace, class_name, model_name, [])
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
def traverse(key_map, namespace, class_name,
|
22
|
+
def traverse(key_map, model_map, namespace, class_name, model_name, path)
|
23
23
|
form = Class.new(base_class)
|
24
|
-
namespace.const_set(class_name, form) if namespace && class_name
|
24
|
+
namespace.const_set(class_name, form) if namespace&.name && class_name
|
25
|
+
define_model_name(form, model_name) if model_name && !form.name
|
25
26
|
|
26
|
-
key_map.each
|
27
|
-
|
27
|
+
key_map.each { |key| define_attribute(form, model_map, key, path) }
|
28
|
+
form
|
29
|
+
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
form.attribute(key.name, form: nested_form, collection: true, **model_name(key_path, model_map))
|
33
|
-
when Dry::Schema::Key::Hash
|
34
|
-
traverse_hash(form, key, model_map, path)
|
35
|
-
when Dry::Schema::Key
|
36
|
-
form.attribute(key.name, **model_name(key_path, model_map))
|
37
|
-
else
|
38
|
-
raise "Unknown key_map key: #{key.class}"
|
39
|
-
end
|
31
|
+
def define_model_name(form, model_name)
|
32
|
+
form.define_singleton_method :model_name do
|
33
|
+
@model_name ||= ActiveModel::Name.new(self, nil, model_name)
|
40
34
|
end
|
35
|
+
end
|
41
36
|
|
42
|
-
|
37
|
+
def define_attribute(form, model_map, key, path)
|
38
|
+
case key
|
39
|
+
when Dry::Schema::Key::Array
|
40
|
+
traverse_array(form, model_map, key, path)
|
41
|
+
when Dry::Schema::Key::Hash
|
42
|
+
traverse_hash(form, model_map, key, path)
|
43
|
+
when Dry::Schema::Key
|
44
|
+
form.attribute(key.name, model_name: model_map&.call(path + [key.name]))
|
45
|
+
else
|
46
|
+
raise "Unknown key_map key: #{key.class}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def traverse_array(form, model_map, key, path)
|
51
|
+
key_path = path + [key.name]
|
52
|
+
nested_form = traverse(key.member, model_map, form, key.name.to_s.underscore.classify, key.name.to_s, key_path)
|
53
|
+
form.attribute(key.name, form: nested_form, collection: true, model_name: model_map&.call(key_path))
|
43
54
|
end
|
44
55
|
|
45
|
-
def traverse_hash(form,
|
56
|
+
def traverse_hash(form, model_map, hash_key, path)
|
46
57
|
nested_attributes_suffix = hash_key.name.match?(NESTED_ATTRIBUTES_SUFFIX)
|
47
58
|
nested_attributes_collection = hash_key.members.all?(Dry::Schema::Key::Hash) &&
|
48
59
|
hash_key.members.map(&:members).uniq.size == 1
|
@@ -55,8 +66,8 @@ class Operations::Form::Builder
|
|
55
66
|
form.define_method :"#{hash_key.name}=", proc { |attributes| attributes } if nested_attributes_suffix
|
56
67
|
|
57
68
|
key_path = path + [name]
|
58
|
-
nested_form = traverse(members, form, name.underscore.camelize,
|
59
|
-
form.attribute(name, form: nested_form, collection: collection,
|
69
|
+
nested_form = traverse(members, model_map, form, name.underscore.camelize, name.to_s.singularize, key_path)
|
70
|
+
form.attribute(name, form: nested_form, collection: collection, model_name: model_map&.call(key_path))
|
60
71
|
end
|
61
72
|
|
62
73
|
def specify_form_attributes(hash_key, nested_attributes_suffix, nested_attributes_collection)
|
@@ -68,18 +79,4 @@ class Operations::Form::Builder
|
|
68
79
|
[hash_key.name, hash_key.members, false]
|
69
80
|
end
|
70
81
|
end
|
71
|
-
|
72
|
-
def model_name(path, model_map)
|
73
|
-
_, model_name = model_map.find do |pathspec, _model|
|
74
|
-
path.size == pathspec.size && path.zip(pathspec).all? do |slug, pattern|
|
75
|
-
pattern.is_a?(Regexp) ? pattern.match?(slug) : slug == pattern
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
if model_name
|
80
|
-
{ model_name: model_name }
|
81
|
-
else
|
82
|
-
{}
|
83
|
-
end
|
84
|
-
end
|
85
82
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Traverses the passed {Dry::Schema::KeyMap} and generates
|
4
|
+
# {Operations::Form::Base} classes on the fly. Handles nested structures.
|
5
|
+
#
|
6
|
+
# This class will be removed along with Operations::Command#form_model_map
|
7
|
+
# option removal along with the rest of form-related stuff.
|
8
|
+
#
|
9
|
+
# Don't use this class in you code, instead, copy it and modify to you liking.
|
10
|
+
#
|
11
|
+
# @see Operations::Form::Base
|
12
|
+
class Operations::Form::DeprecatedLegacyModelMapImplementation
|
13
|
+
extend Dry::Initializer
|
14
|
+
|
15
|
+
TYPE = Operations::Types::Hash.map(
|
16
|
+
Operations::Types::Coercible::Array.of(
|
17
|
+
Operations::Types::String | Operations::Types::Symbol | Operations::Types.Instance(Regexp)
|
18
|
+
),
|
19
|
+
Operations::Types::String
|
20
|
+
)
|
21
|
+
|
22
|
+
param :model_map_hash, TYPE, default: proc { {} }
|
23
|
+
|
24
|
+
def call(path)
|
25
|
+
model_map_hash.find do |pathspec, _model|
|
26
|
+
path.size == pathspec.size && path.zip(pathspec).all? do |slug, pattern|
|
27
|
+
pattern.is_a?(Regexp) ? pattern.match?(slug) : slug == pattern
|
28
|
+
end
|
29
|
+
end&.second
|
30
|
+
end
|
31
|
+
end
|
data/lib/operations/form.rb
CHANGED
@@ -1,194 +1,96 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
3
|
+
# Configures and defines a form object factory.
|
4
|
+
# Forms can be defined on top of commants and used in the user-facing controllers.
|
5
|
+
# Form objects are Rails-specific and support everything that is needed for Rails'
|
6
|
+
# form rendering helpers. They are designed to replace direct usage of ActiveRecord
|
7
|
+
# models in controllers and views and act as an integration bridge from Rails
|
8
|
+
# application to the Operations framework.
|
9
|
+
#
|
6
10
|
# @example
|
7
11
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
12
|
+
# command = Operations::Command.new(...)
|
13
|
+
# form = Operations::Form.new(command)
|
14
|
+
#
|
15
|
+
# @form_object = form.build(params)
|
11
16
|
#
|
12
|
-
#
|
13
|
-
# attribute :title
|
14
|
-
# attribute :tags, collection: true
|
15
|
-
# attribute :author, form: AuthorForm
|
16
|
-
# end
|
17
|
+
# form_for @form_object, url: ...
|
17
18
|
#
|
18
|
-
#
|
19
|
-
#
|
19
|
+
# @form_object = form.persist(params)
|
20
|
+
# respond_with @form_object
|
20
21
|
#
|
21
|
-
# @see Operations::Form::Builder
|
22
22
|
class Operations::Form
|
23
|
-
|
24
|
-
include Dry::Equalizer(:
|
25
|
-
|
26
|
-
param :data,
|
27
|
-
type: Operations::Types::Hash.map(Operations::Types::Symbol, Operations::Types::Any),
|
28
|
-
default: proc { {} },
|
29
|
-
reader: :private
|
30
|
-
option :messages,
|
31
|
-
type: Operations::Types::Hash.map(
|
32
|
-
Operations::Types::Nil | Operations::Types::Coercible::Symbol,
|
33
|
-
Operations::Types::Any
|
34
|
-
),
|
35
|
-
default: proc { {} },
|
36
|
-
reader: :private
|
37
|
-
|
38
|
-
class_attribute :attributes, instance_accessor: false, default: {}
|
39
|
-
|
40
|
-
def self.attribute(name, **options)
|
41
|
-
attribute = Operations::Form::Attribute.new(name, **options)
|
42
|
-
|
43
|
-
self.attributes = attributes.merge(
|
44
|
-
attribute.name => attribute
|
45
|
-
)
|
46
|
-
end
|
47
|
-
|
48
|
-
def self.human_attribute_name(name, options = {})
|
49
|
-
if attributes[name.to_sym]
|
50
|
-
attributes[name.to_sym].model_human_name(options)
|
51
|
-
else
|
52
|
-
name.to_s.humanize
|
53
|
-
end
|
54
|
-
end
|
23
|
+
include Dry::Core::Constants
|
24
|
+
include Dry::Equalizer(:command, :model_map, :params_transformations, :hydrator, :form_class)
|
25
|
+
include Operations::Inspect.new(:model_name, :model_map, :params_transformations, :hydrator, :form_class)
|
55
26
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
def
|
61
|
-
|
62
|
-
end
|
27
|
+
# We need to make deprecated inheritance from Operations::Form act exactly the
|
28
|
+
# same way as from Operations::Form::Base. In order to do this, we are encapsulating all the
|
29
|
+
# inheritable functionality in 2 modules and removing methods defined in Operations::Form
|
30
|
+
# from the result class.
|
31
|
+
def self.inherited(subclass)
|
32
|
+
super
|
63
33
|
|
64
|
-
|
65
|
-
self.class.attributes[name.to_sym].model_localized_attr_name(locale)
|
66
|
-
end
|
34
|
+
return unless self == Operations::Form
|
67
35
|
|
68
|
-
|
69
|
-
|
70
|
-
end
|
36
|
+
ActiveSupport::Deprecation.new.warn("Inheritance from Operations::Form is deprecated and will be " \
|
37
|
+
"removed in 1.0.0. Please inherit from Operations::Form::Base instead")
|
71
38
|
|
72
|
-
|
73
|
-
|
74
|
-
[name, read_attribute(name)]
|
39
|
+
(Operations::Form.instance_methods - Object.instance_methods).each do |method|
|
40
|
+
subclass.undef_method(method)
|
75
41
|
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def assigned_attributes
|
79
|
-
(self.class.attributes.keys & data.keys).to_h do |name|
|
80
|
-
[name, read_attribute(name)]
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def method_missing(name, *)
|
85
|
-
read_attribute(name)
|
86
|
-
end
|
87
42
|
|
88
|
-
|
89
|
-
|
43
|
+
subclass.extend Operations::Form::Base::ClassMethods
|
44
|
+
subclass.prepend Operations::Form::Base::InstanceMethods
|
90
45
|
end
|
91
46
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
47
|
+
include Dry::Initializer.define(lambda do
|
48
|
+
param :command, type: Operations::Types.Interface(:operation, :contract, :call)
|
49
|
+
option :model_name, type: Operations::Types::String.optional, default: proc {}, reader: false
|
50
|
+
option :model_map, type: Operations::Types.Interface(:call).optional, default: proc {}
|
51
|
+
option :params_transformations, type: Operations::Types::Coercible::Array.of(Operations::Types.Interface(:call)),
|
52
|
+
default: proc { [] }
|
53
|
+
option :hydrator, type: Operations::Types.Interface(:call).optional, default: proc {}
|
54
|
+
option :base_class, type: Operations::Types::Class, default: proc { ::Operations::Form::Base }
|
55
|
+
end)
|
101
56
|
|
102
|
-
|
103
|
-
|
104
|
-
def to_key
|
105
|
-
nil
|
57
|
+
def build(params = EMPTY_HASH, **context)
|
58
|
+
instantiate_form(command.callable(transform_params(params, **context), **context))
|
106
59
|
end
|
107
60
|
|
108
|
-
def
|
109
|
-
|
110
|
-
self.class.attributes.each do |name, attribute|
|
111
|
-
add_messages(errors, name, messages[name])
|
112
|
-
add_messages_to_collection(errors, name, messages[name]) if attribute.collection
|
113
|
-
end
|
114
|
-
|
115
|
-
add_messages(errors, :base, messages[nil])
|
116
|
-
end
|
61
|
+
def persist(params = EMPTY_HASH, **context)
|
62
|
+
instantiate_form(command.call(transform_params(params, **context), **context))
|
117
63
|
end
|
118
64
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
def read_attribute(name)
|
124
|
-
cached_attribute(name) do |value, attribute|
|
125
|
-
if attribute.collection && attribute.form
|
126
|
-
wrap_collection([name], value, attribute.form)
|
127
|
-
elsif attribute.form
|
128
|
-
wrap_object([name], value, attribute.form)
|
129
|
-
elsif attribute.collection
|
130
|
-
value.nil? ? [] : value
|
131
|
-
else
|
132
|
-
value
|
133
|
-
end
|
134
|
-
end
|
65
|
+
def form_class
|
66
|
+
@form_class ||= Operations::Form::Builder.new(base_class: base_class)
|
67
|
+
.build(key_map: key_map, model_map: model_map, model_name: model_name)
|
135
68
|
end
|
136
69
|
|
137
70
|
private
|
138
71
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def add_messages_to_collection(errors, key, messages)
|
149
|
-
return unless messages.is_a?(Hash)
|
150
|
-
|
151
|
-
read_attribute(key).size.times do |i|
|
152
|
-
add_messages(errors, "#{key}[#{i}]", messages[i])
|
72
|
+
def transform_params(params, **context)
|
73
|
+
params = params.to_unsafe_hash if params.respond_to?(:to_unsafe_hash)
|
74
|
+
params = params.deep_symbolize_keys
|
75
|
+
params = params.merge(params[form_class.model_name.param_key.to_sym] || {})
|
76
|
+
params_transformations.inject(params) do |value, transformation|
|
77
|
+
transformation.call(form_class, value, **context)
|
153
78
|
end
|
154
79
|
end
|
155
80
|
|
156
|
-
def
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
(@attributes_cache ||= {})[name] ||= yield(value, self.class.attributes[name])
|
81
|
+
def instantiate_form(operation_result)
|
82
|
+
form_class.new(
|
83
|
+
hydrator&.call(form_class, operation_result.params, **operation_result.context) || {},
|
84
|
+
messages: operation_result.errors.to_h,
|
85
|
+
operation_result: operation_result
|
86
|
+
)
|
164
87
|
end
|
165
88
|
|
166
|
-
def
|
167
|
-
|
168
|
-
|
169
|
-
case collection
|
170
|
-
when Hash
|
171
|
-
collection.values.map.with_index do |data, i|
|
172
|
-
wrap_object(path + [i], data, form)
|
173
|
-
end
|
174
|
-
when Array
|
175
|
-
collection.map.with_index do |data, i|
|
176
|
-
wrap_object(path + [i], data, form)
|
177
|
-
end
|
178
|
-
else
|
179
|
-
collection
|
180
|
-
end
|
89
|
+
def key_map
|
90
|
+
@key_map ||= command.contract.schema.key_map
|
181
91
|
end
|
182
92
|
|
183
|
-
def
|
184
|
-
|
185
|
-
|
186
|
-
if data.is_a?(Hash)
|
187
|
-
nested_messages = messages.dig(*path)
|
188
|
-
nested_messages = {} unless nested_messages.is_a?(Hash)
|
189
|
-
form.new(data, messages: nested_messages)
|
190
|
-
else
|
191
|
-
data
|
192
|
-
end
|
93
|
+
def model_name
|
94
|
+
@model_name ||= ("#{command.operation.class.name.underscore}_form" if command.operation.class.name)
|
193
95
|
end
|
194
96
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configures inspect/pretty_print methods on object.
|
4
|
+
class Operations::Inspect < Module
|
5
|
+
extend Dry::Initializer
|
6
|
+
|
7
|
+
param :attributes, Operations::Types::Coercible::Array.of(Operations::Types::Symbol), reader: false
|
8
|
+
param :value_methods, Operations::Types::Hash.map(Operations::Types::Symbol, Operations::Types::Symbol)
|
9
|
+
|
10
|
+
def initialize(*attributes, **kwargs)
|
11
|
+
super(attributes.flatten(1), kwargs)
|
12
|
+
|
13
|
+
define_pretty_print(@attributes, @value_methods)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def define_pretty_print(attributes, value_methods)
|
19
|
+
define_method(:pretty_print) do |pp|
|
20
|
+
object_group_method = self.class.name ? :object_group : :object_address_group
|
21
|
+
pp.public_send(object_group_method, self) do
|
22
|
+
pp.seplist(attributes, -> { pp.text "," }) do |name|
|
23
|
+
pp.breakable " "
|
24
|
+
pp.group(1) do
|
25
|
+
pp.text name.to_s
|
26
|
+
pp.text "="
|
27
|
+
pp.pp __send__(value_methods[name] || name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/operations/result.rb
CHANGED
@@ -6,9 +6,9 @@
|
|
6
6
|
# execution).
|
7
7
|
# Also able to spawn a form object basing on the operation params and errors.
|
8
8
|
class Operations::Result
|
9
|
+
extend Dry::Initializer
|
9
10
|
include Dry::Monads[:result]
|
10
11
|
include Dry::Equalizer(:operation, :component, :params, :context, :on_success, :errors)
|
11
|
-
extend Dry::Initializer
|
12
12
|
|
13
13
|
option :operation, type: Operations::Types::Instance(Operations::Command), optional: true
|
14
14
|
option :component, type: Operations::Types::Symbol.enum(*Operations::Command::COMPONENTS)
|
@@ -19,6 +19,8 @@ class Operations::Result
|
|
19
19
|
option :errors, type: Operations::Types.Interface(:call) | Operations::Types::Instance(Dry::Validation::MessageSet),
|
20
20
|
default: proc { Dry::Validation::MessageSet.new([]).freeze }
|
21
21
|
|
22
|
+
include Operations::Inspect.new(dry_initializer.attributes(self).keys)
|
23
|
+
|
22
24
|
# Instantiates a new result with the given fields updated
|
23
25
|
def merge(**changes)
|
24
26
|
self.class.new(**self.class.dry_initializer.attributes(self), **changes)
|
@@ -77,31 +79,20 @@ class Operations::Result
|
|
77
79
|
)
|
78
80
|
end
|
79
81
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
83
|
-
pp.object_group(self) do
|
84
|
-
pp.seplist(attributes.keys, -> { pp.text "," }) do |name|
|
85
|
-
pp.breakable " "
|
86
|
-
pp.group(1) do
|
87
|
-
pp.text name.to_s
|
88
|
-
pp.text " = "
|
89
|
-
pp.pp send(name)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
82
|
+
def as_json(options = {})
|
83
|
+
to_hash(**options.slice(:include_command))
|
93
84
|
end
|
94
85
|
|
95
|
-
def
|
86
|
+
def to_hash(include_command: false)
|
96
87
|
hash = {
|
97
88
|
component: component,
|
98
89
|
params: params,
|
99
|
-
context:
|
90
|
+
context: context_to_hash,
|
100
91
|
on_success: on_success.as_json,
|
101
92
|
on_failure: on_failure.as_json,
|
102
93
|
errors: errors(full: true).to_h
|
103
94
|
}
|
104
|
-
hash[:command] = operation
|
95
|
+
hash[:command] = operation&.to_hash if include_command
|
105
96
|
hash
|
106
97
|
end
|
107
98
|
|
@@ -112,7 +103,7 @@ class Operations::Result
|
|
112
103
|
(errors.map { |error| error.meta[:code] } & names).present?
|
113
104
|
end
|
114
105
|
|
115
|
-
def
|
106
|
+
def context_to_hash
|
116
107
|
context.transform_values do |context_value|
|
117
108
|
next context_value.class.name unless context_value.respond_to?(:id)
|
118
109
|
|
data/lib/operations/version.rb
CHANGED
data/lib/operations.rb
CHANGED
@@ -11,15 +11,18 @@ require "active_model/naming"
|
|
11
11
|
require "after_commit_everywhere"
|
12
12
|
require "operations/version"
|
13
13
|
require "operations/types"
|
14
|
+
require "operations/inspect"
|
14
15
|
require "operations/configuration"
|
15
16
|
require "operations/contract"
|
16
17
|
require "operations/contract/messages_resolver"
|
17
18
|
require "operations/convenience"
|
18
|
-
require "operations/command"
|
19
|
-
require "operations/result"
|
20
19
|
require "operations/form"
|
20
|
+
require "operations/form/base"
|
21
21
|
require "operations/form/attribute"
|
22
22
|
require "operations/form/builder"
|
23
|
+
require "operations/form/deprecated_legacy_model_map_implementation"
|
24
|
+
require "operations/command"
|
25
|
+
require "operations/result"
|
23
26
|
|
24
27
|
# The root gem module
|
25
28
|
module Operations
|
data/operations.gemspec
CHANGED
@@ -29,14 +29,14 @@ Gem::Specification.new do |spec|
|
|
29
29
|
|
30
30
|
spec.add_development_dependency "appraisal"
|
31
31
|
spec.add_development_dependency "database_cleaner-active_record"
|
32
|
-
spec.add_development_dependency "sqlite3"
|
32
|
+
spec.add_development_dependency "sqlite3", "~> 1.4"
|
33
33
|
|
34
|
-
spec.
|
35
|
-
spec.
|
36
|
-
spec.
|
37
|
-
spec.
|
38
|
-
spec.
|
39
|
-
spec.
|
34
|
+
spec.add_dependency "activerecord", ">= 5.2.0"
|
35
|
+
spec.add_dependency "activesupport", ">= 5.2.0"
|
36
|
+
spec.add_dependency "after_commit_everywhere"
|
37
|
+
spec.add_dependency "dry-monads"
|
38
|
+
spec.add_dependency "dry-struct"
|
39
|
+
spec.add_dependency "dry-validation"
|
40
40
|
spec.metadata = {
|
41
41
|
"rubygems_mfa_required" => "true"
|
42
42
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: operations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arkadiy Zabazhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: appraisal
|
@@ -42,16 +42,16 @@ dependencies:
|
|
42
42
|
name: sqlite3
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '1.4'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '1.4'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: activerecord
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -180,7 +180,10 @@ files:
|
|
180
180
|
- lib/operations/convenience.rb
|
181
181
|
- lib/operations/form.rb
|
182
182
|
- lib/operations/form/attribute.rb
|
183
|
+
- lib/operations/form/base.rb
|
183
184
|
- lib/operations/form/builder.rb
|
185
|
+
- lib/operations/form/deprecated_legacy_model_map_implementation.rb
|
186
|
+
- lib/operations/inspect.rb
|
184
187
|
- lib/operations/result.rb
|
185
188
|
- lib/operations/test_helpers.rb
|
186
189
|
- lib/operations/types.rb
|
@@ -206,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
209
|
- !ruby/object:Gem::Version
|
207
210
|
version: '0'
|
208
211
|
requirements: []
|
209
|
-
rubygems_version: 3.4.
|
212
|
+
rubygems_version: 3.4.19
|
210
213
|
signing_key:
|
211
214
|
specification_version: 4
|
212
215
|
summary: Operations framework
|