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.
- checksums.yaml +7 -0
- data/lib/service_operation.rb +17 -0
- data/lib/service_operation/base.rb +73 -0
- data/lib/service_operation/context.rb +122 -0
- data/lib/service_operation/delay.rb +22 -0
- data/lib/service_operation/error_handling.rb +94 -0
- data/lib/service_operation/errors.rb +46 -0
- data/lib/service_operation/failure.rb +13 -0
- data/lib/service_operation/hooks.rb +102 -0
- data/lib/service_operation/params.rb +129 -0
- data/lib/service_operation/params/attribute.rb +164 -0
- data/lib/service_operation/params/dsl.rb +79 -0
- data/lib/service_operation/params/types.rb +106 -0
- data/lib/service_operation/rack_mountable.rb +74 -0
- data/lib/service_operation/service_notification.rb +92 -0
- data/lib/service_operation/spec/spec_helper.rb +44 -0
- data/lib/service_operation/spec/support/action_contexts.rb +56 -0
- data/lib/service_operation/spec/support/operation_contexts.rb +56 -0
- data/lib/service_operation/validations.rb +29 -0
- data/lib/service_operation/version.rb +5 -0
- metadata +103 -0
@@ -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
|
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: []
|