hmibo 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 +7 -0
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +64 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/Rakefile +12 -0
- data/examples/logger_head_integration.rb +71 -0
- data/examples/using_hmibo_in_rails.rb +121 -0
- data/hmibo.gemspec +39 -0
- data/lib/hmibo/base.rb +62 -0
- data/lib/hmibo/bulk_creation.rb +58 -0
- data/lib/hmibo/error.rb +63 -0
- data/lib/hmibo/result.rb +57 -0
- data/lib/hmibo/test_helpers.rb +74 -0
- data/lib/hmibo/version.rb +5 -0
- data/lib/hmibo.rb +14 -0
- data/log/test.log +0 -0
- data/test_basic.rb +67 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6c7f53b6038e3600d9f1190977b7bfbca6554297b260972230565dcc5eb2872a
|
4
|
+
data.tar.gz: 3477105cd41caa85e5a7eb33b4bfb9b4dbd7bf41e3dd8ff0670f256c266a7449
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5e7344a7a1a8b70a6b7446f9de4f7a3d437951207741ef0048c0b142cfd6e6e3a4284ebb34f5dcd946b7fd263e5b18c0869d8ce60f683e20027b3d0b260d0cf2
|
7
|
+
data.tar.gz: 0c932a2a11b444c6c05f766ee2d62555521aab46caeb573bc6646c6e949a56d3b08319974a68b8916dcb9fbc7b7edb14bddec004ae6d9cbd90a795af9eec3949
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [0.1.0] - 2025-07-19
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Initial release of Hmibo gem
|
12
|
+
- Base service class pattern for clean business logic organization
|
13
|
+
- Structured error handling with automatic logging via LoggerHead
|
14
|
+
- Result pattern with success/failure states and data
|
15
|
+
- BulkCreation service for bulk record operations with individual error tracking
|
16
|
+
- Test helpers for RSpec integration
|
17
|
+
- Comprehensive test coverage
|
18
|
+
- Documentation and examples
|
19
|
+
|
20
|
+
### Features
|
21
|
+
- Simple, lightweight service object pattern
|
22
|
+
- Consistent error collection with flexible formats
|
23
|
+
- Automatic exception logging with contextual information
|
24
|
+
- Support for bulk operations with per-record error tracking
|
25
|
+
- Rails-friendly with minimal dependencies
|
26
|
+
- Clean API: `SomeService.call(params)`
|
27
|
+
- Error interface: `result.errors?`, `result.errors`, `result.data`
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
Hmibo is a lightweight Ruby gem providing simple, dependency-free service object patterns inspired by DetectionTek conventions. The gem focuses on business logic encapsulation, error handling, and bulk operations for Rails applications.
|
8
|
+
|
9
|
+
## Development Commands
|
10
|
+
|
11
|
+
- **Run tests**: `rake spec` or `bundle exec rspec`
|
12
|
+
- **Run linting**: `rake rubocop` or `bundle exec rubocop`
|
13
|
+
- **Run all checks**: `rake` (runs both tests and rubocop)
|
14
|
+
- **Build gem**: `rake build`
|
15
|
+
- **Install gem locally**: `rake install`
|
16
|
+
|
17
|
+
## Core Architecture
|
18
|
+
|
19
|
+
### Service Object Pattern
|
20
|
+
All services inherit from `Hmibo::Base` (lib/hmibo/base.rb), which provides:
|
21
|
+
- Error collection and handling via `@errors` array
|
22
|
+
- `call` class method for service execution
|
23
|
+
- Exception handling with `handle_error`
|
24
|
+
- Logging integration (Rails-aware)
|
25
|
+
|
26
|
+
Services implement business logic in the private `perform` method and use:
|
27
|
+
- `add_error(message, code:, id:)` for error tracking
|
28
|
+
- `errors?` to check for validation failures
|
29
|
+
|
30
|
+
### Result Objects
|
31
|
+
`Hmibo::Result` (lib/hmibo/result.rb) provides consistent return values with:
|
32
|
+
- `success?` / `failure?` status methods
|
33
|
+
- `message`, `data`, and `errors` attributes
|
34
|
+
- JSON serialization via `to_h` and `to_json`
|
35
|
+
- Factory methods: `Result.success(message, data)` and `Result.failure(message, errors)`
|
36
|
+
|
37
|
+
### Bulk Operations
|
38
|
+
`Hmibo::BulkCreation` (lib/hmibo/bulk_creation.rb) handles batch record creation:
|
39
|
+
- Processes arrays of parameters with `client_side_id` tracking
|
40
|
+
- Individual error tracking per record
|
41
|
+
- Validation for input parameters and target class
|
42
|
+
|
43
|
+
### Test Integration
|
44
|
+
`Hmibo::TestHelpers` (lib/hmibo/test_helpers.rb) provides RSpec matchers:
|
45
|
+
- `expect_service_success(service)` - assert successful execution
|
46
|
+
- `expect_service_failure(service, expected_error_count:)` - assert failure
|
47
|
+
- `expect_service_error(service, message)` - assert specific error messages
|
48
|
+
- `expect_service_error_with_attributes(service, attributes)` - assert error structure
|
49
|
+
- `mock_service` and `stub_service` for test doubles
|
50
|
+
|
51
|
+
## File Structure
|
52
|
+
- `lib/hmibo.rb` - Main entry point and exception classes
|
53
|
+
- `lib/hmibo/base.rb` - Core service class
|
54
|
+
- `lib/hmibo/result.rb` - Result object implementation
|
55
|
+
- `lib/hmibo/bulk_creation.rb` - Bulk operation service
|
56
|
+
- `lib/hmibo/error.rb` - Custom exception definitions
|
57
|
+
- `lib/hmibo/concerns/` - Reusable service modules
|
58
|
+
- `spec/` - RSpec test suite with Rails integration tests
|
59
|
+
|
60
|
+
## Testing Approach
|
61
|
+
- Uses RSpec with custom matchers from TestHelpers
|
62
|
+
- Rails integration specs in spec/integration/
|
63
|
+
- Service-specific specs follow naming convention: `service_name_spec.rb`
|
64
|
+
- Test helpers automatically included via spec_helper.rb configuration
|
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in hmibo.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem 'rake', '~> 13.0'
|
9
|
+
gem 'rspec', '~> 3.0'
|
10
|
+
gem 'rubocop', '~> 1.21'
|
11
|
+
gem 'rubocop-rspec', '~> 2.0'
|
12
|
+
|
13
|
+
# Development and testing dependencies
|
14
|
+
group :development, :test do
|
15
|
+
gem 'logger_head', path: '../logger_head'
|
16
|
+
gem 'rails', '~> 7.1.0'
|
17
|
+
gem 'sprockets-rails'
|
18
|
+
gem 'sqlite3', '~> 1.4'
|
19
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Daniel Brown
|
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,190 @@
|
|
1
|
+
# Hmibo
|
2
|
+
|
3
|
+
**How May I Be Of service!**
|
4
|
+
|
5
|
+
Hmibo is a lightweight Ruby gem that provides simple, consistent patterns for service objects. Inspired by personal patterns, it offers structured error handling and logging for business logic in your Rails applications.
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- **Simple Base Service Class**: Clean pattern following personal conventions
|
10
|
+
- **Structured Error Logging**: Integrated with LoggerHead for contextual error logging
|
11
|
+
- **Consistent Error Handling**: Structured error collection with flexible formats
|
12
|
+
- **Bulk Operations**: Specialized service for bulk record creation with individual error tracking
|
13
|
+
- **Rails Testing Helpers**: RSpec helpers for easy service testing
|
14
|
+
- **Minimal Dependencies**: Uses LoggerHead for enhanced error logging (works great with Rails 7.1+)
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'hmibo'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
$ bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
### Basic Service Object
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class CreateUserService < Hmibo::Base
|
36
|
+
def initialize(name:, email:, role: 'user')
|
37
|
+
@name = name
|
38
|
+
@email = email
|
39
|
+
@role = role
|
40
|
+
super()
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :name, :email, :role
|
46
|
+
|
47
|
+
def perform
|
48
|
+
add_error("Name is required") if name.blank?
|
49
|
+
add_error("Email is required") if email.blank?
|
50
|
+
add_error("Email format is invalid") unless valid_email?
|
51
|
+
|
52
|
+
return self if errors?
|
53
|
+
|
54
|
+
user = User.create!(name: name, email: email, role: role)
|
55
|
+
@data = user
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid_email?
|
60
|
+
email.match?(/\A[^@\s]+@[^@\s]+\z/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Usage
|
65
|
+
result = CreateUserService.call(name: "John Doe", email: "john@example.com")
|
66
|
+
|
67
|
+
if result.errors?
|
68
|
+
puts "Errors: #{result.errors.map { |e| e[:message] }.join(', ')}"
|
69
|
+
else
|
70
|
+
puts "User created: #{result.data.name}"
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
### Bulk Creation Service
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
# Create multiple records with error handling
|
78
|
+
params = [
|
79
|
+
{ name: "John", email: "john@example.com", client_side_id: "temp-1" },
|
80
|
+
{ name: "Jane", email: "invalid-email", client_side_id: "temp-2" },
|
81
|
+
{ name: "Bob", email: "bob@example.com", client_side_id: "temp-3" }
|
82
|
+
]
|
83
|
+
|
84
|
+
result = Hmibo::BulkCreation.call(params, User)
|
85
|
+
|
86
|
+
if result.errors?
|
87
|
+
result.errors.each do |error|
|
88
|
+
puts "Error for #{error[:id]}: #{error[:message]}"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
puts "All #{result.data.length} users created successfully"
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
## Service Response
|
96
|
+
|
97
|
+
Every service returns itself with the following interface:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
result = SomeService.call(params)
|
101
|
+
|
102
|
+
result.errors? # => true/false if there are errors
|
103
|
+
result.data # => Any data set by the service
|
104
|
+
result.errors # => Array of error hashes: [{message: "...", code: 422, id: nil}]
|
105
|
+
```
|
106
|
+
|
107
|
+
## Error Handling
|
108
|
+
|
109
|
+
Services provide structured error handling with automatic logging:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class ExampleService < Hmibo::Base
|
113
|
+
private
|
114
|
+
|
115
|
+
def perform
|
116
|
+
# Add individual errors
|
117
|
+
add_error("Something went wrong")
|
118
|
+
|
119
|
+
# Errors are automatically logged with context using LoggerHead
|
120
|
+
return self if errors?
|
121
|
+
|
122
|
+
@data = { success: true }
|
123
|
+
self
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
### Automatic Error Logging
|
129
|
+
|
130
|
+
Hmibo integrates with [LoggerHead](https://github.com/lordofthedanse/logger_head) to provide structured error logging with context:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
class CreateUserService < Hmibo::Base
|
134
|
+
def perform
|
135
|
+
# Any unhandled exceptions are automatically logged with context
|
136
|
+
raise StandardError, "Database connection failed"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
result = CreateUserService.call
|
141
|
+
# Automatically logs:
|
142
|
+
# ERROR -- : There was an error in CreateUserService execution: Database connection failed
|
143
|
+
# ERROR -- : /path/to/backtrace...
|
144
|
+
```
|
145
|
+
|
146
|
+
### Custom Error Context
|
147
|
+
|
148
|
+
You can provide custom context for error logging:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class PaymentService < Hmibo::Base
|
152
|
+
private
|
153
|
+
|
154
|
+
def perform
|
155
|
+
process_payment
|
156
|
+
rescue => error
|
157
|
+
log_error(error, context: "processing payment for user #{user_id}")
|
158
|
+
add_error("Payment processing failed")
|
159
|
+
self
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
## Exception Classes
|
165
|
+
|
166
|
+
Hmibo provides custom exception classes:
|
167
|
+
|
168
|
+
- `Hmibo::ServiceError` - Base service error
|
169
|
+
|
170
|
+
## Dependencies
|
171
|
+
|
172
|
+
Hmibo has one very lightweight dependency:
|
173
|
+
|
174
|
+
- [LoggerHead](https://github.com/lordofthedanse/logger_head) - Structured error logging with context
|
175
|
+
|
176
|
+
## Development
|
177
|
+
|
178
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
179
|
+
|
180
|
+
## Contributing
|
181
|
+
|
182
|
+
1. Fork it
|
183
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
184
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
185
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
186
|
+
5. Create new Pull Request
|
187
|
+
|
188
|
+
## License
|
189
|
+
|
190
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example demonstrating LoggerHead integration in Hmibo services
|
4
|
+
# This example shows how LoggerHead provides structured error logging with context
|
5
|
+
|
6
|
+
require_relative '../lib/hmibo'
|
7
|
+
|
8
|
+
# Example service that demonstrates LoggerHead integration
|
9
|
+
class ExampleService < Hmibo::Base
|
10
|
+
def initialize(data:)
|
11
|
+
@data_input = data
|
12
|
+
super()
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :data_input
|
18
|
+
|
19
|
+
def perform
|
20
|
+
# Validate input
|
21
|
+
if data_input.nil?
|
22
|
+
add_error('Data cannot be nil')
|
23
|
+
return self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Simulate some processing that might fail
|
27
|
+
add_error('Name is required') if data_input[:name].nil? || data_input[:name].empty?
|
28
|
+
|
29
|
+
add_error('Age must be positive') if data_input[:age] && data_input[:age] < 0
|
30
|
+
|
31
|
+
return self if errors?
|
32
|
+
|
33
|
+
# Simulate a potential runtime error for demonstration
|
34
|
+
raise StandardError, 'Simulated processing error' if data_input[:name] == 'trigger_error'
|
35
|
+
|
36
|
+
@data = { processed: true, name: data_input[:name], age: data_input[:age] }
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Example usage with LoggerHead integration
|
42
|
+
puts '=== LoggerHead Integration in Hmibo Example ==='
|
43
|
+
puts
|
44
|
+
|
45
|
+
# Example 1: Successful execution
|
46
|
+
puts '1. Successful execution:'
|
47
|
+
result = ExampleService.call(data: { name: 'John', age: 25 })
|
48
|
+
puts " Success: #{!result.errors?}"
|
49
|
+
puts " Data: #{result.data}"
|
50
|
+
puts
|
51
|
+
|
52
|
+
# Example 2: Validation errors (logged with context)
|
53
|
+
puts '2. Validation errors:'
|
54
|
+
result = ExampleService.call(data: { name: '', age: -5 })
|
55
|
+
puts " Success: #{!result.errors?}"
|
56
|
+
puts " Errors: #{result.errors}"
|
57
|
+
puts
|
58
|
+
|
59
|
+
# Example 3: Runtime exception (logged with LoggerHead)
|
60
|
+
puts '3. Runtime exception (check logs for LoggerHead output):'
|
61
|
+
result = ExampleService.call(data: { name: 'trigger_error', age: 30 })
|
62
|
+
puts " Success: #{!result.errors?}"
|
63
|
+
puts " Errors: #{result.errors}"
|
64
|
+
puts
|
65
|
+
|
66
|
+
puts '=== LoggerHead Features ==='
|
67
|
+
puts '✓ Structured error logging with context'
|
68
|
+
puts '✓ Automatic backtrace logging'
|
69
|
+
puts '✓ Service class context included'
|
70
|
+
puts '✓ Works in Rails and non-Rails environments'
|
71
|
+
puts '✓ Consistent with DetectionTek logging patterns'
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example: Simple service object patterns using Hmibo
|
4
|
+
# Inspired by DetectionTek patterns - clean, dependency-free Ruby classes
|
5
|
+
|
6
|
+
# Example 1: Basic service pattern
|
7
|
+
class CreateUser < Hmibo::Base
|
8
|
+
def initialize(user_params)
|
9
|
+
@user_params = user_params
|
10
|
+
super()
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def perform
|
16
|
+
return add_error('Name is required') if @user_params[:name].nil?
|
17
|
+
return add_error('Email is required') if @user_params[:email].nil?
|
18
|
+
|
19
|
+
user = User.new(@user_params)
|
20
|
+
|
21
|
+
if user.save
|
22
|
+
@data = user
|
23
|
+
else
|
24
|
+
user.errors.full_messages.each { |msg| add_error(msg) }
|
25
|
+
end
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Example 2: Bulk creation with error tracking
|
32
|
+
class BulkCreation < Hmibo::Base
|
33
|
+
def initialize(params, klass)
|
34
|
+
@params = params
|
35
|
+
@klass = klass
|
36
|
+
super()
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def perform
|
42
|
+
return add_error('Params cannot be blank') if @params.nil? || @params.empty?
|
43
|
+
return add_error('Class cannot be blank') if @klass.nil?
|
44
|
+
|
45
|
+
created_records = []
|
46
|
+
|
47
|
+
@params.each do |param_set|
|
48
|
+
record = @klass.new(param_set.except(:client_side_id))
|
49
|
+
|
50
|
+
if record.save
|
51
|
+
created_records << record
|
52
|
+
else
|
53
|
+
message = record.errors.full_messages.join(', ')
|
54
|
+
add_error(message, id: param_set[:client_side_id])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@data = created_records
|
59
|
+
self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Example 3: Simple email validation service (DetectionTek style)
|
64
|
+
class EmailValidator < Hmibo::Base
|
65
|
+
def initialize(emails)
|
66
|
+
@emails = emails
|
67
|
+
super()
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def perform
|
73
|
+
valid_emails = []
|
74
|
+
|
75
|
+
@emails.each do |email|
|
76
|
+
if valid_email?(email)
|
77
|
+
valid_emails << email
|
78
|
+
else
|
79
|
+
add_error("#{email} is not a valid email address")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
@data = valid_emails
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def valid_email?(email)
|
88
|
+
email.match?(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Usage examples:
|
93
|
+
|
94
|
+
# Simple service call
|
95
|
+
user_service = CreateUser.call(name: 'John', email: 'john@example.com')
|
96
|
+
if user_service.errors?
|
97
|
+
puts "Errors: #{user_service.errors}"
|
98
|
+
else
|
99
|
+
puts "Created user: #{user_service.data.name}"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Bulk creation with error handling
|
103
|
+
params = [
|
104
|
+
{ name: 'John', email: 'john@example.com', client_side_id: 'temp-1' },
|
105
|
+
{ name: 'Jane', email: 'invalid-email', client_side_id: 'temp-2' }
|
106
|
+
]
|
107
|
+
|
108
|
+
bulk_service = BulkCreation.call(params, User)
|
109
|
+
if bulk_service.errors?
|
110
|
+
puts 'Some records failed:'
|
111
|
+
bulk_service.errors.each do |error|
|
112
|
+
puts "- #{error[:id] || 'N/A'}: #{error[:message] || error}"
|
113
|
+
end
|
114
|
+
else
|
115
|
+
puts "All users created: #{bulk_service.data.map(&:name).join(', ')}"
|
116
|
+
end
|
117
|
+
|
118
|
+
# Email validation
|
119
|
+
email_service = EmailValidator.call(['good@email.com', 'bad-email', 'another@good.com'])
|
120
|
+
puts "Valid emails: #{email_service.data}"
|
121
|
+
puts "Errors: #{email_service.errors}" if email_service.errors?
|
data/hmibo.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/hmibo/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'hmibo'
|
7
|
+
spec.version = Hmibo::VERSION
|
8
|
+
spec.authors = ['Daniel Brown']
|
9
|
+
spec.email = ['daniel@wendcare.com']
|
10
|
+
|
11
|
+
spec.summary = 'Simple service object patterns for Ruby applications'
|
12
|
+
spec.description = 'Hmibo (How May I Be Of service) provides lightweight, dependency-free service object patterns inspired by DetectionTek conventions'
|
13
|
+
spec.homepage = 'https://github.com/lordofthedanse/hmibo'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.0.0'
|
16
|
+
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/lordofthedanse/hmibo'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/lordofthedanse/hmibo/blob/main/CHANGELOG.md'
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
# Dependencies
|
33
|
+
spec.add_dependency 'logger_head', '~> 0.1.0'
|
34
|
+
|
35
|
+
# Development dependencies
|
36
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
37
|
+
spec.add_development_dependency 'rubocop', '~> 1.0'
|
38
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.0'
|
39
|
+
end
|
data/lib/hmibo/base.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hmibo
|
4
|
+
# Base service class that provides common patterns for service objects
|
5
|
+
# Inspired by DetectionTek patterns - simple, dependency-free Ruby classes
|
6
|
+
class Base
|
7
|
+
attr_accessor :errors, :data
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
@errors = []
|
11
|
+
@data = nil
|
12
|
+
setup(*args) if respond_to?(:setup, true)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Main entry point for service execution
|
16
|
+
def call
|
17
|
+
perform
|
18
|
+
rescue StandardError => e
|
19
|
+
handle_error(e)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Class method for convenient service execution
|
24
|
+
def self.call(*args, **kwargs)
|
25
|
+
new(*args, **kwargs).call
|
26
|
+
end
|
27
|
+
|
28
|
+
def errors?
|
29
|
+
!@errors.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Override this method in subclasses to implement business logic
|
35
|
+
def perform
|
36
|
+
raise NotImplementedError, 'Subclasses must implement #perform'
|
37
|
+
end
|
38
|
+
|
39
|
+
# Handle errors that occur during execution
|
40
|
+
def handle_error(error)
|
41
|
+
log_error(error, context: "#{self.class.name} execution")
|
42
|
+
@errors << error.message
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add an error to the errors collection
|
46
|
+
def add_error(message, code: 422, id: nil)
|
47
|
+
error = if message.is_a?(Hash)
|
48
|
+
message
|
49
|
+
else
|
50
|
+
{ message: message, code: code, id: id }
|
51
|
+
end
|
52
|
+
@errors << error
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Log error using LoggerHead for structured logging with context
|
57
|
+
def log_error(error, context: nil)
|
58
|
+
provided_context = context || "in #{self.class.name}"
|
59
|
+
LoggerHead.new(error, provided_context: provided_context).call
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hmibo
|
4
|
+
# Service for bulk creation of records with error handling
|
5
|
+
# Follows DetectionTek patterns for simple, consistent service objects
|
6
|
+
class BulkCreation < Base
|
7
|
+
def initialize(params, klass)
|
8
|
+
@params = params
|
9
|
+
@klass = klass
|
10
|
+
super()
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :params, :klass
|
16
|
+
|
17
|
+
def perform
|
18
|
+
return add_validation_errors if invalid_inputs?
|
19
|
+
|
20
|
+
created_records = []
|
21
|
+
|
22
|
+
@params.each do |param_set|
|
23
|
+
record = @klass.new(param_set.except(:client_side_id))
|
24
|
+
|
25
|
+
if record.save
|
26
|
+
created_records << record
|
27
|
+
else
|
28
|
+
add_error_for_record(record, param_set[:client_side_id])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@data = created_records
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def invalid_inputs?
|
37
|
+
return true if @params.nil? || (defined?(@params.blank?) && @params.blank?) || (@params.respond_to?(:empty?) && @params.empty?)
|
38
|
+
return true if @klass.nil?
|
39
|
+
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_validation_errors
|
44
|
+
add_error('Params cannot be blank') if @params.nil? || (defined?(@params.blank?) && @params.blank?) || (@params.respond_to?(:empty?) && @params.empty?)
|
45
|
+
add_error('Class cannot be blank') if @klass.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_error_for_record(record, client_side_id)
|
49
|
+
if record.errors.respond_to?(:full_messages)
|
50
|
+
record.errors.full_messages.each do |message|
|
51
|
+
add_error(message, code: 422, id: client_side_id)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
add_error('Record validation failed', code: 422, id: client_side_id)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/hmibo/error.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hmibo
|
4
|
+
# Error class for structured error handling
|
5
|
+
class Error
|
6
|
+
attr_reader :id, :message, :code, :field
|
7
|
+
|
8
|
+
def initialize(message, code: 422, id: nil, field: nil)
|
9
|
+
@message = message
|
10
|
+
@code = code
|
11
|
+
@id = id
|
12
|
+
@field = field
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
{
|
17
|
+
message: @message,
|
18
|
+
code: @code,
|
19
|
+
id: @id,
|
20
|
+
field: @field
|
21
|
+
}.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_json(*args)
|
25
|
+
to_h.to_json(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.from_active_record(record, id: nil)
|
29
|
+
errors = []
|
30
|
+
|
31
|
+
record.errors.each do |error|
|
32
|
+
errors << new(
|
33
|
+
error.full_message,
|
34
|
+
code: 422,
|
35
|
+
id: id,
|
36
|
+
field: error.attribute
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
errors
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.validation_error(message, field: nil)
|
44
|
+
new(message, code: 422, field: field)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.not_found(message = 'Record not found')
|
48
|
+
new(message, code: 404)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.unauthorized(message = 'Unauthorized')
|
52
|
+
new(message, code: 401)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.forbidden(message = 'Forbidden')
|
56
|
+
new(message, code: 403)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.server_error(message = 'Internal server error')
|
60
|
+
new(message, code: 500)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/hmibo/result.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hmibo
|
4
|
+
# Result object for service operations
|
5
|
+
class Result
|
6
|
+
attr_reader :success, :message, :data, :errors
|
7
|
+
|
8
|
+
def initialize(success, message, data = nil, errors = [])
|
9
|
+
@success = success
|
10
|
+
@message = message
|
11
|
+
@data = data
|
12
|
+
@errors = Array(errors)
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
@success
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure?
|
20
|
+
!@success
|
21
|
+
end
|
22
|
+
|
23
|
+
def error?
|
24
|
+
failure?
|
25
|
+
end
|
26
|
+
|
27
|
+
def errors?
|
28
|
+
@errors.present?
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_h
|
32
|
+
{
|
33
|
+
success: @success,
|
34
|
+
message: @message,
|
35
|
+
data: @data,
|
36
|
+
errors: @errors
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_json(*args)
|
41
|
+
to_h.to_json(*args)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Factory methods for creating results
|
45
|
+
def self.success(message = 'Success', data = nil)
|
46
|
+
new(true, message, data, [])
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.failure(message, errors = [])
|
50
|
+
new(false, message, nil, Array(errors))
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.error(message, errors = [])
|
54
|
+
failure(message, errors)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hmibo
|
4
|
+
# Test helpers for RSpec testing of Hmibo services
|
5
|
+
module TestHelpers
|
6
|
+
# Assert that a service executed successfully
|
7
|
+
def expect_service_success(service)
|
8
|
+
expect(service.errors?).to be(false),
|
9
|
+
"Expected service to succeed, but got errors: #{service.errors}"
|
10
|
+
service
|
11
|
+
end
|
12
|
+
|
13
|
+
# Assert that a service failed with errors
|
14
|
+
def expect_service_failure(service, expected_error_count: nil)
|
15
|
+
expect(service.errors?).to be(true),
|
16
|
+
"Expected service to fail, but it succeeded with data: #{service.data}"
|
17
|
+
|
18
|
+
if expected_error_count
|
19
|
+
expect(service.errors.length).to eq(expected_error_count),
|
20
|
+
"Expected #{expected_error_count} errors, but got #{service.errors.length}: #{service.errors}"
|
21
|
+
end
|
22
|
+
|
23
|
+
service
|
24
|
+
end
|
25
|
+
|
26
|
+
# Assert that a service has a specific error message
|
27
|
+
def expect_service_error(service, message)
|
28
|
+
expect(service.errors?).to be(true),
|
29
|
+
'Expected service to have errors, but it succeeded'
|
30
|
+
|
31
|
+
error_messages = service.errors.map do |error|
|
32
|
+
error.is_a?(Hash) ? error[:message] : error.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
expect(error_messages).to include(message),
|
36
|
+
"Expected error '#{message}' but got: #{error_messages}"
|
37
|
+
|
38
|
+
service
|
39
|
+
end
|
40
|
+
|
41
|
+
# Assert that a service has errors with specific attributes
|
42
|
+
def expect_service_error_with_attributes(service, attributes = {})
|
43
|
+
expect(service.errors?).to be(true),
|
44
|
+
'Expected service to have errors, but it succeeded'
|
45
|
+
|
46
|
+
matching_error = service.errors.find do |error|
|
47
|
+
next false unless error.is_a?(Hash)
|
48
|
+
|
49
|
+
attributes.all? { |key, value| error[key] == value }
|
50
|
+
end
|
51
|
+
|
52
|
+
expect(matching_error).to be_present,
|
53
|
+
"Expected error with attributes #{attributes} but got: #{service.errors}"
|
54
|
+
|
55
|
+
service
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create a mock service for testing
|
59
|
+
def mock_service(success: true, data: nil, errors: [])
|
60
|
+
service = instance_double('MockService')
|
61
|
+
allow(service).to receive(:errors?).and_return(!errors.empty?)
|
62
|
+
allow(service).to receive(:data).and_return(data)
|
63
|
+
allow(service).to receive(:errors).and_return(errors)
|
64
|
+
service
|
65
|
+
end
|
66
|
+
|
67
|
+
# Stub a service class to return a specific result
|
68
|
+
def stub_service(service_class, success: true, data: nil, errors: [])
|
69
|
+
mock = mock_service(success: success, data: data, errors: errors)
|
70
|
+
allow(service_class).to receive(:call).and_return(mock)
|
71
|
+
mock
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/hmibo.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger_head'
|
4
|
+
require_relative 'hmibo/version'
|
5
|
+
require_relative 'hmibo/base'
|
6
|
+
require_relative 'hmibo/result'
|
7
|
+
require_relative 'hmibo/error'
|
8
|
+
require_relative 'hmibo/bulk_creation'
|
9
|
+
require_relative 'hmibo/test_helpers'
|
10
|
+
|
11
|
+
module Hmibo
|
12
|
+
# Exception classes
|
13
|
+
class ServiceError < StandardError; end
|
14
|
+
end
|
data/log/test.log
ADDED
File without changes
|
data/test_basic.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
5
|
+
|
6
|
+
require 'hmibo'
|
7
|
+
|
8
|
+
# Test basic service
|
9
|
+
class TestService < Hmibo::Base
|
10
|
+
def initialize(name: nil)
|
11
|
+
@name = name
|
12
|
+
super()
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def perform
|
18
|
+
return add_error('Name is required') if @name.nil? || @name.empty?
|
19
|
+
|
20
|
+
@data = { name: @name }
|
21
|
+
self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
puts 'Testing Hmibo gem...'
|
26
|
+
|
27
|
+
# Test successful service
|
28
|
+
service = TestService.call(name: 'John')
|
29
|
+
puts "Success test: #{!service.errors?} (#{service.data})"
|
30
|
+
|
31
|
+
# Test error case
|
32
|
+
service = TestService.call(name: '')
|
33
|
+
puts "Error test: #{service.errors?} (#{service.errors.first})"
|
34
|
+
|
35
|
+
# Test BulkCreation
|
36
|
+
class MockRecord
|
37
|
+
attr_reader :errors
|
38
|
+
|
39
|
+
def initialize(params)
|
40
|
+
@params = params
|
41
|
+
@errors = []
|
42
|
+
end
|
43
|
+
|
44
|
+
def save
|
45
|
+
if @params[:name] && !@params[:name].empty?
|
46
|
+
true
|
47
|
+
else
|
48
|
+
@errors << "Name can't be blank"
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def full_messages
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def join(_separator)
|
59
|
+
"Name can't be blank"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
bulk = Hmibo::BulkCreation.call([{ name: 'John' }, { name: '' }], MockRecord)
|
65
|
+
puts "Bulk creation test: #{bulk.errors?} (#{bulk.errors.length} errors)"
|
66
|
+
|
67
|
+
puts 'All tests completed!'
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hmibo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Brown
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: logger_head
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.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.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.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop-rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
69
|
+
description: Hmibo (How May I Be Of service) provides lightweight, dependency-free
|
70
|
+
service object patterns inspired by DetectionTek conventions
|
71
|
+
email:
|
72
|
+
- daniel@wendcare.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- CHANGELOG.md
|
78
|
+
- CLAUDE.md
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- examples/logger_head_integration.rb
|
84
|
+
- examples/using_hmibo_in_rails.rb
|
85
|
+
- hmibo.gemspec
|
86
|
+
- lib/hmibo.rb
|
87
|
+
- lib/hmibo/base.rb
|
88
|
+
- lib/hmibo/bulk_creation.rb
|
89
|
+
- lib/hmibo/error.rb
|
90
|
+
- lib/hmibo/result.rb
|
91
|
+
- lib/hmibo/test_helpers.rb
|
92
|
+
- lib/hmibo/version.rb
|
93
|
+
- log/test.log
|
94
|
+
- test_basic.rb
|
95
|
+
homepage: https://github.com/lordofthedanse/hmibo
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata:
|
99
|
+
allowed_push_host: https://rubygems.org
|
100
|
+
source_code_uri: https://github.com/lordofthedanse/hmibo
|
101
|
+
changelog_uri: https://github.com/lordofthedanse/hmibo/blob/main/CHANGELOG.md
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 3.0.0
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubygems_version: 3.5.22
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: Simple service object patterns for Ruby applications
|
121
|
+
test_files: []
|