rider-kick 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-09-01
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 kotarominami
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # RiderKick
2
+ This gem provides helper interfaces and classes to assist in the construction of application with
3
+ Clean Architecture, as described in [Robert Martin's seminal book](https://www.amazon.com/gp/product/0134494164).
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'rider-kick'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle install
17
+ $ bundle binstubs rider-kick
18
+
19
+ ## Philosophy
20
+
21
+ The intention of this gem is to help you build applications that are built from the use case down,
22
+ and decisions about I/O can be deferred until the last possible moment.
23
+
24
+ ## Clean Architecture
25
+ This structure provides helper interfaces and classes to assist in the construction of application with Clean Architecture, as described in Robert Martin's seminal book.
26
+
27
+ ```
28
+ - app
29
+ - services
30
+ - api
31
+ - ...
32
+ - domains
33
+ - entities (Contract Response)
34
+ - builder
35
+ - repositories (Business logic)
36
+ - use_cases (Just Usecase)
37
+ - utils (Class Reusable)
38
+ ```
39
+ ## Screaming architecture - use cases as an organisational principle
40
+ Uncle Bob suggests that your source code organisation should allow developers to easily find a listing of all use cases your application provides. Here's an example of how this might look in a this application.
41
+ ```
42
+ - app
43
+ - domains
44
+ - core
45
+ ...
46
+ - usecase
47
+ - retail_customer_opens_bank_account.rb
48
+ - retail_customer_makes_deposit.rb
49
+ - ...
50
+ ```
51
+ Note that the use case name contains:
52
+
53
+ - the user role
54
+ - the action
55
+ - the (sometimes implied) subject
56
+ ```ruby
57
+ [user role][action][subject].rb
58
+ # retail_customer_opens_bank_account.rb
59
+ # admin_fetch_info.rb [specific usecase]
60
+ # fetch_info.rb [generic usecase] every role can access it
61
+ ```
62
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/lib/rider-kick.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rider_kick/entities/failure_details'
4
+ require 'rider_kick/builders/abstract_active_record_entity_builder'
5
+ require 'rider_kick/matchers/use_case_result'
6
+ require 'rider_kick/use_cases/abstract_use_case'
7
+ require 'rider_kick/use_cases/contract'
8
+
9
+ require 'rider_kick/types'
10
+ require 'rider_kick/version'
11
+
12
+ module RiderKick
13
+ class Error < StandardError; end
14
+ end
@@ -0,0 +1,124 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module RiderKick
5
+ module Builders
6
+ # Helps to take an instance of an AR model and wrap it up in the given Entity
7
+ # Any columns from the AR model that do not directly map to an attribute on the Entity
8
+ # can be specified by overriding #attributes_for_entity.
9
+ class AbstractActiveRecordEntityBuilder
10
+ # @param [Class] A Dry::Struct based entity that this builder will construct instances of
11
+ def self.acts_as_builder_for_entity(entity_class)
12
+ @has_many_builders = []
13
+ @belongs_to_builders = []
14
+
15
+ define_singleton_method :has_many_builders do
16
+ @has_many_builders
17
+ end
18
+
19
+ define_singleton_method :belongs_to_builders do
20
+ @belongs_to_builders
21
+ end
22
+
23
+ define_method :entity_class do
24
+ entity_class
25
+ end
26
+
27
+ private :entity_class
28
+ end
29
+
30
+ def self.has_many(relation_name, use:)
31
+ @has_many_builders << [relation_name, use]
32
+ end
33
+
34
+ def self.belongs_to(relation_name, use:)
35
+ @belongs_to_builders << [relation_name, use]
36
+ end
37
+
38
+ # @param [ActiveRecord::Base] An ActiveRecord model to map to the entity
39
+ def initialize(params)
40
+ @params = params
41
+ end
42
+
43
+ def build
44
+ entity_class.new(all_attributes_for_entity)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :params
50
+
51
+ def entity_attribute_names
52
+ @entity_attributes ||= begin
53
+ if entity_class.respond_to?(:schema) # Dry::Struct
54
+ schema_keys = entity_class.schema.keys
55
+ elsif entity_class.respond_to?(:decorator) # T::Struct
56
+ schema_keys = entity_class.decorator.props.keys
57
+ else
58
+ raise 'Cannot determine schema format'
59
+ end
60
+ first_key = schema_keys.first
61
+ if first_key.is_a?(Symbol)
62
+ schema_keys
63
+ elsif first_key.respond_to?(:name)
64
+ schema_keys.map(&:name)
65
+ else
66
+ raise 'Cannot determine schema format'
67
+ end
68
+ end
69
+ end
70
+
71
+ def params_attributes
72
+ @params_attributes ||= @params.attributes
73
+ end
74
+
75
+ def symbolized_params_attributes
76
+ @symbolized_params_attributes ||= Hash[
77
+ params_attributes.map { |(key, value)| [key.to_sym, value] }
78
+ ]
79
+ end
80
+
81
+ def ar_attributes_for_entity
82
+ symbolized_params_attributes.slice(*entity_attribute_names)
83
+ end
84
+
85
+ def attributes_for_belongs_to_relations
86
+ self.class.belongs_to_builders.map do |belongs_to_builder_config|
87
+ relation_name, builder_class = belongs_to_builder_config
88
+ relation = @params.public_send(relation_name)
89
+
90
+ [
91
+ relation_name,
92
+ relation ? builder_class.new(relation).build : nil
93
+ ]
94
+ end.to_h
95
+ end
96
+
97
+ def attributes_for_has_many_relations
98
+ self.class.has_many_builders.map do |has_many_builder_config|
99
+ relation_name, builder_class = has_many_builder_config
100
+ relations = @params.public_send(relation_name)
101
+ built_relations = relations.map do |relation|
102
+ builder_class.new(relation).build
103
+ end
104
+
105
+ [
106
+ relation_name,
107
+ built_relations
108
+ ]
109
+ end.to_h
110
+ end
111
+
112
+ def attributes_for_entity
113
+ {}
114
+ end
115
+
116
+ def all_attributes_for_entity
117
+ ar_attributes_for_entity
118
+ .merge(attributes_for_belongs_to_relations)
119
+ .merge(attributes_for_has_many_relations)
120
+ .merge(attributes_for_entity)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,30 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'rider_kick/types'
5
+ require 'dry/struct'
6
+
7
+ module RiderKick
8
+ module Entities
9
+ class FailureDetails < Dry::Struct
10
+ failure_types = Types::Strict::String.enum(
11
+ 'error',
12
+ 'expectation_failed',
13
+ 'not_found',
14
+ 'unauthorized',
15
+ 'unprocessable_entity'
16
+ )
17
+ attribute :type, failure_types
18
+ attribute :message, Types::Strict::String
19
+ attribute :other_properties, Types::Strict::Hash.default({}.freeze)
20
+
21
+ def self.from_array(array)
22
+ new(message: 'failure 1, failure 2', other_properties: {}, type: 'error')
23
+ end
24
+
25
+ def self.from_string(string)
26
+ new(message: string, other_properties: {}, type: 'error')
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'rider_kick/entities/failure_details'
5
+ require 'dry-matcher'
6
+ require 'dry/monads/all'
7
+
8
+ module RiderKick
9
+ module Matchers
10
+ class UseCaseResult
11
+ def self.call(result, &block)
12
+ new.matcher.call(result, &block)
13
+ end
14
+
15
+ def matcher
16
+ Dry::Matcher.new(success: success_case, failure: failure_case)
17
+ end
18
+
19
+ private
20
+
21
+ def success_case
22
+ Dry::Matcher::Case.new(
23
+ match: ->(value) { value.is_a?(Dry::Monads::Success) },
24
+ resolve: ->(value) { value.value! }
25
+ )
26
+ end
27
+
28
+ def failure_case
29
+ Dry::Matcher::Case.new(
30
+ match: ->(value) { value.is_a?(Dry::Monads::Failure) },
31
+ resolve: ->(value) { resolve_failure_value(value) }
32
+ )
33
+ end
34
+
35
+ def resolve_failure_value(value)
36
+ failure = value.failure
37
+ case failure
38
+ when Array
39
+ Entities::FailureDetails.from_array(failure)
40
+ when String
41
+ Entities::FailureDetails.from_string(failure)
42
+ when Entities::FailureDetails
43
+ failure
44
+ else
45
+ type_list = [Array, String, Entities::FailureDetails].map(&:to_s).join(' or ')
46
+ raise ArgumentError, "Unexpected failure value - must be #{type_list}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rider_kick/matchers/use_case_result'
4
+
5
+ RSpec.describe RiderKick::Matchers::UseCaseResult do
6
+ describe '.call' do
7
+ subject(:call) do
8
+ described_class.call(result) do |matcher|
9
+ matcher.success(&:to_s)
10
+ matcher.failure { |failure_details| failure_details }
11
+ end
12
+ end
13
+
14
+ context do
15
+ let(:result) { Dry::Monads::Success('success!') }
16
+
17
+ it { is_expected.to eq 'success!' }
18
+ end
19
+
20
+ context do
21
+ let(:result) { Dry::Monads::Failure('failure!') }
22
+
23
+ specify do
24
+ expect(call).to eq RiderKick::Entities::FailureDetails.new(
25
+ message: 'failure!',
26
+ other_properties: {},
27
+ type: 'error'
28
+ )
29
+ end
30
+ end
31
+
32
+ context do
33
+ let(:result) { Dry::Monads::Failure(['failure 1', 'failure 2']) }
34
+
35
+ specify do
36
+ expect(call).to eq RiderKick::Entities::FailureDetails.new(
37
+ message: 'failure 1, failure 2',
38
+ other_properties: {},
39
+ type: 'error'
40
+ )
41
+ end
42
+ end
43
+
44
+ context do
45
+ let(:failure_details) do
46
+ RiderKick::Entities::FailureDetails.new(
47
+ message: 'failure!',
48
+ other_properties: { a: :b },
49
+ type: 'unauthorized'
50
+ )
51
+ end
52
+ let(:result) { Dry::Monads::Failure(failure_details) }
53
+
54
+ it { is_expected.to eq failure_details }
55
+ end
56
+
57
+ context do
58
+ let(:result) { Dry::Monads::Failure(:no) }
59
+
60
+ specify { expect { call }.to raise_error ArgumentError }
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,10 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ require 'dry-types'
5
+
6
+ module Types
7
+ include Dry.Types()
8
+ # File = Types.Instance(::File) | Types.Instance(ActionDispatch::Http::UploadedFile)
9
+ # public_constant :File
10
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'dry/monads/result'
5
+ require 'dry/validation'
6
+ require 'dry/matcher/result_matcher'
7
+ require 'rider_kick/use_cases/contract'
8
+
9
+ module RiderKick
10
+ module UseCases
11
+ class AbstractUseCase
12
+ include Dry::Monads[:result]
13
+
14
+ def self.contract(base_contract = Contract, &proc)
15
+ @contract ||= Class.new(base_contract, &proc)
16
+ end
17
+
18
+ def self.contract!(args)
19
+ context = args.fetch(:context, {})
20
+ @results = @contract.new(context).call(args)
21
+ end
22
+
23
+ def initialize(contract)
24
+ @contract = contract
25
+ end
26
+
27
+ def build_parameter!
28
+ if @contract.success?
29
+ Success(Hashie::Mash.new(@contract.to_h))
30
+ else
31
+ Failure(@contract.errors.to_h)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module RiderKick
5
+ module UseCases
6
+ class Contract < Dry::Validation::Contract
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RiderKick
4
+ VERSION = '0.0.1'
5
+ public_constant :VERSION
6
+ end
@@ -0,0 +1,4 @@
1
+ module RiderKick
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rider-kick
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kotaro Minami
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-matcher
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-monads
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.6.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-struct
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.6.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.6.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: dry-transaction
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.16.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.16.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: dry-types
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.7.2
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.7.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: dry-validation
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.10.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.10.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.5.18
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.5.18
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 13.2.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 13.2.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 3.13.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 3.13.0
139
+ description: An attempt at building a reusable Clean Architecture framework for Ruby.
140
+ email:
141
+ - kotaroisme@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".rspec"
147
+ - ".rubocop.yml"
148
+ - CHANGELOG.md
149
+ - LICENSE.txt
150
+ - README.md
151
+ - Rakefile
152
+ - lib/rider-kick.rb
153
+ - lib/rider_kick/builders/abstract_active_record_entity_builder.rb
154
+ - lib/rider_kick/entities/failure_details.rb
155
+ - lib/rider_kick/matchers/use_case_result.rb
156
+ - lib/rider_kick/matchers/use_case_result_spec.rb
157
+ - lib/rider_kick/types.rb
158
+ - lib/rider_kick/use_cases/abstract_use_case.rb
159
+ - lib/rider_kick/use_cases/contract.rb
160
+ - lib/rider_kick/version.rb
161
+ - sig/rider_kick.rbs
162
+ homepage: https://github.com/kotaroisme/rider-kick
163
+ licenses:
164
+ - MIT
165
+ metadata: {}
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: 3.0.0
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 3.5.18
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: Clean Architecture Framework.
185
+ test_files: []