yabi 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: 1cfbcf34e0863ca1a9587e29490380dd2d29c684a798a93c7472b17ae23d3f65
4
+ data.tar.gz: eaa224344ee120279edec5a06c8c79ebf3ef8c2ee697199b7b03b36f14413bb1
5
+ SHA512:
6
+ metadata.gz: 2f131d23e479a08122ef412c5187efa93878db7f5d78300e7239fdff3a52ac4637523438d419729f5ea8cf56ad0691e8250c1bbb9223f78fe50ebfe53d0950c9
7
+ data.tar.gz: 836db8ee15f1839b6d4644a9d74fa4a0896b5e34257153344abcc2718c87a4d64fd095a3b2d339f7e7c4ac383b5a4da985cc7be133daa85b5f5ea5c5821238fb
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-02-05
4
+ - Extracted BaseInteractor (formerly BaseService) into the YABI gem.
5
+ - Added BaseContract wrapper and global shims (BaseInteractor/BaseService/BaseContract).
6
+ - Switched contract attribute capture to `dry_initializer.attributes` with instance-variable fallback.
7
+ - Removed bundled HTTP interactor (to drop Faraday dependency); README now includes an optional copy/paste example.
8
+ - BaseContract now defaults to the I18n message backend and loads dry-validation translations.
9
+ - Removed ActiveSupport dependency; internal helpers now handle symbolization and class-level contract accessors, reducing runtime deps.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fortune Teller Team
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # YABI — Yet Another Base Interactor
2
+
3
+ A tiny base class for service objects/interactors built on `dry-monads`, `dry-initializer`, and `dry-validation`, plus a minimal base contract.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'yabi', git: 'https://example.com/yabi.git'
11
+ ```
12
+
13
+ Require the gem (Rails autoloading works too):
14
+
15
+ ```ruby
16
+ require 'yabi'
17
+ ```
18
+
19
+ By default the gem exposes `Yabi::BaseInteractor` plus shims `BaseInteractor` and `BaseService` (only defined if missing) to ease migration of existing code.
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ class Users::Questions::Create < BaseInteractor
25
+ option :question_text
26
+
27
+ class ValidationContract < BaseContract
28
+ params { required(:question_text).filled(:string) }
29
+ end
30
+
31
+ def call
32
+ Success(question_text.upcase)
33
+ end
34
+ end
35
+
36
+ Users::Questions::Create.call(question_text: 'hello')
37
+ # => Success(\"HELLO\")
38
+ ```
39
+
40
+ ### Contracts
41
+
42
+ Pass a contract via the `contract:` keyword when calling, or define an inner `ValidationContract` constant. Validation runs before `call`; failures return `Failure(errors)`.
43
+
44
+ YABI ships with `Yabi::BaseContract` (also available as `BaseContract`) which is a light wrapper around `Dry::Validation::Contract`. It uses the `:i18n` messages backend by default and loads the built‑in dry-validation translations. Customize it in your app if you want different load paths or locales:
45
+
46
+ ```ruby
47
+ class ApplicationContract < Yabi::BaseContract
48
+ config.messages.default_locale = :es
49
+ config.messages.load_paths << Rails.root.join('config/locales/es.yml')
50
+ end
51
+ ```
52
+
53
+ ### Error messages & I18n
54
+
55
+ The gem ships an English locale file at `config/locales/en.yml` and loads it automatically. Errors raised inside YABI (e.g., for unexpected positional arguments) are translated via `I18n.t('yabi.errors.*')`. Override translations by adding your own locale files earlier in `I18n.load_path` or by setting `I18n.locale`.
56
+
57
+ ### Helpers
58
+
59
+ - `safe_call { ... }` wraps a block into `Try` and returns a Result.
60
+ - `in_transaction { ... }` delegates to `ActiveRecord::Base.transaction` when ActiveRecord is available.
61
+
62
+ ### Attribute capture
63
+
64
+ `Yabi::BaseInteractor` uses `dry_initializer.attributes(self)` to collect declared options/params for validation instead of scraping every instance variable. This avoids leaking internal state while keeping compatibility with dry-initializer defaults. A fallback to the previous instance-variable scan remains for non-dry objects.
65
+
66
+ ## Example: HTTP request interactor (not included in the gem)
67
+
68
+ The gem no longer ships an HTTP interactor to avoid adding Faraday as a runtime
69
+ dependency. If you want one, you can copy/paste or adapt the example below.
70
+ Remember to add Faraday (or your preferred adapter) to your own Gemfile.
71
+
72
+ ```ruby
73
+ require 'faraday'
74
+ require 'uri'
75
+
76
+ class Integrations::Http::Requests::Make < BaseInteractor
77
+ option :http_method
78
+ option :url
79
+ option :request_params, default: -> { {} }
80
+ option :request_headers, default: -> { {} }
81
+ option :options, default: -> { {} }
82
+
83
+ class ValidationContract < BaseContract
84
+ params do
85
+ required(:http_method).filled(:string)
86
+ required(:url).filled(:string)
87
+ optional(:request_params)
88
+ optional(:request_headers)
89
+ optional(:options)
90
+ end
91
+
92
+ rule(:http_method) do
93
+ key.failure('is not a supported HTTP method') unless %w[get post put patch delete].include?(value.to_s.downcase)
94
+ end
95
+
96
+ rule(:url) do
97
+ key.failure('is not a valid URL') unless value.to_s.match?(URI::DEFAULT_PARSER.make_regexp)
98
+ end
99
+ end
100
+
101
+ def call
102
+ response = yield safe_call { faraday_client.public_send(http_method.to_sym, '', prepared_request_params) }
103
+ Success(response)
104
+ end
105
+
106
+ private
107
+
108
+ def faraday_client
109
+ Faraday.new(url:, headers: request_headers, **options) do |faraday|
110
+ faraday.request :url_encoded
111
+ faraday.adapter Faraday.default_adapter
112
+ end
113
+ end
114
+
115
+ def prepared_request_params
116
+ request_params.respond_to?(:to_h) ? request_params.to_h : request_params
117
+ end
118
+ end
119
+
120
+ Integrations::Http::Requests::Make.call(
121
+ http_method: 'get',
122
+ url: 'https://jsonplaceholder.typicode.com/posts/1'
123
+ ).either(
124
+ ->(response) { puts \"Success: #{response.status}\" },
125
+ ->(error) { puts \"Error: #{error}\" }
126
+ )
127
+ ```
128
+ This interactor is only an example; include it in your own app if desired and add
129
+ Faraday (or another HTTP client) to your dependencies.
130
+
131
+ ## Development
132
+
133
+ ```sh
134
+ bundle install
135
+ bundle exec rspec
136
+ ```
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,4 @@
1
+ en:
2
+ yabi:
3
+ errors:
4
+ unexpected_positional_arguments: "unexpected positional arguments: %{args}"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation'
4
+ require 'i18n'
5
+
6
+ module Yabi
7
+ # Opinionated base contract to share common Dry::Validation config.
8
+ # Adjust load paths / message backends in your host app initializer if needed.
9
+ class BaseContract < Dry::Validation::Contract
10
+ # example defaults; host app can override via subclassing
11
+ config.messages.backend = :i18n
12
+ config.messages.default_locale = :en
13
+ if defined?(Gem) && Gem.loaded_specs['dry-validation']
14
+ default_messages_path = File.join(Gem.loaded_specs['dry-validation'].full_gem_path, 'config', 'errors.yml')
15
+ config.messages.load_paths << default_messages_path if File.exist?(default_messages_path)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Provide a global constant for easy migration.
21
+ BaseContract = Yabi::BaseContract unless defined?(BaseContract)
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/all'
4
+ require 'dry/validation'
5
+ require 'dry/initializer'
6
+ require 'dry/matcher/result_matcher'
7
+ module Yabi
8
+ # Base object for building service objects / interactors backed by dry-rb.
9
+ class BaseInteractor
10
+ include Dry::Monads[:result, :do, :try, :maybe]
11
+ extend Dry::Initializer
12
+
13
+ # Optional Dry::Validation::Contract subclass to run before #call.
14
+ class << self
15
+ attr_writer :contract
16
+
17
+ def inherited(subclass)
18
+ super
19
+ subclass.contract = contract
20
+ end
21
+
22
+ def contract
23
+ return @contract if defined?(@contract) && @contract
24
+
25
+ const_get(:ValidationContract) if const_defined?(:ValidationContract)
26
+ end
27
+ end
28
+
29
+ class << self
30
+ # Entrypoint. Instantiates, runs validation, then #call.
31
+ def call(*positional_args, contract: nil, **args, &block)
32
+ merged_args =
33
+ if positional_args.first.is_a?(Hash)
34
+ args.merge(positional_args.first)
35
+ elsif positional_args.any?
36
+ raise ArgumentError,
37
+ I18n.t('yabi.errors.unexpected_positional_arguments', args: positional_args.inspect)
38
+ else
39
+ args
40
+ end
41
+
42
+ normalized_args = transform_values_to_hash(merged_args)
43
+ validation_contract = contract || self.contract
44
+
45
+ instance = new(**normalized_args)
46
+ validation = instance.validate_contract(validation_contract)
47
+ result = validation.success? ? instance.call : instance.log_warning_and_return_failure(validation)
48
+
49
+ return result unless block
50
+
51
+ Dry::Matcher::ResultMatcher.call(result, &block)
52
+ end
53
+
54
+ private
55
+
56
+ # Normalize params: convert ActionController::Parameters and other to_h-capable
57
+ # values, then deep-symbolize keys for dry-validation compatibility.
58
+ def transform_values_to_hash(args)
59
+ symbolized = args.transform_values do |value|
60
+ if value.respond_to?(:to_h) && !value.is_a?(Hash)
61
+ value.to_h
62
+ else
63
+ value
64
+ end
65
+ end
66
+ deep_symbolize_keys(symbolized)
67
+ end
68
+
69
+ def deep_symbolize_keys(obj)
70
+ case obj
71
+ when Hash
72
+ obj.each_with_object({}) do |(k, v), acc|
73
+ acc[k.to_sym] = deep_symbolize_keys(v)
74
+ end
75
+ when Array
76
+ obj.map { |v| deep_symbolize_keys(v) }
77
+ else
78
+ obj
79
+ end
80
+ end
81
+ end
82
+
83
+ # Run validation if a contract is provided; return Success() on skip.
84
+ def validate_contract(validation_contract)
85
+ return Success() unless validation_contract
86
+
87
+ validation_contract.new.call(**attributes_for_contract)
88
+ end
89
+
90
+ # Hook for app-specific logging; override to plug in your logger.
91
+ def log_warning_and_return_failure(validation)
92
+ # LoggerService.call(message: "Validation failed: #{validation.errors.inspect}", level: :warn)
93
+ errors = validation.respond_to?(:errors) ? validation.errors : validation
94
+ errors = errors.to_h if errors.respond_to?(:to_h)
95
+ Failure(errors)
96
+ end
97
+
98
+ # Implement in subclasses.
99
+ def call
100
+ raise NotImplementedError
101
+ end
102
+
103
+ # Convenience wrapper; no-op if ActiveRecord is missing.
104
+ def in_transaction(&)
105
+ ActiveRecord::Base.transaction(&)
106
+ end
107
+
108
+ # Wrap a block in Try and convert to Result with optional handlers.
109
+ def safe_call(on_success: ->(s) { Success(s) }, on_error: ->(e) { Failure(e) }, &)
110
+ Try(&).to_result.either(on_success, on_error)
111
+ end
112
+
113
+ private
114
+
115
+ # Uses dry-initializer metadata to capture declared options/params rather than
116
+ # every instance variable (which may include internal ones).
117
+ def attributes_for_contract
118
+ return self.class.dry_initializer.attributes(self) if self.class.respond_to?(:dry_initializer)
119
+
120
+ fallback_instance_variables_hash
121
+ end
122
+
123
+ def fallback_instance_variables_hash
124
+ instance_variables.each_with_object({}) do |var, hash|
125
+ key = var.to_s.delete('@').to_sym
126
+ hash[key] = instance_variable_get(var)
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_interactor'
4
+
5
+ module Yabi
6
+ # Backwards compatibility wrapper. Prefer Yabi::BaseInteractor.
7
+ BaseService = BaseInteractor
8
+ end
9
+
10
+ BaseService = Yabi::BaseService unless defined?(BaseService)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yabi
4
+ VERSION = '0.1.0'
5
+ end
data/lib/yabi.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'yabi/version'
4
+ require_relative 'yabi/base_interactor'
5
+ require_relative 'yabi/base_contract'
6
+ require_relative 'yabi/base_service'
7
+
8
+ require 'i18n'
9
+
10
+ # Load bundled translations for error messages.
11
+ YABI_LOCALE_PATH = File.expand_path('../config/locales/en.yml', __dir__)
12
+ unless I18n.load_path.include?(YABI_LOCALE_PATH)
13
+ I18n.load_path << YABI_LOCALE_PATH
14
+ I18n.backend.load_translations
15
+ end
16
+
17
+ module Yabi
18
+ end
19
+
20
+ # Provide global constants for easier adoption in existing Rails apps.
21
+ BaseInteractor = Yabi::BaseInteractor unless defined?(BaseInteractor)
22
+
23
+ # Backwards compatibility shim for legacy code using BaseService naming.
24
+ BaseService = Yabi::BaseInteractor unless defined?(BaseService)
metadata ADDED
@@ -0,0 +1,197 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yabi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nikkie Grom
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-initializer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: dry-matcher
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: dry-monads
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.6'
54
+ - !ruby/object:Gem::Dependency
55
+ name: dry-validation
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1.10'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '1.10'
68
+ - !ruby/object:Gem::Dependency
69
+ name: i18n
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '1.6'
75
+ - - "<"
76
+ - !ruby/object:Gem::Version
77
+ version: '2'
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '1.6'
85
+ - - "<"
86
+ - !ruby/object:Gem::Version
87
+ version: '2'
88
+ - !ruby/object:Gem::Dependency
89
+ name: rake
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '13.0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '13.0'
102
+ - !ruby/object:Gem::Dependency
103
+ name: rspec
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '3.12'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '3.12'
116
+ - !ruby/object:Gem::Dependency
117
+ name: rubocop
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '1.66'
123
+ type: :development
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '1.66'
130
+ - !ruby/object:Gem::Dependency
131
+ name: rubocop-rspec
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '2.30'
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '2.30'
144
+ - !ruby/object:Gem::Dependency
145
+ name: rubocop-rspec_rails
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '2.29'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '2.29'
158
+ description: A small dry-rb-based base service/interactor with optional contract validation.
159
+ email:
160
+ - nikkie@nikkie.dev
161
+ executables: []
162
+ extensions: []
163
+ extra_rdoc_files: []
164
+ files:
165
+ - CHANGELOG.md
166
+ - LICENSE
167
+ - README.md
168
+ - config/locales/en.yml
169
+ - lib/yabi.rb
170
+ - lib/yabi/base_contract.rb
171
+ - lib/yabi/base_interactor.rb
172
+ - lib/yabi/base_service.rb
173
+ - lib/yabi/version.rb
174
+ homepage: https://github.com/theendcomplete/yabi
175
+ licenses:
176
+ - MIT
177
+ metadata:
178
+ allowed_push_host: https://rubygems.org
179
+ rubygems_mfa_required: 'true'
180
+ rdoc_options: []
181
+ require_paths:
182
+ - lib
183
+ required_ruby_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '3.1'
188
+ required_rubygems_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ requirements: []
194
+ rubygems_version: 3.7.2
195
+ specification_version: 4
196
+ summary: Yet Another Base Interactor
197
+ test_files: []