mojones 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f0e3e1098a30a3cb361430de99a97ede30f422ed06c4778d20f140bd313c7a3d
4
+ data.tar.gz: addc8a5acded5a911a9fd214a1983aeac71258a3d55e66f34e59f2ad7b18afba
5
+ SHA512:
6
+ metadata.gz: 1705669f6ef6e3084bbe785385ebe343a8217825532255e0b48338eff104f6a2d1ba4e77e7dbd70666e43648030626833ffad5b45cc48e2c039a10f04bfdef91
7
+ data.tar.gz: 9907b26c7a0af8ccb7233a7f8c6830ae89ac4a806f963c21c41d88a652effa073b505769bacb480ab97448b950911a15b5c85612d77623184681fe375cc00bf0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Mojones
6
+ module Generators
7
+ class ServiceGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def create_service_file
11
+ template "service.rb.tt", File.join("app/services", class_path, "#{file_name}.rb")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mojones
4
+ class Base
5
+ include Dry::Monads[:result, :do]
6
+
7
+ module Errors
8
+ class NoHandlerMatched < StandardError
9
+ def initialize(value)
10
+ super
11
+ @value = value
12
+ end
13
+
14
+ def message = "No handler matched for #{@value.inspect}"
15
+ end
16
+
17
+ class ServiceReturnedNonResult < StandardError
18
+ def initialize(service, value)
19
+ super(value)
20
+ @service_name = service.class.inspect.sub(/^Mojones::/, "")
21
+ @value = value
22
+ end
23
+
24
+ def message
25
+ "Service #{@service_name} returned non-Result value (#{@value.inspect} : #{@value.class})"
26
+ end
27
+ end
28
+ end
29
+
30
+ class ReturnedValue
31
+ def initialize(value)
32
+ @value = value
33
+ end
34
+
35
+ def value!
36
+ @value
37
+ end
38
+
39
+ def fmap
40
+ yield @value
41
+ end
42
+
43
+ def success?
44
+ @value.success?
45
+ end
46
+
47
+ def failure?
48
+ @value.failure?
49
+ end
50
+
51
+ def original
52
+ @value.either(:itself.to_proc, :itself.to_proc)
53
+ end
54
+
55
+ def no_matches!
56
+ raise Errors::NoHandlerMatched, @value
57
+ end
58
+ end
59
+
60
+ class InitializerReturnedValue < ReturnedValue
61
+ def success? = true
62
+ def failure? = true
63
+ def original = @value
64
+
65
+ def assert_call_returned_result_monad_or_raised!
66
+ self
67
+ end
68
+ end
69
+
70
+ class CallReturnedValue < ReturnedValue
71
+ def initialize(service, value)
72
+ super(value)
73
+ @service = service
74
+ @value = value
75
+ end
76
+
77
+ def assert_call_returned_result_monad_or_raised!
78
+ return self if @value.is_a?(Dry::Monads::Result)
79
+
80
+ raise Errors::ServiceReturnedNonResult.new(@service, @value)
81
+ end
82
+ end
83
+
84
+ class RaisedError
85
+ def initialize(error)
86
+ @error = error
87
+ end
88
+
89
+ def success? = false
90
+ def failure? = true
91
+
92
+ def value!
93
+ raise @error
94
+ end
95
+
96
+ def original
97
+ @error
98
+ end
99
+
100
+ def fmap
101
+ self
102
+ end
103
+
104
+ def no_matches!
105
+ raise @error
106
+ end
107
+
108
+ def assert_call_returned_result_monad_or_raised!
109
+ self
110
+ end
111
+ end
112
+
113
+ def self.call(*args, **kwargs, &)
114
+ result = (
115
+ begin
116
+ InitializerReturnedValue.new(new(*args, **kwargs))
117
+ rescue StandardError => e
118
+ RaisedError.new(e)
119
+ end
120
+ ).fmap do |service_object|
121
+ CallReturnedValue.new(service_object, service_object.call)
122
+ rescue StandardError => e
123
+ RaisedError.new(e)
124
+ end.assert_call_returned_result_monad_or_raised!
125
+
126
+ return result.value! unless block_given?
127
+
128
+ Matcher.new(result, self, &).result
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Mojones
6
+ class Matcher
7
+ class Match
8
+ attr_reader :matched, :result
9
+
10
+ def initialize(matched:, result:)
11
+ @matched = matched
12
+ @result = result
13
+ end
14
+
15
+ class Success < self; end
16
+ class Failure < self; end
17
+ end
18
+
19
+ def initialize(service_result, service)
20
+ @service = service
21
+ @service_result = service_result
22
+ @matches = []
23
+ yield(self) if block_given?
24
+ end
25
+
26
+ def result
27
+ return @service_result.no_matches! if @matches.empty?
28
+
29
+ @matches.last.result.tap do |chosen|
30
+ if @matches.size > 1
31
+ logger.debug <<~WARNING
32
+ #{service.name} matched multiple handlers; returning last result (#{chosen})
33
+ WARNING
34
+ end
35
+ end
36
+ end
37
+
38
+ def success(*match_values, &block)
39
+ return unless @service_result.success?
40
+
41
+ value = @service_result.original
42
+
43
+ return unless match_values.empty? || match_values.any? { _1 === value } # rubocop:disable Style/CaseEquality
44
+
45
+ @matches << Match::Success.new(
46
+ matched: value,
47
+ result: block.call(value)
48
+ )
49
+ end
50
+
51
+ def failure(*match_errors, &block)
52
+ return unless @service_result.failure?
53
+
54
+ error = @service_result.original
55
+
56
+ return unless match_errors.empty? || match_errors.any? { _1 === error } # rubocop:disable Style/CaseEquality
57
+
58
+ translated = translate_error(error)
59
+
60
+ @matches << Match::Failure.new(
61
+ matched: error,
62
+ result: block.arity == 2 ? block.call(error, translated) : block.call(translated)
63
+ )
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :service
69
+
70
+ def logger
71
+ @logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout)
72
+ end
73
+
74
+ def service_name_lookup
75
+ @service_name_lookup ||= service.name.gsub("::", ".").underscore
76
+ end
77
+
78
+ def translate_error(error)
79
+ return error.to_s unless defined?(I18n)
80
+
81
+ key =
82
+ case error
83
+ when Symbol
84
+ error
85
+ when StandardError
86
+ error.class.name.gsub("::", ".").underscore
87
+ end
88
+
89
+ default =
90
+ case error
91
+ when Symbol then error.to_s.humanize
92
+ when StandardError then error.message
93
+ end
94
+
95
+ I18n.t("#{service_name_lookup}.#{key}", default: default)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mojones
4
+ VERSION = "0.1.0"
5
+ end
data/lib/mojones.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require "dry/monads/do"
5
+
6
+ require_relative "mojones/base"
7
+ require_relative "mojones/matcher"
8
+ require_relative "mojones/version"
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mojones
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Phil Brockwell
8
+ - Habib Alamin
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-monads
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ description: A lightweight framework for Ruby service objects using Dry::Monads with
28
+ enforced result types and matching.
29
+ email:
30
+ - phil@trueflux.agency
31
+ - habib@trueflux.agency
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/generators/mojones/service_generator.rb
37
+ - lib/mojones.rb
38
+ - lib/mojones/base.rb
39
+ - lib/mojones/matcher.rb
40
+ - lib/mojones/version.rb
41
+ homepage: https://trueflux.agency
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ rubygems_mfa_required: 'true'
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.7
61
+ specification_version: 4
62
+ summary: Service objects with monadic result handling
63
+ test_files: []