api_engine_base 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/api_engine_base/application_controller.rb +47 -0
  6. data/app/controllers/api_engine_base/auth/plain_text_controller.rb +132 -0
  7. data/app/controllers/api_engine_base/username_controller.rb +26 -0
  8. data/app/controllers/concerns/api_engine_base/schematizable.rb +5 -0
  9. data/app/helpers/api_engine_base/application_helper.rb +4 -0
  10. data/app/helpers/api_engine_base/schema_helper.rb +29 -0
  11. data/app/jobs/api_engine_base/application_job.rb +4 -0
  12. data/app/mailers/api_engine_base/application_mailer.rb +8 -0
  13. data/app/mailers/api_engine_base/email_verification_mailer.rb +12 -0
  14. data/app/models/api_engine_base/application_record.rb +7 -0
  15. data/app/models/user.rb +50 -0
  16. data/app/models/user_secret.rb +72 -0
  17. data/app/services/api_engine_base/argument_validation/class_methods.rb +179 -0
  18. data/app/services/api_engine_base/argument_validation/instance_methods.rb +136 -0
  19. data/app/services/api_engine_base/argument_validation.rb +11 -0
  20. data/app/services/api_engine_base/jwt/authenticate_user.rb +71 -0
  21. data/app/services/api_engine_base/jwt/decode.rb +21 -0
  22. data/app/services/api_engine_base/jwt/encode.rb +15 -0
  23. data/app/services/api_engine_base/jwt/login_create.rb +21 -0
  24. data/app/services/api_engine_base/jwt/time_delay_token.rb +17 -0
  25. data/app/services/api_engine_base/login_strategy/plain_text/create.rb +42 -0
  26. data/app/services/api_engine_base/login_strategy/plain_text/email_verification/generate.rb +29 -0
  27. data/app/services/api_engine_base/login_strategy/plain_text/email_verification/required.rb +20 -0
  28. data/app/services/api_engine_base/login_strategy/plain_text/email_verification/send.rb +23 -0
  29. data/app/services/api_engine_base/login_strategy/plain_text/email_verification/verify.rb +24 -0
  30. data/app/services/api_engine_base/login_strategy/plain_text/login.rb +50 -0
  31. data/app/services/api_engine_base/secrets/cleanse.rb +14 -0
  32. data/app/services/api_engine_base/secrets/generate.rb +62 -0
  33. data/app/services/api_engine_base/secrets/verify.rb +27 -0
  34. data/app/services/api_engine_base/secrets.rb +15 -0
  35. data/app/services/api_engine_base/service_base.rb +90 -0
  36. data/app/services/api_engine_base/service_logging.rb +41 -0
  37. data/app/services/api_engine_base/username/available.rb +64 -0
  38. data/app/views/api_engine_base/email_verification_mailer/verify_email.html.erb +26 -0
  39. data/config/routes.rb +23 -0
  40. data/db/migrate/20241117043720_create_api_engine_base_users.rb +33 -0
  41. data/db/migrate/20241204065708_create_api_engine_base_user_secrets.rb +16 -0
  42. data/lib/api_engine_base/configuration/application/config.rb +40 -0
  43. data/lib/api_engine_base/configuration/base.rb +11 -0
  44. data/lib/api_engine_base/configuration/config.rb +59 -0
  45. data/lib/api_engine_base/configuration/email/config.rb +87 -0
  46. data/lib/api_engine_base/configuration/jwt/config.rb +22 -0
  47. data/lib/api_engine_base/configuration/login/config.rb +18 -0
  48. data/lib/api_engine_base/configuration/login/strategy/plain_text/config.rb +57 -0
  49. data/lib/api_engine_base/configuration/login/strategy/plain_text/email_verify.rb +50 -0
  50. data/lib/api_engine_base/configuration/login/strategy/plain_text/lockable.rb +27 -0
  51. data/lib/api_engine_base/configuration/otp/config.rb +54 -0
  52. data/lib/api_engine_base/configuration/username/check.rb +31 -0
  53. data/lib/api_engine_base/configuration/username/config.rb +41 -0
  54. data/lib/api_engine_base/engine.rb +21 -0
  55. data/lib/api_engine_base/schema/error/base.rb +15 -0
  56. data/lib/api_engine_base/schema/error/invalid_argument.rb +15 -0
  57. data/lib/api_engine_base/schema/error/invalid_argument_response.rb +17 -0
  58. data/lib/api_engine_base/schema/plain_text/create_user_request.rb +18 -0
  59. data/lib/api_engine_base/schema/plain_text/create_user_response.rb +17 -0
  60. data/lib/api_engine_base/schema/plain_text/email_verify_request.rb +11 -0
  61. data/lib/api_engine_base/schema/plain_text/email_verify_response.rb +11 -0
  62. data/lib/api_engine_base/schema/plain_text/email_verify_send_request.rb +9 -0
  63. data/lib/api_engine_base/schema/plain_text/email_verify_send_response.rb +11 -0
  64. data/lib/api_engine_base/schema/plain_text/login_request.rb +15 -0
  65. data/lib/api_engine_base/schema/plain_text/login_response.rb +13 -0
  66. data/lib/api_engine_base/schema.rb +25 -0
  67. data/lib/api_engine_base/spec_helper.rb +18 -0
  68. data/lib/api_engine_base/version.rb +5 -0
  69. data/lib/api_engine_base.rb +33 -0
  70. data/lib/generators/api_engine_base/configure/USAGE +8 -0
  71. data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
  72. data/lib/tasks/auto_annotate_models.rake +60 -0
  73. metadata +216 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 237d043d261233c9e45d4b7466dcacbfb4792636246fa2e211e765ee21b75141
4
+ data.tar.gz: 55d598e1abaf7a12a253fbc9ea8d010c9a11ac9ca5ea293e2a30f6cb42ffc759
5
+ SHA512:
6
+ metadata.gz: 53180702bea719474e8f9922858da3c1af7b6c44ce3ad1db78bb5c633c039b9b5bb7a524984a637e41dd16499478f896731f033a8c8beece0441eff76ac40fa9
7
+ data.tar.gz: '03397ea3efb5e7a68554ae600d4eda0d0f467d2990a14dbb4e1bf2ea2cd13a96e82abd679894e2943c51e097eab095c10434116303c1004404626d84d2805c5a'
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # ApiEngineBase
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "api_engine_base"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install api_engine_base
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ApiEngineBase'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("rails_app/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiEngineBase
4
+ class ApplicationController < ActionController::API
5
+ AUTHORIZATION_HEADER = "AUTHORIZATION"
6
+
7
+ ###
8
+ # AUTHORIZATION_HEADER="Bearer: {token value}"
9
+ def authenticate_user!(bypass_email_validation: false)
10
+ raw_token = request.headers[AUTHORIZATION_HEADER]
11
+ if raw_token.nil?
12
+ status = 401
13
+ schema = ApiEngineBase::Schema::Error::Base.new(status:, message: "Bearer token missing")
14
+ render(json: schema.to_h, status:)
15
+ return false
16
+ end
17
+
18
+ token = raw_token.split("Bearer:")[1].strip
19
+ result = ApiEngineBase::Jwt::AuthenticateUser.(token:, bypass_email_validation:)
20
+ if result.success?
21
+ @current_user = result.user
22
+ true
23
+ else
24
+ status = 401
25
+ schema = ApiEngineBase::Schema::Error::Base.new(status:, message: result.msg)
26
+ render(json: schema.to_h, status:)
27
+ # Must return false so callbacks know to halt propagation
28
+ false
29
+ end
30
+ end
31
+
32
+ def authenticate_user_without_email_verification!
33
+ authenticate_user!(bypass_email_validation: true)
34
+ end
35
+
36
+ def current_user
37
+ @current_user ||= nil
38
+ end
39
+
40
+ def add_to_body
41
+ # {
42
+ # token_valid_till:,
43
+ # needs_email_verification:,
44
+ # }
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,132 @@
1
+ module ApiEngineBase
2
+ module Auth
3
+ class PlainTextController < ::ApiEngineBase::ApplicationController
4
+ include ApiEngineBase::SchemaHelper
5
+
6
+ before_action :authenticate_user_without_email_verification!, only: [:email_verify_post, :email_verify_resend_post]
7
+
8
+ # POST /auth/login
9
+ # Login to the application and create/set the JWT token
10
+ def login_post
11
+ result = ApiEngineBase::LoginStrategy::PlainText::Login.(**login_params)
12
+ if result.success?
13
+ schema = ApiEngineBase::Schema::PlainText::LoginResponse.new(
14
+ token: result.token,
15
+ header_name: AUTHORIZATION_HEADER,
16
+ message: "Successfully logged user in"
17
+ )
18
+ status = 201
19
+ schema_succesful!(status:, schema:)
20
+ else
21
+ if result.invalid_arguments
22
+ invalid_arguments!(
23
+ status: 401,
24
+ message: result.msg,
25
+ argument_object: result.invalid_argument_hash,
26
+ schema: ApiEngineBase::Schema::PlainText::LoginRequest
27
+ )
28
+ else
29
+ json_result = { msg: result.msg }
30
+ status = 400
31
+ render(json: schema.to_h, status:)
32
+ end
33
+ end
34
+ end
35
+
36
+ # POST /auth/create
37
+ # New PlainText user creation
38
+ def create_post
39
+ result = ApiEngineBase::LoginStrategy::PlainText::Create.(**create_params)
40
+ if result.success?
41
+ schema = ApiEngineBase::Schema::PlainText::CreateUserResponse.new(
42
+ full_name: result.user.full_name,
43
+ first_name: result.first_name,
44
+ last_name: result.last_name,
45
+ username: result.username,
46
+ email: result.email,
47
+ msg: "Successfully created new User",
48
+ )
49
+ status = 201
50
+ schema_succesful!(status:, schema:)
51
+ else
52
+ if result.invalid_arguments
53
+ invalid_arguments!(
54
+ status: 400,
55
+ message: result.msg,
56
+ argument_object: result.invalid_argument_hash,
57
+ schema: ApiEngineBase::Schema::PlainText::CreateUserRequest
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ # POST /auth/email/verify
64
+ # Verifies a logged in users email verification code when enabled
65
+ def email_verify_post
66
+ if current_user.email_validated
67
+ schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Email is already verified.")
68
+ status = 200
69
+ schema_succesful!(status:, schema:)
70
+ else
71
+ result = ApiEngineBase::LoginStrategy::PlainText::EmailVerification::Verify.(user: current_user, code: params[:code])
72
+ if result.success?
73
+ schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Successfully verified email")
74
+ status = 201
75
+ schema_succesful!(status:, schema:)
76
+ else
77
+ if result.invalid_arguments
78
+ invalid_arguments!(
79
+ status: result.status || 403,
80
+ message: result.msg,
81
+ argument_object: result.invalid_argument_hash,
82
+ schema: ApiEngineBase::Schema::PlainText::EmailVerifyRequest
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ # POST /auth/email/send
90
+ # Sends a logged in users email verification code
91
+ def email_verify_resend_post
92
+ if current_user.email_validated
93
+ schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Email is already verified. No code required")
94
+ status = 200
95
+ schema_succesful!(status:, schema:)
96
+ else
97
+ result = ApiEngineBase::LoginStrategy::PlainText::EmailVerification::Send.(user: current_user)
98
+ if result.success?
99
+ schema = ApiEngineBase::Schema::PlainText::EmailVerifyResponse.new(message: "Successfully sent Email verification code")
100
+ status = 201
101
+ schema_succesful!(status:, schema:)
102
+ else
103
+ schema = ApiEngineBase::Schema::Error::Base.new(status:, message: result.msg)
104
+ status = result.status || 401
105
+ render(json: schema.to_h, status:)
106
+ end
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def login_params
113
+ {
114
+ username: params[:username],
115
+ email: params[:email],
116
+ password: params[:password],
117
+ }
118
+ end
119
+
120
+ def create_params
121
+ {
122
+ first_name: params[:first_name],
123
+ last_name: params[:last_name],
124
+ username: params[:username],
125
+ email: params[:email],
126
+ password: params[:password],
127
+ password_confirmation: params[:password_confirmation],
128
+ }
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,26 @@
1
+ module ApiEngineBase
2
+ class UsernameController < ::ApiEngineBase::ApplicationController
3
+
4
+ # GET /username/available/:username
5
+ def username_availability
6
+ result = ApiEngineBase::Username::Available.(username: params[:username])
7
+
8
+ if result.success?
9
+ json_result = {
10
+ username: {
11
+ available: result.available,
12
+ valid: result.valid,
13
+ description: ApiEngineBase.config.username.username_failure_message
14
+ }
15
+ }
16
+ status = 200
17
+ else
18
+ json_result = { msg: result.msg }
19
+ json_result[:invalid_arguments] = true if result.invalid_arguments
20
+ status = 401
21
+ end
22
+
23
+ render json: json_result, status: status
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ module ApiEngineBase
2
+ class Schematizable
3
+ end
4
+ end
5
+
@@ -0,0 +1,4 @@
1
+ module ApiEngineBase
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiEngineBase
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
+ ApiEngineBase::Schema::Error::InvalidArgument.new(
12
+ schema:,
13
+ argument: key,
14
+ argument_type: metadata[:type],
15
+ reason: metadata[:msg],
16
+ )
17
+ end
18
+
19
+ result = ApiEngineBase::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 ApiEngineBase
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiEngineBase
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 ApiEngineBase
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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiEngineBase
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,50 @@
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
+ # successful_login :integer default(0)
20
+ # username :string(255)
21
+ # verifier_token :string(255)
22
+ # verifier_token_last_reset :datetime
23
+ # created_at :datetime not null
24
+ # updated_at :datetime not null
25
+ #
26
+ # Indexes
27
+ #
28
+ # index_users_on_username (username) UNIQUE
29
+ #
30
+ require "securerandom"
31
+
32
+ class User < ApiEngineBase::ApplicationRecord
33
+ has_secure_password
34
+
35
+ validates :username, uniqueness: true
36
+ validates :email, uniqueness: true
37
+
38
+ def full_name
39
+ "#{first_name} #{last_name}"
40
+ end
41
+
42
+ def retreive_verifier_token!
43
+ return verifier_token if verifier_token
44
+
45
+ value = SecureRandom.alphanumeric(32)
46
+ update!(verifier_token: value)
47
+
48
+ value
49
+ end
50
+ 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,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiEngineBase::ArgumentValidation
4
+ module ClassMethods
5
+ ON_ARGUMENT_VALIDATION = [
6
+ DEFAULT_VALIDATION = :raise,
7
+ :fail_early,
8
+ :log,
9
+ ]
10
+
11
+ def on_argument_validation_assigned
12
+ @on_argument_validation ||= DEFAULT_VALIDATION
13
+ end
14
+
15
+ def on_argument_validation(set)
16
+ raise "Must be one of #{ON_ARGUMENT_VALIDATION}" unless ON_ARGUMENT_VALIDATION.include?(set)
17
+
18
+ @on_argument_validation = set
19
+ end
20
+
21
+ def compose_exact(count, name, required:, delegation: false, &block)
22
+ raise ApiEngineBase::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
23
+
24
+ validation_proc = Proc.new do |input_count, keys|
25
+
26
+ language = (input_count > 1) ? "But more than #{count} did" : "But no key had a value"
27
+ {
28
+ message: "Expected [#{count}] of the keys to have a value assigned. #{language}",
29
+ is_valid: (input_count == count),
30
+ requirement: "Exactly #{count} key(s) of #{keys} must be provided.",
31
+ }
32
+ end
33
+
34
+ composition_validation_proc = Proc.new do
35
+ keys = compositions[__stacked_type.last[:name]][:keys]
36
+ if keys.length < count
37
+ raise ApiEngineBase::ServiceBase::CompositionValidationError, "Composition configuration error. Key [#{name}] expects EXACTLY #{count} keys to create an instance. Only #{keys.length} is provided for the composition. Please add more keys or reduce the expectation"
38
+ end
39
+ end
40
+ composition(name:, type: :compose_exact, required:, delegation:, composition_validation_proc:, validation_proc:, &block)
41
+ end
42
+
43
+ def at_most(count, name, required:, &block)
44
+ raise ApiEngineBase::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
45
+
46
+ validation_proc = Proc.new do |input_count, keys|
47
+ {
48
+ message: "Expected at most #{count} keys assigned",
49
+ is_valid: (input_count <= count),
50
+ requirement: "Validation Error: At most #{count} key(s) of #{keys} can be provided.",
51
+ }
52
+ end
53
+
54
+ composition(name:, type: :at_most, required:, delegation: false, validation_proc:, &block)
55
+ end
56
+
57
+ def at_least(count, name, required:, &block)
58
+ raise ApiEngineBase::ServiceBase::CompositionValidationError, "Count must be greater than 0" if count < 1
59
+
60
+ validation_proc = Proc.new do |input_count, keys|
61
+ {
62
+ message: "Expected at least #{count} keys assigned. Available keys",
63
+ is_valid: (input_count >= count),
64
+ requirement: "Validation Error: At least #{count} key(s) of #{keys} must be provided.",
65
+ }
66
+ end
67
+
68
+ composition_validation_proc = Proc.new do
69
+ keys = compositions[__stacked_type.last[:name]][:keys]
70
+ if keys.length < count
71
+ raise ApiEngineBase::ServiceBase::CompositionValidationError, "Composition configuration error. Key [#{name}] expects AT LEAST #{count} keys to create an instance. Only #{keys.length} is provided for the composition. Please add more keys or reduce the expectation"
72
+ end
73
+ end
74
+ composition(name:, type: :at_least, required:, delegation: false, composition_validation_proc:, validation_proc:, &block)
75
+ end
76
+
77
+ def at_least_one(name, required:, &block)
78
+ at_least(1, name, required:, &block)
79
+ end
80
+
81
+ def one_of(name, required:, delegation: true, &block)
82
+ compose_exact(1, name, required:, delegation:, &block)
83
+ end
84
+
85
+ def composition(name:, type:, required:, delegation:, validation_proc:, composition_validation_proc: nil, &block)
86
+ compositions[name] ||= { type:, name:, keys: [], required:, delegation:, validation_proc: }
87
+ if __stacked_type.map { _1[:type] }.include?(type)
88
+ raise ApiEngineBase::ServiceBase::NestedDuplicateTypeError, "Duplicate Nested type's are not allowed. #{type} composition was included more than once"
89
+ end
90
+
91
+ __stacked_type << { type:, name: }
92
+
93
+ yield
94
+
95
+ if composition_validation_proc
96
+ composition_validation_proc.()
97
+ end
98
+
99
+ if __existing_names.include?(name)
100
+ raise ApiEngineBase::ServiceBase::NameConflictError, "Name conflict for #{name}. Duplicated as a key"
101
+ end
102
+
103
+ __existing_names << name
104
+ # returning from the yield...pop it from the stack.
105
+ # This allows us to know the current depth of where we are in the nested stack
106
+ __stacked_type.pop
107
+
108
+ if delegation
109
+ delegate name, to: :context
110
+ delegate :"#{name}_key", to: :context
111
+ end
112
+ end
113
+
114
+ def validate(name, default: nil, length: false, matches: nil, is_a: nil, is_one: nil, lt: nil, lte: nil, eq: nil, gt: nil, gte: nil, delegation: true, sensitive: false, required: false)
115
+ if __existing_names.include?(name)
116
+ raise ApiEngineBase::ServiceBase::NameConflictError, "Duplicate key name found. [#{name}] can only be defined once"
117
+ end
118
+
119
+ __existing_names << name
120
+
121
+ if default
122
+ if is_a
123
+ if Array(is_a).none? { _1 === default }
124
+ raise ApiEngineBase::ServiceBase::DefaultValueError, "Default value provided [#{default}] does not match any `is_a` value(s) of #{is_a}."
125
+ end
126
+ end
127
+
128
+ if is_one
129
+ if Array(is_one).none? { _1 == default }
130
+ raise ApiEngineBase::ServiceBase::DefaultValueError, "Default value provided [#{default}] does not match any `is_one` value(s) of #{is_one}."
131
+ end
132
+ end
133
+ end
134
+
135
+ if __stacked_type.length > 0
136
+ compositions[__stacked_type.last[:name]][:keys] << name
137
+ end
138
+
139
+ validate_params << {
140
+ name:,
141
+ is_a:,
142
+ lt:,
143
+ lte:,
144
+ eq:,
145
+ gt:,
146
+ gte:,
147
+ length:,
148
+ required:,
149
+ is_one:,
150
+ default:,
151
+ }
152
+ sensitive_params << name if sensitive
153
+
154
+ if delegation
155
+ delegate name, to: :context
156
+ end
157
+ end
158
+
159
+ def sensitive_params
160
+ @sensitive_params ||= []
161
+ end
162
+
163
+ def validate_params
164
+ @validate_params ||= []
165
+ end
166
+
167
+ def compositions
168
+ @compositions ||= {}
169
+ end
170
+
171
+ def __stacked_type
172
+ @__stacked_type ||= []
173
+ end
174
+
175
+ def __existing_names
176
+ @__existing_names ||= []
177
+ end
178
+ end
179
+ end