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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc7557987e49805062efa789a9ecc5b0344839b5f8217b9a59fbd6904246ec8f
4
- data.tar.gz: 801b5edbde46dba2c005ef8bad826691ca00ce7adda5654d04f602406d15847f
3
+ metadata.gz: 37feed93ae3f90b8e08f2ab960faf4195c8891e6bc4d60a78d03666c5c035de3
4
+ data.tar.gz: 29b2bad869d32e4bf22f188eb4c57a199300a315431f373d5cf988f15f63ab8b
5
5
  SHA512:
6
- metadata.gz: bcec088edb29345af8934525274ddf100a5d0276545c2153ef1eeef4e6a255040dd0eabfd287afc5d68c3b041a4ac335ef8a258d653598cbb449eb2a78b94c63
7
- data.tar.gz: b660a95428e98a9e6839f2b154623dd5226532c646a486f606f4c318c71bef8dd7861859a5775a28be47098ffeecd7c54974baaf31195246084d2ace7daeac08
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 2023-12-20 10:30:23 UTC using RuboCop version 1.57.2.
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: 2
22
+ # Offense count: 1
23
23
  # Configuration parameters: CountComments, CountAsOne.
24
24
  Metrics/ClassLength:
25
- Max: 161
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.6.3](https://github.com/BookingSync/operations/tree/main)
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::Monad::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.
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::Monad-compatible interfaces.
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
- @post_update = Post::Update.default.callable(
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 @post_update
863
+ respond_with @post_update_form
867
864
  end
868
865
 
869
866
  def update
870
- # With operations we don't need strong parameters as the operation contract takes care of this.
871
- @post_update = Post::Update.default.call(
872
- { **params[:post_update_default_form], post_id: params[:id] },
873
- current_user: current_user
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
- respond_with @post_update, location: edit_post_url(@post_update.context[:post])
882
+ def self.default_form
883
+ @default_form ||= Operations::Form.new(default)
877
884
  end
878
885
  end
879
886
  ```
880
887
 
881
- The key here is to use `Operations::Result#form` method for the form builder.
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 @post_update.form, url: post_url(@post_update.context[:post]), method: :patch do |f| %>
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.default
896
- @default ||= Operations::Command.new(
897
- ...,
898
- form_hydrator: Post::Update::Hydrator.new
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 an optional `form_model_map:` operation option:
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.default
920
- @default ||= Operations::Command.new(
921
- ...,
922
- form_hydrator: Post::Update::Hydrator.new,
923
- form_model_map: {
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
- Here we define all the fields mapping to a model class with a regexp or just string values.
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
 
@@ -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::Types::Hash.map(
162
- Operations::Types::Coercible::Array.of(
163
- Operations::Types::String | Operations::Types::Symbol | Operations::Types.Instance(Regexp)
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: UNDEFINED, policies: [UNDEFINED],
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 - [UNDEFINED] unless policies_sum == [UNDEFINED, UNDEFINED]
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 pretty_print(pp)
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
- **main_components_as_json,
297
- **form_components_as_json,
298
- configuration: configuration.as_json
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 main_components_as_json
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 form_components_as_json
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(message: message, meta: meta, **rest)
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, :form, :model_name)
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, optional: true, default: proc { false }
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
- optional: true
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:, namespace:, class_name:, model_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, model_map, [])
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, model_map, path)
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 do |key|
27
- key_path = path + [key.name]
27
+ key_map.each { |key| define_attribute(form, model_map, key, path) }
28
+ form
29
+ end
28
30
 
29
- case key
30
- when Dry::Schema::Key::Array
31
- nested_form = traverse(key.member, form, key.name.to_s.underscore.classify, model_map, key_path)
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
- form
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, hash_key, model_map, path)
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, model_map, key_path)
59
- form.attribute(name, form: nested_form, collection: collection, **model_name(key_path, model_map))
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
@@ -1,194 +1,96 @@
1
1
  # frozen_string_literal: true
2
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}
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
- # class AuthorForm < Operations::Form
9
- # attribute :name
10
- # end
12
+ # command = Operations::Command.new(...)
13
+ # form = Operations::Form.new(command)
14
+ #
15
+ # @form_object = form.build(params)
11
16
  #
12
- # class PostForm < Operations::Form
13
- # attribute :title
14
- # attribute :tags, collection: true
15
- # attribute :author, form: AuthorForm
16
- # end
17
+ # form_for @form_object, url: ...
17
18
  #
18
- # PostForm.new({ tags: ["foobar"], author: { name: "Batman" } })
19
- # # => #<PostForm attributes={:title=>nil, :tags=>["foobar"], :author=>#<AuthorForm attributes={:name=>"Batman"}>}>
19
+ # @form_object = form.persist(params)
20
+ # respond_with @form_object
20
21
  #
21
- # @see Operations::Form::Builder
22
22
  class Operations::Form
23
- extend Dry::Initializer
24
- include Dry::Equalizer(:attributes, :errors)
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
- def self.validators_on(name)
57
- attributes[name.to_sym]&.model_validators || []
58
- end
59
-
60
- def type_for_attribute(name)
61
- self.class.attributes[name.to_sym].model_type
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
- def localized_attr_name_for(name, locale)
65
- self.class.attributes[name.to_sym].model_localized_attr_name(locale)
66
- end
34
+ return unless self == Operations::Form
67
35
 
68
- def has_attribute?(name) # rubocop:disable Naming/PredicateName
69
- self.class.attributes.key?(name.to_sym)
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
- def attributes
73
- self.class.attributes.keys.to_h do |name|
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
- def respond_to_missing?(name, *)
89
- self.class.attributes.key?(name)
43
+ subclass.extend Operations::Form::Base::ClassMethods
44
+ subclass.prepend Operations::Form::Base::InstanceMethods
90
45
  end
91
46
 
92
- def model_name
93
- ActiveModel::Name.new(self.class)
94
- end
95
-
96
- # This should return false if we want to use POST.
97
- # Now it is going to generate PATCH form.
98
- def persisted?
99
- true
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
- # Probably can be always nil, it is used in automated URL derival.
103
- # We can make it work later but it will require additional concepts.
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 errors
109
- @errors ||= ActiveModel::Errors.new(self).tap do |errors|
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 valid?
120
- errors.empty?
121
- end
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 add_messages(errors, key, messages)
140
- return unless messages.is_a?(Array)
141
-
142
- messages.each do |message|
143
- message = message[:text] if message.is_a?(Hash) && message.key?(:text)
144
- errors.add(key, message)
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 cached_attribute(name)
157
- name = name.to_sym
158
- return unless self.class.attributes.key?(name)
159
-
160
- nested_name = :"#{name}_attributes"
161
- value = data.key?(nested_name) ? data[nested_name] : data[name]
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 wrap_collection(path, collection, form)
167
- collection = [] if collection.nil?
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 wrap_object(path, data, form)
184
- data = {} if data.nil?
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
@@ -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 pretty_print(pp)
81
- attributes = self.class.dry_initializer.attributes(self)
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 as_json(*, include_command: false, **)
86
+ def to_hash(include_command: false)
96
87
  hash = {
97
88
  component: component,
98
89
  params: params,
99
- context: context_as_json,
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.as_json if include_command
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 context_as_json
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Operations
4
- VERSION = "0.6.3"
4
+ VERSION = "0.7.0"
5
5
  end
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.add_runtime_dependency "activerecord", ">= 5.2.0"
35
- spec.add_runtime_dependency "activesupport", ">= 5.2.0"
36
- spec.add_runtime_dependency "after_commit_everywhere"
37
- spec.add_runtime_dependency "dry-monads"
38
- spec.add_runtime_dependency "dry-struct"
39
- spec.add_runtime_dependency "dry-validation"
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.6.3
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: 2023-12-20 00:00:00.000000000 Z
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: '0'
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: '0'
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.7
212
+ rubygems_version: 3.4.19
210
213
  signing_key:
211
214
  specification_version: 4
212
215
  summary: Operations framework