solid-process 0.0.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -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 +7 -0
  20. data/lib/solid/model/access.rb +26 -0
  21. data/lib/solid/model.rb +30 -0
  22. data/lib/solid/process/active_record.rb +26 -0
  23. data/lib/solid/process/callbacks.rb +35 -0
  24. data/lib/solid/process/caller.rb +35 -0
  25. data/lib/solid/process/class_methods.rb +42 -0
  26. data/lib/solid/process/config.rb +35 -0
  27. data/lib/solid/process/error.rb +5 -0
  28. data/lib/solid/process/version.rb +2 -2
  29. data/lib/solid/process.rb +134 -4
  30. data/lib/solid/result.rb +17 -0
  31. data/lib/solid/validators/all.rb +12 -0
  32. data/lib/solid/validators/bool_validator.rb +9 -0
  33. data/lib/solid/validators/email_validator.rb +9 -0
  34. data/lib/solid/validators/instance_of_validator.rb +13 -0
  35. data/lib/solid/validators/is_a_validator.rb +6 -0
  36. data/lib/solid/validators/kind_of_validator.rb +13 -0
  37. data/lib/solid/validators/persisted_validator.rb +9 -0
  38. data/lib/solid/validators/respond_to_validator.rb +13 -0
  39. data/lib/solid/validators/singleton_validator.rb +21 -0
  40. data/lib/solid/validators/uuid_validator.rb +21 -0
  41. metadata +88 -8
  42. data/.rubocop.yml +0 -13
  43. 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: d4b6ab233b8f225123d7a74114c281011f94f7a186faeaee54b3d25e9ba51c2e
4
+ data.tar.gz: 57a1de84e07d74971640ce4c03b1e12812d1da7765eb83b6ebb460e92972d1bb
5
5
  SHA512:
6
- metadata.gz: 2c00495ba7a86b10bd4637afc094f6f746257a4c4e36d4a9673ac025bd1a073123a13f994fade68a79f70e8102f39f40d888a6c1a135091fd4e5f92ff265eee7
7
- data.tar.gz: 5675db51ae0032c7d4f41ea3b164a241b7c5addc534ae1b2ebfd94c6ce526b81409ebacd05f674e254ee6e1be08b827e0001f698e2510159005da77c49e7cc64
6
+ metadata.gz: 378aff92aaacc388198e4c72a5ce0fb72df788b1dd7763d2b0f11024629c8988d95e4e52015393dddd4cfbd1116539f1c6f5e1a541422e296117a27e7a7e778b
7
+ data.tar.gz: 0040c63c885ff1177dd11c864d208a64c2b46dec1bee9bf71ea2e7df5ac2340668ac8e524c848c30b736457c3a64051e0084e01c3d401cf3bbaf1293959e6764
data/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2024-03-06
3
+ ## [0.2.0] - 2024-03-18
4
+
5
+ ## [0.1.0] - 2024-03-16
4
6
 
5
7
  - 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+
5
+ class Solid::Input
6
+ include ::Solid::Model
7
+ 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,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model/access"
4
+
5
+ module Solid::Model
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
+ module ClassMethods
18
+ def [](...)
19
+ new(...)
20
+ end
21
+
22
+ def inherited(subclass)
23
+ subclass.include(::Solid::Model)
24
+ end
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class.name} #{attributes.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}>"
29
+ end
30
+ 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,35 @@
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(:call, :success, :failure)
9
+
10
+ subclass.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ def before_call(*filters, &block)
15
+ set_callback(:call, :before, *filters, &block)
16
+ end
17
+
18
+ def around_call(*filters, &block)
19
+ set_callback(:call, :around, *filters, &block)
20
+ end
21
+
22
+ def after_call(*filters, &block)
23
+ set_callback(:call, :after, *filters, &block)
24
+ end
25
+
26
+ def after_success(*filters, &block)
27
+ set_callback(:success, :after, *filters, &block)
28
+ end
29
+
30
+ def after_failure(*filters, &block)
31
+ set_callback(:failure, :after, *filters, &block)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Lint/RescueException
4
+ class Solid::Process
5
+ module Caller
6
+ def call(arg = nil)
7
+ output_already_set! if output?
8
+
9
+ self.input = arg
10
+
11
+ run_callbacks(:call) do
12
+ ::BCDD::Result.event_logs(name: self.class.name) do
13
+ self.output =
14
+ if dependencies&.invalid?
15
+ Failure(:invalid_dependencies, dependencies: dependencies)
16
+ elsif input.invalid?
17
+ Failure(:invalid_input, input: input)
18
+ else
19
+ super(input.attributes.deep_symbolize_keys)
20
+ end
21
+ rescue ::Exception => exception
22
+ rescue_with_handler(exception) || raise
23
+
24
+ output
25
+ end
26
+ end
27
+
28
+ run_callbacks(:success) if output.success?
29
+ run_callbacks(:failure) if output.failure?
30
+
31
+ output
32
+ end
33
+ end
34
+ end
35
+ # rubocop:enable Lint/RescueException
@@ -0,0 +1,42 @@
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
+ const_set(:Input, Config::SolidModel[klass])
9
+ end
10
+
11
+ def input(&block)
12
+ return const_get(:Input, false) if const_defined?(:Input, false)
13
+
14
+ block.nil? and raise Error, "#{self}::Input is undefined. Use #{self}.input { ... } to define it."
15
+
16
+ klass = ::Class.new(Config.instance.input_class)
17
+ klass.class_eval(&block)
18
+
19
+ self.input = klass
20
+ end
21
+
22
+ def dependencies=(klass)
23
+ const_defined?(:Dependencies, false) and raise Error, "#{const_get(:Dependencies, false)} class already defined"
24
+
25
+ const_set(:Dependencies, Config::SolidModel[klass])
26
+ end
27
+
28
+ def dependencies(&block)
29
+ return const_get(:Dependencies, false) if const_defined?(:Dependencies, false)
30
+
31
+ return if block.nil?
32
+
33
+ klass = ::Class.new(Config.instance.dependencies_class)
34
+ klass.class_eval(&block)
35
+
36
+ self.dependencies = klass
37
+ end
38
+
39
+ alias_method :deps, :dependencies
40
+ alias_method :deps=, :dependencies=
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Process
4
+ class Config
5
+ SolidModel = ->(klass) do
6
+ return klass if klass.is_a?(::Class) && klass < ::Solid::Model
7
+
8
+ raise ArgumentError, "#{klass.inspect} must be a class that includes #{::Solid::Model}"
9
+ end
10
+
11
+ attr_reader :input_class, :dependencies_class
12
+
13
+ def initialize
14
+ self.input_class = ::Solid::Input
15
+ self.dependencies_class = ::Solid::Input
16
+ end
17
+
18
+ def input_class=(klass)
19
+ @input_class = SolidModel[klass]
20
+ end
21
+
22
+ def dependencies_class=(klass)
23
+ @dependencies_class = SolidModel[klass]
24
+ end
25
+
26
+ alias_method :deps_class, :dependencies_class
27
+ alias_method :deps_class=, :dependencies_class=
28
+
29
+ class << self
30
+ attr_reader :instance
31
+ end
32
+
33
+ @instance = new
34
+ end
35
+ 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.2.0"
6
6
  end
7
7
  end
data/lib/solid/process.rb CHANGED
@@ -1,10 +1,140 @@
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/config"
15
+ require "solid/process/caller"
16
+ require "solid/process/callbacks"
17
+ require "solid/process/class_methods"
18
+ require "solid/process/active_record"
19
+
20
+ extend ClassMethods
21
+
22
+ include Callbacks
23
+ include ::ActiveSupport::Rescuable
24
+ include ::BCDD::Context.mixin(config: {addon: {continue: true}})
25
+
26
+ def self.inherited(subclass)
27
+ super
28
+
29
+ subclass.prepend(Caller)
30
+ end
31
+
32
+ def self.call(arg = nil)
33
+ new.call(arg)
34
+ end
35
+
36
+ def self.configuration(&block)
37
+ yield config
38
+
39
+ config.freeze
40
+ end
41
+
42
+ def self.config
43
+ Config.instance
44
+ end
45
+
46
+ attr_reader :output, :input, :dependencies
47
+
48
+ def initialize(arg = nil)
49
+ self.dependencies = arg
50
+ end
51
+
52
+ def call(_arg = nil)
53
+ raise Error, "#{self.class}#call must be implemented."
54
+ end
55
+
56
+ def with(dependencies)
57
+ self.class.new(dependencies.with_indifferent_access.with_defaults(deps&.attributes))
58
+ end
59
+
60
+ def new(dependencies = {})
61
+ with(dependencies)
62
+ end
63
+
64
+ def input?
65
+ !input.nil?
66
+ end
67
+
68
+ def output?(type = nil)
69
+ type.nil? ? !output.nil? : !!output&.is?(type)
70
+ end
71
+
72
+ def dependencies?
73
+ !dependencies.nil?
74
+ end
75
+
76
+ def success?(type = nil)
77
+ !!output&.success?(type)
78
+ end
79
+
80
+ def failure?(type = nil)
81
+ !!output&.failure?(type)
82
+ end
83
+
84
+ def inspect
85
+ "#<#{self.class.name} dependencies=#{dependencies.inspect} input=#{input.inspect} output=#{output.inspect}>"
86
+ end
87
+
88
+ def method_missing(name, *args, &block)
89
+ name.end_with?("?") ? output&.is?(name.to_s.chomp("?")) : super
90
+ end
91
+
92
+ def respond_to_missing?(name, include_private = false)
93
+ name.end_with?("?") || super
94
+ end
95
+
96
+ alias_method :deps, :dependencies
97
+ alias_method :deps?, :dependencies?
98
+ alias_method :result, :output
99
+ alias_method :result?, :output?
100
+
101
+ private
102
+
103
+ def dependencies=(arg)
104
+ raise Error, "The `#{self.class}#dependencies` is already set." unless dependencies.nil?
105
+
106
+ @dependencies = self.class.dependencies&.then { arg.instance_of?(_1) ? arg : _1.new(arg) }
107
+ end
108
+
109
+ def input=(arg)
110
+ raise Error, "The `#{self.class}#input` is already set." unless input.nil?
111
+
112
+ @input = self.class.input.then { arg.instance_of?(_1) ? arg : _1.new(arg) }
113
+ end
114
+
115
+ def output_already_set!
116
+ raise Error, "The `#{self.class}#output` is already set. " \
117
+ "Use `.output` to access the result or create a new instance to call again."
118
+ end
119
+
120
+ def output=(result)
121
+ output_already_set! unless output.nil?
122
+
123
+ raise Error, "The result #{result.inspect} must be a BCDD::Context." unless result.is_a?(::BCDD::Context)
124
+
125
+ @output = result
126
+ end
127
+
128
+ def Success!(...)
129
+ return self.output = Success(...) if output.nil?
130
+
131
+ raise Error, "`Success!()` cannot be called because the `#{self.class}#output` is already set."
132
+ end
133
+
134
+ def Failure!(...)
135
+ return self.output = Failure(...) if output.nil?
136
+
137
+ raise Error, "`Failure!()` cannot be called because the `#{self.class}#output` is already set."
138
+ end
9
139
  end
10
140
  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,111 @@
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.2.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-19 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/config.rb
96
+ - lib/solid/process/error.rb
27
97
  - lib/solid/process/version.rb
28
- - sig/solid/process.rbs
98
+ - lib/solid/result.rb
99
+ - lib/solid/validators/all.rb
100
+ - lib/solid/validators/bool_validator.rb
101
+ - lib/solid/validators/email_validator.rb
102
+ - lib/solid/validators/instance_of_validator.rb
103
+ - lib/solid/validators/is_a_validator.rb
104
+ - lib/solid/validators/kind_of_validator.rb
105
+ - lib/solid/validators/persisted_validator.rb
106
+ - lib/solid/validators/respond_to_validator.rb
107
+ - lib/solid/validators/singleton_validator.rb
108
+ - lib/solid/validators/uuid_validator.rb
29
109
  homepage: https://github.com/serradura/solid-process
30
110
  licenses:
31
111
  - MIT
@@ -49,8 +129,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
129
  - !ruby/object:Gem::Version
50
130
  version: '0'
51
131
  requirements: []
52
- rubygems_version: 3.5.3
132
+ rubygems_version: 3.1.6
53
133
  signing_key:
54
134
  specification_version: 4
55
- summary: Ruby on Rails/Business Processes
135
+ summary: Ruby on Rails + Business Processes
56
136
  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