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 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,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]
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hmibo
4
+ VERSION = '0.1.0'
5
+ 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: []