command_tower 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +32 -0
- data/app/controllers/command_tower/admin_controller.rb +104 -0
- data/app/controllers/command_tower/application_controller.rb +81 -0
- data/app/controllers/command_tower/auth/plain_text_controller.rb +132 -0
- data/app/controllers/command_tower/inbox/message_blast_controller.rb +89 -0
- data/app/controllers/command_tower/inbox/message_controller.rb +79 -0
- data/app/controllers/command_tower/user_controller.rb +49 -0
- data/app/controllers/command_tower/username_controller.rb +26 -0
- data/app/helpers/command_tower/application_helper.rb +4 -0
- data/app/helpers/command_tower/schema_helper.rb +29 -0
- data/app/jobs/command_tower/application_job.rb +4 -0
- data/app/mailers/command_tower/application_mailer.rb +8 -0
- data/app/mailers/command_tower/email_verification_mailer.rb +12 -0
- data/app/models/command_tower/application_record.rb +45 -0
- data/app/models/message.rb +30 -0
- data/app/models/message_blast.rb +27 -0
- data/app/models/user.rb +61 -0
- data/app/models/user_secret.rb +72 -0
- data/app/services/command_tower/README.md +49 -0
- data/app/services/command_tower/argument_validation/README.md +192 -0
- data/app/services/command_tower/argument_validation/class_methods.rb +178 -0
- data/app/services/command_tower/argument_validation/instance_methods.rb +148 -0
- data/app/services/command_tower/argument_validation.rb +11 -0
- data/app/services/command_tower/authorize/validate.rb +49 -0
- data/app/services/command_tower/inbox_service/blast/delete.rb +23 -0
- data/app/services/command_tower/inbox_service/blast/metadata.rb +26 -0
- data/app/services/command_tower/inbox_service/blast/new_user_blaster.rb +24 -0
- data/app/services/command_tower/inbox_service/blast/retrieve.rb +30 -0
- data/app/services/command_tower/inbox_service/blast/upsert.rb +67 -0
- data/app/services/command_tower/inbox_service/message/metadata.rb +35 -0
- data/app/services/command_tower/inbox_service/message/modify.rb +44 -0
- data/app/services/command_tower/inbox_service/message/retrieve.rb +36 -0
- data/app/services/command_tower/inbox_service/message/send.rb +33 -0
- data/app/services/command_tower/jwt/authenticate_user.rb +86 -0
- data/app/services/command_tower/jwt/decode.rb +21 -0
- data/app/services/command_tower/jwt/encode.rb +15 -0
- data/app/services/command_tower/jwt/login_create.rb +21 -0
- data/app/services/command_tower/jwt/time_delay_token.rb +17 -0
- data/app/services/command_tower/login_strategy/plain_text/create.rb +43 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/generate.rb +29 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/required.rb +20 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/send.rb +23 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/verify.rb +24 -0
- data/app/services/command_tower/login_strategy/plain_text/login.rb +50 -0
- data/app/services/command_tower/secrets/cleanse.rb +14 -0
- data/app/services/command_tower/secrets/generate.rb +62 -0
- data/app/services/command_tower/secrets/verify.rb +27 -0
- data/app/services/command_tower/secrets.rb +15 -0
- data/app/services/command_tower/service_base.rb +89 -0
- data/app/services/command_tower/service_logging.rb +41 -0
- data/app/services/command_tower/user_attributes/modify.rb +68 -0
- data/app/services/command_tower/user_attributes/roles.rb +27 -0
- data/app/services/command_tower/username/available.rb +64 -0
- data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +26 -0
- data/config/routes.rb +55 -0
- data/db/migrate/20241117043720_create_command_tower_users.rb +42 -0
- data/db/migrate/20241204065708_create_command_tower_user_secrets.rb +16 -0
- data/db/migrate/20250223023306_create_command_tower_messages.rb +12 -0
- data/db/migrate/20250223023313_create_command_tower_message_blasts.rb +14 -0
- data/lib/command_tower/authorization/default.yml +42 -0
- data/lib/command_tower/authorization/entity.rb +101 -0
- data/lib/command_tower/authorization/role.rb +101 -0
- data/lib/command_tower/authorization.rb +85 -0
- data/lib/command_tower/configuration/admin/config.rb +18 -0
- data/lib/command_tower/configuration/application/config.rb +40 -0
- data/lib/command_tower/configuration/authorization/config.rb +24 -0
- data/lib/command_tower/configuration/base.rb +11 -0
- data/lib/command_tower/configuration/config.rb +77 -0
- data/lib/command_tower/configuration/email/config.rb +87 -0
- data/lib/command_tower/configuration/jwt/config.rb +22 -0
- data/lib/command_tower/configuration/login/config.rb +18 -0
- data/lib/command_tower/configuration/login/strategy/plain_text/config.rb +57 -0
- data/lib/command_tower/configuration/login/strategy/plain_text/email_verify.rb +50 -0
- data/lib/command_tower/configuration/login/strategy/plain_text/lockable.rb +27 -0
- data/lib/command_tower/configuration/otp/config.rb +54 -0
- data/lib/command_tower/configuration/user/config.rb +56 -0
- data/lib/command_tower/configuration/username/check.rb +31 -0
- data/lib/command_tower/configuration/username/config.rb +41 -0
- data/lib/command_tower/engine.rb +53 -0
- data/lib/command_tower/error.rb +5 -0
- data/lib/command_tower/schema/admin/users.rb +15 -0
- data/lib/command_tower/schema/error/base.rb +15 -0
- data/lib/command_tower/schema/error/invalid_argument.rb +15 -0
- data/lib/command_tower/schema/error/invalid_argument_response.rb +17 -0
- data/lib/command_tower/schema/inbox/blast_request.rb +15 -0
- data/lib/command_tower/schema/inbox/blast_response.rb +16 -0
- data/lib/command_tower/schema/inbox/message_blast_entity.rb +16 -0
- data/lib/command_tower/schema/inbox/message_blast_metadata.rb +16 -0
- data/lib/command_tower/schema/inbox/message_entity.rb +14 -0
- data/lib/command_tower/schema/inbox/metadata.rb +18 -0
- data/lib/command_tower/schema/inbox/modified.rb +13 -0
- data/lib/command_tower/schema/page.rb +14 -0
- data/lib/command_tower/schema/plain_text/create_user_request.rb +18 -0
- data/lib/command_tower/schema/plain_text/create_user_response.rb +17 -0
- data/lib/command_tower/schema/plain_text/email_verify_request.rb +11 -0
- data/lib/command_tower/schema/plain_text/email_verify_response.rb +11 -0
- data/lib/command_tower/schema/plain_text/email_verify_send_request.rb +9 -0
- data/lib/command_tower/schema/plain_text/email_verify_send_response.rb +11 -0
- data/lib/command_tower/schema/plain_text/login_request.rb +15 -0
- data/lib/command_tower/schema/plain_text/login_response.rb +13 -0
- data/lib/command_tower/schema/user.rb +28 -0
- data/lib/command_tower/schema.rb +38 -0
- data/lib/command_tower/spec_helper.rb +19 -0
- data/lib/command_tower/version.rb +5 -0
- data/lib/command_tower.rb +33 -0
- data/lib/generators/api_engine_base/configure/USAGE +8 -0
- data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
- data/lib/tasks/auto_annotate_models.rake +60 -0
- metadata +255 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower
|
4
|
+
module SchemaHelper
|
5
|
+
def schema_succesful!(schema:, status:)
|
6
|
+
render(json: schema.to_h, status:)
|
7
|
+
end
|
8
|
+
|
9
|
+
def invalid_arguments!(message:, argument_object:, schema:, status:)
|
10
|
+
bad_arguments = argument_object.map do |key, metadata|
|
11
|
+
CommandTower::Schema::Error::InvalidArgument.new(
|
12
|
+
schema:,
|
13
|
+
argument: key,
|
14
|
+
argument_type: metadata[:type],
|
15
|
+
reason: metadata[:msg],
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
result = CommandTower::Schema::Error::InvalidArgumentResponse.new(
|
20
|
+
invalid_arguments: bad_arguments,
|
21
|
+
invalid_argument_keys: argument_object.keys,
|
22
|
+
status:,
|
23
|
+
message:,
|
24
|
+
)
|
25
|
+
|
26
|
+
render(json: result.to_h, status:)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
def self.attribute_to_type_mapping
|
8
|
+
@attribute_to_type_mapping ||= begin
|
9
|
+
mapping = ActiveSupport::HashWithIndifferentAccess.new
|
10
|
+
columns_hash.each do |attribute_name, metadata|
|
11
|
+
base = nil
|
12
|
+
ruby_type = nil
|
13
|
+
allowed_types = nil
|
14
|
+
serialized_type = nil
|
15
|
+
case metadata.type
|
16
|
+
when :string, :text
|
17
|
+
base = ruby_type = String
|
18
|
+
when :integer, :bigint
|
19
|
+
base = ruby_type = Integer
|
20
|
+
when :datetime, :time, :date
|
21
|
+
base = String
|
22
|
+
ruby_type = [DateTime, Time]
|
23
|
+
when :float, :decimal
|
24
|
+
base = ruby_type = Float
|
25
|
+
when :boolean
|
26
|
+
base = "Boolean"
|
27
|
+
ruby_type = [TrueClass, FalseClass]
|
28
|
+
allowed_types = [true, false]
|
29
|
+
else
|
30
|
+
# All else fails convert to String and continue
|
31
|
+
base = ruby_type = String
|
32
|
+
end
|
33
|
+
|
34
|
+
attribute_type = attribute_types[attribute_name]
|
35
|
+
if attribute_type.is_a?(ActiveRecord::Type::Serialized)
|
36
|
+
serialized_type = attribute_type.coder.object_class
|
37
|
+
end
|
38
|
+
mapping[attribute_name] = { serialized_type:, base:, ruby_type:, allowed_types: }.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
mapping
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: messages
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# pushed :boolean default(FALSE)
|
9
|
+
# text :text(65535)
|
10
|
+
# title :string(255)
|
11
|
+
# viewed :boolean default(FALSE)
|
12
|
+
# created_at :datetime not null
|
13
|
+
# updated_at :datetime not null
|
14
|
+
# message_blast_id :bigint
|
15
|
+
# user_id :bigint not null
|
16
|
+
#
|
17
|
+
# Indexes
|
18
|
+
#
|
19
|
+
# index_messages_on_message_blast_id (message_blast_id)
|
20
|
+
# index_messages_on_user_id (user_id)
|
21
|
+
#
|
22
|
+
# Foreign Keys
|
23
|
+
#
|
24
|
+
# fk_rails_... (message_blast_id => message_blasts.id)
|
25
|
+
# fk_rails_... (user_id => users.id)
|
26
|
+
#
|
27
|
+
class Message < ApplicationRecord
|
28
|
+
belongs_to :user
|
29
|
+
belongs_to :message_blast, optional: true
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: message_blasts
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# existing_users :boolean default(FALSE)
|
9
|
+
# new_users :boolean default(FALSE)
|
10
|
+
# text :text(65535)
|
11
|
+
# title :string(255)
|
12
|
+
# created_at :datetime not null
|
13
|
+
# updated_at :datetime not null
|
14
|
+
# user_id :bigint not null
|
15
|
+
#
|
16
|
+
# Indexes
|
17
|
+
#
|
18
|
+
# index_message_blasts_on_user_id (user_id)
|
19
|
+
#
|
20
|
+
# Foreign Keys
|
21
|
+
#
|
22
|
+
# fk_rails_... (user_id => users.id)
|
23
|
+
#
|
24
|
+
class MessageBlast < ApplicationRecord
|
25
|
+
has_many :messages
|
26
|
+
belongs_to :user
|
27
|
+
end
|
data/app/models/user.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: users
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# email :string(255) default(""), not null
|
9
|
+
# email_validated :boolean default(FALSE)
|
10
|
+
# first_name :string(255) default(""), not null
|
11
|
+
# last_known_timezone :string(255)
|
12
|
+
# last_known_timezone_update :datetime
|
13
|
+
# last_login :datetime
|
14
|
+
# last_login_strategy :string(255)
|
15
|
+
# last_name :string(255) default(""), not null
|
16
|
+
# password_consecutive_fail :integer default(0)
|
17
|
+
# password_digest :string(255) default(""), not null
|
18
|
+
# recovery_password_digest :string(255) default(""), not null
|
19
|
+
# roles :string(255) default([])
|
20
|
+
# successful_login :integer default(0)
|
21
|
+
# username :string(255)
|
22
|
+
# verifier_token :string(255)
|
23
|
+
# verifier_token_last_reset :datetime
|
24
|
+
# created_at :datetime not null
|
25
|
+
# updated_at :datetime not null
|
26
|
+
#
|
27
|
+
# Indexes
|
28
|
+
#
|
29
|
+
# index_users_on_username (username) UNIQUE
|
30
|
+
#
|
31
|
+
require "securerandom"
|
32
|
+
|
33
|
+
class User < CommandTower::ApplicationRecord
|
34
|
+
has_secure_password
|
35
|
+
|
36
|
+
validates :username, uniqueness: true
|
37
|
+
validates :email, uniqueness: true
|
38
|
+
|
39
|
+
###
|
40
|
+
# Serialize the roles column to check for inclusion easily
|
41
|
+
serialize :roles, coder: JSON, type: Array
|
42
|
+
|
43
|
+
has_many :messages
|
44
|
+
|
45
|
+
def full_name
|
46
|
+
"#{first_name} #{last_name}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def reset_verifier_token!
|
50
|
+
value = SecureRandom.alphanumeric(32)
|
51
|
+
update!(verifier_token: value, verifier_token_last_reset: Time.now)
|
52
|
+
|
53
|
+
value
|
54
|
+
end
|
55
|
+
|
56
|
+
def retreive_verifier_token!
|
57
|
+
return verifier_token if verifier_token
|
58
|
+
|
59
|
+
reset_verifier_token!
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: user_secrets
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# death_time :datetime
|
9
|
+
# extra :string(255)
|
10
|
+
# reason :string(255)
|
11
|
+
# secret :string(255)
|
12
|
+
# use_count :integer default(0)
|
13
|
+
# use_count_max :integer
|
14
|
+
# created_at :datetime not null
|
15
|
+
# updated_at :datetime not null
|
16
|
+
# user_id :bigint not null
|
17
|
+
#
|
18
|
+
# Indexes
|
19
|
+
#
|
20
|
+
# index_user_secrets_on_secret (secret) UNIQUE
|
21
|
+
# index_user_secrets_on_user_id (user_id)
|
22
|
+
#
|
23
|
+
# Foreign Keys
|
24
|
+
#
|
25
|
+
# fk_rails_... (user_id => users.id)
|
26
|
+
#
|
27
|
+
class UserSecret < ApplicationRecord
|
28
|
+
belongs_to :user
|
29
|
+
|
30
|
+
def self.find_record(secret:, reason: nil, access_count: true)
|
31
|
+
params = { secret:, reason: }.compact
|
32
|
+
record = where(**params).first
|
33
|
+
return { found: false } if record.nil?
|
34
|
+
|
35
|
+
record.access_count! if access_count
|
36
|
+
|
37
|
+
{
|
38
|
+
found: true,
|
39
|
+
valid: record.is_valid?,
|
40
|
+
record: record,
|
41
|
+
user: record.user,
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def invalid_reason
|
46
|
+
arr = []
|
47
|
+
arr << "Expired secret." if !still_alive?
|
48
|
+
arr << "Secret used too many times." if !valid_use_count?
|
49
|
+
|
50
|
+
arr
|
51
|
+
end
|
52
|
+
|
53
|
+
def access_count!
|
54
|
+
update(use_count: use_count + 1)
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_valid?
|
58
|
+
valid_use_count? && still_alive?
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid_use_count?
|
62
|
+
return true if use_count_max.nil?
|
63
|
+
|
64
|
+
use_count <= use_count_max
|
65
|
+
end
|
66
|
+
|
67
|
+
def still_alive?
|
68
|
+
return true if death_time.nil?
|
69
|
+
|
70
|
+
death_time > Time.now
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# CommandTower Service
|
2
|
+
|
3
|
+
`CommandTower::ServiceBase` an abstraction around the Ruby Gem Interactor. It dds custom functionality to the base Service and is intended to be an inherited Class to create Application logic code. All Services in `CommandTower` utilize this base Service class for convenience and DRYness.
|
4
|
+
|
5
|
+
## What does ServiceBase offer
|
6
|
+
|
7
|
+
### Logging
|
8
|
+
`ServiceBase` offers a convenient way to tag logs. It keeps track of:
|
9
|
+
- The start of the the Logic call
|
10
|
+
- The time it took to complete the logic
|
11
|
+
- The status of the logic
|
12
|
+
|
13
|
+
Additionally, it provides some convenience methods for logging
|
14
|
+
- `log_debug`
|
15
|
+
- `log_info`
|
16
|
+
- `log_warn`
|
17
|
+
- `log_error`
|
18
|
+
|
19
|
+
### Argument Validation
|
20
|
+
Argument Validation is the powerhouse behind ServiceBase
|
21
|
+
|
22
|
+
Customized argument validation can be created by adding the method `validate!`
|
23
|
+
```ruby
|
24
|
+
class MyServiceClass < CommandTower::ServiceBase
|
25
|
+
|
26
|
+
def call
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
# run custom validations before executing call
|
31
|
+
end
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
Other more complex Argument validation includes:
|
36
|
+
- Validating Presence of Argument
|
37
|
+
- Validating Type of argument
|
38
|
+
- Validating a composition of argument values (At least, At Most, Exactly)
|
39
|
+
- Delegate context variable to the class for simplicity
|
40
|
+
- Validating Argument length or size is `<` `≤` `==` `>` `≥`
|
41
|
+
|
42
|
+
For More information, Check out the [ArgumentValidation ReadMe](argument_validation/README.md)
|
43
|
+
|
44
|
+
|
45
|
+
## Basic Examples:
|
46
|
+
Check out the examples used in this directory!
|
47
|
+
|
48
|
+
|
49
|
+
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# Argument Validation
|
2
|
+
|
3
|
+
Argument validation provides a robust framework to ensure correctness of arguments before executing any application logic code. This was created because when in use with an API, this can help provide reusable messaging directly back to the API when the parameters are incorrect.
|
4
|
+
|
5
|
+
## Argument Validation
|
6
|
+
Argument Validation provides service object code assurances on what to expect for inputted arguments.
|
7
|
+
|
8
|
+
Available arguments:
|
9
|
+
- `default`: Default value to set the argument when not provided by user
|
10
|
+
- `is_a`: The allowed types of the passed in argument. Will also check if the type is in the ancestral tree
|
11
|
+
- `is_one`: Checks a direct comparison if the input is one of these values. Note: while not disallowed, `is_a` and `is_one` should not be used together
|
12
|
+
- `length`: (used with operators exclusively) When set to true, the operators will use the length of value rather than the exact value
|
13
|
+
- `lt`: When provided, argument must be less than this value
|
14
|
+
- `lte`: When provided, argument must be less than or equal to this value
|
15
|
+
- `eq`: When provided, argument must be equal to this value
|
16
|
+
- `gte`: When provided, argument must be greater than or equal to this value
|
17
|
+
- `gt`: When provided, argument must be greater than this value
|
18
|
+
- `delegation`: (Default set to true) - Sets the delegation on the object. This allows you to reference the argument name rather than the context.{argument_name}
|
19
|
+
- `sensitive`: This marks the argument as sensitive. It will scrub the value of the argument when returning the context to the caller
|
20
|
+
- `required`: When set, this marks the argument as required. If not provided, validations are not run. When provided, validations must pass
|
21
|
+
|
22
|
+
## Argument Composition
|
23
|
+
Argument Compositions are made up of 1 or more Argument Validations. The intention of compositions are to ensure `at_most`, `at_least`, or `exactly` X argument validations are provided by the user.
|
24
|
+
|
25
|
+
### Composition: At Most
|
26
|
+
At most composition expects at most X arguments to get passed into the instance.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class ServiceExample < CommandTower::ServiceBase
|
30
|
+
at_most 2, :name_of_composition, required: true do
|
31
|
+
validate :email, is_a: String
|
32
|
+
validate :phone, is_a: String
|
33
|
+
validate :username, is_a: String
|
34
|
+
end
|
35
|
+
|
36
|
+
def call; end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
rails-app(dev)> ServiceExample.(email: "email", phone: "phone", username: "username")
|
42
|
+
=> # Composite Key failure for name_of_composition [name_of_composition]. Expected at most 2 keys assigned. Provided values for the following keys: [:email, :phone, :username]. Available keys [:email, :phone, :username] (CommandTower::ServiceBase::CompositionValidationError)
|
43
|
+
```
|
44
|
+
|
45
|
+
### Composition: At Least
|
46
|
+
At least composition expects at least X arguments to get passed into the instance.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class ServiceExample < CommandTower::ServiceBase
|
50
|
+
at_least 2, :name_of_composition, required: true do
|
51
|
+
validate :email, is_a: String
|
52
|
+
validate :phone, is_a: String
|
53
|
+
validate :username, is_a: String
|
54
|
+
end
|
55
|
+
|
56
|
+
def call; end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
rails-app(dev)> ServiceExample.(email: "email")
|
62
|
+
=> # Composite Key failure for name_of_composition [name_of_composition]. Expected at least 2 keys assigned. Available keys. Provided values for the following keys: [:email]. Available keys [:email, :phone, :username] (CommandTower::ServiceBase::CompositionValidationError)
|
63
|
+
```
|
64
|
+
|
65
|
+
**Noteworthy**: `at_least` can take in any integer for its `count`. However, we found that most people just need one. For that reason, the convenience method of `at_least_one` was created. It can be used without the `count` argument in `at_least`
|
66
|
+
|
67
|
+
### Composition: Compose Exact
|
68
|
+
Compose Exact composition expects exactly X arguments to get passed into the instance. For this composition to be valid, there must be X or more validations.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class ServiceExample < CommandTower::ServiceBase
|
72
|
+
compose_exact 2, :name_of_composition, required: true do
|
73
|
+
validate :email, is_a: String
|
74
|
+
validate :phone, is_a: String
|
75
|
+
validate :username, is_a: String
|
76
|
+
end
|
77
|
+
|
78
|
+
def call; end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
```ruby
|
82
|
+
rails-app(dev)> ServiceExample.(email: "email")
|
83
|
+
=> # Composite Key failure for name_of_composition [name_of_composition]. Expected [2] of the keys to have a value assigned. But 1 keys were assigned. Provided values for the following keys: [:email]. Available keys [:email, :phone, :username] (CommandTower::ServiceBase::CompositionValidationError)
|
84
|
+
```
|
85
|
+
|
86
|
+
**Noteworthy**: `compose_exact` can take any `count` value to dynamically provision the exact component. However, we found that we almost only just needed count == 1. We have provided a convenience method of `one_of` without the `count` variable to simplify. There are quite a few examples of this already created
|
87
|
+
|
88
|
+
### Custom Compositions
|
89
|
+
All compositions are built on top of the same underlying function. This allows you to build additional compositions to add custom logic for validations and what not.
|
90
|
+
Check out the [ClassMethods Source Code](class_methods.rb) on what method arguments are required.
|
91
|
+
|
92
|
+
|
93
|
+
## Argument validation Failures
|
94
|
+
When an argument validation fails (whether that is a single `validate` or a composition), there are 3 options on what to do:
|
95
|
+
|
96
|
+
### Raise an error (Default)
|
97
|
+
As you can see in the examples above, the default for argument validation failures is to raise the following error
|
98
|
+
```ruby
|
99
|
+
CommandTower::ServiceBase::CompositionValidationError
|
100
|
+
```
|
101
|
+
|
102
|
+
The expected behavior is:
|
103
|
+
- Downstream code catches the failure and handles it correctly
|
104
|
+
- Service Logic code is not executed
|
105
|
+
|
106
|
+
This failure mode can get explicitly set via:
|
107
|
+
```ruby
|
108
|
+
class ServiceExample < CommandTower::ServiceBase
|
109
|
+
on_argument_validation :raise
|
110
|
+
|
111
|
+
one_of :name_of_composition, required: true do
|
112
|
+
validate :email, is_a: String
|
113
|
+
validate :phone, is_a: String
|
114
|
+
validate :username, is_a: String
|
115
|
+
end
|
116
|
+
|
117
|
+
def call; end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
### Fail the context Early (Recommended)
|
122
|
+
Failing the context early is we recommend to do for your service objects. This mode provides an exceptionally amount of context into **HOW** the validation failed and what needs to get corrected.
|
123
|
+
|
124
|
+
|
125
|
+
The expected behavior is:
|
126
|
+
- Downstream code checks for `result.failure?` and continues accordingly
|
127
|
+
- Service Logic code is not executed
|
128
|
+
- Nothing is raised
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
class ServiceExample < CommandTower::ServiceBase
|
132
|
+
on_argument_validation :fail_early
|
133
|
+
|
134
|
+
one_of :name_of_composition, required: true do
|
135
|
+
validate :email, is_a: String
|
136
|
+
validate :phone, is_a: String
|
137
|
+
validate :username, is_a: String
|
138
|
+
end
|
139
|
+
|
140
|
+
def call; end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
```ruby
|
144
|
+
result = ServiceExample.(email: :not_a_string)
|
145
|
+
if result.success?
|
146
|
+
else
|
147
|
+
if result.invalid_arguments
|
148
|
+
# context.fail! was called by argument validation
|
149
|
+
puts result.invalid_arguments
|
150
|
+
puts result.invalid_argument_hash
|
151
|
+
puts result.invalid_argument_keys
|
152
|
+
else
|
153
|
+
# context.fail! was called by user
|
154
|
+
end
|
155
|
+
end
|
156
|
+
=> true
|
157
|
+
=> {:email=>{:msg=>"Parameter [email] must be of type String. Given Symbol [not_a_string]", :required=>nil, :is_a=>String}}
|
158
|
+
=> [:email]
|
159
|
+
|
160
|
+
result = ServiceExample.()
|
161
|
+
result.invalid_arguments
|
162
|
+
=> true
|
163
|
+
result.invalid_argument_hash
|
164
|
+
=> {:name_of_composition=>{:msg=>"Composite Key failure for name_of_composition [name_of_composition]. Expected [1] of the keys to have a value assigned. But no key was assigned. Provided values for the following keys: []. Available keys [:email, :phone, :username]", :required=>nil, :is_a=>nil}}
|
165
|
+
result.invalid_argument_keys
|
166
|
+
=> [:name_of_composition]
|
167
|
+
|
168
|
+
result = ServiceExample.(email: 7, username: 8)
|
169
|
+
result.invalid_arguments
|
170
|
+
=> true
|
171
|
+
result.invalid_argument_hash
|
172
|
+
=> {:email=>{:msg=>"Parameter [email] must be of type String. Given Integer [7]", :required=>nil, :is_a=>String}, :username=>{:msg=>"Parameter [username] must be of type String. Given Integer [8]", :required=>nil, :is_a=>String}, :name_of_composition=>{:msg=>"Composite Key failure for name_of_composition [name_of_composition]. Expected [1] of the keys to have a value assigned. But 2 keys were assigned. Provided values for the following keys: [:email, :username]. Available keys [:email, :phone, :username]", :required=>nil, :is_a=>nil}}
|
173
|
+
=> [:email, :username, :name_of_composition]
|
174
|
+
```
|
175
|
+
|
176
|
+
### Log and Continue (Not Recommended)
|
177
|
+
This mode will allow you to log the validation failure and continue. We do not recommend this
|
178
|
+
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
class ServiceExample < CommandTower::ServiceBase
|
182
|
+
on_argument_validation :log
|
183
|
+
|
184
|
+
one_of :name_of_composition, required: true do
|
185
|
+
validate :email, is_a: String
|
186
|
+
validate :phone, is_a: String
|
187
|
+
validate :username, is_a: String
|
188
|
+
end
|
189
|
+
|
190
|
+
def call; end
|
191
|
+
end
|
192
|
+
```
|