operational 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ module Operational
2
+ class Form
3
+ include ActiveModel::Model
4
+ include ActiveModel::Attributes
5
+ include ActiveModel::Dirty
6
+
7
+ def self.inherited(subclass)
8
+ super
9
+ subclass.instance_variable_set(:@_operational_sync_check_pending, true)
10
+ end
11
+
12
+ def self.method_added(method_name)
13
+ super
14
+ if method_name == :sync && @_operational_sync_check_pending
15
+ raise MethodCollision,
16
+ "#{self} defines #sync, which collides with Operational::Form#sync. " \
17
+ "Rename your method to #on_sync — it will be called automatically during sync."
18
+ end
19
+ end
20
+
21
+ def self.build(model: nil, model_persisted: nil, state: {}, build_method: :on_build)
22
+ form = new
23
+
24
+ if model
25
+ raise InvalidContractModel unless model.respond_to?(:attributes)
26
+ valid_form_attrs = form.attribute_names
27
+ valid_params = model.attributes.slice(*valid_form_attrs).compact
28
+ form.assign_attributes(valid_params)
29
+ end
30
+
31
+ form.instance_variable_set(:@_operational_model_persisted, (model_persisted.nil? ? model&.persisted? || false : !!model_persisted))
32
+ form.instance_variable_set(form.send(:_operational_state_variable), state.dup.freeze)
33
+ form.send(build_method, state) if form.respond_to?(build_method)
34
+ form.changes_applied if form.respond_to?(:changes_applied)
35
+ form
36
+ end
37
+
38
+ def validate(params = {})
39
+ params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
40
+ params = params.with_indifferent_access if params.respond_to?(:with_indifferent_access)
41
+ valid_params = params.slice(*attribute_names)
42
+ assign_attributes(valid_params)
43
+ valid?
44
+ end
45
+
46
+ def sync(model: nil, state: {}, sync_method: :on_sync)
47
+ if model
48
+ raise InvalidContractModel unless model.respond_to?(:attributes)
49
+ valid_model_attrs = model.attribute_names
50
+ valid_params = attributes.slice(*valid_model_attrs)
51
+ model.assign_attributes(valid_params)
52
+ end
53
+
54
+ send(sync_method, state) if respond_to?(sync_method)
55
+ true
56
+ end
57
+
58
+ def persisted?
59
+ @_operational_model_persisted
60
+ end
61
+
62
+ def other_validators_have_passed?
63
+ errors.blank?
64
+ end
65
+
66
+ protected
67
+
68
+ def _operational_state_variable
69
+ :@state
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,42 @@
1
+ module Operational
2
+ class Operation
3
+ module Contract
4
+ def self.Build(contract:, name: :contract, model_key: :model, model_persisted: nil, build_method: :on_build)
5
+ lambda do |state|
6
+ model = state.key?(model_key) ? state[model_key] : nil
7
+ raise InvalidContractModel if state.key?(model_key) && model.nil?
8
+
9
+ state[name] = contract.build(
10
+ model: model,
11
+ model_persisted: model_persisted,
12
+ state: state,
13
+ build_method: build_method
14
+ )
15
+ true
16
+ end
17
+ end
18
+
19
+ def self.Validate(name: :contract, params_path: nil)
20
+ lambda do |state|
21
+ valid_path = case
22
+ when params_path.nil? then [:params]
23
+ when params_path.is_a?(Symbol) then [:params, params_path]
24
+ when params_path.is_a?(Array) then params_path
25
+ end
26
+
27
+ raw_params = state.dig(*valid_path) || {}
28
+ state[name].validate(raw_params)
29
+ end
30
+ end
31
+
32
+ def self.Sync(name: :contract, model_key: :model, sync_method: :on_sync)
33
+ lambda do |state|
34
+ model = state.key?(model_key) ? state[model_key] : nil
35
+ raise InvalidContractModel if state.key?(model_key) && model.nil?
36
+
37
+ state[name].sync(model: model, state: state, sync_method: sync_method)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module Operational
2
+ class Operation
3
+ module Nested
4
+ def self.Operation(operation:)
5
+ lambda do |state|
6
+ nested_result = operation.call(state)
7
+ state.merge!(nested_result.state)
8
+ nested_result.succeeded?
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,66 @@
1
+ module Operational
2
+ class Operation
3
+ def self.step(action)
4
+ add_step(:step, action)
5
+ end
6
+
7
+ def self.pass(action)
8
+ add_step(:pass, action)
9
+ end
10
+
11
+ def self.fail(action)
12
+ add_step(:fail, action)
13
+ end
14
+
15
+ def self.call(state={})
16
+ instance = self.new
17
+ instance.instance_variable_set(:@_operational_state, state)
18
+ instance.instance_variable_set(:@_operational_path, [])
19
+ instance.instance_variable_set(:@_operational_succeeded, true)
20
+
21
+ failure_circuit = false
22
+
23
+ instance.send(:_operational_steps).each do |railway_step|
24
+ type, action = railway_step
25
+
26
+ next if !failure_circuit && type == :fail
27
+ next if failure_circuit && type != :fail
28
+
29
+ result = if action.is_a?(Symbol)
30
+ instance.send(action, instance.send(:_operational_state))
31
+ elsif action.respond_to?(:call)
32
+ action.call(instance.send(:_operational_state))
33
+ else
34
+ raise UnknownStepType
35
+ end
36
+
37
+ result = true if type == :pass
38
+ instance.instance_variable_set(:@_operational_succeeded, result ? true : false)
39
+ instance.instance_variable_get(:@_operational_path) << (result ? true : false)
40
+
41
+ failure_circuit = !instance.instance_variable_get(:@_operational_succeeded) && type != :pass
42
+ end
43
+
44
+ return Result.new(
45
+ succeeded: (instance.instance_variable_get(:@_operational_succeeded) ? true : false),
46
+ state: instance.instance_variable_get(:@_operational_state),
47
+ operation: instance)
48
+ end
49
+
50
+ private
51
+
52
+ def self.add_step(type, action)
53
+ railway = class_variable_defined?(:@@_operational_steps) ? class_variable_get(:@@_operational_steps) : []
54
+ railway << [type, action]
55
+ class_variable_set(:@@_operational_steps, railway)
56
+ end
57
+
58
+ def _operational_steps
59
+ self.class.class_variable_defined?(:@@_operational_steps) ? self.class.class_variable_get(:@@_operational_steps) : []
60
+ end
61
+
62
+ def _operational_state
63
+ @_operational_state
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ module Operational
2
+ class Result
3
+ def initialize(succeeded:, state:, operation:)
4
+ @succeeded = succeeded
5
+ @state = state.dup.freeze
6
+ @operation = operation
7
+ end
8
+
9
+ def succeeded?
10
+ @succeeded
11
+ end
12
+
13
+ def failed?
14
+ !@succeeded
15
+ end
16
+
17
+ def operation
18
+ @operation
19
+ end
20
+
21
+ def state
22
+ @state
23
+ end
24
+
25
+ def [](i)
26
+ @state[i]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Operational
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_model'
2
+
3
+ require "operational/version"
4
+ require "operational/error"
5
+ require "operational/operation"
6
+ require "operational/result"
7
+ require "operational/form"
8
+ require "operational/controller"
9
+
10
+ require "operational/operation/contract"
11
+ require "operational/operation/nested"
12
+
13
+ module Operational
14
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: operational
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bryan Rite
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activemodel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 7.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: bundler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Operational wraps your business logic into operations — small classes
69
+ with a railway of steps that succeed or fail. Pair them with form objects and contracts
70
+ to decouple your UI and APIs from your models.
71
+ email:
72
+ - bryan@bryanrite.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - AI_README.md
78
+ - CHANGELOG.md
79
+ - LICENSE
80
+ - README.md
81
+ - lib/operational.rb
82
+ - lib/operational/controller.rb
83
+ - lib/operational/error.rb
84
+ - lib/operational/form.rb
85
+ - lib/operational/operation.rb
86
+ - lib/operational/operation/contract.rb
87
+ - lib/operational/operation/nested.rb
88
+ - lib/operational/result.rb
89
+ - lib/operational/version.rb
90
+ homepage: https://github.com/bryanrite/operational
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ changelog_uri: https://github.com/bryanrite/operational/blob/master/CHANGELOG.md
95
+ source_code_uri: https://github.com/bryanrite/operational
96
+ documentation_uri: https://github.com/bryanrite/operational#readme
97
+ bug_tracker_uri: https://github.com/bryanrite/operational/issues
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '3.0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 3.6.9
113
+ specification_version: 4
114
+ summary: Lightweight, railway-oriented operation and form objects for business logic
115
+ test_files: []