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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/command_tower/admin_controller.rb +104 -0
  6. data/app/controllers/command_tower/application_controller.rb +81 -0
  7. data/app/controllers/command_tower/auth/plain_text_controller.rb +132 -0
  8. data/app/controllers/command_tower/inbox/message_blast_controller.rb +89 -0
  9. data/app/controllers/command_tower/inbox/message_controller.rb +79 -0
  10. data/app/controllers/command_tower/user_controller.rb +49 -0
  11. data/app/controllers/command_tower/username_controller.rb +26 -0
  12. data/app/helpers/command_tower/application_helper.rb +4 -0
  13. data/app/helpers/command_tower/schema_helper.rb +29 -0
  14. data/app/jobs/command_tower/application_job.rb +4 -0
  15. data/app/mailers/command_tower/application_mailer.rb +8 -0
  16. data/app/mailers/command_tower/email_verification_mailer.rb +12 -0
  17. data/app/models/command_tower/application_record.rb +45 -0
  18. data/app/models/message.rb +30 -0
  19. data/app/models/message_blast.rb +27 -0
  20. data/app/models/user.rb +61 -0
  21. data/app/models/user_secret.rb +72 -0
  22. data/app/services/command_tower/README.md +49 -0
  23. data/app/services/command_tower/argument_validation/README.md +192 -0
  24. data/app/services/command_tower/argument_validation/class_methods.rb +178 -0
  25. data/app/services/command_tower/argument_validation/instance_methods.rb +148 -0
  26. data/app/services/command_tower/argument_validation.rb +11 -0
  27. data/app/services/command_tower/authorize/validate.rb +49 -0
  28. data/app/services/command_tower/inbox_service/blast/delete.rb +23 -0
  29. data/app/services/command_tower/inbox_service/blast/metadata.rb +26 -0
  30. data/app/services/command_tower/inbox_service/blast/new_user_blaster.rb +24 -0
  31. data/app/services/command_tower/inbox_service/blast/retrieve.rb +30 -0
  32. data/app/services/command_tower/inbox_service/blast/upsert.rb +67 -0
  33. data/app/services/command_tower/inbox_service/message/metadata.rb +35 -0
  34. data/app/services/command_tower/inbox_service/message/modify.rb +44 -0
  35. data/app/services/command_tower/inbox_service/message/retrieve.rb +36 -0
  36. data/app/services/command_tower/inbox_service/message/send.rb +33 -0
  37. data/app/services/command_tower/jwt/authenticate_user.rb +86 -0
  38. data/app/services/command_tower/jwt/decode.rb +21 -0
  39. data/app/services/command_tower/jwt/encode.rb +15 -0
  40. data/app/services/command_tower/jwt/login_create.rb +21 -0
  41. data/app/services/command_tower/jwt/time_delay_token.rb +17 -0
  42. data/app/services/command_tower/login_strategy/plain_text/create.rb +43 -0
  43. data/app/services/command_tower/login_strategy/plain_text/email_verification/generate.rb +29 -0
  44. data/app/services/command_tower/login_strategy/plain_text/email_verification/required.rb +20 -0
  45. data/app/services/command_tower/login_strategy/plain_text/email_verification/send.rb +23 -0
  46. data/app/services/command_tower/login_strategy/plain_text/email_verification/verify.rb +24 -0
  47. data/app/services/command_tower/login_strategy/plain_text/login.rb +50 -0
  48. data/app/services/command_tower/secrets/cleanse.rb +14 -0
  49. data/app/services/command_tower/secrets/generate.rb +62 -0
  50. data/app/services/command_tower/secrets/verify.rb +27 -0
  51. data/app/services/command_tower/secrets.rb +15 -0
  52. data/app/services/command_tower/service_base.rb +89 -0
  53. data/app/services/command_tower/service_logging.rb +41 -0
  54. data/app/services/command_tower/user_attributes/modify.rb +68 -0
  55. data/app/services/command_tower/user_attributes/roles.rb +27 -0
  56. data/app/services/command_tower/username/available.rb +64 -0
  57. data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +26 -0
  58. data/config/routes.rb +55 -0
  59. data/db/migrate/20241117043720_create_command_tower_users.rb +42 -0
  60. data/db/migrate/20241204065708_create_command_tower_user_secrets.rb +16 -0
  61. data/db/migrate/20250223023306_create_command_tower_messages.rb +12 -0
  62. data/db/migrate/20250223023313_create_command_tower_message_blasts.rb +14 -0
  63. data/lib/command_tower/authorization/default.yml +42 -0
  64. data/lib/command_tower/authorization/entity.rb +101 -0
  65. data/lib/command_tower/authorization/role.rb +101 -0
  66. data/lib/command_tower/authorization.rb +85 -0
  67. data/lib/command_tower/configuration/admin/config.rb +18 -0
  68. data/lib/command_tower/configuration/application/config.rb +40 -0
  69. data/lib/command_tower/configuration/authorization/config.rb +24 -0
  70. data/lib/command_tower/configuration/base.rb +11 -0
  71. data/lib/command_tower/configuration/config.rb +77 -0
  72. data/lib/command_tower/configuration/email/config.rb +87 -0
  73. data/lib/command_tower/configuration/jwt/config.rb +22 -0
  74. data/lib/command_tower/configuration/login/config.rb +18 -0
  75. data/lib/command_tower/configuration/login/strategy/plain_text/config.rb +57 -0
  76. data/lib/command_tower/configuration/login/strategy/plain_text/email_verify.rb +50 -0
  77. data/lib/command_tower/configuration/login/strategy/plain_text/lockable.rb +27 -0
  78. data/lib/command_tower/configuration/otp/config.rb +54 -0
  79. data/lib/command_tower/configuration/user/config.rb +56 -0
  80. data/lib/command_tower/configuration/username/check.rb +31 -0
  81. data/lib/command_tower/configuration/username/config.rb +41 -0
  82. data/lib/command_tower/engine.rb +53 -0
  83. data/lib/command_tower/error.rb +5 -0
  84. data/lib/command_tower/schema/admin/users.rb +15 -0
  85. data/lib/command_tower/schema/error/base.rb +15 -0
  86. data/lib/command_tower/schema/error/invalid_argument.rb +15 -0
  87. data/lib/command_tower/schema/error/invalid_argument_response.rb +17 -0
  88. data/lib/command_tower/schema/inbox/blast_request.rb +15 -0
  89. data/lib/command_tower/schema/inbox/blast_response.rb +16 -0
  90. data/lib/command_tower/schema/inbox/message_blast_entity.rb +16 -0
  91. data/lib/command_tower/schema/inbox/message_blast_metadata.rb +16 -0
  92. data/lib/command_tower/schema/inbox/message_entity.rb +14 -0
  93. data/lib/command_tower/schema/inbox/metadata.rb +18 -0
  94. data/lib/command_tower/schema/inbox/modified.rb +13 -0
  95. data/lib/command_tower/schema/page.rb +14 -0
  96. data/lib/command_tower/schema/plain_text/create_user_request.rb +18 -0
  97. data/lib/command_tower/schema/plain_text/create_user_response.rb +17 -0
  98. data/lib/command_tower/schema/plain_text/email_verify_request.rb +11 -0
  99. data/lib/command_tower/schema/plain_text/email_verify_response.rb +11 -0
  100. data/lib/command_tower/schema/plain_text/email_verify_send_request.rb +9 -0
  101. data/lib/command_tower/schema/plain_text/email_verify_send_response.rb +11 -0
  102. data/lib/command_tower/schema/plain_text/login_request.rb +15 -0
  103. data/lib/command_tower/schema/plain_text/login_response.rb +13 -0
  104. data/lib/command_tower/schema/user.rb +28 -0
  105. data/lib/command_tower/schema.rb +38 -0
  106. data/lib/command_tower/spec_helper.rb +19 -0
  107. data/lib/command_tower/version.rb +5 -0
  108. data/lib/command_tower.rb +33 -0
  109. data/lib/generators/api_engine_base/configure/USAGE +8 -0
  110. data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
  111. data/lib/tasks/auto_annotate_models.rake +60 -0
  112. 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,4 @@
1
+ module CommandTower
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: "from@example.com"
6
+ layout "mailer"
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ class EmailVerificationMailer < ApplicationMailer
5
+ def verify_email(email, user, code)
6
+ subject = "Welcome to #{}"
7
+ @user = user
8
+ @code = code
9
+ mail(to: email, subject:)
10
+ end
11
+ end
12
+ 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
@@ -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
+ ```