f_service 0.1.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2a994a8e96a8aa3f6db41ba6f9dcb358a716b2c1cdce8e82c8b92f70458e941c
4
+ data.tar.gz: 99906aece6b382536fcda4ccb21e3874ccc755d6dfc2418fe98dd2019504513d
5
+ SHA512:
6
+ metadata.gz: 0430b236523a17dae5c2afeb3db37d8354fbdce15f96789121ec184a41004242ccce40bd2a73d742f49a5312efa8dda9e6da207ceff41ef9eafabd03794e88d9
7
+ data.tar.gz: 3d7c7439d7283f69d494c8e402b2c08f906073c4089d7e031e6b7a00e40b66577ce7aac860b8ed49378335d4be9f867dfeb5a982a2fd6cd9bcb1b34a1094e839
@@ -0,0 +1,28 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby: [2.4, 2.5, 2.6, 2.7]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - name: Set up Ruby ${{ matrix.ruby }}
19
+ uses: actions/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby }}
22
+ - name: Build and test with Rake
23
+ run: |
24
+ gem install bundler
25
+ bundle install --jobs 4 --retry 3
26
+ bundle exec rake
27
+ - name: Rubocop Linter Action
28
+ run: bundle exec rubocop --parallel
@@ -0,0 +1,58 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
57
+
58
+ .rspec_status
@@ -0,0 +1,15 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+
7
+ Layout/LineLength:
8
+ Max: 120
9
+
10
+ Metrics/BlockLength:
11
+ Exclude:
12
+ - "spec/**/*"
13
+
14
+ Style/DocumentationMethod:
15
+ Enabled: true
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## Unreleased
8
+ _Nothing here_
9
+
10
+ ## 0.1.1
11
+ ### Added
12
+ First usable version with:
13
+ - Result based services;
14
+ - Type check on results;
15
+ - Pattern matching with `#call`ables;
16
+ - Safe chaining calls with `#then`;
17
+
18
+ ## 0.1.0
19
+ - **Yanked**
data/Gemfile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in f_service.gemspec
6
+ gemspec
7
+
8
+ group :development, :test do
9
+ gem 'rake', '~> 13.0.0'
10
+ gem 'rubocop', '~> 0.82.0', require: false
11
+ gem 'rubocop-rspec', require: false
12
+ end
13
+
14
+ group :docs do
15
+ gem 'yard'
16
+ end
17
+
18
+ group :optional do
19
+ gem 'solargraph'
20
+ end
21
+
22
+ group :test do
23
+ gem 'rspec', '~> 3.0'
24
+ gem 'simplecov', require: false
25
+ end
@@ -0,0 +1,90 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ f_service (0.1.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.0)
10
+ backport (1.1.2)
11
+ benchmark (0.1.0)
12
+ diff-lcs (1.3)
13
+ docile (1.3.2)
14
+ e2mmap (0.1.0)
15
+ jaro_winkler (1.5.4)
16
+ maruku (0.7.3)
17
+ mini_portile2 (2.4.0)
18
+ nokogiri (1.10.9)
19
+ mini_portile2 (~> 2.4.0)
20
+ parallel (1.19.1)
21
+ parser (2.7.1.1)
22
+ ast (~> 2.4.0)
23
+ rainbow (3.0.0)
24
+ rake (13.0.1)
25
+ reverse_markdown (1.4.0)
26
+ nokogiri
27
+ rexml (3.2.4)
28
+ rspec (3.9.0)
29
+ rspec-core (~> 3.9.0)
30
+ rspec-expectations (~> 3.9.0)
31
+ rspec-mocks (~> 3.9.0)
32
+ rspec-core (3.9.1)
33
+ rspec-support (~> 3.9.1)
34
+ rspec-expectations (3.9.1)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.9.0)
37
+ rspec-mocks (3.9.1)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.9.0)
40
+ rspec-support (3.9.2)
41
+ rubocop (0.82.0)
42
+ jaro_winkler (~> 1.5.1)
43
+ parallel (~> 1.10)
44
+ parser (>= 2.7.0.1)
45
+ rainbow (>= 2.2.2, < 4.0)
46
+ rexml
47
+ ruby-progressbar (~> 1.7)
48
+ unicode-display_width (>= 1.4.0, < 2.0)
49
+ rubocop-rspec (1.38.1)
50
+ rubocop (>= 0.68.1)
51
+ ruby-progressbar (1.10.1)
52
+ simplecov (0.18.2)
53
+ docile (~> 1.1)
54
+ simplecov-html (~> 0.11)
55
+ simplecov-html (0.12.0)
56
+ solargraph (0.38.6)
57
+ backport (~> 1.1)
58
+ benchmark
59
+ bundler (>= 1.17.2)
60
+ e2mmap
61
+ jaro_winkler (~> 1.5)
62
+ maruku (~> 0.7, >= 0.7.3)
63
+ nokogiri (~> 1.9, >= 1.9.1)
64
+ parser (~> 2.3)
65
+ reverse_markdown (~> 1.0, >= 1.0.5)
66
+ rubocop (~> 0.52)
67
+ thor (~> 1.0)
68
+ tilt (~> 2.0)
69
+ yard (~> 0.9)
70
+ thor (1.0.1)
71
+ tilt (2.0.10)
72
+ unicode-display_width (1.7.0)
73
+ yard (0.9.24)
74
+
75
+ PLATFORMS
76
+ ruby
77
+
78
+ DEPENDENCIES
79
+ bundler (~> 2.0)
80
+ f_service!
81
+ rake (~> 13.0.0)
82
+ rspec (~> 3.0)
83
+ rubocop (~> 0.82.0)
84
+ rubocop-rspec
85
+ simplecov
86
+ solargraph
87
+ yard
88
+
89
+ BUNDLED WITH
90
+ 2.0.2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Matheus Richard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,147 @@
1
+ ![CI](https://github.com/Fretadao/f_service/workflows/Ruby/badge.svg)
2
+
3
+ # FService
4
+
5
+ FService is a small gem that provides a base class for your services (aka operations).
6
+ The goal is to make services simpler, safer, and more composable.
7
+ It uses the Result monad for handling operations.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'f_service'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install f_service
24
+
25
+ ## Usage
26
+ ### Creating your service
27
+ To start using it, you have to create your service class inheriting from FService::Base.
28
+
29
+ ```ruby
30
+ class User::Create < FService::Base
31
+ end
32
+ ```
33
+
34
+ Now, define your initializer to setup data.
35
+ ```ruby
36
+ class User::Create < FService::Base
37
+ def initialize(name:)
38
+ @name = name
39
+ end
40
+ end
41
+ ```
42
+
43
+ The next step is writing the `#run` method, which is where the work should be done.
44
+ Use the methods `#success` and `#failure` to handle your return values. The return can be any value.
45
+
46
+ ```ruby
47
+ class User::Create < FService::Base
48
+ # ...
49
+ def run
50
+ return failure("No name given") if @name.nil?
51
+
52
+ user = UserRepository.create(name: @name)
53
+ if user.valid?
54
+ success(status: "User successfully created!", data: user)
55
+ else
56
+ failure(status: "User could not be created!", data: user.errors)
57
+ end
58
+ end
59
+ end
60
+ ```
61
+ > Remember, you **have** to return an `FService::Result` at the end of your services.
62
+
63
+ ### Using your service
64
+
65
+ To run your service, use the method `#call` provided by `FService::Base`. We like to use the [implicit call](https://stackoverflow.com/a/19108981/8650655), but you can use it in the form you like most.
66
+
67
+ ```ruby
68
+ User::Create.(name: name)
69
+ # or
70
+ User::Create.call(name: name)
71
+ ```
72
+
73
+ > We do **not** recommend manually initializing your service because it **will not** type check your result (and you could lose nice features like [pattern matching](#pattern-matching) and [service chaining](#chaining-services))!
74
+
75
+ ### Using the result
76
+
77
+ Use the methods `#successful?` and `#failed?` to check the status of your result. If it is successful, you can access the value with `#value`, and if your service fails, you can access the error with `#error`.
78
+
79
+ A hypothetical controller action using the example service could look like this:
80
+
81
+ ```ruby
82
+ class UsersController < BaseController
83
+ def create
84
+ result = User::Create.(user_params)
85
+
86
+ if result.successful?
87
+ json_success(result.value)
88
+ else
89
+ json_error(result.error)
90
+ end
91
+ end
92
+ end
93
+ ```
94
+ > Note that you're not limited to using services inside controllers. They're just PORO's (Play Old Ruby Objects), so you can use in controllers, models, etc. (even other services!).
95
+
96
+ ### Pattern matching
97
+ The code above could be rewritten using the `#on` matcher too. It works similar to pattern matching:
98
+
99
+ ```ruby
100
+ class UsersController < BaseController
101
+ def create
102
+ User::Create.(user_params).on(
103
+ success: ->(value) { return json_success(value) },
104
+ failure: ->(error) { return json_error(error) }
105
+ )
106
+ end
107
+ end
108
+ ```
109
+ > You can use any object that responds to #call, not only Lambdas.
110
+
111
+ ### Chaining services
112
+ Since all services return Results, you can chain service calls making a data pipeline.
113
+ If some step fails, it will short circuit the call chain.
114
+
115
+ ```ruby
116
+ class UsersController < BaseController
117
+ def create
118
+ result = User::Create.(user_params)
119
+ .then { |user| User::Login.(user) }
120
+ .then { |user| User::SendWelcomeEmail.(user) }
121
+
122
+ if result.successful?
123
+ json_success(result.value)
124
+ else
125
+ json_error(result.error)
126
+ end
127
+ end
128
+ end
129
+ ```
130
+
131
+ ## API Docs
132
+
133
+ You can access the API docs [here](https://www.rubydoc.info/gems/f_service/).
134
+
135
+ ## Development
136
+
137
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that allows you to experiment.
138
+
139
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
140
+
141
+ ## Contributing
142
+
143
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Fretadao/f_service.
144
+
145
+ ## License
146
+
147
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'yard'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: :spec
10
+
11
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'f_service'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'f_service/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'f_service'
9
+ spec.version = FService::VERSION
10
+ spec.authors = ['Matheus Richard']
11
+ spec.email = ['matheusrichardt@gmail.com']
12
+
13
+ spec.summary = 'A small, monad-based service class'
14
+ spec.description = <<-DESCRIPTION
15
+ FService is a small gem that provides a base class for your services (aka operations).
16
+ The goal is to make services simpler, safer and more composable.
17
+ It uses the Result monad for handling operations.
18
+ DESCRIPTION
19
+
20
+ spec.homepage = 'https://github.com/Fretadao/f_service'
21
+ spec.license = 'MIT'
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/Fretadao/f_service'
25
+ spec.metadata['documentation_uri'] = 'https://www.rubydoc.info/gems/f_service'
26
+ # spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.bindir = 'exe'
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ['lib']
36
+
37
+ spec.add_development_dependency 'bundler', '~> 2.0'
38
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'f_service/base'
4
+
5
+ # A small, monad-based service class
6
+ #
7
+ # @api public
8
+ module FService
9
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'result/base'
4
+ require_relative 'result/failure'
5
+ require_relative 'result/success'
6
+
7
+ module FService
8
+ # Abstract base class for services.
9
+ # It provides the basic interface to return and handle results.
10
+ #
11
+ # @abstract
12
+ class Base
13
+ # Initializes and runs a new service.
14
+ #
15
+ # @example
16
+ # User::UpdateName.(user: user, new_name: new_name)
17
+ # # or
18
+ # User::UpdateName.call(user: user, new_name: new_name)
19
+ #
20
+ # @note this method shouldn't be overridden in the subclasses
21
+ # @return [FService::Result::Success, FService::Result::Failure]
22
+ def self.call(*params)
23
+ result = new(*params).run
24
+ raise(FService::Error, 'Services must return a Result') unless result.is_a? Result::Base
25
+
26
+ result
27
+ end
28
+
29
+ # This method is where the main work of your service must be.
30
+ # It is called after initilizing the service and should return
31
+ # an FService::Result.
32
+ #
33
+ # @example
34
+ # class User::UpdateName < FService::Base
35
+ # def initialize(user:, new_name:)
36
+ # @user = user
37
+ # @new_name = new_name
38
+ # end
39
+ #
40
+ # def run
41
+ # return failure('Missing user') if user.nil?
42
+ #
43
+ # if @user.update(name: @new_name)
44
+ # success(status: 'User successfully updated!', data: user)
45
+ # else
46
+ # failure(status: 'User could not be updated.', data: user.errors)
47
+ # end
48
+ # end
49
+ # end
50
+ #
51
+ # @note this method SHOULD be overridden in the subclasses
52
+ # @return [FService::Result::Success, FService::Result::Failure]
53
+ def run
54
+ raise NotImplementedError, 'Services must implement #run'
55
+ end
56
+
57
+ # Returns a successful operation.
58
+ # You'll probably want to return this inside {#run}.
59
+ #
60
+ #
61
+ # @example
62
+ # class User::ValidateAge < FService::Base
63
+ # def initialize(age:)
64
+ # @age = age
65
+ # end
66
+ #
67
+ # def run
68
+ # return failure(status: 'No age given!', data: @age) if age.blank?
69
+ # return failure(status: 'Too young!', data: @age) if age < 18
70
+ #
71
+ # success(status: 'Valid age.', data: @age)
72
+ # end
73
+ # end
74
+ #
75
+ # @return [FService::Result::Success] - a successful operation
76
+ def success(data = nil)
77
+ FService::Result::Success.new(data)
78
+ end
79
+
80
+ # Returns a failed operation.
81
+ # You'll probably want to return this inside {#run}.
82
+ # @example
83
+ # class User::ValidateAge < FService::Base
84
+ # def initialize(age:)
85
+ # @age = age
86
+ # end
87
+ #
88
+ # def run
89
+ # return failure(status: 'No age given!', data: @age) if age.blank?
90
+ # return failure(status: 'Too young!', data: @age) if age < 18
91
+ #
92
+ # success(status: 'Valid age.', data: @age)
93
+ # end
94
+ # end
95
+ #
96
+ # @return [FService::Result::Failure] - a failed operation
97
+ def failure(data = nil)
98
+ FService::Result::Failure.new(data)
99
+ end
100
+
101
+ # Return either {FService::Result::Failure Success} or {FService::Result::Failure Failure}
102
+ # given the condition.
103
+ #
104
+ # @example
105
+ # class YearIsLeap < FService::Base
106
+ # def initialize(year:)
107
+ # @year = year
108
+ # end
109
+ #
110
+ # def run
111
+ # return failure(status: 'No year given!', data: @year) if @year.nil?
112
+ #
113
+ # result(leap?, @year)
114
+ # end
115
+ #
116
+ # private
117
+ #
118
+ # def leap?
119
+ # ((@year % 4).zero? && @year % 100 != 0) || (@year % 400).zero?
120
+ # end
121
+ # end
122
+ #
123
+ # @return [FService::Result::Success, FService::Result::Failure]
124
+ def result(condition, data = nil)
125
+ condition ? success(data) : failure(data)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FService
4
+ # General FService error
5
+ class Error < StandardError; end
6
+
7
+ module Result
8
+ # Fservice::Result related error
9
+ class Error < Error; end
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FService
4
+ # Includes representations of operations that can be successful or failed.
5
+ module Result
6
+ # Abstract base class for Result::Success and Result::Failure.
7
+ #
8
+ # @abstract
9
+ class Base
10
+ %i[initialize then successful? failed? value value! error].each do |method_name|
11
+ define_method(method_name) do |*_args|
12
+ raise NotImplementedError, "called #{method_name} on class Result::Base"
13
+ end
14
+ end
15
+
16
+ # "Pattern matching"-like method for results.
17
+ # It will run the success path if Result is a Success.
18
+ # Otherwise, it will run the failure path.
19
+ #
20
+ #
21
+ # @example
22
+ # class UsersController < BaseController
23
+ # def update
24
+ # User::Update.(user: user).on(
25
+ # success: ->(value) { return json_success(value) },
26
+ # failure: ->(error) { return json_error(error) }
27
+ # )
28
+ # end
29
+ #
30
+ # private
31
+ #
32
+ # def user
33
+ # @user ||= User.find_by!(slug: params[:slug])
34
+ # end
35
+ # end
36
+ #
37
+ # @param success [#call] a lambda (or anything that responds to #call) to run on success
38
+ # @param failure [#call] a lambda (or anything that responds to #call) to run on failure
39
+ # @api public
40
+ def on(success:, failure:)
41
+ if successful?
42
+ success.call(value)
43
+ else
44
+ failure.call(error)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../errors'
5
+
6
+ module FService
7
+ module Result
8
+ # Represents a value of a failed operation.
9
+ # The error field can contain any information you want.
10
+ #
11
+ # @api public
12
+ class Failure < Result::Base
13
+ # Returns the provided error
14
+ attr_reader :error
15
+
16
+ # Creates a failed operation.
17
+ # You usually shouldn't call this directly. See {FService::Base#failure}.
18
+ #
19
+ # @param error [Object] failure value.
20
+ def initialize(error)
21
+ @error = error
22
+ freeze
23
+ end
24
+
25
+ # Returns false.
26
+ #
27
+ #
28
+ # @example
29
+ # # Suppose that User::Update returns an FService::Result
30
+ #
31
+ # log_errors(user) unless User::Update.(user: user).successful?
32
+ def successful?
33
+ false
34
+ end
35
+
36
+ # Returns true.
37
+ #
38
+ #
39
+ # @example
40
+ # # Suppose that User::Update returns an FService::Result
41
+ #
42
+ # log_errors(user) if User::Update.(user: user).failed?
43
+ def failed?
44
+ true
45
+ end
46
+
47
+ # Failed operations do not have value.
48
+ def value
49
+ nil
50
+ end
51
+
52
+ # Raises an exception if called.
53
+ # (see #value)
54
+ def value!
55
+ raise Result::Error, 'Failure objects do not have value'
56
+ end
57
+
58
+ # Returns itself to the given block.
59
+ # Use this to chain multiple service calls (since all services return Results).
60
+ # It will short circuit your service call chain.
61
+ #
62
+ #
63
+ # @example
64
+ # class UsersController < BaseController
65
+ # def create
66
+ # result = User::Create.(user_params) # if this fails the following calls won't run
67
+ # .then { |user| User::SendWelcomeEmail.(user: user) }
68
+ # .then { |user| User::Login.(user: user) }
69
+ #
70
+ # if result.successful?
71
+ # json_success(result.value)
72
+ # else
73
+ # json_error(result.error)
74
+ # end
75
+ # end
76
+ # end
77
+ #
78
+ # @return [self]
79
+ def then
80
+ self
81
+ end
82
+
83
+ # Outputs a string representation of the object
84
+ #
85
+ #
86
+ # @example
87
+ # puts FService::Result::Failure.new("Oh no!")
88
+ # # => Failure("Oh no!")
89
+ #
90
+ # @return [String] the object's string representation
91
+ def to_s
92
+ error.nil? ? 'Failure()' : "Failure(#{error.inspect})"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module FService
6
+ module Result
7
+ # Represents a value of a successful operation.
8
+ # The value field can contain any information you want.
9
+ #
10
+ # @api public
11
+ class Success < Result::Base
12
+ # Returns the provided value.
13
+ attr_reader :value
14
+
15
+ # Creates a successful operation.
16
+ # You usually shouldn't call this directly. See {FService::Base#success}.
17
+ #
18
+ # @param value [Object] success value.
19
+ def initialize(value)
20
+ @value = value
21
+ freeze
22
+ end
23
+
24
+ # Returns true.
25
+ #
26
+ #
27
+ # @example
28
+ # # Suppose that User::Update returns an FService::Result
29
+ #
30
+ # log_errors(user) unless User::Update.(user: user).successful?
31
+ def successful?
32
+ true
33
+ end
34
+
35
+ # Returns false.
36
+ #
37
+ #
38
+ # @example
39
+ # # Suppose that User::Update returns an FService::Result
40
+ #
41
+ # log_errors(user) if User::Update.(user: user).failed?
42
+ def failed?
43
+ false
44
+ end
45
+
46
+ # (see #value)
47
+ def value!
48
+ value
49
+ end
50
+
51
+ # Successful operations do not have error.
52
+ #
53
+ # @return [nil]
54
+ def error
55
+ nil
56
+ end
57
+
58
+ # Returns its value to the given block.
59
+ # Use this to chain multiple service calls (since all services return Results).
60
+ #
61
+ #
62
+ # @example
63
+ # class UsersController < BaseController
64
+ # def create
65
+ # result = User::Create.(user_params)
66
+ # .then { |user| User::SendWelcomeEmail.(user: user) }
67
+ # .then { |user| User::Login.(user: user) }
68
+ #
69
+ # if result.successful?
70
+ # json_success(result.value)
71
+ # else
72
+ # json_error(result.error)
73
+ # end
74
+ # end
75
+ # end
76
+ #
77
+ # @yieldparam [result] value pass {#value} to a block
78
+ def then
79
+ yield value
80
+ end
81
+
82
+ # Outputs a string representation of the object
83
+ #
84
+ #
85
+ # @example
86
+ # puts FService::Result::Success.new("Yay!")
87
+ # # => Success("Yay!")
88
+ #
89
+ # @return [String] the object's string representation
90
+ def to_s
91
+ value.nil? ? 'Success()' : "Success(#{value.inspect})"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FService
4
+ # Current version of the gem
5
+ VERSION = '0.1.1'
6
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: f_service
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Matheus Richard
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-05-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ description: |2
28
+ FService is a small gem that provides a base class for your services (aka operations).
29
+ The goal is to make services simpler, safer and more composable.
30
+ It uses the Result monad for handling operations.
31
+ email:
32
+ - matheusrichardt@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - ".github/workflows/tests-and-linter.yml"
38
+ - ".gitignore"
39
+ - ".rubocop.yml"
40
+ - CHANGELOG.md
41
+ - Gemfile
42
+ - Gemfile.lock
43
+ - LICENSE
44
+ - README.md
45
+ - Rakefile
46
+ - bin/console
47
+ - bin/setup
48
+ - f_service.gemspec
49
+ - lib/f_service.rb
50
+ - lib/f_service/base.rb
51
+ - lib/f_service/errors.rb
52
+ - lib/f_service/result/base.rb
53
+ - lib/f_service/result/failure.rb
54
+ - lib/f_service/result/success.rb
55
+ - lib/f_service/version.rb
56
+ homepage: https://github.com/Fretadao/f_service
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ homepage_uri: https://github.com/Fretadao/f_service
61
+ source_code_uri: https://github.com/Fretadao/f_service
62
+ documentation_uri: https://www.rubydoc.info/gems/f_service
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.0.3
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: A small, monad-based service class
82
+ test_files: []