activeinteractor 2.0.0.alpha.2.0.0 → 2.0.0.alpha.2.2.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: 2a13c3091acf37b48e249f912e5551da92b039e17920e53c1082ebcc4b34f4c2
4
- data.tar.gz: 8e1e80f06675574384eeae45835f6372bf04d7378cdf64f7f21323d36857a7e4
3
+ metadata.gz: e288a5c77ab45d9b0009db2a3d1a2997bd577207bb20f16386c8aa29be50f454
4
+ data.tar.gz: 6e045fd1af8f93d2f22aeed4d96ddb44ac93f83cce65ea9237da3e2de3d167db
5
5
  SHA512:
6
- metadata.gz: 6b006f6e815419f58a6b6f063d37db8beae1e1ad239d98635545bb267d2c09df9307ec80528c1785035013aeaf07f27a73fcd1668dac44115e466109da675910
7
- data.tar.gz: 3f0d5b6fd50726ec6539b691257d00d85254e1ca74c06727d84da7763e8545d033650c6ec7482f307e622e77bff2bc8fcae25e54c2f6a15208377258dac1d814
6
+ metadata.gz: e22e0efcf6e53c4c70cf1b30e7871b17de7ebf13933779b989b40119ce3628eab3ade0228f1f3bbf96e7976440661cb0d4b8ec8d09f7af64d56074bf15e47ed1
7
+ data.tar.gz: 7cef976acbe9bfd3ef6a9e7d24237587d4a51220b5c08b8595cdfa4522ae6e84aba8903c6ab5fadb97d1529529fb216a00eab92adc171cc3c997eb0f05c7cdda
@@ -3,8 +3,20 @@
3
3
  module ActiveInteractor
4
4
  class Base
5
5
  class << self
6
- def argument(name, type, description = nil, **options)
7
- attributes[:arguments][name.to_sym] = { name: name, type: type, description: description }.merge(options)
6
+ delegate :argument, :argument_names, :arguments, to: :input_context_class
7
+ delegate :returns, :field_names, :fields, to: :output_context_class
8
+
9
+ def input_context_class
10
+ @input_context_class ||= const_set(:InputContext, Class.new(Context::Input))
11
+ end
12
+
13
+ def accepts_arguments_matching(set_input_context_class)
14
+ @input_context_class = set_input_context_class
15
+ end
16
+ alias input_context accepts_arguments_matching
17
+
18
+ def output_context_class
19
+ @output_context_class ||= const_set(:OutputContext, Class.new(Context::Output))
8
20
  end
9
21
 
10
22
  def perform!(input_context = {})
@@ -19,25 +31,32 @@ module ActiveInteractor
19
31
  Result.failure(errors: e.message)
20
32
  end
21
33
 
22
- def returns(name, type, description = nil, **options)
23
- attributes[:fields][name.to_sym] = { name: name, type: type, description: description }.merge(options)
34
+ def returns_data_matching(set_output_context_class)
35
+ @output_context_class = set_output_context_class
24
36
  end
25
-
26
- private
27
-
28
- def attributes
29
- @attributes ||= { arguments: {}, fields: {} }
37
+ alias output_context returns_data_matching
38
+
39
+ def runtime_context_class
40
+ @runtime_context_class ||= begin
41
+ context_class = const_set(:RuntimeContext, Class.new(Context::Runtime))
42
+ context_class.send(:attribute_set).merge(input_context_class.send(:attribute_set).attributes)
43
+ context_class.send(:attribute_set).merge(output_context_class.send(:attribute_set).attributes)
44
+ context_class
45
+ end
30
46
  end
31
47
  end
32
48
 
33
49
  def initialize(input = {})
34
- @input = input.freeze
35
- @context = parse_input!
50
+ @raw_input = input.dup
51
+ validate_input_and_generate_runtime_context!
36
52
  end
37
53
 
38
54
  def perform!
39
- interact
40
- Result.success(data: parse_output!)
55
+ with_notification(:perform) do |payload|
56
+ interact
57
+ generate_and_validate_output_context!
58
+ payload[:result] = Result.success(data: @output)
59
+ end
41
60
  end
42
61
 
43
62
  def perform
@@ -56,40 +75,36 @@ module ActiveInteractor
56
75
  attr_accessor :context
57
76
 
58
77
  def fail!(errors = {})
59
- rollback
60
- raise Error, Result.failure(data: context, errors: errors)
61
- end
62
-
63
- def parse_input!
64
- errors = {}
65
- context = self.class.send(:attributes)[:arguments].each_with_object({}) do |(name, options), hash|
66
- errors = validate_attribute_value(@input[name], name, options, errors)
67
- hash[name] = @input[name] || options[:default]
78
+ result = nil
79
+ with_notification(:rollback) do |payload|
80
+ rollback
81
+ result = Result.failure(data: parse_output!, errors: errors)
82
+ payload[:result] = result
68
83
  end
69
84
 
70
- raise Error, Result.failure(errors: errors, status: Result::STATUS[:failed_on_input]) unless errors.empty?
71
-
72
- context
85
+ raise Error, result
73
86
  end
74
87
 
75
- def parse_output!
76
- errors = {}
77
- context = self.class.send(:attributes)[:fields].each_with_object({}) do |(name, options), hash|
78
- errors = validate_attribute_value(self.context[name], name, options, errors)
79
- hash[name] = self.context[name] || options[:default]
80
- end
81
-
82
- raise Error, Result.failure(errors: errors, status: Result::STATUS[:failed_on_output]) unless errors.empty?
88
+ def generate_and_validate_output_context!
89
+ @output = self.class.output_context_class.new(context.attributes)
90
+ @output.validate!
91
+ return @output if @output.errors.empty?
83
92
 
84
- context
93
+ raise Error, Result.failure(errors: @output.errors, status: Result::STATUS[:failed_at_output])
85
94
  end
86
95
 
87
- def validate_attribute_value(value, attribute_name, attribute_options, errors)
88
- return errors unless value.blank? && attribute_options[:required] && !attribute_options[:default]
96
+ def validate_input_and_generate_runtime_context!
97
+ @input = self.class.input_context_class.new(@raw_input)
98
+ @input.validate!
99
+ return (@context = self.class.runtime_context_class.new(@raw_input)) if @input.errors.empty?
100
+
101
+ raise Error, Result.failure(errors: @input.errors, status: Result::STATUS[:failed_at_input])
102
+ end
89
103
 
90
- errors[attribute_name] ||= []
91
- errors[attribute_name] << :blank
92
- errors
104
+ def with_notification(action)
105
+ ActiveSupport::Notifications.instrument("#{self.class.name}::#{action.to_s.classify}") do |payload|
106
+ yield payload if block_given?
107
+ end
93
108
  end
94
109
  end
95
110
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ class Attribute
6
+ NO_DEFAULT_VALUE = :__no_default_value__
7
+ attr_reader :description, :error_messages, :name
8
+
9
+ def initialize(owner, name, type, description = nil, **options)
10
+ @owner = owner
11
+ @name = name.to_sym
12
+ @type_expression = type
13
+ @description = description || options[:description]
14
+ @options = { required: false, default: NO_DEFAULT_VALUE }.merge(options)
15
+ @error_messages = []
16
+ end
17
+
18
+ def assign_value(value)
19
+ @user_provided_value = value
20
+ end
21
+
22
+ def default_value
23
+ return if @options[:default] == NO_DEFAULT_VALUE
24
+
25
+ @options[:default]
26
+ end
27
+
28
+ def required?
29
+ @options[:required]
30
+ end
31
+
32
+ def type
33
+ @type_expression
34
+ end
35
+
36
+ def validate!
37
+ return true unless required? && value.nil?
38
+
39
+ error_messages << :blank
40
+ end
41
+
42
+ def value
43
+ @user_provided_value || default_value
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ module AttributeAssignment
6
+ extend ActiveSupport::Concern
7
+
8
+ def [](attribute_name)
9
+ read_attribute_value(attribute_name)
10
+ end
11
+
12
+ def []=(attribute_name, value)
13
+ assign_attribute_value(attribute_name, value)
14
+ end
15
+
16
+ protected
17
+
18
+ def assign_attribute_value(attribute_name, value)
19
+ attribute = attribute_set.find(attribute_name)
20
+ raise NoMethodError, "unknown attribute '#{attribute_name}' for #{self.class.name}" unless attribute
21
+
22
+ attribute.assign_value(value)
23
+ end
24
+
25
+ def assignment_method_missing(method_name, *arguments)
26
+ if arguments.length != 1
27
+ raise ArgumentError,
28
+ "wrong number of arguments (given #{arguments.length}, expected 1)"
29
+ end
30
+
31
+ assign_attribute_value(method_name, arguments.first)
32
+ end
33
+
34
+ def method_missing(method_id, *arguments)
35
+ return super unless respond_to_missing?(method_id)
36
+
37
+ method_name = method_id[/.*(?==\z)/m]
38
+ return assignment_method_missing(method_name, *arguments) if method_name
39
+
40
+ read_attribute_value(method_id)
41
+ end
42
+
43
+ def read_attribute_value(attribute_name)
44
+ attribute = attribute_set.find(attribute_name)
45
+ raise NoMethodError, "unknown attribute '#{attribute_name}' for #{self.class.name}" unless attribute
46
+
47
+ attribute.value
48
+ end
49
+
50
+ def respond_to_missing?(method_name, _include_private = false)
51
+ return true if attribute_set.attribute_names.include?(method_name.to_sym)
52
+ return true if attribute_set.attribute_names.include?(method_name[/.*(?==\z)/m]&.to_sym)
53
+
54
+ super
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ module AttributeRegistration
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ protected
10
+
11
+ def attribute_set
12
+ @attribute_set ||= AttributeSet.new(self)
13
+ end
14
+ end
15
+
16
+ included do
17
+ extend ClassMethods
18
+ end
19
+
20
+ protected
21
+
22
+ def attribute_set
23
+ @attribute_set ||= AttributeSet.new(self, *self.class.send(:attribute_set).attributes.map(&:dup))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ class AttributeSet
6
+ def initialize(owner, *attributes)
7
+ @owner = owner
8
+ @set = {}
9
+ attributes.each { |attribute| @set[attribute.name] = attribute }
10
+ end
11
+
12
+ def add(*attribute_args)
13
+ attribute_options = attribute_args.extract_options!
14
+ attribute = Attribute.new(@owner, *attribute_args, **attribute_options)
15
+ @set[attribute.name] = attribute
16
+ end
17
+
18
+ def attribute_names
19
+ @set.keys
20
+ end
21
+
22
+ def attributes
23
+ @set.values
24
+ end
25
+
26
+ def find(attribute_name)
27
+ @set[attribute_name.to_sym]
28
+ end
29
+
30
+ def merge(attributes)
31
+ attributes.each { |attribute| @set[attribute.name] = attribute }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ class Base
6
+ include ActiveModel::Validations
7
+ include HasActiveModelErrors
8
+ include AttributeRegistration
9
+ include AttributeAssignment
10
+
11
+ attr_reader :errors
12
+
13
+ def initialize(attributes = {})
14
+ @errors = ActiveModel::Errors.new(self)
15
+ attribute_set.attributes.each do |attribute|
16
+ next unless attributes.with_indifferent_access.key?(attribute.name)
17
+
18
+ assign_attribute_value(attribute.name, attributes.with_indifferent_access[attribute.name])
19
+ end
20
+ end
21
+
22
+ def validate!
23
+ attribute_set.attributes.each do |attribute|
24
+ attribute.validate!
25
+ attribute.error_messages.each { |message| errors.add(attribute.name, message) }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ class Input < Base
6
+ def self.argument(*attribute_args)
7
+ attribute_set.add(*attribute_args)
8
+ end
9
+
10
+ def self.argument_names
11
+ attribute_set.attribute_names
12
+ end
13
+
14
+ def self.arguments
15
+ attribute_set.attributes
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ class Output < Base
6
+ delegate :to_h, :to_hash, :to_json, to: :fields
7
+
8
+ def self.returns(*attribute_args)
9
+ attribute_set.add(*attribute_args)
10
+ end
11
+
12
+ def self.field_names
13
+ attribute_set.attribute_names
14
+ end
15
+
16
+ def self.fields
17
+ attribute_set.attributes
18
+ end
19
+
20
+ def fields
21
+ attribute_set.attributes.each_with_object({}) do |attribute, result|
22
+ result[attribute.name] = attribute.value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ class Runtime < Base
6
+ def initialize(attributes = {})
7
+ super
8
+ @table = {}
9
+ end
10
+
11
+ def attributes
12
+ clean = attribute_set.attributes.each_with_object({}) do |attribute, result|
13
+ result[attribute.name] = attribute.value
14
+ end
15
+ @table.merge(clean)
16
+ end
17
+
18
+ private
19
+
20
+ def assign_attribute_value(attribute_name, value)
21
+ attribute = attribute_set.find(attribute_name)
22
+ return super if attribute
23
+
24
+ assign_dirty_attribute_value(attribute_name, value)
25
+ end
26
+
27
+ def assign_dirty_attribute_value(attribute_name, value)
28
+ @table[attribute_name.to_sym] = value
29
+ end
30
+
31
+ def method_missing(method_id, *arguments)
32
+ method_name = method_id[/.*(?==\z)/m]
33
+ return assignment_method_missing(method_name, *arguments) if method_name
34
+
35
+ read_attribute_value(method_id)
36
+ end
37
+
38
+ def read_attribute_value(attribute_name)
39
+ attribute = attribute_set.find(attribute_name)
40
+ return super if attribute
41
+
42
+ read_dirty_attribute_value(attribute_name)
43
+ end
44
+
45
+ def read_dirty_attribute_value(attribute_name)
46
+ @table[attribute_name.to_sym]
47
+ end
48
+
49
+ def respond_to_missing?(_method_name, _include_private = false)
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Attribute
8
+ autoload :AttributeRegistration
9
+ autoload :AttributeSet
10
+ autoload :AttributeAssignment
11
+ autoload :Base
12
+ autoload :Input
13
+ autoload :Output
14
+ autoload :Runtime
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module HasActiveModelErrors
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def self.human_attribute_name(attribute, _options = {})
9
+ attribute.respond_to?(:to_s) ? attribute.to_s.humanize : attribute
10
+ end
11
+
12
+ def self.lookup_ancestors
13
+ [self]
14
+ end
15
+ end
16
+
17
+ included do
18
+ extend ActiveModel::Naming
19
+ extend ClassMethods
20
+
21
+ attr_reader :errors
22
+ end
23
+
24
+ def read_attribute_for_validation(attribute_name)
25
+ send(attribute_name.to_sym)
26
+ end
27
+ end
28
+ end
@@ -2,7 +2,9 @@
2
2
 
3
3
  module ActiveInteractor
4
4
  class Result
5
- extend ActiveModel::Naming
5
+ include HasActiveModelErrors
6
+
7
+ delegate :to_json, to: :to_hash
6
8
 
7
9
  STATUS = {
8
10
  success: 0,
@@ -26,14 +28,6 @@ module ActiveInteractor
26
28
  result
27
29
  end
28
30
 
29
- def human_attribute_name(attribute, _options = {})
30
- attribute.respond_to?(:to_s) ? attribute.to_s.humanize : attribute
31
- end
32
-
33
- def lookup_ancestors
34
- [self]
35
- end
36
-
37
31
  private
38
32
 
39
33
  def parse_errors(errors)
@@ -62,12 +56,21 @@ module ActiveInteractor
62
56
  alias failed? failure?
63
57
 
64
58
  def read_attribute_for_validation(attribute_name)
65
- data[attribute_name]
59
+ data.send(attribute_name.to_sym)
66
60
  end
67
61
 
68
62
  def success?
69
63
  @status == STATUS[:success]
70
64
  end
71
65
  alias successful? success?
66
+
67
+ def to_hash
68
+ {
69
+ success: success?,
70
+ errors: errors.to_hash,
71
+ data: data.to_hash
72
+ }
73
+ end
74
+ alias to_h to_hash
72
75
  end
73
76
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'active_model'
4
4
  require 'active_support'
5
+ require 'active_support/core_ext'
5
6
 
6
7
  require_relative 'active_interactor/errors'
7
8
 
@@ -9,5 +10,7 @@ module ActiveInteractor
9
10
  extend ActiveSupport::Autoload
10
11
 
11
12
  autoload :Base
13
+ autoload :Context
14
+ autoload :HasActiveModelErrors
12
15
  autoload :Result
13
16
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeinteractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.alpha.2.0.0
4
+ version: 2.0.0.alpha.2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-10 00:00:00.000000000 Z
11
+ date: 2023-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -64,7 +64,17 @@ files:
64
64
  - README.md
65
65
  - lib/active_interactor.rb
66
66
  - lib/active_interactor/base.rb
67
+ - lib/active_interactor/context.rb
68
+ - lib/active_interactor/context/attribute.rb
69
+ - lib/active_interactor/context/attribute_assignment.rb
70
+ - lib/active_interactor/context/attribute_registration.rb
71
+ - lib/active_interactor/context/attribute_set.rb
72
+ - lib/active_interactor/context/base.rb
73
+ - lib/active_interactor/context/input.rb
74
+ - lib/active_interactor/context/output.rb
75
+ - lib/active_interactor/context/runtime.rb
67
76
  - lib/active_interactor/errors.rb
77
+ - lib/active_interactor/has_active_model_errors.rb
68
78
  - lib/active_interactor/result.rb
69
79
  - sig/active_interactor.rbs
70
80
  homepage: https://github.com/activeinteractor/activeinteractor
@@ -72,9 +82,9 @@ licenses:
72
82
  - MIT
73
83
  metadata:
74
84
  bug_tracker_uri: https://github.com/activeinteractor/activeinteractor/issues
75
- changelog_uri: https://github.com/activeinteractor/activeinteractor/blob/v2.0.0-alpha.2.0.0/CHANGELOG.md
85
+ changelog_uri: https://github.com/activeinteractor/activeinteractor/blob/v2.0.0-alpha.2.2.0/CHANGELOG.md
76
86
  homepage_uri: https://github.com/activeinteractor/activeinteractor
77
- source_code_uri: https://github.com/activeinteractor/activeinteractor/tree/v2.0.0-alpha.2.0.0
87
+ source_code_uri: https://github.com/activeinteractor/activeinteractor/tree/v2.0.0-alpha.2.2.0
78
88
  wiki_uri: https://github.com/activeinteractor/activeinteractor/wiki
79
89
  rubygems_mfa_required: 'true'
80
90
  post_install_message: