service_operation 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []