nueca_rails_interfaces 0.2.6 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31ae17c6c4a1bc6e73e1e0cf9804c160d3cf5faf685b49c594c47c4be6a6e168
4
- data.tar.gz: 1fd912144144b1c8a5ee9431dd58f5c416216da361adf8f20d80646597d6f4cf
3
+ metadata.gz: 375e0a33491c00461a59c058bc16531aa7297fb5974420baecc7d5bdf095984e
4
+ data.tar.gz: 70fdf20fd30f3ac098c871bcb41470f5705e6b4a616d753ee6c362f7d8271589
5
5
  SHA512:
6
- metadata.gz: 2a3fb6374aebdfe04125d0a37b2dbdbc7397bebbe9ca9801bec2991c5355f5b2ee2b649cb24a676ffecf4f4eb7155cb343a0f3047a5836ba1bc837b6b56e4d84
7
- data.tar.gz: f1a29e95b7fee07b6a7bef84e34791c0e7f7177eb2ea0336f359dc34b24c893084c4c030cb226632051f3dbff9c72fcea0b704a076eca00e87777d75f9902933
6
+ metadata.gz: 8bf069562ba00aaeba977893057fbfad0e6c5e5f4ef8ed1d4208e343e90174824eead398f7ba7ae952597a5c5ce0c8c7a705bc6a087a60eded76d1715d372508
7
+ data.tar.gz: 756f5471261a09504b020119056f884f89a264ea29ae6c38d4402eb24876b1eed8751e1b561d67998ae8814d4ba86110060703eb830b47de6f92b7f094b822d8
data/.rubocop.yml CHANGED
@@ -1,6 +1,8 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-rails
3
3
  - rubocop-rake
4
+
5
+ require:
4
6
  - rubocop-rspec
5
7
 
6
8
  AllCops:
data/README.md CHANGED
@@ -14,6 +14,14 @@ gem 'nueca_rails_interfaces'
14
14
 
15
15
  Simply include the interfaces in classes and they will be enforced.
16
16
 
17
+ ```ruby
18
+ class MyForm
19
+ include NuecaRailsInterfaces::V2::FormInterface
20
+
21
+ # ...
22
+ end
23
+ ```
24
+
17
25
  ## Development
18
26
 
19
27
  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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ NRI = NuecaRailsInterfaces
4
+
3
5
  module NuecaRailsInterfaces
4
6
  # Utility module helper.
5
7
  module Util
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V1
5
+ module DataSource
6
+ # Error class for when the data source class is not found. Only use this for the context of data sources.
7
+ class NotFound < StandardError; end
8
+
9
+ # The data source base. Extend this module to create a data source base.
10
+ # It is used to invoke the data source so that it automatically searches for data source nodes
11
+ # based on the type of the record.
12
+ module BaseInterface
13
+ # Creates a new data source instance for the given record.
14
+ # It will return the data source node class instance instead of itself.
15
+ # Do not override.
16
+ def new(record)
17
+ record = modify_record(record)
18
+ data_source = data_source_class(record).new(record)
19
+ modify_data_source(data_source)
20
+ end
21
+
22
+ private
23
+
24
+ # Hook for easily altering the record for processing. Override this instead of new.
25
+ # Make sure this returns the record.
26
+ def modify_record(record)
27
+ record
28
+ end
29
+
30
+ # Hook for easily altering the detected data source for processing. Override this instead of new.
31
+ # Make sure this returns an object that includes DataSource::Node.
32
+ def modify_data_source(data_source)
33
+ data_source
34
+ end
35
+
36
+ # Method responsible for finding the data source node for the given record. Do not override.
37
+ # It raises a DataSouce::NotFound error if the node is not found.
38
+ def data_source_class(record)
39
+ resolver_logic(record)
40
+ rescue NameError
41
+ raise NotFound, 'Data Source node not found. Please check the namespace and class ' \
42
+ "name for #{record.class.name}#{" in #{namespace}" if namespace.present?}."
43
+ end
44
+
45
+ # Contains the logic on how to resolve the constants toward the data source node classes.
46
+ # Override this instead of data_source_class.
47
+ def resolver_logic(record)
48
+ "#{namespace}::#{record.class.name}Ds".constantize
49
+ end
50
+
51
+ # Returns the namespace of the data source base automatically.
52
+ # Override if needed when there is a different file stucture and different namespace.
53
+ def namespace
54
+ name.split('::')[0...-1].join('::')
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V1
5
+ module DataSource
6
+ # Data source node. They are used as actual sources of data for records,
7
+ # may it be in terms of presentation or multiple sources of basis for data.
8
+ # Include this module to create a data source node.
9
+ module NodeInterface
10
+ class << self
11
+ # Automatically delegate missing methods to the record.
12
+ def included(subclass)
13
+ subclass.delegate_missing_to(:record)
14
+ end
15
+ end
16
+
17
+ attr_reader :record
18
+
19
+ # The record itself!
20
+ def initialize(record)
21
+ @record = record
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V1
5
+ # Form Interface. Include this module to create a new form.
6
+ # In this version, a form is defined as a data verifier, mainly coming from parameters
7
+ # to shoulder off validation from processors such as services and interactors, or action controllers.
8
+ # A form should be treated like a custom model without the need for a database.
9
+ # Forms are responsible for validating data and returning the data in a format that is ready for processing.
10
+ # Forms are responsible for handling bad data, returning errors provided by ActiveModel.
11
+ # Forms should implement the `attributes` method to define the attributes of the form.
12
+ # Forms should not override methods from ActiveModel for customization.
13
+ # The `attributes` method should return a hash of the attributes of the form (strictly not an array).
14
+ # It is up to the developer what `attributes` method will contain if there is an error. Treat as such like an API.
15
+ module FormInterface
16
+ # Allows the form mixin to include ActiveModel::Model powers.
17
+ # @param [self] base Instance of the base form that would include this module.
18
+ # @return [void]
19
+ def self.included(_base)
20
+ raise NuecaRailsInterfaces::DeprecatedError
21
+
22
+ # base.include(ActiveModel::Model)
23
+ # Rails.logger&.warn(
24
+ # <<~MSG
25
+ # ##############################################
26
+ # # DEPRECATION WARNING #
27
+ # # V1::FormInterface will be deprecated soon. #
28
+ # # Please use V2::FormInterface instead. #
29
+ # ##############################################
30
+ # MSG
31
+ # )
32
+ end
33
+
34
+ # Final attributes to be returned by the form after validation.
35
+ # This is the data that is expected of the form to produce for processing.
36
+ # @raise [NotImplementedError] If the method is not overridden.
37
+ def attributes
38
+ raise NotImplementedError, 'Requires implementation of attributes.'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V1
5
+ # Query Mixin Interface. Include this module to a class that will be used as a query object.
6
+ # Query objects are eaily identified as helpers for querying with filters and sorting with complex algorithms.
7
+ # Thanks to ActiveRecord's inner workings, Ruby alone can handle the avdanced filtering
8
+ # before firing the query in the database. In this version, all query objects will be paginated.
9
+ # This is to avoid really heavy queries from hitting the database, either be it intentnional or malicious.
10
+ # In this version, pagination is enforced but only lightly encouraged.
11
+ # There will be a deprecation warning when no_pagination is used.
12
+ # However, the implementation of no_pagination is still a 1 page result;
13
+ # just that it supports a large number of queries in a single page.
14
+ # Developers will be required to move away from V1 of Query Interface soon to enforce strict pagination.
15
+ module QueryInterface
16
+ # The basis for validity of pagination settings. It also contains default values.
17
+ VALID_PAGINATION_HASH = {
18
+ max: 20, # Absolute maximum number of records per page, even if the query requests for more.
19
+ min: 1, # Absolute minimum number of records per page, even if the query requests for less.
20
+ per_page: 20, # Default number of records per page if not specified in the query.
21
+ page: 1 # Default page number if not specified in the query.
22
+ }.freeze
23
+
24
+ # Basis for considering a non-paging result even when the query is being processed for pagination.
25
+ # This number states the invalidity of pagination, but it exists for legacy support.
26
+ NO_PAGING_THRESHOLD = 1_000_000
27
+
28
+ class << self
29
+ def included(base)
30
+ # This is the method to call outside this object to apply the query filters, sortings and paginations.
31
+ # @param [Hash] query The query parameters.
32
+ # @param [ActiveRecord::Relation] collection The collection to be queried.
33
+ base.define_singleton_method(:call) do |query, collection|
34
+ new(query, collection).call
35
+ end
36
+ end
37
+ end
38
+
39
+ attr_reader :query, :collection
40
+
41
+ # Do not override! This is how we will always initialize our query objects.
42
+ # No processing should be done in the initialize method.
43
+ # @param [Hash] query The query parameters.
44
+ # @param [ActiveRecord::Relation] collection The collection to be queried.
45
+ def initialize(query, collection, pagination: true)
46
+ @query = query
47
+ @collection = collection
48
+ @pagination_flag = pagination
49
+ query_aliases
50
+ end
51
+
52
+ # Do not override. This is the method to call outside this object
53
+ # to apply the query filters, sortings and paginations.
54
+ def call
55
+ apply_filters!
56
+ apply_sorting!
57
+ apply_pagination!
58
+ collection
59
+ end
60
+
61
+ private
62
+
63
+ # Place here filters. Be sure to assign @collection to override the original collection. Be sure it is private!
64
+ def filters; end
65
+
66
+ # Place here sorting logic. Be sure to assign @collection to override the original collection.
67
+ # Be sure it is private!
68
+ def sorts; end
69
+
70
+ # Pagination settings to modify the default behavior of a query object.
71
+ # Default values are in VALID_PAGINATION_HASH constant.
72
+ # Override the method to change the default values.
73
+ def pagination_settings
74
+ {}
75
+ end
76
+
77
+ # Always updated alias of filters.
78
+ def apply_filters!
79
+ filters
80
+ end
81
+
82
+ # Always updated alias of sorts.
83
+ def apply_sorting!
84
+ sorts
85
+ end
86
+
87
+ # Paginates the collection based on query or settings.
88
+ def apply_pagination!
89
+ raise 'Invalid pagination settings.' unless correct_pagination_settings?
90
+ return unless @pagination_flag
91
+
92
+ @collection = collection.paginate(page: fetch_page_value, per_page: fetch_per_page_value)
93
+ end
94
+
95
+ # Logic for fetching the page value from the query or settings.
96
+ def fetch_page_value
97
+ query&.key?(:page) ? query[:page].to_i : merged_pagination_settings[:page]
98
+ end
99
+
100
+ # Logic for fetching the per page value from the query or settings.
101
+ def fetch_per_page_value
102
+ per_page = query&.key?(:per_page) ? query[:per_page].to_i : merged_pagination_settings[:per_page]
103
+ per_page.clamp(merged_pagination_settings[:min], merged_pagination_settings[:max])
104
+ end
105
+
106
+ # Checks if the pagination settings are correct.
107
+ # The app crashes on misconfiguration.
108
+ def correct_pagination_settings?
109
+ return false unless pagination_settings.is_a?(Hash)
110
+
111
+ detected_keys = []
112
+ merged_pagination_settings.each_key do |key|
113
+ return false unless VALID_PAGINATION_HASH.key?(key)
114
+
115
+ detected_keys << key
116
+ end
117
+
118
+ detected_keys.sort == VALID_PAGINATION_HASH.keys.sort
119
+ end
120
+
121
+ # The final result of pagination settings, and thus the used one.
122
+ def merged_pagination_settings
123
+ @merged_pagination_settings ||= VALID_PAGINATION_HASH.merge(pagination_settings)
124
+ end
125
+
126
+ # Aliases for query parameters for legacy support.
127
+ # No need to override this in children. Directly modify this method in this interface if need be.
128
+ def query_aliases
129
+ query[:per_page] = query[:limit] if query[:limit].present? && query[:per_page].blank?
130
+ end
131
+
132
+ # For deprecation. Use this for queries that do not need pagination.
133
+ # Queries will still be paginated as a result, but with the use of the threshold,
134
+ # the result is as good as a non-paginated result,
135
+ # and it will be treated as such.
136
+ def no_pagination
137
+ Rails.logger.warn 'Querying without paging is deprecated. Enforce paging in queries!'
138
+ { max: NO_PAGING_THRESHOLD, min: NO_PAGING_THRESHOLD }
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V1
5
+ # Service Mixin Interface. Include from this module when creating a new service.
6
+ # In this version, a service is defined as an absolute reusable piece of code.
7
+ # They should not be related to views and presentation of data; they are instead processors.
8
+ # Because of this, errors encountered should be raised as exceptions naturally.
9
+ # Errors here are treated as bad data. Services are not responsible for handling bad data.
10
+ # Services assume all data passed is correct and valid. Services should no longer validate these data.
11
+ # Services can still store warnings; these warnings are sent to Sentry, although not yet implemented.
12
+ # All services will have a `perform` method that will call the `action` method. This is the invoker of the service.
13
+ # All services will have an `action` method that will contain the main logic of the service.
14
+ # All services will have a `data` method that will contain the resulting data that the service produced.
15
+ # Developers will mainly override `action` method and `data method`.
16
+ module ServiceInterface
17
+ def self.included(_)
18
+ raise NuecaRailsInterfaces::DeprecatedError
19
+
20
+ # Rails.logger&.warn(
21
+ # <<~MSG
22
+ # #################################################
23
+ # # DEPRECATION WARNING #
24
+ # # V1::ServiceInterface will be deprecated soon. #
25
+ # # Please use V2::ServiceInterface instead. #
26
+ # #################################################
27
+ # MSG
28
+ # )
29
+ end
30
+
31
+ # This is the main method of the service. This is the method that should be called to perform the service.
32
+ # Do not override this method. Instead, override the `action` method.
33
+ # @return [self] Instance of the service.
34
+ def perform
35
+ unless performed?
36
+ action
37
+ @performed = true
38
+ process_warnings
39
+ end
40
+
41
+ self
42
+ end
43
+
44
+ # Override this method and put the main logic of the service here.
45
+ # @raise [NotImplementedError] If the method is not overridden.
46
+ def action
47
+ raise NotImplementedError, 'Requires implementation of action.'
48
+ end
49
+
50
+ # Override this method and put the resulting data of the service here.
51
+ # If blank, then return an empty hash manually.
52
+ # Reason being is for readability's sake in the services.
53
+ # @raise [NotImplementedError] If the method is not overridden.
54
+ def data
55
+ raise NotImplementedError, 'Requires implementation of data.'
56
+ end
57
+
58
+ # Method used to add a warning. Do not override.
59
+ # @param [String] warning_message Descriptive sentence of the warning.
60
+ # @return [Array<String>] Array of all warnings.
61
+ def add_warning(warning_message)
62
+ _warnings << warning_message
63
+ warnings
64
+ end
65
+
66
+ # Checks if the service has been performed. Do not override.
67
+ # @return [Boolean] True or False
68
+ def performed?
69
+ performed
70
+ end
71
+
72
+ # Used to check if the service has encountered any warnings. Do not override.
73
+ # @return [Boolean] True or False
74
+ def warnings?
75
+ _warnings.any?
76
+ end
77
+
78
+ # This should contain all the warnings that the service has encountered.
79
+ # Intentionally made to be frozen to avoid free modification.
80
+ # Do not override this method.
81
+ # @return [Array<String>] Array of all warnings.
82
+ def warnings
83
+ _warnings.dup.freeze
84
+ end
85
+
86
+ private
87
+
88
+ # Status of the service. If the service has been performed, this should be true.
89
+ # Do not override this method.
90
+ # @return [Boolean] True or False
91
+ def performed
92
+ @performed ||= false
93
+ end
94
+
95
+ # The real warnings array. Do not override this method. Use `add_warning` method to add warnings.
96
+ # @return [Array<String>] Array of all warnings.
97
+ def _warnings
98
+ @warnings ||= []
99
+ end
100
+
101
+ # Iterates through the warnings and sends them to Sentry, supposedly. Unimplemented so far.
102
+ # Do not override the method.
103
+ # @return [void]
104
+ def process_warnings; end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V2
5
+ # V2 Form Interface is the same as V1 Form Interface,
6
+ # except forces an exception on the attributes method when intializing a form.
7
+ module FormInterface
8
+ class << self
9
+ # Allows the form mixin to include ActiveModel::Model powers.
10
+ def included(base)
11
+ base.include(ActiveModel::Model)
12
+
13
+ # Initializes the form in a class context with the options passed in.
14
+ base.define_singleton_method(:check) do |*arguments|
15
+ instance = NuecaRailsInterfaces::Util.process_class_arguments(self, *arguments)
16
+ instance.valid?
17
+ instance
18
+ end
19
+ end
20
+ end
21
+
22
+ # Initializes the form with the options passed in.
23
+ # It also calls the attributes method to ensure it is implemented.
24
+ def initialize(options = {})
25
+ super(**options)
26
+ attributes
27
+ end
28
+
29
+ # Final attributes to be returned by the form after validation.
30
+ # This is the data that is expected of the form to produce for processing.
31
+ # @raise [NotImplementedError] If the method is not overridden.
32
+ def attributes
33
+ raise NotImplementedError, 'Requires implementation of attributes.'
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NuecaRailsInterfaces
4
+ module V2
5
+ # V2 Service Interface is the same as V1 Service Interface, except it is more straightforward.
6
+ # When performing the service, it will immediately return the data
7
+ module ServiceInterface
8
+ class << self
9
+ def included(base)
10
+ # This is the main method of the service in a class context.
11
+ # This is the method that should be called to perform the service statically.
12
+ # Use this instead if the service instance is not needed.
13
+ # Do not override this method. Instead, override the `action` method. Returns the data immediately.
14
+ # @return [Object] Data of the service.
15
+ base.define_singleton_method(:perform) do |*arguments|
16
+ instance = NuecaRailsInterfaces::Util.process_class_arguments(self, *arguments)
17
+ instance.perform
18
+ end
19
+ end
20
+ end
21
+
22
+ # This is the main method of the service. This is the method that should be called to perform the service.
23
+ # Do not override this method. Instead, override the `action` method. Returns the data immediately.
24
+ # @return [Object] Data of the service.
25
+ def perform
26
+ unless performed?
27
+ action
28
+ @performed = true
29
+ process_warnings
30
+ end
31
+
32
+ data
33
+ end
34
+
35
+ # Override this method and put the main logic of the service here.
36
+ # @raise [NotImplementedError] If the method is not overridden.
37
+ # @return [void]
38
+ def action
39
+ raise NotImplementedError, 'Requires implementation of action.'
40
+ end
41
+
42
+ # Override this method and put the resulting data of the service here.
43
+ # If blank, then return an empty hash manually.
44
+ # Reason being is for readability's sake in the services.
45
+ # @raise [NotImplementedError] If the method is not overridden.
46
+ # @return [Object] Data of the service.
47
+ def data
48
+ raise NotImplementedError, 'Requires implementation of data.'
49
+ end
50
+
51
+ # Method used to add a warning. Do not override.
52
+ # @param [String] warning_message Descriptive sentence of the warning.
53
+ # @return [Array<String>] Array of all warnings.
54
+ def add_warning(warning_message)
55
+ _warnings << warning_message
56
+ warnings
57
+ end
58
+
59
+ # Checks if the service has been performed. Do not override.
60
+ # @return [Boolean] True or False
61
+ def performed?
62
+ performed
63
+ end
64
+
65
+ # Used to check if the service has encountered any warnings. Do not override.
66
+ # @return [Boolean] True or False
67
+ def warnings?
68
+ _warnings.any?
69
+ end
70
+
71
+ # This should contain all the warnings that the service has encountered.
72
+ # Intentionally made to be frozen to avoid free modification.
73
+ # Do not override this method.
74
+ # @return [Array<String>] Array of all warnings.
75
+ def warnings
76
+ _warnings.dup.freeze
77
+ end
78
+
79
+ private
80
+
81
+ # Status of the service. If the service has been performed, this should be true.
82
+ # Do not override this method.
83
+ # @return [Boolean] True or False
84
+ def performed
85
+ @performed ||= false
86
+ end
87
+
88
+ # The real warnings array. Do not override this method. Use `add_warning` method to add warnings.
89
+ # @return [Array<String>] Array of all warnings.
90
+ def _warnings
91
+ @warnings ||= []
92
+ end
93
+
94
+ # Iterates through the warnings and sends them to Sentry, supposedly. Unimplemented so far.
95
+ # Do not override the method.
96
+ # @return [void]
97
+ def process_warnings; end
98
+ end
99
+ end
100
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NuecaRailsInterfaces
4
- VERSION = '0.2.6'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -8,9 +8,15 @@ require_relative 'nueca_rails_interfaces/version'
8
8
 
9
9
  # This module is a namespace for the gem.
10
10
  module NuecaRailsInterfaces
11
+ # Class that forces an error due to being deprecated.
12
+ class DeprecatedError < StandardError
13
+ def initialize
14
+ super('This feature is deprecated.')
15
+ end
16
+ end
11
17
  end
12
18
 
13
19
  require_relative 'nueca_rails_interfaces/util'
14
20
 
15
21
  # Require all interfaces.
16
- Dir["#{__dir__}/v*/**/*_interface.rb"].each { |file| require_relative file }
22
+ Dir["#{__dir__}/nueca_rails_interfaces/v*/**/*_interface.rb"].each { |file| require_relative file }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nueca_rails_interfaces
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tien
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-03 00:00:00.000000000 Z
10
+ date: 2025-06-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -58,14 +58,14 @@ files:
58
58
  - Rakefile
59
59
  - lib/nueca_rails_interfaces.rb
60
60
  - lib/nueca_rails_interfaces/util.rb
61
+ - lib/nueca_rails_interfaces/v1/data_source/base_interface.rb
62
+ - lib/nueca_rails_interfaces/v1/data_source/node_interface.rb
63
+ - lib/nueca_rails_interfaces/v1/form_interface.rb
64
+ - lib/nueca_rails_interfaces/v1/query_interface.rb
65
+ - lib/nueca_rails_interfaces/v1/service_interface.rb
66
+ - lib/nueca_rails_interfaces/v2/form_interface.rb
67
+ - lib/nueca_rails_interfaces/v2/service_interface.rb
61
68
  - lib/nueca_rails_interfaces/version.rb
62
- - lib/v1/data_source/base_interface.rb
63
- - lib/v1/data_source/node_interface.rb
64
- - lib/v1/form_interface.rb
65
- - lib/v1/query_interface.rb
66
- - lib/v1/service_interface.rb
67
- - lib/v2/form_interface.rb
68
- - lib/v2/service_interface.rb
69
69
  - nueca_rails_interfaces.gemspec
70
70
  homepage: https://github.com/tieeeeen1994/nueca-rails-interfaces
71
71
  licenses:
@@ -86,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  requirements: []
89
- rubygems_version: 3.6.5
89
+ rubygems_version: 3.6.2
90
90
  specification_version: 4
91
91
  summary: Interfaces for known object entities in Rails Development at Nueca.
92
92
  test_files: []
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module V1
4
- module DataSource
5
- # Error class for when the data source class is not found. Only use this for the context of data sources.
6
- class NotFound < StandardError; end
7
-
8
- # The data source base. Extend this module to create a data source base.
9
- # It is used to invoke the data source so that it automatically searches for data source nodes
10
- # based on the type of the record.
11
- module BaseInterface
12
- # Creates a new data source instance for the given record.
13
- # It will return the data source node class instance instead of itself.
14
- # Do not override.
15
- def new(record)
16
- record = modify_record(record)
17
- data_source = data_source_class(record).new(record)
18
- modify_data_source(data_source)
19
- end
20
-
21
- private
22
-
23
- # Hook for easily altering the record for processing. Override this instead of new.
24
- # Make sure this returns the record.
25
- def modify_record(record)
26
- record
27
- end
28
-
29
- # Hook for easily altering the detected data source for processing. Override this instead of new.
30
- # Make sure this returns an object that includes DataSource::Node.
31
- def modify_data_source(data_source)
32
- data_source
33
- end
34
-
35
- # Method responsible for finding the data source node for the given record. Do not override.
36
- # It raises a DataSouce::NotFound error if the node is not found.
37
- def data_source_class(record)
38
- resolver_logic(record)
39
- rescue NameError
40
- raise NotFound, 'Data Source node not found. Please check the namespace and class ' \
41
- "name for #{record.class.name}#{" in #{namespace}" if namespace.present?}."
42
- end
43
-
44
- # Contains the logic on how to resolve the constants toward the data source node classes.
45
- # Override this instead of data_source_class.
46
- def resolver_logic(record)
47
- "#{namespace}::#{record.class.name}Ds".constantize
48
- end
49
-
50
- # Returns the namespace of the data source base automatically.
51
- # Override if needed when there is a different file stucture and different namespace.
52
- def namespace
53
- name.split('::')[0...-1].join('::')
54
- end
55
- end
56
- end
57
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module V1
4
- module DataSource
5
- # Data source node. They are used as actual sources of data for records,
6
- # may it be in terms of presentation or multiple sources of basis for data.
7
- # Include this module to create a data source node.
8
- module NodeInterface
9
- class << self
10
- # Automatically delegate missing methods to the record.
11
- def included(subclass)
12
- subclass.delegate_missing_to(:record)
13
- end
14
- end
15
-
16
- attr_reader :record
17
-
18
- # The record itself!
19
- def initialize(record)
20
- @record = record
21
- end
22
- end
23
- end
24
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module V1
4
- # Form Interface. Include this module to create a new form.
5
- # In this version, a form is defined as a data verifier, mainly coming from parameters
6
- # to shoulder off validation from processors such as services and interactors, or action controllers.
7
- # A form should be treated like a custom model without the need for a database.
8
- # Forms are responsible for validating data and returning the data in a format that is ready for processing.
9
- # Forms are responsible for handling bad data, returning errors provided by ActiveModel.
10
- # Forms should implement the `attributes` method to define the attributes of the form.
11
- # Forms should not override methods from ActiveModel for customization.
12
- # The `attributes` method should return a hash of the attributes of the form (strictly not an array).
13
- # It is up to the developer what `attributes` method will contain if there is an error. Treat as such like an API.
14
- module FormInterface
15
- # Allows the form mixin to include ActiveModel::Model powers.
16
- # @param [self] base Instance of the base form that would include this module.
17
- # @return [void]
18
- def self.included(base)
19
- base.include(ActiveModel::Model)
20
- Rails.logger&.warn(
21
- <<~MSG
22
- ##############################################
23
- # DEPRECATION WARNING #
24
- # V1::FormInterface will be deprecated soon. #
25
- # Please use V2::FormInterface instead. #
26
- ##############################################
27
- MSG
28
- )
29
- end
30
-
31
- # Final attributes to be returned by the form after validation.
32
- # This is the data that is expected of the form to produce for processing.
33
- # @raise [NotImplementedError] If the method is not overridden.
34
- def attributes
35
- raise NotImplementedError, 'Requires implementation of attributes.'
36
- end
37
- end
38
- end
@@ -1,140 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module V1
4
- # Query Mixin Interface. Include this module to a class that will be used as a query object.
5
- # Query objects are eaily identified as helpers for querying with filters and sorting with complex algorithms.
6
- # Thanks to ActiveRecord's inner workings, Ruby alone can handle the avdanced filtering
7
- # before firing the query in the database. In this version, all query objects will be paginated.
8
- # This is to avoid really heavy queries from hitting the database, either be it intentnional or malicious.
9
- # In this version, pagination is enforced but only lightly encouraged.
10
- # There will be a deprecation warning when no_pagination is used.
11
- # However, the implementation of no_pagination is still a 1 page result;
12
- # just that it supports a large number of queries in a single page.
13
- # Developers will be required to move away from V1 of Query Interface soon to enforce strict pagination.
14
- module QueryInterface
15
- # The basis for validity of pagination settings. It also contains default values.
16
- VALID_PAGINATION_HASH = {
17
- max: 20, # Absolute maximum number of records per page, even if the query requests for more.
18
- min: 1, # Absolute minimum number of records per page, even if the query requests for less.
19
- per_page: 20, # Default number of records per page if not specified in the query.
20
- page: 1 # Default page number if not specified in the query.
21
- }.freeze
22
-
23
- # Basis for considering a non-paging result even when the query is being processed for pagination.
24
- # This number states the invalidity of pagination, but it exists for legacy support.
25
- NO_PAGING_THRESHOLD = 1_000_000
26
-
27
- class << self
28
- def included(base)
29
- # This is the method to call outside this object to apply the query filters, sortings and paginations.
30
- # @param [Hash] query The query parameters.
31
- # @param [ActiveRecord::Relation] collection The collection to be queried.
32
- base.define_singleton_method(:call) do |query, collection|
33
- new(query, collection).call
34
- end
35
- end
36
- end
37
-
38
- attr_reader :query, :collection
39
-
40
- # Do not override! This is how we will always initialize our query objects.
41
- # No processing should be done in the initialize method.
42
- # @param [Hash] query The query parameters.
43
- # @param [ActiveRecord::Relation] collection The collection to be queried.
44
- def initialize(query, collection, pagination: true)
45
- @query = query
46
- @collection = collection
47
- @pagination_flag = pagination
48
- query_aliases
49
- end
50
-
51
- # Do not override. This is the method to call outside this object
52
- # to apply the query filters, sortings and paginations.
53
- def call
54
- apply_filters!
55
- apply_sorting!
56
- apply_pagination!
57
- collection
58
- end
59
-
60
- private
61
-
62
- # Place here filters. Be sure to assign @collection to override the original collection. Be sure it is private!
63
- def filters; end
64
-
65
- # Place here sorting logic. Be sure to assign @collection to override the original collection.
66
- # Be sure it is private!
67
- def sorts; end
68
-
69
- # Pagination settings to modify the default behavior of a query object.
70
- # Default values are in VALID_PAGINATION_HASH constant.
71
- # Override the method to change the default values.
72
- def pagination_settings
73
- {}
74
- end
75
-
76
- # Always updated alias of filters.
77
- def apply_filters!
78
- filters
79
- end
80
-
81
- # Always updated alias of sorts.
82
- def apply_sorting!
83
- sorts
84
- end
85
-
86
- # Paginates the collection based on query or settings.
87
- def apply_pagination!
88
- raise 'Invalid pagination settings.' unless correct_pagination_settings?
89
- return unless @pagination_flag
90
-
91
- @collection = collection.paginate(page: fetch_page_value, per_page: fetch_per_page_value)
92
- end
93
-
94
- # Logic for fetching the page value from the query or settings.
95
- def fetch_page_value
96
- query&.key?(:page) ? query[:page].to_i : merged_pagination_settings[:page]
97
- end
98
-
99
- # Logic for fetching the per page value from the query or settings.
100
- def fetch_per_page_value
101
- per_page = query&.key?(:per_page) ? query[:per_page].to_i : merged_pagination_settings[:per_page]
102
- per_page.clamp(merged_pagination_settings[:min], merged_pagination_settings[:max])
103
- end
104
-
105
- # Checks if the pagination settings are correct.
106
- # The app crashes on misconfiguration.
107
- def correct_pagination_settings?
108
- return false unless pagination_settings.is_a?(Hash)
109
-
110
- detected_keys = []
111
- merged_pagination_settings.each_key do |key|
112
- return false unless VALID_PAGINATION_HASH.key?(key)
113
-
114
- detected_keys << key
115
- end
116
-
117
- detected_keys.sort == VALID_PAGINATION_HASH.keys.sort
118
- end
119
-
120
- # The final result of pagination settings, and thus the used one.
121
- def merged_pagination_settings
122
- @merged_pagination_settings ||= VALID_PAGINATION_HASH.merge(pagination_settings)
123
- end
124
-
125
- # Aliases for query parameters for legacy support.
126
- # No need to override this in children. Directly modify this method in this interface if need be.
127
- def query_aliases
128
- query[:per_page] = query[:limit] if query[:limit].present? && query[:per_page].blank?
129
- end
130
-
131
- # For deprecation. Use this for queries that do not need pagination.
132
- # Queries will still be paginated as a result, but with the use of the threshold,
133
- # the result is as good as a non-paginated result,
134
- # and it will be treated as such.
135
- def no_pagination
136
- Rails.logger.warn 'Querying without paging is deprecated. Enforce paging in queries!'
137
- { max: NO_PAGING_THRESHOLD, min: NO_PAGING_THRESHOLD }
138
- end
139
- end
140
- end
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module V1
4
- # Service Mixin Interface. Include from this module when creating a new service.
5
- # In this version, a service is defined as an absolute reusable piece of code.
6
- # They should not be related to views and presentation of data; they are instead processors.
7
- # Because of this, errors encountered should be raised as exceptions naturally.
8
- # Errors here are treated as bad data. Services are not responsible for handling bad data.
9
- # Services assume all data passed is correct and valid. Services should no longer validate these data.
10
- # Services can still store warnings; these warnings are sent to Sentry, although not yet implemented.
11
- # All services will have a `perform` method that will call the `action` method. This is the invoker of the service.
12
- # All services will have an `action` method that will contain the main logic of the service.
13
- # All services will have a `data` method that will contain the resulting data that the service produced.
14
- # Developers will mainly override `action` method and `data method`.
15
- module ServiceInterface
16
- def self.included(_)
17
- Rails.logger&.warn(
18
- <<~MSG
19
- #################################################
20
- # DEPRECATION WARNING #
21
- # V1::ServiceInterface will be deprecated soon. #
22
- # Please use V2::ServiceInterface instead. #
23
- #################################################
24
- MSG
25
- )
26
- end
27
-
28
- # This is the main method of the service. This is the method that should be called to perform the service.
29
- # Do not override this method. Instead, override the `action` method.
30
- # @return [self] Instance of the service.
31
- def perform
32
- unless performed?
33
- action
34
- @performed = true
35
- process_warnings
36
- end
37
-
38
- self
39
- end
40
-
41
- # Override this method and put the main logic of the service here.
42
- # @raise [NotImplementedError] If the method is not overridden.
43
- def action
44
- raise NotImplementedError, 'Requires implementation of action.'
45
- end
46
-
47
- # Override this method and put the resulting data of the service here.
48
- # If blank, then return an empty hash manually.
49
- # Reason being is for readability's sake in the services.
50
- # @raise [NotImplementedError] If the method is not overridden.
51
- def data
52
- raise NotImplementedError, 'Requires implementation of data.'
53
- end
54
-
55
- # Method used to add a warning. Do not override.
56
- # @param [String] warning_message Descriptive sentence of the warning.
57
- # @return [Array<String>] Array of all warnings.
58
- def add_warning(warning_message)
59
- _warnings << warning_message
60
- warnings
61
- end
62
-
63
- # Checks if the service has been performed. Do not override.
64
- # @return [Boolean] True or False
65
- def performed?
66
- performed
67
- end
68
-
69
- # Used to check if the service has encountered any warnings. Do not override.
70
- # @return [Boolean] True or False
71
- def warnings?
72
- _warnings.any?
73
- end
74
-
75
- # This should contain all the warnings that the service has encountered.
76
- # Intentionally made to be frozen to avoid free modification.
77
- # Do not override this method.
78
- # @return [Array<String>] Array of all warnings.
79
- def warnings
80
- _warnings.dup.freeze
81
- end
82
-
83
- private
84
-
85
- # Status of the service. If the service has been performed, this should be true.
86
- # Do not override this method.
87
- # @return [Boolean] True or False
88
- def performed
89
- @performed ||= false
90
- end
91
-
92
- # The real warnings array. Do not override this method. Use `add_warning` method to add warnings.
93
- # @return [Array<String>] Array of all warnings.
94
- def _warnings
95
- @warnings ||= []
96
- end
97
-
98
- # Iterates through the warnings and sends them to Sentry, supposedly. Unimplemented so far.
99
- # Do not override the method.
100
- # @return [void]
101
- def process_warnings; end
102
- end
103
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module V2
4
- # V2 Form Interface is the same as V1 Form Interface,
5
- # except forces an exception on the attributes method when intializing a form.
6
- module FormInterface
7
- class << self
8
- # Allows the form mixin to include ActiveModel::Model powers.
9
- def included(base)
10
- base.include(ActiveModel::Model)
11
-
12
- # Initializes the form in a class context with the options passed in.
13
- base.define_singleton_method(:check) do |*arguments|
14
- instance = NuecaRailsInterfaces::Util.process_class_arguments(self, *arguments)
15
- instance.valid?
16
- instance
17
- end
18
- end
19
- end
20
-
21
- # Initializes the form with the options passed in.
22
- # It also calls the attributes method to ensure it is implemented.
23
- def initialize(options = {})
24
- super(**options)
25
- attributes
26
- end
27
-
28
- # Final attributes to be returned by the form after validation.
29
- # This is the data that is expected of the form to produce for processing.
30
- # @raise [NotImplementedError] If the method is not overridden.
31
- def attributes
32
- raise NotImplementedError, 'Requires implementation of attributes.'
33
- end
34
- end
35
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module V2
4
- # V2 Service Interface is the same as V1 Service Interface, except it is more straightforward.
5
- # When performing the service, it will immediately return the data
6
- module ServiceInterface
7
- class << self
8
- def included(base)
9
- # This is the main method of the service in a class context.
10
- # This is the method that should be called to perform the service statically.
11
- # Use this instead if the service instance is not needed.
12
- # Do not override this method. Instead, override the `action` method. Returns the data immediately.
13
- # @return [Object] Data of the service.
14
- base.define_singleton_method(:perform) do |*arguments|
15
- instance = NuecaRailsInterfaces::Util.process_class_arguments(self, *arguments)
16
- instance.perform
17
- end
18
- end
19
- end
20
-
21
- # This is the main method of the service. This is the method that should be called to perform the service.
22
- # Do not override this method. Instead, override the `action` method. Returns the data immediately.
23
- # @return [Object] Data of the service.
24
- def perform
25
- unless performed?
26
- action
27
- @performed = true
28
- process_warnings
29
- end
30
-
31
- data
32
- end
33
-
34
- # Override this method and put the main logic of the service here.
35
- # @raise [NotImplementedError] If the method is not overridden.
36
- # @return [void]
37
- def action
38
- raise NotImplementedError, 'Requires implementation of action.'
39
- end
40
-
41
- # Override this method and put the resulting data of the service here.
42
- # If blank, then return an empty hash manually.
43
- # Reason being is for readability's sake in the services.
44
- # @raise [NotImplementedError] If the method is not overridden.
45
- # @return [Object] Data of the service.
46
- def data
47
- raise NotImplementedError, 'Requires implementation of data.'
48
- end
49
-
50
- # Method used to add a warning. Do not override.
51
- # @param [String] warning_message Descriptive sentence of the warning.
52
- # @return [Array<String>] Array of all warnings.
53
- def add_warning(warning_message)
54
- _warnings << warning_message
55
- warnings
56
- end
57
-
58
- # Checks if the service has been performed. Do not override.
59
- # @return [Boolean] True or False
60
- def performed?
61
- performed
62
- end
63
-
64
- # Used to check if the service has encountered any warnings. Do not override.
65
- # @return [Boolean] True or False
66
- def warnings?
67
- _warnings.any?
68
- end
69
-
70
- # This should contain all the warnings that the service has encountered.
71
- # Intentionally made to be frozen to avoid free modification.
72
- # Do not override this method.
73
- # @return [Array<String>] Array of all warnings.
74
- def warnings
75
- _warnings.dup.freeze
76
- end
77
-
78
- private
79
-
80
- # Status of the service. If the service has been performed, this should be true.
81
- # Do not override this method.
82
- # @return [Boolean] True or False
83
- def performed
84
- @performed ||= false
85
- end
86
-
87
- # The real warnings array. Do not override this method. Use `add_warning` method to add warnings.
88
- # @return [Array<String>] Array of all warnings.
89
- def _warnings
90
- @warnings ||= []
91
- end
92
-
93
- # Iterates through the warnings and sends them to Sentry, supposedly. Unimplemented so far.
94
- # Do not override the method.
95
- # @return [void]
96
- def process_warnings; end
97
- end
98
- end