From 2fa97c6c59fb2788484dcddfa19bf8f9d5705f10 Mon Sep 17 00:00:00 2001 From: Nicolai Seerup Date: Tue, 30 Jan 2024 13:43:04 +0100 Subject: [PATCH] Add configuration option to service --- CHANGELOG.md | 4 ++- Dockerfile | 3 +- Gemfile.lock | 16 ++++++++--- README.md | 52 +++++++++++++++++++---------------- VERSION | 2 +- docker-compose.yml | 14 +++------- hanikamu-base_service.gemspec | 3 +- lib/hanikamu/service.rb | 6 +++- spec/hanikamu/service_spec.rb | 40 +++++++++++++++++++++++++-- 9 files changed, 95 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f49e40..c139cd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## v0.1.4 +January 30 2024 + ## v0.1.3 September 18 2023 @@ -11,4 +14,3 @@ March 03 2022 - Add the response helper. Refactor the code a bit :) Include Dockerfile and docker-compose - diff --git a/Dockerfile b/Dockerfile index 8a87e1f..1b207b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,10 @@ # Base image -FROM ruby:3.2.1-slim +FROM ruby:3.2.3-bullseye # Add our Gemfile and install gems ADD Gemfile* ./ ADD hanikamu-base_service.gemspec ./ - RUN bundle check || bundle install WORKDIR "/app" diff --git a/Gemfile.lock b/Gemfile.lock index df3286f..9a09bc1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,8 @@ PATH remote: . specs: - hanikamu-service (0.1.3) + hanikamu-service (0.1.4) + dry-configurable (~> 1.1.0) dry-monads (~> 1.6.0) dry-struct (~> 1.6.0) @@ -10,9 +11,13 @@ GEM specs: ast (2.4.2) base64 (0.1.1) + bigdecimal (3.1.6) coderay (1.1.3) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) diff-lcs (1.5.0) + dry-configurable (1.1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) @@ -30,7 +35,8 @@ GEM dry-types (>= 1.7, < 2) ice_nine (~> 0.11) zeitwerk (~> 2.6) - dry-types (1.7.1) + dry-types (1.7.2) + bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) @@ -92,9 +98,11 @@ GEM rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) unicode-display_width (2.4.2) - zeitwerk (2.6.11) + zeitwerk (2.6.12) PLATFORMS + aarch64-linux + arm64-darwin-23 x86_64-darwin-20 x86_64-linux x86_64-linux-musl diff --git a/README.md b/README.md index 45aa92e..1154c04 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,13 @@ #### Definition Services enforce a pattern design as service objects. -#### Why? -- We want SRP(single responsibility classes) for easier testing. - -- We want to have a common pattern on our service design preventing a fragmentation of patterns. - -- We want to avoid business logic in our models. - -- We want to test our business logic in isolation. - -- We want a complete overview over our domain model and business logic transactions - -- We want to express intent over data structure. +#### Objectives +- Adherence to the Single Responsibility Principle for simplified testing. +- Establishment of a uniform pattern for service design to prevent divergent practices. +- Removal of business logic from models to maintain clean architecture. +- Isolation of business logic for focused testing. +- Comprehensive understanding of domain models and business transactions. +- Emphasis on clear intent rather than mere data structuring. #### Principles @@ -27,11 +22,11 @@ Services enforce a pattern design as service objects. - A `Service.call!` will success on it's task or will raise an error -- `.call!` will raise a set of known errors(validation, ...) +- `.call!` will raise a predefined set of errors (e.g., validation errors). -- `Service.call` will success on it's task returning a monadic Success reponse or will fail returning a monadic Failure +- `Service.call` should either successfully return a Success response or a Failure response in a monadic format. -- `.call` will only catch a set of known Service Errors inheriting from Hanikamu::Service::WhiteListedError and it's implemented as a wrapper of `.call!` +- `.call` will catch only specific Service Errors inheriting from Hanikamu::Service::WhiteListedError and it's implemented as a wrapper of `.call!` #### Responsibilities @@ -58,13 +53,12 @@ Services enforce a pattern design as service objects. - When asking for a banana return only the banana https://www.johndcook.com/blog/2011/07/19/you-wanted-banana/ -###### Schema +###### Schema Validation Checks if the input types are valid. -E.g. job_id is required and of type integer -Errors can be corrected by the client passing different input types to the operation - -#### Example +- Validates input types (e.g., ensuring job_id is an integer and required). +- Errors can be corrected by the client passing different input types to the service. +#### Example Usage ```ruby module Types @@ -98,6 +92,21 @@ Errors can be corrected by the client passing different input types to the opera monadic_response.success if monadic_response.success? ``` + +#### Configuration + +If you wish to include additional error classes to the existing whitelisted_errors array, which is used for determining which errors should result in a Failure response when calling services without a bang (e.g., SomeService.call), you can do so by appending these classes to the whitelisted_errors list in an initializer. + +```ruby + +# in initializer config/initializers/hanikamu_service + +Hanikamu::Service.configure do |config| + config.whitelisted_errors = [SomeError] +end + +``` + #### Using docker Rename Makefile.example to Makefile @@ -105,6 +114,3 @@ Errors can be corrected by the client passing different input types to the opera - `make shell` to get a shell console with the ruby environment - `make console` get a ruby console - `make rspec` run the specs - - - \ No newline at end of file diff --git a/VERSION b/VERSION index b1e80bb..845639e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.3 +0.1.4 diff --git a/docker-compose.yml b/docker-compose.yml index a90262a..46223e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,11 @@ -version: "3" +version: "3.6" networks: - docker-compose-example-tier: + hanikamu-service-network: driver: bridge services: app: - build: - context: . - dockerfile: Dockerfile + build: . volumes: - .:/app networks: - - docker-compose-example-tier - volumes: - - .:/app - networks: - - docker-compose-example-tier + - hanikamu-service-network diff --git a/hanikamu-base_service.gemspec b/hanikamu-base_service.gemspec index 103312b..f9fdb32 100644 --- a/hanikamu-base_service.gemspec +++ b/hanikamu-base_service.gemspec @@ -4,7 +4,7 @@ $LOAD_PATH.push File.expand_path("lib", __dir__) Gem::Specification.new do |s| s.name = "hanikamu-service" - s.version = "0.1.3" + s.version = "0.1.4" s.authors = ["Nicolai Seerup", "Alejandro Jimenez"] s.summary = "This is the base service for all pattern designs used in hanikamu design" s.required_ruby_version = ">= 3.1" @@ -15,6 +15,7 @@ Gem::Specification.new do |s| s.files = Dir["{config,lib}/**/*", "Rakefile"] s.require_paths = ["lib"] + s.add_dependency "dry-configurable", "~> 1.1.0" s.add_dependency "dry-monads", "~> 1.6.0" s.add_dependency "dry-struct", "~> 1.6.0" diff --git a/lib/hanikamu/service.rb b/lib/hanikamu/service.rb index 7cd05f8..13812d8 100644 --- a/lib/hanikamu/service.rb +++ b/lib/hanikamu/service.rb @@ -6,9 +6,12 @@ module Hanikamu # :nodoc class Service < Dry::Struct + extend Dry::Configurable extend Dry::Monads[:result] Error = Class.new(StandardError) + setting :whitelisted_errors, default: [] + class << self def call(options = {}) Success(call!(options)) @@ -27,7 +30,8 @@ def call!(options = {}) def whitelisted_error?(error_klass) error_klass == Hanikamu::Service::Error || error_klass.superclass == Hanikamu::Service::Error || - error_klass == Dry::Struct::Error + error_klass == Dry::Struct::Error || + config.whitelisted_errors.include?(error_klass) end end diff --git a/spec/hanikamu/service_spec.rb b/spec/hanikamu/service_spec.rb index 170a444..7b141cf 100644 --- a/spec/hanikamu/service_spec.rb +++ b/spec/hanikamu/service_spec.rb @@ -2,6 +2,10 @@ require "spec_helper" +module Types + include Dry.Types() +end + RSpec.describe Hanikamu::Service do describe "#new" do it "is private" do @@ -10,13 +14,45 @@ end end + describe ".config" do + describe "#whitelisted_errors" do + let(:error) { Class.new(StandardError) } + let(:service_with_error) do + Class.new(described_class) do + attribute :error, Types::Strict::Class + + def call! + raise error, "Oh, no!" + end + + define_singleton_method(:name) { "RSpecServiceWithError" } + end + end + + it "raises an error when the error is not whitelisted" do + expect { service_with_error.call(error:) }.to raise_error(error) + end + + context "when the error is whitelisted" do + before do + described_class.configure do |config| + config.whitelisted_errors = [error] + end + end + + it "returns a Failure object when the error is whitelisted" do + expect(service_with_error.call(error:)).to be_a(Dry::Monads::Failure) + end + end + end + end + describe "#call!" do it "has to be declared in subclasses" do expect { described_class.call! }.to raise_error(NoMethodError) end end - context "when passing the wrong argument type" do let(:failing_service) do class TestFooModule::Bar < Hanikamu::Service @@ -67,7 +103,7 @@ def call! end end - context "when raising a custom error inheriting from Hanikamu::Service::Error" do + context "when raising a custom error inheriting from Hanikamu::Service::Error" do let(:failing_service) do class TestFooModule::Bar < Hanikamu::Service CustomError = Class.new(self::Error)