solid-process 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +39 -13
  4. data/Rakefile +17 -5
  5. data/examples/business_processes/.rubocop.yml +3 -0
  6. data/examples/business_processes/.ruby-version +1 -0
  7. data/examples/business_processes/.standard.yml +1 -0
  8. data/examples/business_processes/Gemfile +12 -0
  9. data/examples/business_processes/Rakefile +100 -0
  10. data/examples/business_processes/app/models/account/member.rb +10 -0
  11. data/examples/business_processes/app/models/account/owner_creation.rb +53 -0
  12. data/examples/business_processes/app/models/account.rb +11 -0
  13. data/examples/business_processes/app/models/user/creation.rb +62 -0
  14. data/examples/business_processes/app/models/user/token/creation.rb +39 -0
  15. data/examples/business_processes/app/models/user/token.rb +7 -0
  16. data/examples/business_processes/app/models/user.rb +15 -0
  17. data/examples/business_processes/config.rb +14 -0
  18. data/examples/business_processes/db/setup.rb +49 -0
  19. data/lib/solid/input.rb +13 -0
  20. data/lib/solid/model/access.rb +26 -0
  21. data/lib/solid/model.rb +20 -0
  22. data/lib/solid/process/active_record.rb +26 -0
  23. data/lib/solid/process/callbacks.rb +41 -0
  24. data/lib/solid/process/caller.rb +31 -0
  25. data/lib/solid/process/class_methods.rb +50 -0
  26. data/lib/solid/process/error.rb +5 -0
  27. data/lib/solid/process/version.rb +2 -2
  28. data/lib/solid/process.rb +67 -4
  29. data/lib/solid/result.rb +17 -0
  30. data/lib/solid/validators/all.rb +12 -0
  31. data/lib/solid/validators/bool_validator.rb +9 -0
  32. data/lib/solid/validators/email_validator.rb +9 -0
  33. data/lib/solid/validators/instance_of_validator.rb +13 -0
  34. data/lib/solid/validators/is_a_validator.rb +6 -0
  35. data/lib/solid/validators/kind_of_validator.rb +13 -0
  36. data/lib/solid/validators/persisted_validator.rb +9 -0
  37. data/lib/solid/validators/respond_to_validator.rb +13 -0
  38. data/lib/solid/validators/singleton_validator.rb +21 -0
  39. data/lib/solid/validators/uuid_validator.rb +21 -0
  40. metadata +87 -8
  41. data/.rubocop.yml +0 -13
  42. data/sig/solid/process.rbs +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7f0a6aab80305b40e97489d58f9ad14663124dc8e0a82d9c71f7c3ae025e20f
4
- data.tar.gz: a162e6cd3c7a71799e06e5ee104ee7913485c76a5e334e22fd219b805e3d79fe
3
+ metadata.gz: 1190b2778ef23e74384d8526ff747c3a741c62a23de0308dc0bcce85c427fef6
4
+ data.tar.gz: b3dd1f4009959aed58c08943e3ae642f5322c7d989036161bdaa71559c36fa5d
5
5
  SHA512:
6
- metadata.gz: 2c00495ba7a86b10bd4637afc094f6f746257a4c4e36d4a9673ac025bd1a073123a13f994fade68a79f70e8102f39f40d888a6c1a135091fd4e5f92ff265eee7
7
- data.tar.gz: 5675db51ae0032c7d4f41ea3b164a241b7c5addc534ae1b2ebfd94c6ce526b81409ebacd05f674e254ee6e1be08b827e0001f698e2510159005da77c49e7cc64
6
+ metadata.gz: 3e81a5319038e788e726573abf7f53c2fccdd4ad5cd24d8b57db426cc4d63bd3e2c10bcc041d24c32c268b8ad7abfa61e97fbf3394e096974a2ee6c90262ca3a
7
+ data.tar.gz: 3bbdadd724e66a871f3ec83701e9feff39f26fbf59886c83954575e16dc80849ae3d5787acffeec01b43acdafdd4b1d2e19a1b12f55503602a4e71e73317866f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2024-03-06
3
+ ## [0.1.0] - 2024-03-16
4
4
 
5
5
  - Initial release
data/README.md CHANGED
@@ -1,20 +1,46 @@
1
- # Solid::Process
2
-
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/solid/process`. To experiment with that code, run `bin/console` for an interactive prompt.
1
+ <p align="center">
2
+ <h1 align="center" id="-solidprocess">🚄 Solid::Process</h1>
3
+ <p align="center"><i>Ruby on Rails + Business Processes</i></p>
4
+ <p align="center">
5
+ <a href="https://codeclimate.com/github/serradura/solid-process/maintainability"><img src="https://api.codeclimate.com/v1/badges/643a53e99bb591321c9f/maintainability" /></a>
6
+ <a href="https://codeclimate.com/github/serradura/solid-process/test_coverage"><img src="https://api.codeclimate.com/v1/badges/643a53e99bb591321c9f/test_coverage" /></a>
7
+ <img src="https://img.shields.io/badge/Ruby%20%3E%3D%202.7%2C%20%3C%3D%20Head-ruby.svg?colorA=444&colorB=333" alt="Ruby">
8
+ <img src="https://img.shields.io/badge/Rails%20%3E%3D%206.0%2C%20%3C%3D%20Edge-rails.svg?colorA=444&colorB=333" alt="Rails">
9
+ </p>
10
+ </p>
11
+
12
+ ## Supported Ruby and Rails
13
+
14
+ This library is tested against:
15
+
16
+ | Ruby / Rails | 6.0 | 6.1 | 7.0 | 7.1 | Edge |
17
+ |--------------|-----|-----|-----|-----|------|
18
+ | 2.7 | ✅ | ✅ | ✅ | ✅ | |
19
+ | 3.0 | ✅ | ✅ | ✅ | ✅ | |
20
+ | 3.1 | ✅ | ✅ | ✅ | ✅ | ✅ |
21
+ | 3.2 | ✅ | ✅ | ✅ | ✅ | ✅ |
22
+ | 3.3 | ✅ | ✅ | ✅ | ✅ | ✅ |
23
+ | Head | | | | ✅ | ✅ |
6
24
 
7
25
  ## Installation
8
26
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem 'solid-process'
31
+ ```
10
32
 
11
- Install the gem and add to the application's Gemfile by executing:
33
+ And then execute:
12
34
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
35
+ ```bash
36
+ $ bundle install
37
+ ```
14
38
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
39
+ Or install it yourself as:
16
40
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
41
+ ```bash
42
+ $ gem install solid-process
43
+ ```
18
44
 
19
45
  ## Usage
20
46
 
@@ -22,13 +48,13 @@ TODO: Write usage instructions here
22
48
 
23
49
  ## Development
24
50
 
25
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
51
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake dev` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
52
 
27
53
  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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
54
 
29
55
  ## Contributing
30
56
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/solid-process. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/solid-process/blob/master/CODE_OF_CONDUCT.md).
57
+ Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/solid-process. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/serradura/solid-process/blob/master/CODE_OF_CONDUCT.md).
32
58
 
33
59
  ## License
34
60
 
@@ -36,4 +62,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
36
62
 
37
63
  ## Code of Conduct
38
64
 
39
- Everyone interacting in the Solid::Process project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/solid-process/blob/master/CODE_OF_CONDUCT.md).
65
+ Everyone interacting in the Solid::Process project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/serradura/solid-process/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,12 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "minitest/test_task"
4
+ require "rake/testtask"
5
5
 
6
- Minitest::TestTask.create
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs += %w[lib test]
7
8
 
8
- require "rubocop/rake_task"
9
+ t.test_files = FileList.new("test/**/*_test.rb")
10
+ end
9
11
 
10
- RuboCop::RakeTask.new
12
+ require "appraisal/task"
11
13
 
12
- task default: %i[test rubocop]
14
+ Appraisal::Task.new
15
+
16
+ require "standard/rake"
17
+
18
+ task :dev do
19
+ exec "bundle exec appraisal rails-7-1 rake test"
20
+
21
+ Rake::Task[:standard].invoke
22
+ end
23
+
24
+ task default: %i[test]
@@ -0,0 +1,3 @@
1
+ inherit_gem:
2
+ standard:
3
+ - config/ruby-3.1.yml
@@ -0,0 +1 @@
1
+ 3.2.2
@@ -0,0 +1 @@
1
+ ruby_version: 3.1
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.7"
6
+ gem "bcrypt", "~> 3.1.20"
7
+ gem "activerecord", "~> 7.1", ">= 7.1.3", require: "active_record"
8
+ gem "type_validator"
9
+ gem "solid-process", path: "../../"
10
+
11
+ gem "rake", "~> 13.0", require: false
12
+ gem "standard", "~> 1.34", require: false
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ Bundler.require(:default)
6
+
7
+ require "standard/rake"
8
+
9
+ if RUBY_VERSION <= "3.1"
10
+ puts "This example requires Ruby 3.1 or higher."
11
+ exit! 1
12
+ end
13
+
14
+ task :config do
15
+ require_relative "config"
16
+ end
17
+
18
+ task default: %i[solid_result]
19
+
20
+ desc "Do pattern matching in Solid::Result"
21
+ task solid_result: %i[config] do
22
+ input = {
23
+ uuid: SecureRandom.uuid,
24
+ owner: {
25
+ name: "\tJohn Doe \n",
26
+ email: " JOHN.doe@email.com",
27
+ password: "123123123",
28
+ password_confirmation: "123123123"
29
+ }
30
+ }
31
+
32
+ case Account::OwnerCreation.call(input)
33
+ in Solid::Result(user:, account:)
34
+ puts "Account and owner user created: #{account.uuid} and #{user.uuid}"
35
+ in Solid::Result(type:, value:)
36
+ puts "Account creation failed: #{type} - #{value}"
37
+ end
38
+
39
+ # Different ways to match
40
+ #
41
+ # in Solid::Result(value: {user:, account:})
42
+ # in Solid::Result(type:, value: {user:, account:})
43
+ # in Solid::Result(type: :account_owner_created, value: {user:, account:})
44
+ end
45
+
46
+ desc "Do pattern matching in Solid::Output"
47
+ task solid_output: %i[config] do
48
+ input = {
49
+ uuid: SecureRandom.uuid,
50
+ owner: {
51
+ name: "\tJohn Doe \n",
52
+ email: " JOHN.doe@email.com",
53
+ password: "123123123",
54
+ password_confirmation: "123123123"
55
+ }
56
+ }
57
+
58
+ case Account::OwnerCreation.call(input)
59
+ in Solid::Output(user:, account:)
60
+ puts "Account and owner user created: #{account.uuid} and #{user.uuid}"
61
+ in Solid::Output(type:, value:)
62
+ puts "Account creation failed: #{type} - #{value}"
63
+ end
64
+
65
+ # Different ways to match
66
+ #
67
+ # in Solid::Output(value: {user:, account:})
68
+ # in Solid::Output(type:, value: {user:, account:})
69
+ # in Solid::Output(type: :account_owner_created, value: {user:, account:})
70
+ end
71
+
72
+ desc "Do pattern matching in Solid::Success and Solid::Failure"
73
+ task solid_success_and_failure: %i[config] do
74
+ input = {
75
+ uuid: SecureRandom.uuid,
76
+ owner: {
77
+ name: "\tJohn Doe \n",
78
+ email: " JOHN.doe@email.com",
79
+ password: "123123123",
80
+ password_confirmation: "123123123"
81
+ }
82
+ }
83
+
84
+ case Account::OwnerCreation.call(input)
85
+ in Solid::Success(user:, account:)
86
+ puts "Account and owner user created: #{account.uuid} and #{user.uuid}"
87
+ in Solid::Failure(type:, value:)
88
+ puts "Account creation failed: #{type} - #{value}"
89
+ end
90
+
91
+ # Different ways to match
92
+ #
93
+ # in Solid::Success(value: {user:, account:})
94
+ # in Solid::Success(type:, value: {user:, account:})
95
+ # in Solid::Success(type: :account_owner_created, value: {user:, account:})
96
+ #
97
+ # in Solid::Failure(input:)
98
+ # in Solid::Failure(value: {input:})
99
+ # in Solid::Failure(type: :invalid_input, value: {input:})
100
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Account::Member < ActiveRecord::Base
4
+ self.table_name = "account_members"
5
+
6
+ enum role: {owner: 0, admin: 1, contributor: 2}
7
+
8
+ belongs_to :user, inverse_of: :memberships
9
+ belongs_to :account, inverse_of: :memberships
10
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Account
4
+ class OwnerCreation < Solid::Process
5
+ deps do
6
+ attribute :user_creation, default: ::User::Creation
7
+ end
8
+
9
+ input do
10
+ attribute :uuid, :string, default: -> { ::SecureRandom.uuid }
11
+ attribute :owner
12
+
13
+ before_validation do |input|
14
+ input.uuid = input.uuid.strip.downcase
15
+ end
16
+
17
+ validates :uuid, presence: true, format: {with: /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/i}
18
+ validates :owner, presence: true
19
+ end
20
+
21
+ def call(attributes)
22
+ rollback_on_failure {
23
+ Given(attributes)
24
+ .and_then(:create_owner)
25
+ .and_then(:create_account)
26
+ .and_then(:link_owner_to_account)
27
+ }.and_expose(:account_owner_created, %i[user account])
28
+ end
29
+
30
+ private
31
+
32
+ def create_owner(owner:, **)
33
+ case deps.user_creation.call(owner)
34
+ in Solid::Success(user:, token:)
35
+ Continue(user:, user_token: token)
36
+ in Solid::Failure(type:, value:)
37
+ Failure(:invalid_owner, **{type => value})
38
+ end
39
+ end
40
+
41
+ def create_account(uuid:, **)
42
+ account = ::Account.create(uuid:)
43
+
44
+ account.persisted? ? Continue(account:) : Failure(:invalid_account, account:)
45
+ end
46
+
47
+ def link_owner_to_account(account:, user:, **)
48
+ Member.create!(account:, user:, role: :owner)
49
+
50
+ Continue()
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Account < ActiveRecord::Base
4
+ has_many :memberships, inverse_of: :account, dependent: :destroy, class_name: "::Account::Member"
5
+ has_many :users, through: :memberships, inverse_of: :accounts
6
+
7
+ where_ownership = -> { where(account_members: {role: :owner}) }
8
+
9
+ has_one :ownership, where_ownership, dependent: nil, inverse_of: :account, class_name: "::Account::Member"
10
+ has_one :owner, through: :ownership, source: :user
11
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User
4
+ class Creation < Solid::Process
5
+ deps do
6
+ attribute :token_creation, default: ::User::Token::Creation
7
+ end
8
+
9
+ input do
10
+ attribute :uuid, :string, default: -> { ::SecureRandom.uuid }
11
+ attribute :name, :string
12
+ attribute :email, :string
13
+ attribute :password, :string
14
+ attribute :password_confirmation, :string
15
+
16
+ before_validation do |input|
17
+ input.uuid = input.uuid.strip.downcase
18
+ input.name = input.name.strip.gsub(/\s+/, " ")
19
+ input.email = input.email.strip.downcase
20
+ end
21
+
22
+ validates :uuid, presence: true, format: {with: /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/i}
23
+ validates :name, presence: true
24
+ validates :email, presence: true, format: {with: ::URI::MailTo::EMAIL_REGEXP}
25
+ validates :password, :password_confirmation, presence: true
26
+ end
27
+
28
+ def call(attributes)
29
+ Given(attributes)
30
+ .and_then(:validate_email_uniqueness)
31
+ .then { |result|
32
+ rollback_on_failure {
33
+ result
34
+ .and_then(:create_user)
35
+ .and_then(:create_user_token)
36
+ }
37
+ }
38
+ .and_expose(:user_created, %i[user token])
39
+ end
40
+
41
+ private
42
+
43
+ def validate_email_uniqueness(email:, **)
44
+ ::User.exists?(email:) ? Failure(:email_already_taken) : Continue()
45
+ end
46
+
47
+ def create_user(uuid:, name:, email:, password:, password_confirmation:)
48
+ user = ::User.create(uuid:, name:, email:, password:, password_confirmation:)
49
+
50
+ user.persisted? ? Continue(user:) : Failure(:invalid_record, **user.errors.messages)
51
+ end
52
+
53
+ def create_user_token(user:, **)
54
+ case deps.token_creation.call(user: user)
55
+ in Solid::Success(token:)
56
+ Continue(token:)
57
+ in Solid::Failure
58
+ raise "Token creation failed"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User::Token
4
+ class Creation < Solid::Process
5
+ input do
6
+ attribute :user
7
+ attribute :executed_at, :time, default: -> { ::Time.current }
8
+
9
+ validates :user, presence: true
10
+ validates :executed_at, presence: true
11
+ end
12
+
13
+ def call(attributes)
14
+ Given(attributes)
15
+ .and_then(:validate_token_existence)
16
+ .and_then(:create_token_if_not_exists)
17
+ .and_expose(:token_created, %i[token])
18
+ end
19
+
20
+ private
21
+
22
+ def validate_token_existence(user:, **)
23
+ token = user.token
24
+
25
+ token&.persisted? ? Success(:token_already_exists, token:) : Continue()
26
+ end
27
+
28
+ def create_token_if_not_exists(user:, executed_at:, **)
29
+ token = user.create_token(
30
+ access_token: ::SecureRandom.hex(24),
31
+ refresh_token: ::SecureRandom.hex(24),
32
+ access_token_expires_at: executed_at + 15.days,
33
+ refresh_token_expires_at: executed_at + 30.days
34
+ )
35
+
36
+ token.persisted? ? Continue(token:) : Failure(:token_creation_failed, **token.errors.messages)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User::Token < ActiveRecord::Base
4
+ self.table_name = "user_tokens"
5
+
6
+ belongs_to :user, inverse_of: :token
7
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ActiveRecord::Base
4
+ has_secure_password
5
+
6
+ has_many :memberships, inverse_of: :user, dependent: :destroy, class_name: "::Account::Member"
7
+ has_many :accounts, through: :memberships, inverse_of: :users
8
+
9
+ where_ownership = -> { where(account_members: {role: :owner}) }
10
+
11
+ has_one :ownership, where_ownership, inverse_of: :user, class_name: "::Account::Member"
12
+ has_one :account, through: :ownership, inverse_of: :owner
13
+
14
+ has_one :token, inverse_of: :user, dependent: :destroy, class_name: "::User::Token"
15
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(__dir__)
4
+
5
+ require "db/setup"
6
+
7
+ require "app/models/account"
8
+ require "app/models/account/member"
9
+ require "app/models/user"
10
+ require "app/models/user/token"
11
+
12
+ require "app/models/user/token/creation"
13
+ require "app/models/user/creation"
14
+ require "app/models/account/owner_creation"
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+
5
+ ActiveRecord::Base.establish_connection(
6
+ host: "localhost",
7
+ adapter: "sqlite3",
8
+ database: ":memory:"
9
+ )
10
+
11
+ ActiveRecord::Schema.define do
12
+ suppress_messages do
13
+ create_table :accounts do |t|
14
+ t.string :uuid, null: false, index: {unique: true}
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ create_table :users do |t|
20
+ t.string :uuid, null: false, index: {unique: true}
21
+ t.string :name, null: false
22
+ t.string :email, null: false, index: {unique: true}
23
+ t.string :password_digest, null: false
24
+
25
+ t.timestamps
26
+ end
27
+
28
+ create_table :user_tokens do |t|
29
+ t.belongs_to :user, null: false, foreign_key: true, index: true
30
+ t.string :access_token, null: false
31
+ t.string :refresh_token, null: false
32
+ t.datetime :access_token_expires_at, null: false
33
+ t.datetime :refresh_token_expires_at, null: false
34
+
35
+ t.timestamps
36
+ end
37
+
38
+ create_table :account_members do |t|
39
+ t.integer :role, null: false, default: 0
40
+ t.belongs_to :user, null: false, foreign_key: true, index: true
41
+ t.belongs_to :account, null: false, foreign_key: true, index: true
42
+
43
+ t.timestamps
44
+
45
+ t.index %i[account_id role], unique: true, where: "(role = 0)"
46
+ t.index %i[account_id user_id], unique: true
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Input
4
+ require_relative "model"
5
+
6
+ def self.inherited(subclass)
7
+ subclass.include(::Solid::Model)
8
+ end
9
+
10
+ def self.[](...)
11
+ new(...)
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solid::Model
4
+ # Implementation based on ActiveModel::Access
5
+ # https://github.com/rails/rails/blob/7-1-stable/activemodel/lib/active_model/access.rb
6
+ module Access
7
+ # Returns a hash of the given methods with their names as keys and returned
8
+ # values as values.
9
+ #
10
+ # person = Person.new(id: 1, name: "bob")
11
+ # person.slice(:id, :name)
12
+ # => { "id" => 1, "name" => "bob" }
13
+ def slice(*methods)
14
+ methods.flatten.index_with { |method| public_send(method) }.with_indifferent_access
15
+ end
16
+
17
+ # Returns an array of the values returned by the given methods.
18
+ #
19
+ # person = Person.new(id: 1, name: "bob")
20
+ # person.values_at(:id, :name)
21
+ # => [1, "bob"]
22
+ def values_at(*methods)
23
+ methods.flatten.map! { |method| public_send(method) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solid::Model
4
+ require_relative "model/access"
5
+
6
+ extend ::ActiveSupport::Concern
7
+
8
+ included do
9
+ include ::ActiveModel.const_defined?(:Api, false) ? ::ActiveModel::Api : ::ActiveModel::Model
10
+ include ::ActiveModel.const_defined?(:Access, false) ? ::ActiveModel::Access : ::Solid::Model::Access
11
+
12
+ include ::ActiveModel::Attributes
13
+ include ::ActiveModel::Dirty
14
+ include ::ActiveModel::Validations::Callbacks
15
+ end
16
+
17
+ def inspect
18
+ "#<#{self.class.name} #{attributes.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}>"
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "active_record"
5
+ rescue LoadError
6
+ end
7
+
8
+ module Solid
9
+ class Process
10
+ private
11
+
12
+ if defined?(::ActiveRecord)
13
+ def rollback_on_failure(model: ::ActiveRecord::Base)
14
+ result = nil
15
+
16
+ model.transaction do
17
+ result = yield
18
+
19
+ raise ::ActiveRecord::Rollback if result.failure?
20
+ end
21
+
22
+ result
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Process
4
+ module Callbacks
5
+ def self.included(subclass)
6
+ subclass.include ActiveSupport::Callbacks
7
+
8
+ subclass.define_callbacks(:success, :failure, :output)
9
+
10
+ subclass.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ def after_success(*args, &block)
15
+ options = args.extract_options!
16
+ options = options.dup
17
+ options[:prepend] = true
18
+
19
+ set_callback(:success, :after, *args, options, &block)
20
+ end
21
+
22
+ def after_failure(*args, &block)
23
+ options = args.extract_options!
24
+ options = options.dup
25
+ options[:prepend] = true
26
+
27
+ set_callback(:failure, :after, *args, options, &block)
28
+ end
29
+
30
+ def after_output(*args, &block)
31
+ options = args.extract_options!
32
+ options = options.dup
33
+ options[:prepend] = true
34
+
35
+ set_callback(:output, :after, *args, options, &block)
36
+ end
37
+
38
+ alias_method :after_result, :after_output
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Process
4
+ module Caller
5
+ def call(arg = nil)
6
+ if output?
7
+ raise Error, "#{self.class}#call already called. " \
8
+ "Use #{self.class}#output to access the result or create a new instance to call again."
9
+ end
10
+
11
+ self.input = self.class.input.then { arg.instance_of?(_1) ? arg : _1.new(arg) }
12
+
13
+ ::BCDD::Result.event_logs(name: self.class.name) do
14
+ self.output =
15
+ if dependencies&.invalid?
16
+ Failure(:invalid_dependencies, dependencies: dependencies)
17
+ elsif input.invalid?
18
+ Failure(:invalid_input, input: input)
19
+ else
20
+ super(input.attributes.deep_symbolize_keys)
21
+ end
22
+ end
23
+
24
+ run_callbacks(:success) if output.success?
25
+ run_callbacks(:failure) if output.failure?
26
+ run_callbacks(:output)
27
+
28
+ output
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Process
4
+ module ClassMethods
5
+ def input=(klass)
6
+ const_defined?(:Input, false) and raise Error, "#{const_get(:Input, false)} class already defined"
7
+
8
+ unless klass.is_a?(::Class) && klass < ::Solid::Model
9
+ raise ArgumentError, "#{klass.inspect} must be a class that includes #{::Solid::Model}"
10
+ end
11
+
12
+ const_set(:Input, klass)
13
+ end
14
+
15
+ def input(&block)
16
+ return const_get(:Input, false) if const_defined?(:Input, false)
17
+
18
+ block.nil? and raise Error, "#{self}::Input is undefined. Use #{self}.input { ... } to define it."
19
+
20
+ klass = ::Class.new(::Solid::Input)
21
+ klass.class_eval(&block)
22
+
23
+ self.input = klass
24
+ end
25
+
26
+ def dependencies=(klass)
27
+ const_defined?(:Dependencies, false) and raise Error, "#{const_get(:Dependencies, false)} class already defined"
28
+
29
+ unless klass.is_a?(::Class) && klass < ::Solid::Model
30
+ raise ArgumentError, "#{klass.inspect} must be a class that includes #{::Solid::Model}"
31
+ end
32
+
33
+ const_set(:Dependencies, klass)
34
+ end
35
+
36
+ def dependencies(&block)
37
+ return const_get(:Dependencies, false) if const_defined?(:Dependencies, false)
38
+
39
+ return if block.nil?
40
+
41
+ klass = ::Class.new(::Solid::Input)
42
+ klass.class_eval(&block)
43
+
44
+ self.dependencies = klass
45
+ end
46
+
47
+ alias_method :deps, :dependencies
48
+ alias_method :deps=, :dependencies=
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Process
4
+ Error = ::Class.new(::StandardError)
5
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solid
4
- module Process
5
- VERSION = "0.0.0"
4
+ class Process
5
+ VERSION = "0.1.0"
6
6
  end
7
7
  end
data/lib/solid/process.rb CHANGED
@@ -1,10 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "process/version"
3
+ require "active_support/all"
4
+ require "active_model"
5
+ require "bcdd/result"
4
6
 
5
7
  module Solid
6
- module Process
7
- class Error < StandardError; end
8
- # Your code goes here...
8
+ require "solid/input"
9
+ require "solid/result"
10
+
11
+ class Process
12
+ require "solid/process/version"
13
+ require "solid/process/error"
14
+ require "solid/process/caller"
15
+ require "solid/process/callbacks"
16
+ require "solid/process/class_methods"
17
+ require "solid/process/active_record"
18
+
19
+ extend ClassMethods
20
+
21
+ include ::BCDD::Context.mixin(config: {addon: {continue: true}})
22
+
23
+ def self.inherited(subclass)
24
+ subclass.prepend(Caller)
25
+ subclass.include(Callbacks)
26
+ end
27
+
28
+ def self.call(arg = nil)
29
+ new.call(arg)
30
+ end
31
+
32
+ attr_accessor :input, :output, :dependencies
33
+
34
+ private :input=, :output=, :dependencies=
35
+
36
+ def initialize(arg = nil)
37
+ self.dependencies = self.class.dependencies&.then { arg.instance_of?(_1) ? arg : _1.new(arg) }
38
+ end
39
+
40
+ def input?
41
+ !input.nil?
42
+ end
43
+
44
+ def output?(type = nil)
45
+ type.nil? ? !output.nil? : !!output&.is?(type)
46
+ end
47
+
48
+ def dependencies?
49
+ !dependencies.nil?
50
+ end
51
+
52
+ def call(_arg = nil)
53
+ raise Error, "#{self.class}#call must be implemented."
54
+ end
55
+
56
+ def inspect
57
+ "#<#{self.class.name} dependencies=#{dependencies.inspect} input=#{input.inspect} output=#{output.inspect}>"
58
+ end
59
+
60
+ def method_missing(name, *args, &block)
61
+ name.end_with?("?") ? output&.is?(name.to_s.chomp("?")) : super
62
+ end
63
+
64
+ def respond_to_missing?(name, include_private = false)
65
+ name.end_with?("?") || super
66
+ end
67
+
68
+ alias_method :deps, :dependencies
69
+ alias_method :deps?, :dependencies?
70
+ alias_method :result, :output
71
+ alias_method :result?, :output?
9
72
  end
10
73
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solid
4
+ Output = ::BCDD::Context
5
+
6
+ Result = ::BCDD::Context
7
+ Success = Result::Success
8
+ Failure = Result::Failure
9
+
10
+ def self.Success(...)
11
+ Result::Success(...)
12
+ end
13
+
14
+ def self.Failure(...)
15
+ Result::Failure(...)
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instance_of_validator"
4
+ require_relative "is_a_validator"
5
+ require_relative "kind_of_validator"
6
+ require_relative "respond_to_validator"
7
+ require_relative "singleton_validator"
8
+
9
+ require_relative "bool_validator"
10
+ require_relative "email_validator"
11
+ require_relative "persisted_validator"
12
+ require_relative "uuid_validator"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BoolValidator < ActiveModel::EachValidator
4
+ def validate_each(obj, attribute, value)
5
+ return if value == true || value == false
6
+
7
+ obj.errors.add attribute, (options[:message] || "is not a boolean")
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EmailValidator < ActiveModel::EachValidator
4
+ def validate_each(obj, attribute, value)
5
+ return if value.is_a?(String) && URI::MailTo::EMAIL_REGEXP.match?(value)
6
+
7
+ obj.errors.add attribute, (options[:message] || "is not an email")
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InstanceOfValidator < ActiveModel::EachValidator
4
+ def validate_each(obj, attribute, value)
5
+ with_option = Array.wrap(options[:with] || options[:in])
6
+
7
+ return if with_option.any? { |type| value.instance_of?(type) }
8
+
9
+ expectation = with_option.map(&:name).join(" | ")
10
+
11
+ obj.errors.add(attribute, (options[:message] || "is not an instance of #{expectation}"))
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "kind_of_validator"
4
+
5
+ class IsAValidator < KindOfValidator
6
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class KindOfValidator < ActiveModel::EachValidator
4
+ def validate_each(obj, attribute, value)
5
+ with_option = Array.wrap(options[:with] || options[:in])
6
+
7
+ return if with_option.any? { |type| value.is_a?(type) }
8
+
9
+ expectation = with_option.map(&:name).join(" | ")
10
+
11
+ obj.errors.add(attribute, (options[:message] || "is not a #{expectation}"))
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PersistedValidator < ActiveModel::EachValidator
4
+ def validate_each(record, attribute, value)
5
+ return if (options[:allow_nil] && value.nil?) || value.try(:persisted?)
6
+
7
+ record.errors.add(attribute, (options[:message] || "must be persisted"))
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RespondToValidator < ActiveModel::EachValidator
4
+ def validate_each(obj, attribute, value)
5
+ with_option = Array.wrap(options[:with] || options[:in])
6
+
7
+ return if with_option.all? { value.respond_to?(_1) }
8
+
9
+ expectation = with_option.map(&:inspect).join(" & ")
10
+
11
+ obj.errors.add(attribute, (options[:message] || "does not respond to #{expectation}"))
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SingletonValidator < ActiveModel::EachValidator
4
+ def validate_each(obj, attribute, value)
5
+ with_option = Array.wrap(options[:with] || options[:in])
6
+
7
+ unless value.is_a?(Module)
8
+ return obj.errors.add(attribute, options[:message] || "is not a class or module")
9
+ end
10
+
11
+ is_valid = with_option.any? do |type|
12
+ type.is_a?(Module) or raise ArgumentError, "#{type.inspect} is not a class or module"
13
+
14
+ value == type || (value < type || value.is_a?(type))
15
+ end
16
+
17
+ expectation = with_option.map(&:name).join(" | ")
18
+
19
+ is_valid or obj.errors.add(attribute, (options[:message] || "is not #{expectation}"))
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UuidValidator < ActiveModel::EachValidator
4
+ PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
5
+ CASE_SENSITIVE = /\A#{PATTERN}\z/.freeze
6
+ CASE_INSENSITIVE = /\A#{PATTERN}\z/i.freeze
7
+
8
+ def validate_each(obj, attribute, value)
9
+ case_sensitive = options.fetch(:case_sensitive, true)
10
+
11
+ regexp = case_sensitive ? CASE_SENSITIVE : CASE_INSENSITIVE
12
+
13
+ return if value.is_a?(String) && value.match?(regexp)
14
+
15
+ message = options[:message] || "is not a valid UUID (case #{case_sensitive ? "sensitive" : "insensitive"})"
16
+
17
+ obj.errors.add(attribute, message)
18
+ end
19
+
20
+ private_constant :PATTERN
21
+ end
metadata CHANGED
@@ -1,31 +1,110 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid-process
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-06 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Ruby on Rails/Business Processes
11
+ date: 2024-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bcdd-result
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '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: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '8.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '6.0'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '8.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: appraisal
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.5'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.5'
61
+ description: Ruby on Rails + Business Processes
14
62
  email:
15
63
  - rodrigo.serradura@gmail.com
16
64
  executables: []
17
65
  extensions: []
18
66
  extra_rdoc_files: []
19
67
  files:
20
- - ".rubocop.yml"
21
68
  - CHANGELOG.md
22
69
  - CODE_OF_CONDUCT.md
23
70
  - LICENSE.txt
24
71
  - README.md
25
72
  - Rakefile
73
+ - examples/business_processes/.rubocop.yml
74
+ - examples/business_processes/.ruby-version
75
+ - examples/business_processes/.standard.yml
76
+ - examples/business_processes/Gemfile
77
+ - examples/business_processes/Rakefile
78
+ - examples/business_processes/app/models/account.rb
79
+ - examples/business_processes/app/models/account/member.rb
80
+ - examples/business_processes/app/models/account/owner_creation.rb
81
+ - examples/business_processes/app/models/user.rb
82
+ - examples/business_processes/app/models/user/creation.rb
83
+ - examples/business_processes/app/models/user/token.rb
84
+ - examples/business_processes/app/models/user/token/creation.rb
85
+ - examples/business_processes/config.rb
86
+ - examples/business_processes/db/setup.rb
87
+ - lib/solid/input.rb
88
+ - lib/solid/model.rb
89
+ - lib/solid/model/access.rb
26
90
  - lib/solid/process.rb
91
+ - lib/solid/process/active_record.rb
92
+ - lib/solid/process/callbacks.rb
93
+ - lib/solid/process/caller.rb
94
+ - lib/solid/process/class_methods.rb
95
+ - lib/solid/process/error.rb
27
96
  - lib/solid/process/version.rb
28
- - sig/solid/process.rbs
97
+ - lib/solid/result.rb
98
+ - lib/solid/validators/all.rb
99
+ - lib/solid/validators/bool_validator.rb
100
+ - lib/solid/validators/email_validator.rb
101
+ - lib/solid/validators/instance_of_validator.rb
102
+ - lib/solid/validators/is_a_validator.rb
103
+ - lib/solid/validators/kind_of_validator.rb
104
+ - lib/solid/validators/persisted_validator.rb
105
+ - lib/solid/validators/respond_to_validator.rb
106
+ - lib/solid/validators/singleton_validator.rb
107
+ - lib/solid/validators/uuid_validator.rb
29
108
  homepage: https://github.com/serradura/solid-process
30
109
  licenses:
31
110
  - MIT
@@ -49,8 +128,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
128
  - !ruby/object:Gem::Version
50
129
  version: '0'
51
130
  requirements: []
52
- rubygems_version: 3.5.3
131
+ rubygems_version: 3.1.6
53
132
  signing_key:
54
133
  specification_version: 4
55
- summary: Ruby on Rails/Business Processes
134
+ summary: Ruby on Rails + Business Processes
56
135
  test_files: []
data/.rubocop.yml DELETED
@@ -1,13 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
11
-
12
- Layout/LineLength:
13
- Max: 120
@@ -1,6 +0,0 @@
1
- module Solid
2
- module Process
3
- VERSION: String
4
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
- end
6
- end