service_operation 1.0.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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceOperation
4
+ # mount in Rails routes.rb with mount(ServiceName => '/path')
5
+ # @todo remove ActionDispatch dependency
6
+ class RackMountable
7
+ include ServiceOperation::Base
8
+
9
+ IS_RACK_REQUEST_REGEXP = /SERVER_NAME|rack\./.freeze
10
+
11
+ allow_remote!
12
+
13
+ params do
14
+ request optional: true
15
+ end
16
+
17
+ returns do
18
+ body Any(String, Hash)
19
+ headers Hash
20
+ status String
21
+ end
22
+
23
+ after do
24
+ context.body = context.body || context.message || context.error || ''
25
+ context.headers ||= {}
26
+ context.status = (context.status || 200).to_s
27
+ end
28
+
29
+ #
30
+ # Class Methods
31
+ #
32
+
33
+ class << self
34
+ alias base_call call
35
+
36
+ # Wrap the call method with a check to see if its a rack request
37
+ # If so merge in request.params and return a rack response
38
+ def call(*args)
39
+ if request = rack_request(*args)
40
+ rack_response base_call(request: request)
41
+ else
42
+ base_call(*args)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ #
49
+ # Request
50
+ #
51
+
52
+ def rack_request(*args)
53
+ return unless args.first.is_a?(Hash) && args.first.keys.grep(IS_RACK_REQUEST_REGEXP).any?
54
+
55
+ ActionDispatch::Request.new(args.first)
56
+ end
57
+
58
+ #
59
+ # Response
60
+ #
61
+
62
+ def rack_response(context)
63
+ [context.status, context.headers, rack_body(context)]
64
+ end
65
+
66
+ def rack_body(context)
67
+ body = context.body
68
+ body = contextbody.to_json if body.is_a?(Hash)
69
+ body = [body] unless body.is_a?(Array)
70
+ body
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceOperation
4
+ # Extensions to ServiceOperation::Base for sending notifications via the ServiceNotifications gem.
5
+ module ServiceNotification
6
+ VALID_SERVICE_NOTIFICATION_STATUSES = [:created, :ok].freeze
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ # ClassMethods
13
+ module ClassMethods
14
+ # in a Class Method to avoid using allow_any_instance_of in tests
15
+ def service_notifications_post(payload)
16
+ uri = URI payload.delete(:url)
17
+
18
+ request = Net::HTTP::Post.new(uri)
19
+ request.body = payload.to_json
20
+
21
+ response = http(uri).request(request)
22
+
23
+ # status, body
24
+ [
25
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.invert[response.code],
26
+ JSON.parse(response.body, symbolize_names: true)
27
+ ]
28
+ end
29
+
30
+ private
31
+
32
+ def http(uri)
33
+ http = Net::HTTP.new(uri.host, uri.port)
34
+ http.use_ssl = true
35
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
36
+ http
37
+ end
38
+ end
39
+
40
+ #
41
+ # Instances
42
+ #
43
+
44
+ # modify to suit in sub class
45
+ def call
46
+ context.response ||= notify
47
+ end
48
+
49
+ private
50
+
51
+ # @return [Hash] response from ServiceNotifications
52
+ def notify(options = nil)
53
+ options ||= payload
54
+ options = service_notifications_defaults.merge(options)
55
+
56
+ status, body = self.class.service_notifications_post options
57
+
58
+ unless VALID_SERVICE_NOTIFICATION_STATUSES.include?(status)
59
+ context.service_notifications_response = body
60
+ fail!(status)
61
+ end
62
+
63
+ body
64
+ end
65
+
66
+ # A standard ServiceNotification payload for {#notify} to use
67
+ # @abstract
68
+ # @return [Hash]
69
+ def payload
70
+ raise 'define in subclass'
71
+ end
72
+
73
+ def service_notifications_defaults
74
+ {
75
+ url: service_notifications_url,
76
+ api_key: service_notifications_api_key,
77
+ instant: true, notification: 'inline'
78
+ }
79
+ end
80
+
81
+ # define in sub class or pass in payload
82
+ # @abstract
83
+ # @return [String]
84
+ def service_notifications_api_key
85
+ raise 'define in subclass'
86
+ end
87
+
88
+ def service_notifications_url
89
+ ENV['SERVICE_NOTIFICATIONS_URL']
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'service_operation/spec/support/action_contexts'
7
+ require 'service_operation/spec/support/operation_contexts'
8
+
9
+ # Stub ServiceNotifications
10
+ module StubServiceNotifications
11
+ # @example
12
+ # stub_service_notifications(YourOperation, email: user.email)
13
+ # @example
14
+ # stub_service_notifications(objects: a_hash_including(plain: a_string_matching(/Welcome/)))
15
+ def stub_service_notifications(*args)
16
+ options, klass = args.reverse
17
+ klass ||= described_class
18
+
19
+ stubber = allow(klass).to receive(:service_notifications_post)
20
+ stubber = stubber.with stub_service_notifications_options(options) if options
21
+ stubber.and_return [:created, { test: true }]
22
+ end
23
+
24
+ private
25
+
26
+ def stub_service_notifications_options(options)
27
+ recipient = options.delete(:recipient) ||
28
+ { email: options.delete(:email), uid: options.delete(:uid) }.compact
29
+
30
+ options[:recipients] ||= [a_hash_including(recipient)] if recipient[:email]
31
+
32
+ options = {
33
+ url: ENV['SERVICE_NOTIFICATIONS_URL'], api_key: a_string_matching(/^(.*){32}$/)
34
+ }.merge options
35
+
36
+ a_hash_including options
37
+ end
38
+ end
39
+
40
+ RSpec.configure do |config|
41
+ config.include StubServiceNotifications
42
+ config.include_context 'action', type: :action
43
+ config.include_context 'operation', type: :operation
44
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_context 'action', type: :action do
4
+ let(:input) { {} }
5
+ let(:context) { described_class.call(input) }
6
+ let(:contexts) { [] }
7
+ let(:errors) { context.errors || [] }
8
+
9
+ private
10
+
11
+ def call_action
12
+ contexts << described_class.call(input)
13
+ contexts.last
14
+ end
15
+
16
+ def call_action!
17
+ context = call_action
18
+ expect_success(context)
19
+ context
20
+ end
21
+
22
+ def last_context
23
+ contexts.last || call_service
24
+ end
25
+
26
+ def expect_action_to
27
+ expect { context }.to yield
28
+ end
29
+
30
+ def expect_success(obj = context)
31
+ expect(obj.error).to eq(nil)
32
+ expect(obj.errors).to eq({}) if obj.errors
33
+ expect(obj).to be_success
34
+ end
35
+
36
+ def expect_failure(obj = context)
37
+ raise 'failed without error' if obj.failure? && obj.error.nil? && obj.errors.blank?
38
+ expect(obj).not_to be_success
39
+ end
40
+
41
+ # expect_error('blah')
42
+ # expect_error(/blah/)
43
+ # expect_error(/blah/).not_to eq('blah')
44
+ # expect_error.to include("blah")
45
+ def expect_error(error = nil)
46
+ expect_failure
47
+
48
+ expectation = expect(context.error)
49
+
50
+ if error
51
+ expectation.to eq(error)
52
+ else
53
+ expectation
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_context 'operation', type: :operation do
4
+ let(:input) { {} }
5
+ let(:output) { described_class.call(input) }
6
+ let(:outputs) { [] }
7
+ let(:errors) { output.errors || [] }
8
+
9
+ private
10
+
11
+ def call_operation
12
+ outputs << described_class.call(input)
13
+ outputs.last
14
+ end
15
+
16
+ def call_operation!
17
+ output = call_operation
18
+ expect_success(output)
19
+ output
20
+ end
21
+
22
+ def last_operation
23
+ operations.last || call_operation
24
+ end
25
+
26
+ def expect_operation_to
27
+ expect { output }.to yield
28
+ end
29
+
30
+ def expect_success(obj = output)
31
+ expect(obj.error).to eq(nil)
32
+ expect(obj.errors).to eq({}) if obj.errors
33
+ expect(obj).to be_success
34
+ end
35
+
36
+ def expect_failure(obj = output)
37
+ raise 'failed without error' if obj.failure? && obj.error.nil? && obj.errors.blank?
38
+ expect(obj).not_to be_success
39
+ end
40
+
41
+ # expect_error('blah')
42
+ # expect_error(/blah/)
43
+ # expect_error(/blah/).not_to eq('blah')
44
+ # expect_error.to include("blah")
45
+ def expect_error(error = nil)
46
+ expect_failure
47
+
48
+ expectation = expect(output.error)
49
+
50
+ if error
51
+ expectation.to eq(error)
52
+ else
53
+ expectation
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceOperation
4
+ # Validations
5
+ module Validations
6
+ def self.included(base)
7
+ base.class_eval do
8
+ extend ClassMethods
9
+ end
10
+ end
11
+
12
+ # Class Methods
13
+ module ClassMethods
14
+ end
15
+
16
+ def require_at_least_one_of(*args)
17
+ # mothballed:
18
+ # @option args.last [Boolean] :context whether to check context directly rather than send()
19
+ # use if you want to prevent an auto generated value.
20
+ # options = args.last.is_a?(Hash) ? args.pop : {}
21
+ # base = options[:context] ? context : self
22
+
23
+ base = self
24
+ return if args.any? { |k| base.send(k) }
25
+
26
+ errors.add(:base, "One of #{args.map(&:to_s).join(', ')} required.")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceOperation
4
+ VERSION = '1.0.0'
5
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: service_operation
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Zach Powell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry-byebug
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.12.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.12.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.7.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 3.7.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: simplecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.17.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.17.0
55
+ description:
56
+ email: zach@babelian.net
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/service_operation.rb
62
+ - lib/service_operation/base.rb
63
+ - lib/service_operation/context.rb
64
+ - lib/service_operation/delay.rb
65
+ - lib/service_operation/error_handling.rb
66
+ - lib/service_operation/errors.rb
67
+ - lib/service_operation/failure.rb
68
+ - lib/service_operation/hooks.rb
69
+ - lib/service_operation/params.rb
70
+ - lib/service_operation/params/attribute.rb
71
+ - lib/service_operation/params/dsl.rb
72
+ - lib/service_operation/params/types.rb
73
+ - lib/service_operation/rack_mountable.rb
74
+ - lib/service_operation/service_notification.rb
75
+ - lib/service_operation/spec/spec_helper.rb
76
+ - lib/service_operation/spec/support/action_contexts.rb
77
+ - lib/service_operation/spec/support/operation_contexts.rb
78
+ - lib/service_operation/validations.rb
79
+ - lib/service_operation/version.rb
80
+ homepage: https://github.com/babelian/service_operation
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 2.5.3
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.0.1
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Service Operations based on Interactor, Interactor Contracts, and ValueSemantics
103
+ test_files: []