rails_cosmos 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b2487f268d14411c8840336b894b2f8b4766783b9aab845363f9ed5199215d8
4
+ data.tar.gz: bdc8fb1c4828e1281ce99a8c4429186695252da238a72631d806c2e8e18b8a53
5
+ SHA512:
6
+ metadata.gz: 0d7d0a4bfa5dcb3f88dd10d3ecb6e7b2e1b20a9d05c1aa85b0d4718b3404476f5d25d9d25b87e76b7ca7b2f7cb543ab0728e616c487a1a794f2ae5c4f2078576
7
+ data.tar.gz: d58d30397ba9cecb821c48ab41628a34a2896da1579aac03c69c81931d65a36a0f3821f14208cc08e9ef246e3d54fdb211695d46f65c965ce887c880d39970eb
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --order rand
4
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ SuggestExtensions: false
5
+
6
+ Exclude:
7
+ - 'spec/**/*.rb'
8
+ - 'rails_cosmos.gemspec'
9
+ - 'vendor/**/*'
10
+ - 'bin/*'
11
+
12
+ Style/StringLiterals:
13
+ EnforcedStyle: double_quotes
14
+
15
+ Style/StringLiteralsInInterpolation:
16
+ EnforcedStyle: double_quotes
17
+
18
+ Metrics/MethodLength:
19
+ Max: 15
20
+
21
+ Metrics/AbcSize:
22
+ Max: 20
23
+
24
+ Layout/LineLength:
25
+ Max: 170
26
+
27
+ Metrics/ParameterLists:
28
+ Max: 8
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-09-30
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Diogo de Lima
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,156 @@
1
+ # RailsCosmos
2
+
3
+ ![Gem Version](https://img.shields.io/gem/v/rails_cosmos.svg)
4
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/diligasi/rails_cosmos/main.yml)
5
+ ![GitHub top language](https://img.shields.io/github/languages/top/diligasi/rails_cosmos)
6
+ ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/diligasi/rails_cosmos)
7
+ [![License](https://img.shields.io/github/license/diligasi/rails_cosmos.svg)](https://github.com/diligasi/rails_cosmos/blob/main/LICENSE)
8
+
9
+ `rails_cosmos` is a Ruby on Rails gem designed to streamline and promote the use of the COSMOS architecture (Controller, Operation, Service, Model, and Serializer) in Rails projects. This gem provides a set of generators and utilities that assist in setting up and maintaining the architecture, making it easier to follow clean, maintainable, and scalable design patterns.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Installation](#installation)
14
+ - [Usage](#usage)
15
+ - [Generators](#generators)
16
+ - [Operation Generator](#operation-generator)
17
+ - [Testing Framework Support](#testing-framework-support)
18
+ - [Architecture Overview](#architecture-overview)
19
+ - [COSMOS in Rails](#cosmos-in-rails)
20
+ - [Contributing](#contributing)
21
+ - [License](#license)
22
+
23
+ ## Installation
24
+
25
+ Add the gem to your `Gemfile`:
26
+
27
+ ```ruby
28
+ gem 'rails_cosmos'
29
+ ```
30
+
31
+ Then, run:
32
+
33
+ ```bash
34
+ bundle install
35
+ ```
36
+
37
+ After installing, you can run the install generator to set up base files for COSMOS:
38
+
39
+ ```bash
40
+ rails generate rails_cosmos:install
41
+ ```
42
+
43
+ This will generate the necessary base files, including the `ApplicationOperation` class, which serves as the foundation for your operations.
44
+
45
+ ## Usage
46
+
47
+ ### Generators
48
+
49
+ The core functionality of `rails_cosmos` revolves around its generators. These generators help to quickly scaffold various parts of your COSMOS architecture.
50
+
51
+ #### Operation Generator
52
+
53
+ You can create a new operation by running:
54
+
55
+ ```bash
56
+ rails generate rails_cosmos:operation OPERATION_NAME [NAMESPACE]
57
+ ```
58
+
59
+ - **OPERATION_NAME**: The name of the operation you want to create.
60
+ - **NAMESPACE** (optional): If you want the operation to be placed under a namespace (e.g., `User`, `Admin`), you can specify it here.
61
+
62
+ For example, if you want to create a `CreateUser` operation under the `Admin` namespace:
63
+
64
+ ```bash
65
+ rails generate rails_cosmos:operation create_user admin
66
+ ```
67
+
68
+ This will generate:
69
+
70
+ - `app/operations/admin/create_user.rb`
71
+ - A corresponding test file (RSpec or MiniTest) based on the detected testing framework.
72
+
73
+ ##### Operation Template
74
+
75
+ Each generated operation follows a simple template that inherits from `ApplicationOperation`:
76
+
77
+ ```ruby
78
+ module Admin
79
+ class CreateUser < ApplicationOperation
80
+ def initialize(log_uuid: nil)
81
+ super(log_uuid:)
82
+ end
83
+
84
+ def call
85
+ # Add your business logic here
86
+ success
87
+ end
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### Testing Framework Support
93
+
94
+ `rails_cosmos` detects the testing framework in use (RSpec or MiniTest) and automatically generates test files for your operations:
95
+
96
+ - If **RSpec** is detected, test files will be generated under the `spec/operations` directory.
97
+ - If **MiniTest** is detected, test files will be generated under the `test/operations` directory.
98
+ - If neither is detected, test generation is skipped with a warning.
99
+
100
+ ## Architecture Overview
101
+
102
+ ### COSMOS in Rails
103
+
104
+ The COSMOS architecture is a modular approach to organizing Rails applications. It separates concerns into five key components:
105
+
106
+ 1. **Controller**: Responsible for handling HTTP requests and routing them to the appropriate operation or service.
107
+ 2. **Operation**: Contains the core business logic. This is the entry point for any action that the system performs.
108
+ 3. **Service**: Used for interacting with external services or complex internal logic that doesn't belong in an operation.
109
+ 4. **Model**: Represents the database entities, typically the ActiveRecord models.
110
+ 5. **Serializer**: Transforms the model data into the desired JSON or XML format for API responses.
111
+
112
+ By using this architecture, you can achieve a clean, maintainable, and scalable codebase. Each part of the system has a clear responsibility, promoting SOLID principles and testability.
113
+
114
+ ### ApplicationOperation
115
+
116
+ At the core of the `rails_cosmos` gem is the `ApplicationOperation`, which serves as the base class for all operations. This class provides common functionality, such as logging, error handling, and success/failure result tracking.
117
+
118
+ You can customize and extend `ApplicationOperation` to suit your project’s needs.
119
+
120
+ ## Contributing
121
+
122
+ We welcome contributions to the `rails_cosmos` gem!
123
+
124
+ To contribute:
125
+
126
+ 1. Fork the repository.
127
+ 2. Create a new feature branch.
128
+ 3. Make your changes and add tests where necessary.
129
+ 4. Ensure all tests pass.
130
+ 5. Submit a pull request with a detailed explanation of your changes.
131
+
132
+ Make sure to follow our [contribution guidelines](CONTRIBUTING.md) (placeholder).
133
+
134
+ Bug reports and pull requests are welcome on GitHub at https://github.com/diligasi/rails_cosmos.
135
+
136
+ ### Running Tests
137
+
138
+ To run the test suite:
139
+
140
+ ```bash
141
+ bundle exec rspec
142
+ ```
143
+
144
+ Or, if you're using MiniTest:
145
+
146
+ ```bash
147
+ bundle exec rake test
148
+ ```
149
+
150
+ ## Code of Conduct
151
+
152
+ Everyone interacting in the RailsCosmos project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/diligasi/rails_cosmos/blob/main/CODE_OF_CONDUCT.md).
153
+
154
+ ## License
155
+
156
+ `rails_cosmos` is open-source software licensed under the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsCosmos
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base # :nodoc:
8
+ source_root File.expand_path("../templates", __dir__)
9
+
10
+ def create_operation_base
11
+ operations_dir = "app/operations"
12
+
13
+ empty_directory(operations_dir) unless Dir.exist?(operations_dir)
14
+
15
+ template "application_operation.rb", "#{operations_dir}/application_operation.rb",
16
+ force: file_exists_prompt("#{operations_dir}/application_operation.rb")
17
+ end
18
+
19
+ private
20
+
21
+ def file_exists_prompt(file)
22
+ return true unless File.exist?(file)
23
+
24
+ yes?("File #{file} already exists. Do you want to overwrite it?")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RailsCosmos
6
+ module Generators
7
+ # This generator creates a new operation class within the `app/operations` directory.
8
+ # Optionally, you can specify a namespace, and it will create a subdirectory based on that namespace.
9
+ #
10
+ # Example usage:
11
+ #
12
+ # rails generate operation NAME NAMESPACE
13
+ #
14
+ # If a namespace is provided, the generated operation will be placed inside a folder corresponding to
15
+ # the namespace within `app/operations`. If no namespace is provided, the operation will be created
16
+ # directly within the `app/operations` directory.
17
+ #
18
+ # This generator also creates a test file for the operation. If RSpec is detected, the test will be
19
+ # placed under `spec/operations`, and if MiniTest is detected, it will be placed under `test/operations`.
20
+ # If neither test framework is detected, the test generation will be skipped with a warning.
21
+ #
22
+ # Arguments:
23
+ # NAME - The name of the operation (required).
24
+ # NAMESPACE - The namespace for the operation (optional).
25
+ #
26
+ # Example:
27
+ #
28
+ # rails generate rails_cosmos:operation create_user admin
29
+ #
30
+ # This will generate:
31
+ #
32
+ # app/operations/admin/create_user.rb
33
+ # spec/operations/admin/create_user_spec.rb (if RSpec is used)
34
+ # or
35
+ # test/operations/admin/create_user_test.rb (if MiniTest is used)
36
+ #
37
+ class OperationGenerator < Rails::Generators::NamedBase
38
+ source_root File.expand_path("../templates", __dir__)
39
+
40
+ argument :namespace, type: :string, optional: true, desc: "Optional namespace for the operation"
41
+
42
+ def create_operation_file
43
+ operation_directory = operation_directory_path
44
+ operation_file = "#{operation_directory}/#{file_name}.rb"
45
+
46
+ empty_directory operation_directory
47
+ template "operation_template.rb.tt", operation_file
48
+ end
49
+
50
+ def create_test_file
51
+ if rspec_installed?
52
+ create_rspec_test_file
53
+ elsif minitest_installed?
54
+ create_minitest_test_file
55
+ else
56
+ say_status("warning", "No supported test framework found. Skipping test file generation.", :yellow)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def file_name
63
+ name.underscore
64
+ end
65
+
66
+ def operation_directory_path
67
+ namespace ? File.join("app/operations", namespace.underscore) : "app/operations"
68
+ end
69
+
70
+ def test_directory_path
71
+ if namespace
72
+ namespace_dir = namespace.underscore
73
+ rspec_installed? ? File.join("spec/operations", namespace_dir) : File.join("test/operations", namespace_dir)
74
+ else
75
+ rspec_installed? ? "spec/operations" : "test/operations"
76
+ end
77
+ end
78
+
79
+ def rspec_installed?
80
+ File.exist?(File.join(destination_root, "spec/spec_helper.rb"))
81
+ end
82
+
83
+ def minitest_installed?
84
+ File.exist?(File.join(destination_root, "test/test_helper.rb"))
85
+ end
86
+
87
+ def create_rspec_test_file
88
+ empty_directory test_directory_path
89
+ template "rspec_operation_test.rb.tt", File.join(test_directory_path, "#{file_name}_spec.rb")
90
+ end
91
+
92
+ def create_minitest_test_file
93
+ empty_directory test_directory_path
94
+ template "minitest_operation_test.rb.tt", File.join(test_directory_path, "#{file_name}_test.rb")
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for operation objects that provides a standard interface and error handling.
4
+ # Operation objects should inherit from this class and implement their own logic in the `call` method.
5
+ #
6
+ # Example usage:
7
+ # # Defining a successful operation
8
+ # class MySuccessOperation < ApplicationOperation
9
+ # def initialize(some_param, log_uuid: nil)
10
+ # super(log_uuid: log_uuid)
11
+ # @some_param = some_param
12
+ # end
13
+ #
14
+ # def call
15
+ # # Custom operation logic that succeeds
16
+ # success('Operation successful!', additional_info: 'Processed correctly')
17
+ # end
18
+ # end
19
+ #
20
+ # # Defining a failing operation
21
+ # class MyFailingOperation < ApplicationOperation
22
+ # def initialize(some_param, log_uuid: nil)
23
+ # super(log_uuid: log_uuid)
24
+ # @some_param = some_param
25
+ # end
26
+ #
27
+ # def call
28
+ # # Custom operation logic that fails
29
+ # raise StandardError, 'Something went wrong!'
30
+ # end
31
+ # end
32
+ #
33
+ # Success example:
34
+ # result = MySuccessOperation.call(some_param)
35
+ # => [true, 'Operation successful!', additional_info: 'Processed correctly']
36
+ #
37
+ # Failure example:
38
+ # result = MyFailingOperation.call(some_param)
39
+ # => [false, #<StandardError: Something went wrong!>]
40
+ #
41
+ class ApplicationOperation
42
+ # Entry point for the operation object. Initializes the object and calls the `call_with_rescue` method.
43
+ #
44
+ # @param args [Array] Positional arguments to be passed to the initializer.
45
+ # @param kwargs [Hash] Keyword arguments to be passed to the initializer.
46
+ #
47
+ # @return [Array] A standardized result array where the first element is a boolean indicating success,
48
+ # the second element is either nil or the error object, and additional elements contain extra data.
49
+ def self.call(*args, **kwargs)
50
+ new(*args, **kwargs).call_with_rescue
51
+ end
52
+
53
+ # Initializes the operation object with an optional log_uuid.
54
+ # If no log_uuid is provided, a new UUID is generated automatically.
55
+ #
56
+ # @param log_uuid [String, nil] An optional UUID to associate with the operation.
57
+ # If not provided, a new UUID is generated.
58
+ def initialize(log_uuid: nil)
59
+ self.log_uuid = log_uuid || SecureRandom.uuid
60
+ end
61
+
62
+ # Standardized method to invoke the operation object with exception handling.
63
+ # Captures any unexpected errors and returns a standardized failure response.
64
+ #
65
+ # @return [Array] A standardized result array where the first element is a boolean indicating success,
66
+ # the second element is either nil or the error object, and additional elements contain extra data.
67
+ def call_with_rescue
68
+ log_start
69
+ result = call
70
+ log_success
71
+ result
72
+ rescue StandardError => e
73
+ log_failure(e)
74
+ handle_unexpected_error(e)
75
+ end
76
+
77
+ # The core logic of the operation object should be implemented in this method by the subclass.
78
+ # This method must be overridden in any subclass that inherits from ApplicationOperation.
79
+ #
80
+ # @raise [NotImplementedError] If the method is not overridden by a subclass.
81
+ def call
82
+ raise NotImplementedError, "Subclasses must implement the call method"
83
+ end
84
+
85
+ protected
86
+
87
+ # Returns a standardized success response.
88
+ # The first element of the response is `true`, followed by any additional data provided.
89
+ #
90
+ # @param additional_data [Array] Extra data to be included in the success response.
91
+ #
92
+ # @return [Array] An array with `true` as the first element and optional additional data.
93
+ def success(*additional_data)
94
+ [true, *additional_data]
95
+ end
96
+
97
+ # Returns a standardized failure response.
98
+ # The first element of the response is `false`, followed by the error and any additional data provided.
99
+ #
100
+ # @param error [Exception] The error object that caused the failure.
101
+ # @param additional_data [Array] Extra data to be included in the failure response.
102
+ #
103
+ # @return [Array] An array with `false` as the first element, the error object as the second element,
104
+ # and optional additional data.
105
+ def failure(error, *additional_data)
106
+ [false, error, *additional_data]
107
+ end
108
+
109
+ private
110
+
111
+ # Retrieves the log_uuid associated with the current thread.
112
+ # This ensures thread-safe access to the UUID used for logging.
113
+ #
114
+ # @return [String] The UUID associated with the current thread.
115
+ def log_uuid
116
+ Thread.current[:log_uuid]
117
+ end
118
+
119
+ # Sets the log_uuid for the current thread.
120
+ # This ensures that each thread has its own UUID for logging purposes, providing thread-safe storage.
121
+ #
122
+ # @param value [String] The UUID to be assigned to the current thread.
123
+ def log_uuid=(value)
124
+ Thread.current[:log_uuid] = value
125
+ end
126
+
127
+ # Returns the start time of the operation. If not already set, it initializes the start time to the current time.
128
+ # This is useful for tracking the duration of the operation.
129
+ #
130
+ # @return [DateTime] The start time of the operation.
131
+ def start_time
132
+ Thread.current[:start_time] ||= Time.zone.now
133
+ end
134
+
135
+ # Logs the start of the operation.
136
+ def log_start
137
+ Rails.logger.info "[#{self.class.name} - #{log_uuid}] Started at #{start_time.strftime("%Y-%m-%d %H:%M:%S")}"
138
+ end
139
+
140
+ # Logs the successful completion of the operation, including its duration.
141
+ def log_success
142
+ end_time = Time.zone.now
143
+ converted_time = end_time.strftime("%Y-%m-%d %H:%M:%S")
144
+ duration = end_time - start_time
145
+ Rails.logger.info "[#{self.class.name} - #{log_uuid}] Completed at #{converted_time}. Duration: #{duration} seconds"
146
+ end
147
+
148
+ # Logs a failure, including the error message and stack trace.
149
+ #
150
+ # @param error [StandardError] The error object that was raised.
151
+ def log_failure(error)
152
+ Rails.logger.error "[#{self.class.name} - #{log_uuid}] Failed with error: #{error.message}"
153
+ end
154
+
155
+ # Handles unexpected errors by returning a standardized failure response. This method is
156
+ # automatically called by `call_with_rescue` when an unexpected exception occurs.
157
+ #
158
+ # @param error [StandardError] The error object that was raised unexpectedly.
159
+ #
160
+ # @return [Array] A standardized failure response array with `false` as the first element,
161
+ # and the error object as the second element.
162
+ def handle_unexpected_error(error)
163
+ [false, error]
164
+ end
165
+ end
@@ -0,0 +1,14 @@
1
+ require "test_helper"
2
+
3
+ <% if namespace -%>
4
+ class <%= namespace.camelcase %>::<%= file_name.camelcase %>Test < ActiveSupport::TestCase
5
+ test "executes call successfully" do
6
+ result = <%= namespace.camelcase %>::<%= file_name.camelcase %>.call
7
+ <% else -%>
8
+ class <%= file_name.camelcase %>Test < ActiveSupport::TestCase
9
+ test "executes call successfully" do
10
+ result = <%= file_name.camelcase %>.call
11
+ <% end -%>
12
+ assert result.first
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% if namespace -%>
4
+ module <%= namespace.camelcase %>
5
+ class <%= file_name.camelcase %> < ApplicationOperation
6
+ def initialize(log_uuid: nil)
7
+ super(log_uuid:)
8
+ end
9
+
10
+ def call
11
+ # Add your business logic here, if necessary
12
+ # adding additional methods, we encourage the
13
+ # use of private functions below
14
+
15
+ success(self)
16
+ end
17
+ end
18
+ end
19
+ <% else -%>
20
+ class <%= file_name.camelcase %> < ApplicationOperation
21
+ def initialize(log_uuid: nil)
22
+ super(log_uuid:)
23
+ end
24
+
25
+ def call
26
+ # Add your business logic here, if necessary
27
+ # adding additional methods, we encourage the
28
+ # use of private functions below
29
+
30
+ success(self)
31
+ end
32
+ end
33
+ <% end -%>
@@ -0,0 +1,12 @@
1
+ require "rails_helper"
2
+
3
+ <% if namespace -%>
4
+ RSpec.describe <%= namespace.camelcase %>::<%= file_name.camelcase %>, type: :operation do
5
+ <% else -%>
6
+ RSpec.describe <%= file_name.camelcase %>, type: :operation do
7
+ <% end -%>
8
+ it "executes call successfully" do
9
+ result = described_class.call
10
+ expect(result.first).to be_truthy
11
+ end
12
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday_middleware"
5
+
6
+ module RailsCosmos
7
+ module Http
8
+ # RailsCosmos::Http::Client
9
+ #
10
+ # A wrapper around Faraday for making HTTP requests with enhanced logging and
11
+ # error handling. This class provides a simple interface for sending requests
12
+ # to external APIs and logging request/response details for observability.
13
+ #
14
+ # @example Creating a client instance
15
+ # client = RailsCosmos::Http::Client.new(
16
+ # url: "https://api.example.com",
17
+ # service: "ExampleService",
18
+ # adapter: :typhoeus
19
+ # )
20
+ #
21
+ # @example Making a GET request
22
+ # response = client.get("/endpoint", headers: { "Authorization" => "Bearer token" })
23
+ #
24
+ # @example Making a POST request with a payload
25
+ # response = client.post(
26
+ # "/endpoint",
27
+ # payload: { key: "value" },
28
+ # headers: { "Content-Type" => "application/json" }
29
+ # )
30
+ #
31
+ # @attr_reader [String] url The base URL for the HTTP client.
32
+ # @attr_reader [String] service The name of the service for logging purposes.
33
+ # @attr_reader [Symbol] adapter The Faraday adapter to use for making requests.
34
+ class Client
35
+ def initialize(url:, service:, adapter: :typhoeus)
36
+ @url = url
37
+ @service = service
38
+ @adapter = adapter
39
+ @conn = build_connection
40
+ end
41
+
42
+ def request(method, endpoint, payload: {}, headers: {})
43
+ start_at = Time.zone.now
44
+ response = @conn.send(method, endpoint) do |req|
45
+ req.headers = headers
46
+ req.body = payload.to_json if %i[post put patch delete].include?(method)
47
+ log_request(method: method, url: req.path, headers: headers, payload: payload)
48
+ end
49
+
50
+ response_time = Time.zone.now - start_at
51
+
52
+ log_response(
53
+ method: method, success: response.success?,
54
+ time: response_time, status: response.status, url: endpoint,
55
+ headers: response.headers, body: response.body
56
+ )
57
+
58
+ Response.new(response)
59
+ end
60
+
61
+ # Shorthand methods
62
+ %i[get post put patch delete].each do |http_method|
63
+ define_method(http_method) do |endpoint, payload: {}, headers: {}|
64
+ request(http_method, endpoint, payload: payload, headers: headers)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def build_connection
71
+ Faraday.new(url: @url) do |conn|
72
+ conn.request :json
73
+ conn.response :json, content_type: /\bjson$/
74
+ conn.use FaradayMiddleware::FollowRedirects, limit: 5
75
+ conn.adapter @adapter
76
+ end
77
+ end
78
+
79
+ def log_request(method:, url:, headers:, payload:)
80
+ Rails.logger.info "HTTP Request | #{@service} -- method=#{method} url=#{url} headers=#{headers} payload=#{filter_sensitive_data(payload)}"
81
+ end
82
+
83
+ def log_response(success:, time:, status:, method:, url:, headers:, body:)
84
+ Rails.logger.info "HTTP Response | #{@service} -- success=#{success} status=#{status} time=#{time} method=#{method} url=#{url} headers=#{headers} body=#{body}"
85
+ end
86
+
87
+ def filter_sensitive_data(payload)
88
+ return payload unless payload.is_a?(Hash)
89
+
90
+ payload.each_with_object({}) do |(key, value), result|
91
+ result[key] = if value.is_a?(Hash)
92
+ filter_sensitive_data(value)
93
+ elsif Rails.application.config.filter_parameters.include?(key)
94
+ "[FILTERED]"
95
+ else
96
+ value
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsCosmos
4
+ module Http
5
+ # RailsCosmos::Http::Response
6
+ #
7
+ # A wrapper around HTTP responses to provide a consistent interface for handling
8
+ # API responses. This class parses JSON responses and provides helper methods to
9
+ # check response success.
10
+ #
11
+ # @example Creating a response instance
12
+ # response = RailsCosmos::Http::Response.new(faraday_response)
13
+ #
14
+ # @example Accessing response attributes
15
+ # response.status # => 200
16
+ # response.body # => { "key" => "value" }
17
+ # response.success? # => true
18
+ #
19
+ # @attr_reader [Hash] body The parsed response body as a Hash.
20
+ # @attr_reader [Integer] status The HTTP status code.
21
+ # @attr_reader [String, nil] raw_body The raw response body as a string, or nil if absent.
22
+ class Response
23
+ attr_reader :body, :status, :raw_body
24
+
25
+ def initialize(response)
26
+ @raw_body = response.body.present? ? response.body : {}
27
+ @body = response.body.present? ? JSON.parse(response.body) : {}
28
+ @status = response.status
29
+ end
30
+
31
+ def success?
32
+ (200..299).include?(status)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsCosmos
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rails_cosmos/version"
4
+ require_relative "rails_cosmos/http/client"
5
+ require_relative "rails_cosmos/http/response"
6
+ require_relative "generators/rails_cosmos/install_generator"
7
+ require_relative "generators/rails_cosmos/operation_generator"
8
+
9
+ module RailsCosmos
10
+ class Error < StandardError; end
11
+ end
12
+
13
+ Http = RailsCosmos::Http
@@ -0,0 +1,12 @@
1
+ module RailsCosmos
2
+ module Http
3
+ class Response
4
+ attr_reader body: Hash[untyped, untyped]
5
+ attr_reader status: Integer
6
+ attr_reader raw_body: untyped
7
+
8
+ def initialize: (response: untyped) -> void
9
+ def success?: () -> bool
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module RailsCosmos
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_cosmos
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Diogo de Lima
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-20 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: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.10.4
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.10.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday_middleware
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 1.2.1
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '1.2'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.2.1
61
+ - !ruby/object:Gem::Dependency
62
+ name: typhoeus
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.4'
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 1.4.1
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '1.4'
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 1.4.1
81
+ - !ruby/object:Gem::Dependency
82
+ name: generator_spec
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 0.10.0
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: 0.10.0
95
+ description: COSMOS provides a clean and organized structure for API-driven applications,
96
+ implementing Controllers, Operations, Services, Models, and Serializers.
97
+ email:
98
+ - diligasi@gmail.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - ".rspec"
104
+ - ".rubocop.yml"
105
+ - CHANGELOG.md
106
+ - CODE_OF_CONDUCT.md
107
+ - LICENSE.txt
108
+ - README.md
109
+ - Rakefile
110
+ - lib/generators/rails_cosmos/install_generator.rb
111
+ - lib/generators/rails_cosmos/operation_generator.rb
112
+ - lib/generators/templates/application_operation.rb
113
+ - lib/generators/templates/minitest_operation_test.rb.tt
114
+ - lib/generators/templates/operation_template.rb.tt
115
+ - lib/generators/templates/rspec_operation_test.rb.tt
116
+ - lib/rails_cosmos.rb
117
+ - lib/rails_cosmos/http/client.rb
118
+ - lib/rails_cosmos/http/response.rb
119
+ - lib/rails_cosmos/version.rb
120
+ - sig/rails_cosmos.rbs
121
+ - sig/rails_cosmos/http/response.rbs
122
+ homepage: https://rubygems.org/gems/rails_cosmos
123
+ licenses:
124
+ - MIT
125
+ metadata:
126
+ homepage_uri: https://rubygems.org/gems/rails_cosmos
127
+ source_code_uri: https://github.com/diligasi/rails_cosmos
128
+ changelog_uri: https://github.com/diligasi/rails_cosmos/blob/main/CHANGELOG.md
129
+ post_install_message: "\n### Setting up RailsCosmos ###\n\nThank you for installing
130
+ RailsCosmos! \U0001F680\n\nTo start using the COSMOS architecture in your Rails
131
+ project, you'll need to run the setup generator. \nThis will create the base structure
132
+ for your operations and services, setting up everything you need to follow the COSMOS
133
+ pattern.\n\nRun `bin/rails g rails_cosmos:install` to initialize the COSMOS architecture
134
+ in your app.\n\nFor more details on how to use and customize RailsCosmos, please
135
+ refer to the documentation: https://github.com/diligasi/rails_cosmos\nYou can also
136
+ check out the CHANGELOG for recent updates: https://github.com/diligasi/rails_cosmos/blob/main/CHANGELOG.md\n\nHappy
137
+ coding with COSMOS! ✨\n\n"
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: 3.0.0
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubygems_version: 3.5.16
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: A gem for the COSMOS architecture
156
+ test_files: []