rwc 0.1.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/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +54 -0
- data/Rakefile +12 -0
- data/lib/generators/rwc/decorator/decorator_generator.rb +13 -0
- data/lib/generators/rwc/input/input_generator.rb +22 -0
- data/lib/generators/rwc/service/service_generator.rb +15 -0
- data/lib/generators/rwc/templates/create_input.rb.tt +11 -0
- data/lib/generators/rwc/templates/decorator.rb.tt +5 -0
- data/lib/generators/rwc/templates/query_input.rb.tt +6 -0
- data/lib/generators/rwc/templates/service.rb.tt +19 -0
- data/lib/rwc/concerns/error_handling.rb +82 -0
- data/lib/rwc/concerns/validation.rb +163 -0
- data/lib/rwc/core/base_decorator.rb +54 -0
- data/lib/rwc/core/base_input.rb +95 -0
- data/lib/rwc/core/base_service.rb +41 -0
- data/lib/rwc/core/concerns/error_handling.rb +84 -0
- data/lib/rwc/core/concerns/validation.rb +164 -0
- data/lib/rwc/core/context.rb +75 -0
- data/lib/rwc/core/queries/simple_input.rb +61 -0
- data/lib/rwc/version.rb +5 -0
- data/lib/rwc.rb +9 -0
- data/rwc.gemspec +38 -0
- data/sig/rwc.rbs +4 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ce1d3bb1432f4f7649493b3375e1b4e5e324c1cabb11ea5027993352d46d13a5
|
|
4
|
+
data.tar.gz: cabc69a6e3ad53158220ed03ddbc8e3e1547df18cb23a2f2bf5322351472d169
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4deb613f2ccb4e672da2e6b92d8754f194d69712f990dba8cf7bc2f2b978116b28945a277f3fcb20b2dfdc6d52473d4c8112c265d4d0a83cfa9d44fadc811ac9
|
|
7
|
+
data.tar.gz: a735528f5941b9d0a473de7bfc6c42b29171ef822ec4bb7b4a96fb9366077591cf5a3db8e774c3c93d0371b393a793ed65f482f3dc228b1c2bb3469bcb4ce767
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
|
8
|
+
|
|
9
|
+
## Our Standards
|
|
10
|
+
|
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
|
12
|
+
|
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
|
22
|
+
advances of any kind
|
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
24
|
+
* Public or private harassment
|
|
25
|
+
* Publishing others' private information, such as a physical or email
|
|
26
|
+
address, without their explicit permission
|
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
28
|
+
professional setting
|
|
29
|
+
|
|
30
|
+
## Enforcement Responsibilities
|
|
31
|
+
|
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
|
33
|
+
|
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
|
35
|
+
|
|
36
|
+
## Scope
|
|
37
|
+
|
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
|
39
|
+
|
|
40
|
+
## Enforcement
|
|
41
|
+
|
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hasstrup.ezekiel@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
|
|
43
|
+
|
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
|
45
|
+
|
|
46
|
+
## Enforcement Guidelines
|
|
47
|
+
|
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
|
49
|
+
|
|
50
|
+
### 1. Correction
|
|
51
|
+
|
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
|
53
|
+
|
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
|
55
|
+
|
|
56
|
+
### 2. Warning
|
|
57
|
+
|
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
|
59
|
+
|
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
|
61
|
+
|
|
62
|
+
### 3. Temporary Ban
|
|
63
|
+
|
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
|
65
|
+
|
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
|
67
|
+
|
|
68
|
+
### 4. Permanent Ban
|
|
69
|
+
|
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
|
71
|
+
|
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
|
73
|
+
|
|
74
|
+
## Attribution
|
|
75
|
+
|
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
78
|
+
|
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|
80
|
+
|
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
|
82
|
+
|
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TODO: Write your name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
# RwC (Rails with Context!)
|
|
3
|
+
|
|
4
|
+
This gem helps with generating services, inputs, decorators for Context-Based Rails Applications - A hexagonal way to build rails applications. Wrote about this [here](https://medium.com/@HasstrupEzekiel/context-based-programming-in-rails-0ce951a59c36).
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'rwc'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle install
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install rwc
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# generating services
|
|
27
|
+
rails generate rwc:service AnExampleService
|
|
28
|
+
|
|
29
|
+
# generating inputs
|
|
30
|
+
rails generate rwc:input AnExampleInput::WithNameSpace
|
|
31
|
+
|
|
32
|
+
# generating decorators
|
|
33
|
+
rails generate rwc:decorator AnExampleDecorator::WithDecorators
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Development
|
|
37
|
+
|
|
38
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
39
|
+
|
|
40
|
+
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).
|
|
41
|
+
|
|
42
|
+
## Contributing
|
|
43
|
+
|
|
44
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/hasstrup/rwc). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
49
|
+
|
|
50
|
+
## Code of Conduct
|
|
51
|
+
|
|
52
|
+
Everyone interacting in the RwC project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
53
|
+
|
|
54
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Rwc
|
|
6
|
+
class DecoratorGenerator < Rails::Generators::NamedBase
|
|
7
|
+
source_root File.expand_path("../templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def create_decorator_file
|
|
10
|
+
template "decorator.rb.tt", "app/decorators/#{class_name.underscore}.rb"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwc
|
|
4
|
+
class InputGenerator < Rails::Generators::NamedBase
|
|
5
|
+
QUERY_TYPE = "query"
|
|
6
|
+
source_root File.expand_path("../templates", __dir__)
|
|
7
|
+
|
|
8
|
+
class_option :input_type, type: :string, default: "base",
|
|
9
|
+
desc: "One of query/create"
|
|
10
|
+
|
|
11
|
+
def create_input_file
|
|
12
|
+
source = query_input? ? "query_input.rb.tt" : "create_input.rb.tt"
|
|
13
|
+
template(source, "app/inputs/#{class_name.underscore}.rb")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def query_input?
|
|
19
|
+
class_name.downcase.include?(QUERY_TYPE) || options[:input_type] == QUERY_TYPE
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Rwc
|
|
6
|
+
class ServiceGenerator < ::Rails::Generators::NamedBase
|
|
7
|
+
source_root File.expand_path("../templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def create_service_file
|
|
10
|
+
template "service.rb.tt", "app/services/#{class_name.underscore}.rb"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %> < Rwc::Core::BaseService
|
|
4
|
+
performs_checks
|
|
5
|
+
|
|
6
|
+
def initialize(input:)
|
|
7
|
+
@input = input
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
safely_execute do
|
|
12
|
+
# logic goes in here
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :input
|
|
19
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
# The ErrorHandling module provides mechanisms for managing errors
|
|
6
|
+
# within service classes. It includes methods for safely executing
|
|
7
|
+
# blocks of code and handling known error classes.
|
|
8
|
+
module Rwc
|
|
9
|
+
module Concerns
|
|
10
|
+
module ErrorHandling
|
|
11
|
+
class InvalidInputError < StandardError; end
|
|
12
|
+
class ServiceError < StandardError; end
|
|
13
|
+
|
|
14
|
+
module Core
|
|
15
|
+
# A list of error classes that can be rescued during safe execution.
|
|
16
|
+
ERROR_CLASSES = [
|
|
17
|
+
ActiveRecord::RecordNotFound,
|
|
18
|
+
ActiveRecord::RecordInvalid,
|
|
19
|
+
ActiveModel::UnknownAttributeError,
|
|
20
|
+
ActiveRecord::StatementInvalid,
|
|
21
|
+
InvalidInputError,
|
|
22
|
+
ServiceError
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
# Safely executes a block of code, running checks if needed.
|
|
26
|
+
#
|
|
27
|
+
# @yield [void] The block of code to execute.
|
|
28
|
+
# @return [Context] The context after execution.
|
|
29
|
+
def safely_execute(&block)
|
|
30
|
+
run_checks! if should_run_checks?
|
|
31
|
+
block.call
|
|
32
|
+
context
|
|
33
|
+
rescue *ERROR_CLASSES => e
|
|
34
|
+
fail!(error: e)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Runs input validation checks. Must be implemented by the including class.
|
|
38
|
+
#
|
|
39
|
+
# @raise [NotImplementedError] When not implemented in a subclass.
|
|
40
|
+
def run_checks!
|
|
41
|
+
raise InvalidInputError, input.errors.flat_map(&:message) unless input.valid?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module Validatable
|
|
46
|
+
module ClassMethods
|
|
47
|
+
# Indicates that checks should be performed on input validation.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def performs_checks
|
|
51
|
+
@should_run_checks = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :should_run_checks
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Checks if validation checks should be performed.
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] True if checks should be performed; otherwise, false.
|
|
60
|
+
def should_run_checks?
|
|
61
|
+
self.class.should_run_checks
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Includes core error handling functionality and class methods.
|
|
65
|
+
#
|
|
66
|
+
# @param [Class] base The class to include the module in.
|
|
67
|
+
def self.included(base)
|
|
68
|
+
base.include(Core)
|
|
69
|
+
base.extend(ClassMethods)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Raises a service error with the specified message.
|
|
73
|
+
#
|
|
74
|
+
# @param [String] message The error message to raise.
|
|
75
|
+
# @raise [BaseService::ServiceError] The raised error.
|
|
76
|
+
def raise_error!(message)
|
|
77
|
+
raise ServiceError, message
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwc
|
|
4
|
+
module Concerns
|
|
5
|
+
# Provides validation methods for input classes.
|
|
6
|
+
module Validation
|
|
7
|
+
# Checks if the input is valid by running validations.
|
|
8
|
+
#
|
|
9
|
+
# @return [Boolean] True if valid; otherwise, false.
|
|
10
|
+
def valid?
|
|
11
|
+
validate!
|
|
12
|
+
errors.empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Validates the input. Must be implemented by including class.
|
|
16
|
+
#
|
|
17
|
+
# @raise [NotImplementedError] If not implemented.
|
|
18
|
+
def validate!
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Pipes an error into the errors array.
|
|
25
|
+
#
|
|
26
|
+
# @param [StandardError] error The error to be added.
|
|
27
|
+
def pipe_error(error)
|
|
28
|
+
@errors << error
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Validates the presence of a given key in the input.
|
|
32
|
+
#
|
|
33
|
+
# @param [Symbol] key The key to validate.
|
|
34
|
+
def validate_presence_of(key)
|
|
35
|
+
pipe_error(BaseInput::InputValidationError.new("#{key} is missing")) if input.send(key).blank?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Creates an error with a specified message.
|
|
39
|
+
#
|
|
40
|
+
# @param [String] message The error message.
|
|
41
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
42
|
+
def error(message)
|
|
43
|
+
BaseInput::InputValidationError.new(message)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Validates polymorphic associations based on type and ID.
|
|
47
|
+
#
|
|
48
|
+
# @param [String] type The type of the association.
|
|
49
|
+
# @param [Integer] id The ID of the associated record.
|
|
50
|
+
# @param [Boolean] conditional Optional. If true, skips validation if id is blank.
|
|
51
|
+
def validate_polymorphic_association!(type, id, conditional: false)
|
|
52
|
+
within_error_context do
|
|
53
|
+
return if conditional && id.blank?
|
|
54
|
+
|
|
55
|
+
pipe_error(relation_not_found_error(type, id)) unless type.constantize.find_by(id:)
|
|
56
|
+
rescue NameError
|
|
57
|
+
pipe_error(invalid_type_error(type))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validates an association by checking its existence.
|
|
62
|
+
#
|
|
63
|
+
# @param [Integer] id The ID of the associated record.
|
|
64
|
+
# @param [Class] klass The class of the association.
|
|
65
|
+
# @param [Boolean] conditional Optional. If true, skips validation if id is blank.
|
|
66
|
+
def validate_association!(id, klass, conditional: false)
|
|
67
|
+
within_error_context do
|
|
68
|
+
return if conditional && id.blank?
|
|
69
|
+
|
|
70
|
+
pipe_error(relation_not_found_error(klass.to_s, id)) unless klass.exists?(id:)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Validates the existence of a record associated with the input.
|
|
75
|
+
#
|
|
76
|
+
# @raise [BaseInput::InputValidationError] If validation fails.
|
|
77
|
+
def validate_record!
|
|
78
|
+
within_error_context do
|
|
79
|
+
validate_association!(id, self.class.target) if id
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Validates required keys for the input.
|
|
84
|
+
#
|
|
85
|
+
# @raise [BaseInput::InputValidationError] If any required keys are missing.
|
|
86
|
+
def validate_required_keys!
|
|
87
|
+
within_error_context do
|
|
88
|
+
self.class::REQUIRED_KEYS.each do |key|
|
|
89
|
+
validate_presence_of(key)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Validates the ownership of a record by a user.
|
|
95
|
+
#
|
|
96
|
+
# @param [ActiveRecord::Base] record The record to check ownership against.
|
|
97
|
+
# @param [Integer] user_id The ID of the user to validate ownership.
|
|
98
|
+
def validate_ownership!(record, user_id)
|
|
99
|
+
within_error_context do
|
|
100
|
+
pipe_error(restricted_access_error) unless owner_for(record)&.id == user_id
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Executes a block if there are no errors.
|
|
105
|
+
#
|
|
106
|
+
# @yield The block to execute.
|
|
107
|
+
def within_error_context(&block)
|
|
108
|
+
block.call if errors.empty?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Collates errors from specified keys into the errors array.
|
|
112
|
+
#
|
|
113
|
+
# @param [Array<Symbol>] keys The keys to collate errors for.
|
|
114
|
+
def collate_errors_for(*keys)
|
|
115
|
+
keys.each do |key|
|
|
116
|
+
within_error_context do
|
|
117
|
+
target = send(key)
|
|
118
|
+
next @errors += target.errors if target.respond_to?(:valid?) && !target.valid?
|
|
119
|
+
|
|
120
|
+
target.each do |node|
|
|
121
|
+
within_error_context do
|
|
122
|
+
@errors += node.errors unless node.valid?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Generates an error for an invalid association type.
|
|
130
|
+
#
|
|
131
|
+
# @param [String] type The invalid type.
|
|
132
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
133
|
+
def invalid_type_error(type)
|
|
134
|
+
BaseInput::InputValidationError.new("Invalid association type: #{type.capitalize}")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Generates an error when a relation cannot be found.
|
|
138
|
+
#
|
|
139
|
+
# @param [String] type The type of the relation.
|
|
140
|
+
# @param [Integer] id The ID of the relation.
|
|
141
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
142
|
+
def relation_not_found_error(type, id)
|
|
143
|
+
BaseInput::InputValidationError.new("Could not find any '#{type.capitalize}' with id: #{id}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Generates an error for restricted access.
|
|
147
|
+
#
|
|
148
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
149
|
+
def restricted_access_error
|
|
150
|
+
BaseInput::InputValidationError.new('Unauthorized access')
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Generates an error for a missing field.
|
|
154
|
+
#
|
|
155
|
+
# @param [Symbol] field The missing field.
|
|
156
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
157
|
+
def missing_field_error(field)
|
|
158
|
+
BaseInput::InputValidationError.new("#{field} is missing")
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Rwc
|
|
6
|
+
module Core
|
|
7
|
+
class BaseDecorator
|
|
8
|
+
attr_accessor :context
|
|
9
|
+
attr_reader :object
|
|
10
|
+
|
|
11
|
+
def initialize(object, opts = {})
|
|
12
|
+
@object = object
|
|
13
|
+
@context = OpenStruct.new(**opts)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
attr_reader :extra
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def decorate_collection(collection:, opts: {})
|
|
22
|
+
collection.map { |object| new(object, opts) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delegate_all
|
|
26
|
+
define_method(:method_missing) do |method_name, *args|
|
|
27
|
+
return object.send(method_name, *args) if object.respond_to?(method_name)
|
|
28
|
+
|
|
29
|
+
super(method_name, *args)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def include_in_json(*methods)
|
|
34
|
+
define_method(:as_json) do |*args|
|
|
35
|
+
included_hash = methods.index_with do |method|
|
|
36
|
+
send(method)
|
|
37
|
+
end
|
|
38
|
+
object.send(:as_json, *args).merge(included_hash)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def decorate(relation, context: {})
|
|
43
|
+
new(relation, context)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
def auto_delegate(*methods)
|
|
49
|
+
delegate(*methods, to: :object)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "concerns/validation"
|
|
4
|
+
require "ostruct"
|
|
5
|
+
|
|
6
|
+
module Rwc
|
|
7
|
+
module Core
|
|
8
|
+
# Represents a base input class for handling and validating input data.
|
|
9
|
+
class BaseInput
|
|
10
|
+
class InputValidationError < StandardError; end
|
|
11
|
+
|
|
12
|
+
include Concerns::Validation
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_reader :fields, :target
|
|
16
|
+
|
|
17
|
+
# Defines the attributes that the input class can accept.
|
|
18
|
+
#
|
|
19
|
+
# @param [Symbol] args The attributes to be defined for the input.
|
|
20
|
+
def attributes(*args)
|
|
21
|
+
@fields = args
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Specifies the target class for polymorphic associations.
|
|
25
|
+
#
|
|
26
|
+
# @param [Class] target_klass The target class for validation.
|
|
27
|
+
def input_for(target_klass)
|
|
28
|
+
@target = target_klass
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader :errors
|
|
33
|
+
|
|
34
|
+
# Initializes a new BaseInput instance.
|
|
35
|
+
#
|
|
36
|
+
# @param [Hash] kwargs The keyword arguments for the input.
|
|
37
|
+
# @option kwargs [Object] :context Additional context for the input validation.
|
|
38
|
+
def initialize(**kwargs)
|
|
39
|
+
@input = OpenStruct.new(**kwargs.except(:context))
|
|
40
|
+
@context = kwargs[:context]
|
|
41
|
+
@valid = false
|
|
42
|
+
@errors = []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns human-readable error messages from the errors array.
|
|
46
|
+
#
|
|
47
|
+
# @return [String] The humanized error messages.
|
|
48
|
+
def humanized_error_messages
|
|
49
|
+
errors.map(&:message).join(", ")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Delegates method calls to the input object.
|
|
53
|
+
#
|
|
54
|
+
# @param [Symbol] name The method name to be called.
|
|
55
|
+
# @param [Array] _args The arguments to be passed to the method.
|
|
56
|
+
# @return [Object] The result of the method call on the input object.
|
|
57
|
+
def method_missing(name, *_args)
|
|
58
|
+
input.send(name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Checks if the method is respondable based on defined attributes.
|
|
62
|
+
#
|
|
63
|
+
# @param [Symbol] name The method name.
|
|
64
|
+
# @param [Boolean] _include_private Whether to include private methods.
|
|
65
|
+
# @return [Boolean] True if the method is respondable; otherwise, false.
|
|
66
|
+
def respond_to_missing?(name, _include_private = false)
|
|
67
|
+
self.class.attributes.include?(name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Fetches a value from the input as a hash.
|
|
71
|
+
#
|
|
72
|
+
# @param [Array] args The keys to fetch values for.
|
|
73
|
+
# @return [Object] The fetched value.
|
|
74
|
+
def fetch(*args)
|
|
75
|
+
to_h.fetch(*args)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Slices the input to return only specified keys.
|
|
79
|
+
#
|
|
80
|
+
# @param [Array] args The keys to slice from the input.
|
|
81
|
+
# @return [Hash] A hash containing only the specified keys.
|
|
82
|
+
def slice(*args)
|
|
83
|
+
to_h.slice(*args)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_h(*args)
|
|
87
|
+
input.to_h(*args)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
attr_reader :input, :valid, :context
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative "context"
|
|
2
|
+
require_relative "concerns/error_handling"
|
|
3
|
+
|
|
4
|
+
# BaseService serves as a foundational class for creating service objects
|
|
5
|
+
# in the application. It provides a standardized interface for service calls
|
|
6
|
+
# and integrates error handling mechanisms.
|
|
7
|
+
module Rwc
|
|
8
|
+
module Core
|
|
9
|
+
class BaseService
|
|
10
|
+
include Concerns::ErrorHandling::Validatable
|
|
11
|
+
|
|
12
|
+
# Calls the service with the provided keyword arguments.
|
|
13
|
+
#
|
|
14
|
+
# @param [Hash] kwargs The keyword arguments to be passed to the service.
|
|
15
|
+
# @return [Object] The result of the service call.
|
|
16
|
+
def self.call(**kwargs)
|
|
17
|
+
new(**kwargs).call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Executes the service logic. Must be implemented in subclasses.
|
|
21
|
+
#
|
|
22
|
+
# @raise [NotImplementedError] When not implemented in a subclass.
|
|
23
|
+
def call
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
delegate :fail!, :succeed, to: :context
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :input
|
|
32
|
+
|
|
33
|
+
# Initializes the context for the service.
|
|
34
|
+
#
|
|
35
|
+
# @return [Context] The service context.
|
|
36
|
+
def context
|
|
37
|
+
@context ||= Context.new
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
# The ErrorHandling module provides mechanisms for managing errors
|
|
6
|
+
# within service classes. It includes methods for safely executing
|
|
7
|
+
# blocks of code and handling known error classes.
|
|
8
|
+
module Rwc
|
|
9
|
+
module Core
|
|
10
|
+
module Concerns
|
|
11
|
+
module ErrorHandling
|
|
12
|
+
class InvalidInputError < StandardError; end
|
|
13
|
+
class ServiceError < StandardError; end
|
|
14
|
+
|
|
15
|
+
module Core
|
|
16
|
+
# A list of error classes that can be rescued during safe execution.
|
|
17
|
+
ERROR_CLASSES = [
|
|
18
|
+
ActiveRecord::RecordNotFound,
|
|
19
|
+
ActiveRecord::RecordInvalid,
|
|
20
|
+
ActiveModel::UnknownAttributeError,
|
|
21
|
+
ActiveRecord::StatementInvalid,
|
|
22
|
+
InvalidInputError,
|
|
23
|
+
ServiceError
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
# Safely executes a block of code, running checks if needed.
|
|
27
|
+
#
|
|
28
|
+
# @yield [void] The block of code to execute.
|
|
29
|
+
# @return [Context] The context after execution.
|
|
30
|
+
def safely_execute(&block)
|
|
31
|
+
run_checks! if should_run_checks?
|
|
32
|
+
block.call
|
|
33
|
+
context
|
|
34
|
+
rescue *ERROR_CLASSES => e
|
|
35
|
+
fail!(error: e)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Runs input validation checks. Must be implemented by the including class.
|
|
39
|
+
#
|
|
40
|
+
# @raise [NotImplementedError] When not implemented in a subclass.
|
|
41
|
+
def run_checks!
|
|
42
|
+
raise InvalidInputError, input.errors.flat_map(&:message) unless input.valid?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module Validatable
|
|
47
|
+
module ClassMethods
|
|
48
|
+
# Indicates that checks should be performed on input validation.
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
def performs_checks
|
|
52
|
+
@should_run_checks = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :should_run_checks
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Checks if validation checks should be performed.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] True if checks should be performed; otherwise, false.
|
|
61
|
+
def should_run_checks?
|
|
62
|
+
self.class.should_run_checks
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Includes core error handling functionality and class methods.
|
|
66
|
+
#
|
|
67
|
+
# @param [Class] base The class to include the module in.
|
|
68
|
+
def self.included(base)
|
|
69
|
+
base.include(Core)
|
|
70
|
+
base.extend(ClassMethods)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Raises a service error with the specified message.
|
|
74
|
+
#
|
|
75
|
+
# @param [String] message The error message to raise.
|
|
76
|
+
# @raise [BaseService::ServiceError] The raised error.
|
|
77
|
+
def raise_error!(message)
|
|
78
|
+
raise ServiceError, message
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwc
|
|
4
|
+
module Core
|
|
5
|
+
module Concerns
|
|
6
|
+
# Provides validation methods for input classes.
|
|
7
|
+
module Validation
|
|
8
|
+
# Checks if the input is valid by running validations.
|
|
9
|
+
#
|
|
10
|
+
# @return [Boolean] True if valid; otherwise, false.
|
|
11
|
+
def valid?
|
|
12
|
+
validate!
|
|
13
|
+
errors.empty?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Validates the input. Must be implemented by including class.
|
|
17
|
+
#
|
|
18
|
+
# @raise [NotImplementedError] If not implemented.
|
|
19
|
+
def validate!
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Pipes an error into the errors array.
|
|
26
|
+
#
|
|
27
|
+
# @param [StandardError] error The error to be added.
|
|
28
|
+
def pipe_error(error)
|
|
29
|
+
@errors << error
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Validates the presence of a given key in the input.
|
|
33
|
+
#
|
|
34
|
+
# @param [Symbol] key The key to validate.
|
|
35
|
+
def validate_presence_of(key)
|
|
36
|
+
pipe_error(BaseInput::InputValidationError.new("#{key} is missing")) if input.send(key).blank?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Creates an error with a specified message.
|
|
40
|
+
#
|
|
41
|
+
# @param [String] message The error message.
|
|
42
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
43
|
+
def error(message)
|
|
44
|
+
BaseInput::InputValidationError.new(message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Validates polymorphic associations based on type and ID.
|
|
48
|
+
#
|
|
49
|
+
# @param [String] type The type of the association.
|
|
50
|
+
# @param [Integer] id The ID of the associated record.
|
|
51
|
+
# @param [Boolean] conditional Optional. If true, skips validation if id is blank.
|
|
52
|
+
def validate_polymorphic_association!(type, id, conditional: false)
|
|
53
|
+
within_error_context do
|
|
54
|
+
return if conditional && id.blank?
|
|
55
|
+
|
|
56
|
+
pipe_error(relation_not_found_error(type, id)) unless type.constantize.find_by(id: id)
|
|
57
|
+
rescue NameError
|
|
58
|
+
pipe_error(invalid_type_error(type))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Validates an association by checking its existence.
|
|
63
|
+
#
|
|
64
|
+
# @param [Integer] id The ID of the associated record.
|
|
65
|
+
# @param [Class] klass The class of the association.
|
|
66
|
+
# @param [Boolean] conditional Optional. If true, skips validation if id is blank.
|
|
67
|
+
def validate_association!(id, klass, conditional: false)
|
|
68
|
+
within_error_context do
|
|
69
|
+
return if conditional && id.blank?
|
|
70
|
+
|
|
71
|
+
pipe_error(relation_not_found_error(klass.to_s, id)) unless klass.exists?(id: id)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Validates the existence of a record associated with the input.
|
|
76
|
+
#
|
|
77
|
+
# @raise [BaseInput::InputValidationError] If validation fails.
|
|
78
|
+
def validate_record!
|
|
79
|
+
within_error_context do
|
|
80
|
+
validate_association!(id, self.class.target) if id
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Validates required keys for the input.
|
|
85
|
+
#
|
|
86
|
+
# @raise [BaseInput::InputValidationError] If any required keys are missing.
|
|
87
|
+
def validate_required_keys!
|
|
88
|
+
within_error_context do
|
|
89
|
+
self.class::REQUIRED_KEYS.each do |key|
|
|
90
|
+
validate_presence_of(key)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Validates the ownership of a record by a user.
|
|
96
|
+
#
|
|
97
|
+
# @param [ActiveRecord::Base] record The record to check ownership against.
|
|
98
|
+
# @param [Integer] user_id The ID of the user to validate ownership.
|
|
99
|
+
def validate_ownership!(record, user_id)
|
|
100
|
+
within_error_context do
|
|
101
|
+
pipe_error(restricted_access_error) unless owner_for(record)&.id == user_id
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Executes a block if there are no errors.
|
|
106
|
+
#
|
|
107
|
+
# @yield The block to execute.
|
|
108
|
+
def within_error_context(&block)
|
|
109
|
+
block.call if errors.empty?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Collates errors from specified keys into the errors array.
|
|
113
|
+
#
|
|
114
|
+
# @param [Array<Symbol>] keys The keys to collate errors for.
|
|
115
|
+
def collate_errors_for(*keys)
|
|
116
|
+
keys.each do |key|
|
|
117
|
+
within_error_context do
|
|
118
|
+
target = send(key)
|
|
119
|
+
next @errors += target.errors if target.respond_to?(:valid?) && !target.valid?
|
|
120
|
+
|
|
121
|
+
target.each do |node|
|
|
122
|
+
within_error_context do
|
|
123
|
+
@errors += node.errors unless node.valid?
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Generates an error for an invalid association type.
|
|
131
|
+
#
|
|
132
|
+
# @param [String] type The invalid type.
|
|
133
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
134
|
+
def invalid_type_error(type)
|
|
135
|
+
BaseInput::InputValidationError.new("Invalid association type: #{type.capitalize}")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Generates an error when a relation cannot be found.
|
|
139
|
+
#
|
|
140
|
+
# @param [String] type The type of the relation.
|
|
141
|
+
# @param [Integer] id The ID of the relation.
|
|
142
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
143
|
+
def relation_not_found_error(type, id)
|
|
144
|
+
BaseInput::InputValidationError.new("Could not find any '#{type.capitalize}' with id: #{id}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Generates an error for restricted access.
|
|
148
|
+
#
|
|
149
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
150
|
+
def restricted_access_error
|
|
151
|
+
BaseInput::InputValidationError.new("Unauthorized access")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Generates an error for a missing field.
|
|
155
|
+
#
|
|
156
|
+
# @param [Symbol] field The missing field.
|
|
157
|
+
# @return [BaseInput::InputValidationError] The generated error.
|
|
158
|
+
def missing_field_error(field)
|
|
159
|
+
BaseInput::InputValidationError.new("#{field} is missing")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwc
|
|
4
|
+
# The Context class is used to encapsulate the result of a service operation,
|
|
5
|
+
# holding information about success, errors, messages, and any relevant payload.
|
|
6
|
+
module Core
|
|
7
|
+
class Context
|
|
8
|
+
attr_reader :success, :errors, :messages, :payload
|
|
9
|
+
|
|
10
|
+
# Initializes a new Context instance.
|
|
11
|
+
#
|
|
12
|
+
# @return [void]
|
|
13
|
+
def initialize
|
|
14
|
+
@success = true
|
|
15
|
+
@errors = []
|
|
16
|
+
@messages = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Marks the context as failed, recording the provided error.
|
|
20
|
+
#
|
|
21
|
+
# @param [StandardError] error The error that caused the failure.
|
|
22
|
+
# @return [Context] The current context instance.
|
|
23
|
+
def fail!(error:)
|
|
24
|
+
@success = false
|
|
25
|
+
@errors = [error]
|
|
26
|
+
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Marks the context as successful, optionally providing a payload.
|
|
31
|
+
#
|
|
32
|
+
# @param [Object] payload The payload to be returned on success (default is nil).
|
|
33
|
+
# @return [Context] The current context instance.
|
|
34
|
+
def succeed(payload = nil)
|
|
35
|
+
clear_errors
|
|
36
|
+
@success = true
|
|
37
|
+
@payload = payload
|
|
38
|
+
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Sets the payload and clears any errors.
|
|
43
|
+
#
|
|
44
|
+
# @param [Object] payload The payload to set.
|
|
45
|
+
# @return [Context] The current context instance.
|
|
46
|
+
def payload!(payload:)
|
|
47
|
+
@errors = []
|
|
48
|
+
@payload = payload
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Checks if the operation was successful.
|
|
52
|
+
#
|
|
53
|
+
# @return [Boolean] True if the operation was successful; otherwise, false.
|
|
54
|
+
def success?
|
|
55
|
+
@success
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns a message string, combining messages or errors based on success state.
|
|
59
|
+
#
|
|
60
|
+
# @return [String] The combined success messages or error messages.
|
|
61
|
+
def message
|
|
62
|
+
success ? messages.join(", ") : errors.map(&:message).join(", ")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Clears the recorded errors in the context.
|
|
68
|
+
#
|
|
69
|
+
# @return [void]
|
|
70
|
+
def clear_errors
|
|
71
|
+
@errors = []
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwc
|
|
4
|
+
module Queries
|
|
5
|
+
class SimpleInput
|
|
6
|
+
attr_reader :params
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_reader :target_klass
|
|
10
|
+
|
|
11
|
+
def input_for(klass)
|
|
12
|
+
@target_klass = klass
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(fields:, type: :group, **context)
|
|
17
|
+
@params = fields
|
|
18
|
+
@type = type
|
|
19
|
+
@_context = context || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def conditions
|
|
23
|
+
params.slice(*valid_fields)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def includes
|
|
27
|
+
params.dig(:includes)&.map(&:to_sym) & valid_includes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def context
|
|
31
|
+
@context ||= OpenStruct.new(**@_context)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def sanitize!
|
|
35
|
+
# for now just remove nil keys
|
|
36
|
+
params.compact!
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def single?
|
|
41
|
+
type == :single
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def __raw__
|
|
45
|
+
OpenStruct.new(**params)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :type
|
|
51
|
+
|
|
52
|
+
def valid_fields
|
|
53
|
+
self.class.target_klass.attribute_names.map(&:to_sym)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def valid_includes
|
|
57
|
+
self.class.target_klass.reflect_on_all_associations.map(&:name)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/rwc/version.rb
ADDED
data/lib/rwc.rb
ADDED
data/rwc.gemspec
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/rwc/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "rwc"
|
|
7
|
+
spec.version = Rwc::VERSION
|
|
8
|
+
spec.authors = ["Haldane Engineering"]
|
|
9
|
+
spec.email = ["hasstrup.ezekiel@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "A gem for scaffolding Context-based rails application components."
|
|
12
|
+
spec.description = "https://medium.com/@HasstrupEzekiel/context-based-programming-in-rails-0ce951a59c36"
|
|
13
|
+
spec.homepage = "https://haldaneengineering.com"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
# spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
|
19
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = "exe"
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ["lib"]
|
|
32
|
+
|
|
33
|
+
# Uncomment to register a new dependency of your gem
|
|
34
|
+
|
|
35
|
+
spec.add_dependency "rails", ">= 6.0"
|
|
36
|
+
# For more information and examples about making a new gem, check out our
|
|
37
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
38
|
+
end
|
data/sig/rwc.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rwc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Haldane Engineering
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-08-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
description: https://medium.com/@HasstrupEzekiel/context-based-programming-in-rails-0ce951a59c36
|
|
28
|
+
email:
|
|
29
|
+
- hasstrup.ezekiel@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- ".rspec"
|
|
35
|
+
- ".rubocop.yml"
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- CODE_OF_CONDUCT.md
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- Rakefile
|
|
41
|
+
- lib/generators/rwc/decorator/decorator_generator.rb
|
|
42
|
+
- lib/generators/rwc/input/input_generator.rb
|
|
43
|
+
- lib/generators/rwc/service/service_generator.rb
|
|
44
|
+
- lib/generators/rwc/templates/create_input.rb.tt
|
|
45
|
+
- lib/generators/rwc/templates/decorator.rb.tt
|
|
46
|
+
- lib/generators/rwc/templates/query_input.rb.tt
|
|
47
|
+
- lib/generators/rwc/templates/service.rb.tt
|
|
48
|
+
- lib/rwc.rb
|
|
49
|
+
- lib/rwc/concerns/error_handling.rb
|
|
50
|
+
- lib/rwc/concerns/validation.rb
|
|
51
|
+
- lib/rwc/core/base_decorator.rb
|
|
52
|
+
- lib/rwc/core/base_input.rb
|
|
53
|
+
- lib/rwc/core/base_service.rb
|
|
54
|
+
- lib/rwc/core/concerns/error_handling.rb
|
|
55
|
+
- lib/rwc/core/concerns/validation.rb
|
|
56
|
+
- lib/rwc/core/context.rb
|
|
57
|
+
- lib/rwc/core/queries/simple_input.rb
|
|
58
|
+
- lib/rwc/version.rb
|
|
59
|
+
- rwc.gemspec
|
|
60
|
+
- sig/rwc.rbs
|
|
61
|
+
homepage: https://haldaneengineering.com
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://haldaneengineering.com
|
|
66
|
+
post_install_message:
|
|
67
|
+
rdoc_options: []
|
|
68
|
+
require_paths:
|
|
69
|
+
- lib
|
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 2.6.0
|
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0'
|
|
80
|
+
requirements: []
|
|
81
|
+
rubygems_version: 3.4.6
|
|
82
|
+
signing_key:
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: A gem for scaffolding Context-based rails application components.
|
|
85
|
+
test_files: []
|