servus 0.1.3 → 0.1.5
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 +4 -4
- data/.claude/commands/check-docs.md +1 -0
- data/.claude/commands/consistency-check.md +1 -0
- data/.claude/commands/fine-tooth-comb.md +1 -0
- data/.claude/commands/red-green-refactor.md +5 -0
- data/.claude/settings.json +15 -0
- data/.rubocop.yml +18 -2
- data/.yardopts +6 -0
- data/CHANGELOG.md +47 -0
- data/CLAUDE.md +10 -0
- data/IDEAS.md +5 -0
- data/READme.md +300 -47
- data/Rakefile +33 -0
- data/builds/servus-0.1.3.gem +0 -0
- data/builds/servus-0.1.4.gem +0 -0
- data/builds/servus-0.1.5.gem +0 -0
- data/docs/core/1_overview.md +77 -0
- data/docs/core/2_architecture.md +120 -0
- data/docs/core/3_service_objects.md +121 -0
- data/docs/current_focus.md +569 -0
- data/docs/features/1_schema_validation.md +119 -0
- data/docs/features/2_error_handling.md +121 -0
- data/docs/features/3_async_execution.md +81 -0
- data/docs/features/4_logging.md +64 -0
- data/docs/features/5_event_bus.md +244 -0
- data/docs/guides/1_common_patterns.md +90 -0
- data/docs/guides/2_migration_guide.md +175 -0
- data/docs/integration/1_configuration.md +104 -0
- data/docs/integration/2_testing.md +287 -0
- data/docs/integration/3_rails_integration.md +99 -0
- data/docs/yard/Servus/Base.html +1645 -0
- data/docs/yard/Servus/Config.html +582 -0
- data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
- data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
- data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
- data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
- data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
- data/docs/yard/Servus/Extensions/Async.html +141 -0
- data/docs/yard/Servus/Extensions.html +117 -0
- data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
- data/docs/yard/Servus/Generators.html +115 -0
- data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
- data/docs/yard/Servus/Helpers.html +115 -0
- data/docs/yard/Servus/Railtie.html +134 -0
- data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
- data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
- data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
- data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
- data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
- data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
- data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
- data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
- data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
- data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
- data/docs/yard/Servus/Support/Errors.html +140 -0
- data/docs/yard/Servus/Support/Logger.html +856 -0
- data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
- data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
- data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
- data/docs/yard/Servus/Support/Rescuer.html +267 -0
- data/docs/yard/Servus/Support/Response.html +574 -0
- data/docs/yard/Servus/Support/Validator.html +1150 -0
- data/docs/yard/Servus/Support.html +119 -0
- data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
- data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
- data/docs/yard/Servus/Testing.html +142 -0
- data/docs/yard/Servus.html +343 -0
- data/docs/yard/_index.html +535 -0
- data/docs/yard/class_list.html +54 -0
- data/docs/yard/css/common.css +1 -0
- data/docs/yard/css/full_list.css +58 -0
- data/docs/yard/css/style.css +503 -0
- data/docs/yard/file.1_common_patterns.html +154 -0
- data/docs/yard/file.1_configuration.html +115 -0
- data/docs/yard/file.1_overview.html +142 -0
- data/docs/yard/file.1_schema_validation.html +188 -0
- data/docs/yard/file.2_architecture.html +157 -0
- data/docs/yard/file.2_error_handling.html +190 -0
- data/docs/yard/file.2_migration_guide.html +242 -0
- data/docs/yard/file.2_testing.html +227 -0
- data/docs/yard/file.3_async_execution.html +145 -0
- data/docs/yard/file.3_rails_integration.html +160 -0
- data/docs/yard/file.3_service_objects.html +191 -0
- data/docs/yard/file.4_logging.html +135 -0
- data/docs/yard/file.ErrorHandling.html +190 -0
- data/docs/yard/file.READme.html +674 -0
- data/docs/yard/file.architecture.html +157 -0
- data/docs/yard/file.async_execution.html +145 -0
- data/docs/yard/file.common_patterns.html +154 -0
- data/docs/yard/file.configuration.html +115 -0
- data/docs/yard/file.error_handling.html +190 -0
- data/docs/yard/file.logging.html +135 -0
- data/docs/yard/file.migration_guide.html +242 -0
- data/docs/yard/file.overview.html +142 -0
- data/docs/yard/file.rails_integration.html +160 -0
- data/docs/yard/file.schema_validation.html +188 -0
- data/docs/yard/file.service_objects.html +191 -0
- data/docs/yard/file.testing.html +227 -0
- data/docs/yard/file_list.html +119 -0
- data/docs/yard/frames.html +22 -0
- data/docs/yard/index.html +674 -0
- data/docs/yard/js/app.js +344 -0
- data/docs/yard/js/full_list.js +242 -0
- data/docs/yard/js/jquery.js +4 -0
- data/docs/yard/method_list.html +542 -0
- data/docs/yard/top-level-namespace.html +110 -0
- data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
- data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
- data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
- data/lib/generators/servus/service/service_generator.rb +68 -1
- data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
- data/lib/generators/servus/service/templates/result.json.erb +8 -2
- data/lib/generators/servus/service/templates/service.rb.erb +102 -5
- data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
- data/lib/servus/base.rb +275 -58
- data/lib/servus/config.rb +83 -17
- data/lib/servus/event_handler.rb +275 -0
- data/lib/servus/events/bus.rb +137 -0
- data/lib/servus/events/emitter.rb +162 -0
- data/lib/servus/events/errors.rb +10 -0
- data/lib/servus/extensions/async/call.rb +50 -18
- data/lib/servus/extensions/async/errors.rb +23 -3
- data/lib/servus/extensions/async/ext.rb +10 -2
- data/lib/servus/extensions/async/job.rb +30 -9
- data/lib/servus/helpers/controller_helpers.rb +73 -37
- data/lib/servus/railtie.rb +16 -0
- data/lib/servus/support/errors.rb +135 -45
- data/lib/servus/support/rescuer.rb +189 -36
- data/lib/servus/support/response.rb +49 -7
- data/lib/servus/support/validator.rb +147 -19
- data/lib/servus/testing/example_builders.rb +133 -0
- data/lib/servus/testing/example_extractor.rb +309 -0
- data/lib/servus/testing/matchers.rb +88 -0
- data/lib/servus/testing.rb +19 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +6 -0
- metadata +135 -19
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'example_extractor'
|
|
4
|
+
|
|
5
|
+
module Servus
|
|
6
|
+
module Testing
|
|
7
|
+
# Provides helper methods for extracting example values from service schemas.
|
|
8
|
+
#
|
|
9
|
+
# This module is designed to be included in test files (RSpec, Minitest, etc.)
|
|
10
|
+
# to provide convenient access to schema example values. It's particularly useful
|
|
11
|
+
# for generating test fixtures without manually maintaining separate factory files.
|
|
12
|
+
#
|
|
13
|
+
# The `servus_` prefix on method names prevents naming collisions with other
|
|
14
|
+
# testing libraries and makes it clear these are Servus-specific helpers.
|
|
15
|
+
#
|
|
16
|
+
# @example Include in RSpec
|
|
17
|
+
# # spec/spec_helper.rb
|
|
18
|
+
# require 'servus/testing/example_builders'
|
|
19
|
+
#
|
|
20
|
+
# RSpec.configure do |config|
|
|
21
|
+
# config.include Servus::Testing::ExampleBuilders
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example Include in Rails console (development)
|
|
25
|
+
# # config/environments/development.rb
|
|
26
|
+
# config.to_prepare do
|
|
27
|
+
# require 'servus/testing/example_builders'
|
|
28
|
+
#
|
|
29
|
+
# if defined?(Rails::Console)
|
|
30
|
+
# include Servus::Testing::ExampleBuilders
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @example Use in tests
|
|
35
|
+
# RSpec.describe ProcessPayment::Service do
|
|
36
|
+
# it 'processes payment successfully' do
|
|
37
|
+
# args = servus_arguments_example(ProcessPayment::Service, amount: 50.0)
|
|
38
|
+
# result = ProcessPayment::Service.call(**args)
|
|
39
|
+
#
|
|
40
|
+
# expect(result).to be_success
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
module ExampleBuilders
|
|
44
|
+
# Extracts example argument values from a service's schema.
|
|
45
|
+
#
|
|
46
|
+
# Looks for `example` or `examples` keywords in the service's arguments schema
|
|
47
|
+
# and returns them as a hash ready to be passed to the service's `.call` method.
|
|
48
|
+
#
|
|
49
|
+
# @param service_class [Class] The service class to extract examples from
|
|
50
|
+
# @param overrides [Hash] Optional values to override the schema examples
|
|
51
|
+
# @return [Hash<Symbol, Object>] Hash of example argument values with symbolized keys
|
|
52
|
+
#
|
|
53
|
+
# @example Basic usage
|
|
54
|
+
# args = servus_arguments_example(ProcessPayment::Service)
|
|
55
|
+
# # => { user_id: 123, amount: 100.0, currency: 'USD' }
|
|
56
|
+
#
|
|
57
|
+
# result = ProcessPayment::Service.call(**args)
|
|
58
|
+
#
|
|
59
|
+
# @example With overrides
|
|
60
|
+
# args = servus_arguments_example(ProcessPayment::Service, amount: 50.0, currency: 'EUR')
|
|
61
|
+
# # => { user_id: 123, amount: 50.0, currency: 'EUR' }
|
|
62
|
+
#
|
|
63
|
+
# @example In RSpec tests
|
|
64
|
+
# it 'processes different currencies' do
|
|
65
|
+
# %w[USD EUR GBP].each do |currency|
|
|
66
|
+
# result = ProcessPayment::Service.call(
|
|
67
|
+
# **servus_arguments_example(ProcessPayment::Service, currency: currency)
|
|
68
|
+
# )
|
|
69
|
+
# expect(result).to be_success
|
|
70
|
+
# end
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# @note Override keys can be strings or symbols; they'll be converted to symbols
|
|
74
|
+
# @note Returns empty hash if service has no arguments schema defined
|
|
75
|
+
def servus_arguments_example(service_class, overrides = {})
|
|
76
|
+
extract_example_from(service_class, :arguments, overrides)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Extracts example result values from a service's schema.
|
|
80
|
+
#
|
|
81
|
+
# Looks for `example` or `examples` keywords in the service's result schema
|
|
82
|
+
# and returns them as a hash. Useful for validating service response structure
|
|
83
|
+
# and expected data shapes in tests.
|
|
84
|
+
#
|
|
85
|
+
# @param service_class [Class] The service class to extract examples from
|
|
86
|
+
# @param overrides [Hash] Optional values to override the schema examples
|
|
87
|
+
# @return [Servus::Support::Response] Response object with example result data
|
|
88
|
+
#
|
|
89
|
+
# @example Basic usage
|
|
90
|
+
# expected = servus_result_example(ProcessPayment::Service)
|
|
91
|
+
# # => Servus::Support::Response with data:
|
|
92
|
+
# # { transaction_id: 'txn_abc123', status: 'approved', amount_charged: 100.0 }
|
|
93
|
+
#
|
|
94
|
+
# @example Validate result structure
|
|
95
|
+
# result = ProcessPayment::Service.call(**servus_arguments_example(ProcessPayment::Service))
|
|
96
|
+
#
|
|
97
|
+
# expect(result.data).to match(
|
|
98
|
+
# hash_including(servus_result_example(ProcessPayment::Service).data)
|
|
99
|
+
# )
|
|
100
|
+
#
|
|
101
|
+
# @example Check result has expected keys
|
|
102
|
+
# result = ProcessPayment::Service.call(**args)
|
|
103
|
+
# expected_keys = servus_result_example(ProcessPayment::Service).data.keys
|
|
104
|
+
#
|
|
105
|
+
# expect(result.data.keys).to match_array(expected_keys)
|
|
106
|
+
#
|
|
107
|
+
# @example With overrides
|
|
108
|
+
# expected = servus_result_example(ProcessPayment::Service, status: 'pending').data
|
|
109
|
+
# # => { transaction_id: 'txn_abc123', status: 'pending', amount_charged: 100.0 }
|
|
110
|
+
#
|
|
111
|
+
# @note Override keys can be strings or symbols; they'll be converted to symbols
|
|
112
|
+
# @note Returns empty hash if service has no result schema defined
|
|
113
|
+
def servus_result_example(service_class, overrides = {})
|
|
114
|
+
example = extract_example_from(service_class, :result, overrides)
|
|
115
|
+
# Wrap in a successful Response object
|
|
116
|
+
Servus::Support::Response.new(true, example, nil)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Helper method to extract and merge examples from schema
|
|
122
|
+
#
|
|
123
|
+
# @param service_class [Class] The service class to extract examples from
|
|
124
|
+
# @param schema_type [Symbol] The type of schema (:arguments or :result)
|
|
125
|
+
# @param overrides [Hash] Optional values to override the schema examples
|
|
126
|
+
# @return [Hash<Symbol, Object>] Hash of example values with symbolized keys
|
|
127
|
+
def extract_example_from(service_class, schema_type, overrides = {})
|
|
128
|
+
examples = ExampleExtractor.extract(service_class, schema_type)
|
|
129
|
+
examples.deep_merge(overrides.deep_symbolize_keys)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servus
|
|
4
|
+
module Testing
|
|
5
|
+
# Extracts example values from JSON Schema definitions for use in testing.
|
|
6
|
+
#
|
|
7
|
+
# This class understands both OpenAPI-style `example` (singular) and
|
|
8
|
+
# JSON Schema-style `examples` (plural, array) keywords. It can handle
|
|
9
|
+
# nested objects, arrays, and complex schema structures.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic extraction
|
|
12
|
+
# schema = {
|
|
13
|
+
# type: 'object',
|
|
14
|
+
# properties: {
|
|
15
|
+
# name: { type: 'string', example: 'John Doe' },
|
|
16
|
+
# age: { type: 'integer', example: 30 }
|
|
17
|
+
# }
|
|
18
|
+
# }
|
|
19
|
+
#
|
|
20
|
+
# extractor = ExampleExtractor.new(schema)
|
|
21
|
+
# extractor.extract
|
|
22
|
+
# # => { name: 'John Doe', age: 30 }
|
|
23
|
+
#
|
|
24
|
+
# @example With service class
|
|
25
|
+
# examples = ExampleExtractor.extract(MyService, :arguments)
|
|
26
|
+
# # => { user_id: 123, amount: 100.0 }
|
|
27
|
+
#
|
|
28
|
+
# @see https://json-schema.org/understanding-json-schema/reference/annotations
|
|
29
|
+
# @see https://spec.openapis.org/oas/v3.1.0#schema-object
|
|
30
|
+
class ExampleExtractor
|
|
31
|
+
# Extracts example values from a service class's schema.
|
|
32
|
+
#
|
|
33
|
+
# This is a convenience class method that loads the schema via the
|
|
34
|
+
# Validator and extracts examples in one call.
|
|
35
|
+
#
|
|
36
|
+
# @param service_class [Class] The service class to extract examples from
|
|
37
|
+
# @param schema_type [Symbol] Either :arguments or :result
|
|
38
|
+
# @return [Hash<Symbol, Object>] Extracted example values with symbolized keys
|
|
39
|
+
#
|
|
40
|
+
# @example Extract argument examples
|
|
41
|
+
# ExampleExtractor.extract(ProcessPayment::Service, :arguments)
|
|
42
|
+
# # => { user_id: 123, amount: 100.0, currency: 'USD' }
|
|
43
|
+
#
|
|
44
|
+
# @example Extract result examples
|
|
45
|
+
# ExampleExtractor.extract(ProcessPayment::Service, :result)
|
|
46
|
+
# # => { transaction_id: 'txn_123', status: 'approved' }
|
|
47
|
+
def self.extract(service_class, schema_type)
|
|
48
|
+
schema = load_schema(service_class, schema_type)
|
|
49
|
+
return {} unless schema
|
|
50
|
+
|
|
51
|
+
new(schema).extract
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Initializes a new ExampleExtractor with a schema.
|
|
55
|
+
#
|
|
56
|
+
# The schema is deeply symbolized on initialization to normalize all keys,
|
|
57
|
+
# eliminating the need for double lookups throughout extraction.
|
|
58
|
+
#
|
|
59
|
+
# @param schema [Hash, nil] A JSON Schema hash with properties and examples
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# schema = { type: 'object', properties: { name: { example: 'Test' } } }
|
|
63
|
+
# extractor = ExampleExtractor.new(schema)
|
|
64
|
+
def initialize(schema)
|
|
65
|
+
@schema = deep_symbolize_keys(schema)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Extracts all example values from the schema.
|
|
69
|
+
#
|
|
70
|
+
# Traverses the schema structure and collects example values from:
|
|
71
|
+
# - Simple properties with `example` or `examples` keywords
|
|
72
|
+
# - Nested objects (recursively)
|
|
73
|
+
# - Arrays (using array-level examples or generating from item schemas)
|
|
74
|
+
#
|
|
75
|
+
# @return [Hash<Symbol, Object>] Hash of example values with symbolized keys
|
|
76
|
+
#
|
|
77
|
+
# @example Simple properties
|
|
78
|
+
# schema = {
|
|
79
|
+
# type: 'object',
|
|
80
|
+
# properties: {
|
|
81
|
+
# name: { type: 'string', example: 'John' },
|
|
82
|
+
# age: { type: 'integer', examples: [30, 25, 40] }
|
|
83
|
+
# }
|
|
84
|
+
# }
|
|
85
|
+
# extractor = ExampleExtractor.new(schema)
|
|
86
|
+
# extractor.extract
|
|
87
|
+
# # => { name: 'John', age: 30 }
|
|
88
|
+
#
|
|
89
|
+
# @example Nested objects
|
|
90
|
+
# schema = {
|
|
91
|
+
# type: 'object',
|
|
92
|
+
# properties: {
|
|
93
|
+
# user: {
|
|
94
|
+
# type: 'object',
|
|
95
|
+
# properties: {
|
|
96
|
+
# id: { type: 'integer', example: 123 },
|
|
97
|
+
# name: { type: 'string', example: 'Jane' }
|
|
98
|
+
# }
|
|
99
|
+
# }
|
|
100
|
+
# }
|
|
101
|
+
# }
|
|
102
|
+
# extractor = ExampleExtractor.new(schema)
|
|
103
|
+
# extractor.extract
|
|
104
|
+
# # => { user: { id: 123, name: 'Jane' } }
|
|
105
|
+
def extract
|
|
106
|
+
return {} unless @schema.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
extract_examples_from_properties(@schema)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# Extracts examples from schema properties.
|
|
114
|
+
#
|
|
115
|
+
# Iterates through the properties hash and extracts example values
|
|
116
|
+
# for each property that has one defined.
|
|
117
|
+
#
|
|
118
|
+
# @param schema [Hash] Schema hash containing a :properties key
|
|
119
|
+
# @return [Hash<Symbol, Object>] Extracted examples with symbolized keys
|
|
120
|
+
#
|
|
121
|
+
# @api private
|
|
122
|
+
def extract_examples_from_properties(schema)
|
|
123
|
+
properties = schema[:properties]
|
|
124
|
+
return {} unless properties
|
|
125
|
+
|
|
126
|
+
properties.each_with_object({}) do |(key, property_schema), examples|
|
|
127
|
+
example_value = extract_example_value(property_schema)
|
|
128
|
+
examples[key.to_sym] = example_value unless example_value.nil? && !explicit_nil_example?(property_schema)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Extracts a single example value from a property schema.
|
|
133
|
+
#
|
|
134
|
+
# Handles different types of properties:
|
|
135
|
+
# - Simple types with `example` or `examples` keywords
|
|
136
|
+
# - Nested objects (recursively extracts)
|
|
137
|
+
# - Arrays (uses array example or generates from items)
|
|
138
|
+
#
|
|
139
|
+
# @param property_schema [Hash] The schema for a single property
|
|
140
|
+
# @return [Object, nil] The example value, or nil if none found
|
|
141
|
+
#
|
|
142
|
+
# @api private
|
|
143
|
+
def extract_example_value(property_schema)
|
|
144
|
+
return nil unless property_schema.is_a?(Hash)
|
|
145
|
+
|
|
146
|
+
# Check for direct example keywords first
|
|
147
|
+
return get_example_from_keyword(property_schema) if example_keyword?(property_schema)
|
|
148
|
+
|
|
149
|
+
# Handle nested objects
|
|
150
|
+
return extract_examples_from_properties(property_schema) if nested_object?(property_schema)
|
|
151
|
+
|
|
152
|
+
# Handle arrays
|
|
153
|
+
return extract_array_example(property_schema) if array_type?(property_schema)
|
|
154
|
+
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Checks if property has an example keyword (example or examples).
|
|
159
|
+
#
|
|
160
|
+
# @param property_schema [Hash] Property schema to check
|
|
161
|
+
# @return [Boolean] True if example or examples keyword exists
|
|
162
|
+
#
|
|
163
|
+
# @api private
|
|
164
|
+
def example_keyword?(property_schema)
|
|
165
|
+
property_schema.key?(:example) || property_schema.key?(:examples)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Checks if property explicitly sets example to nil.
|
|
169
|
+
#
|
|
170
|
+
# This is important to distinguish between "no example" and "example is nil".
|
|
171
|
+
#
|
|
172
|
+
# @param property_schema [Hash] Property schema to check
|
|
173
|
+
# @return [Boolean] True if example is explicitly set to nil
|
|
174
|
+
#
|
|
175
|
+
# @api private
|
|
176
|
+
def explicit_nil_example?(property_schema)
|
|
177
|
+
property_schema.key?(:example) && property_schema[:example].nil?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Gets the example value from the example/examples keyword.
|
|
181
|
+
#
|
|
182
|
+
# Handles both:
|
|
183
|
+
# - `:example` (singular): returns the value directly
|
|
184
|
+
# - `:examples` (plural): returns a value from the array
|
|
185
|
+
#
|
|
186
|
+
# @param property_schema [Hash] Property schema with example keyword
|
|
187
|
+
# @return [Object] The example value
|
|
188
|
+
#
|
|
189
|
+
# @api private
|
|
190
|
+
def get_example_from_keyword(property_schema)
|
|
191
|
+
# Check for :example (singular) first - OpenAPI style
|
|
192
|
+
return property_schema[:example] if property_schema.key?(:example)
|
|
193
|
+
|
|
194
|
+
# Check for :examples (plural) - JSON Schema style
|
|
195
|
+
examples = property_schema[:examples]
|
|
196
|
+
return nil unless examples.is_a?(Array) && examples.any?
|
|
197
|
+
|
|
198
|
+
examples.sample
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Checks if property is a nested object type.
|
|
202
|
+
#
|
|
203
|
+
# @param property_schema [Hash] Property schema to check
|
|
204
|
+
# @return [Boolean] True if type is object and has properties
|
|
205
|
+
#
|
|
206
|
+
# @api private
|
|
207
|
+
def nested_object?(property_schema)
|
|
208
|
+
property_schema[:type] == 'object' && property_schema[:properties]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Checks if property is an array type.
|
|
212
|
+
#
|
|
213
|
+
# @param property_schema [Hash] Property schema to check
|
|
214
|
+
# @return [Boolean] True if type is array
|
|
215
|
+
#
|
|
216
|
+
# @api private
|
|
217
|
+
def array_type?(property_schema)
|
|
218
|
+
property_schema[:type] == 'array'
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Extracts example value for an array property.
|
|
222
|
+
#
|
|
223
|
+
# Handles two strategies:
|
|
224
|
+
# 1. If array has direct `example` keyword, use it
|
|
225
|
+
# 2. Otherwise, generate array with one item using item schema examples
|
|
226
|
+
#
|
|
227
|
+
# @param property_schema [Hash] Array property schema
|
|
228
|
+
# @return [Array, nil] Array example or nil if can't be generated
|
|
229
|
+
#
|
|
230
|
+
# @example Array with direct example
|
|
231
|
+
# { type: 'array', example: [1, 2, 3] }
|
|
232
|
+
# # => [1, 2, 3]
|
|
233
|
+
#
|
|
234
|
+
# @example Array with item schema examples
|
|
235
|
+
# {
|
|
236
|
+
# type: 'array',
|
|
237
|
+
# items: {
|
|
238
|
+
# type: 'object',
|
|
239
|
+
# properties: {
|
|
240
|
+
# id: { type: 'integer', examples: [1, 2] },
|
|
241
|
+
# name: { type: 'string', examples: ['John', 'Jane'] }
|
|
242
|
+
# }
|
|
243
|
+
# }
|
|
244
|
+
# }
|
|
245
|
+
# # => [{ id: 1, name: 'John' }]
|
|
246
|
+
#
|
|
247
|
+
# @api private
|
|
248
|
+
def extract_array_example(property_schema)
|
|
249
|
+
# If array has direct example, use it
|
|
250
|
+
return get_example_from_keyword(property_schema) if example_keyword?(property_schema)
|
|
251
|
+
|
|
252
|
+
# Otherwise, try to generate an array with one item from the items schema
|
|
253
|
+
items_schema = property_schema[:items]
|
|
254
|
+
return nil unless items_schema
|
|
255
|
+
|
|
256
|
+
# Generate one example item from the items schema
|
|
257
|
+
if nested_object?(items_schema)
|
|
258
|
+
item_example = extract_examples_from_properties(items_schema)
|
|
259
|
+
return [item_example] if item_example.any?
|
|
260
|
+
elsif example_keyword?(items_schema)
|
|
261
|
+
return [get_example_from_keyword(items_schema)]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Recursively converts all hash keys to symbols.
|
|
268
|
+
#
|
|
269
|
+
# Handles nested hashes and arrays of hashes, ensuring consistent
|
|
270
|
+
# key types throughout the structure.
|
|
271
|
+
#
|
|
272
|
+
# @param value [Object] The value to process
|
|
273
|
+
# @return [Object] The value with all hash keys symbolized
|
|
274
|
+
#
|
|
275
|
+
# @api private
|
|
276
|
+
def deep_symbolize_keys(value)
|
|
277
|
+
case value
|
|
278
|
+
when Hash
|
|
279
|
+
value.each_with_object({}) do |(key, val), result|
|
|
280
|
+
result[key.to_sym] = deep_symbolize_keys(val)
|
|
281
|
+
end
|
|
282
|
+
when Array
|
|
283
|
+
value.map { |item| deep_symbolize_keys(item) }
|
|
284
|
+
else
|
|
285
|
+
value
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Loads schema from service class using Validator.
|
|
290
|
+
#
|
|
291
|
+
# Reuses the existing Validator schema loading logic which handles:
|
|
292
|
+
# - DSL-defined schemas
|
|
293
|
+
# - Constant-defined schemas
|
|
294
|
+
# - File-based schemas
|
|
295
|
+
# - Schema caching
|
|
296
|
+
#
|
|
297
|
+
# @param service_class [Class] The service class
|
|
298
|
+
# @param schema_type [Symbol] Either :arguments or :result
|
|
299
|
+
# @return [Hash, nil] The loaded schema or nil
|
|
300
|
+
#
|
|
301
|
+
# @api private
|
|
302
|
+
def self.load_schema(service_class, schema_type)
|
|
303
|
+
Servus::Support::Validator.load_schema(service_class, schema_type.to_s)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private_class_method :load_schema
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
|
4
|
+
require 'rspec/expectations'
|
|
5
|
+
|
|
6
|
+
module Servus
|
|
7
|
+
module Testing
|
|
8
|
+
# RSpec matchers for testing Servus services and events.
|
|
9
|
+
module Matchers
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Matcher for asserting event emission
|
|
15
|
+
RSpec::Matchers.define :emit_event do |handler_class_or_symbol|
|
|
16
|
+
supports_block_expectations
|
|
17
|
+
|
|
18
|
+
chain :with do |payload|
|
|
19
|
+
@expected_payload = payload
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
match do |block|
|
|
23
|
+
@captured_events = []
|
|
24
|
+
|
|
25
|
+
subscription = ActiveSupport::Notifications.subscribe(/^servus\.events\./) do |name, *_args, payload|
|
|
26
|
+
event_name = name.sub('servus.events.', '').to_sym
|
|
27
|
+
@captured_events << { name: event_name, payload: payload }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
block.call
|
|
31
|
+
|
|
32
|
+
# Determine event name
|
|
33
|
+
@event_name = if handler_class_or_symbol.is_a?(Symbol)
|
|
34
|
+
handler_class_or_symbol
|
|
35
|
+
else
|
|
36
|
+
handler_class_or_symbol.event_name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@matching_event = @captured_events.find { |e| e[:name] == @event_name }
|
|
40
|
+
|
|
41
|
+
return false unless @matching_event
|
|
42
|
+
return true unless @expected_payload
|
|
43
|
+
|
|
44
|
+
RSpec::Matchers::BuiltIn::Match.new(@expected_payload).matches?(@matching_event[:payload])
|
|
45
|
+
ensure
|
|
46
|
+
ActiveSupport::Notifications.unsubscribe(subscription) if subscription
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
failure_message do
|
|
50
|
+
if @matching_event.nil?
|
|
51
|
+
"expected event :#{@event_name} to be emitted, but it was not.\n" \
|
|
52
|
+
"Emitted: #{@captured_events.map { |e| e[:name] }}"
|
|
53
|
+
else
|
|
54
|
+
"expected event :#{@event_name} payload to match #{@expected_payload.inspect}, " \
|
|
55
|
+
"got: #{@matching_event[:payload].inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Matcher for asserting service invocation
|
|
61
|
+
RSpec::Matchers.define :call_service do |service_class|
|
|
62
|
+
supports_block_expectations
|
|
63
|
+
|
|
64
|
+
chain :with do |args|
|
|
65
|
+
@expected_args = args
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
chain :async do
|
|
69
|
+
@expect_async = true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
match do |block|
|
|
73
|
+
method_name = @expect_async ? :call_async : :call
|
|
74
|
+
|
|
75
|
+
expectation = expect(service_class).to receive(method_name)
|
|
76
|
+
expectation.with(@expected_args) if @expected_args
|
|
77
|
+
|
|
78
|
+
block.call
|
|
79
|
+
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
failure_message do
|
|
84
|
+
method = @expect_async ? 'call_async' : 'call'
|
|
85
|
+
"expected #{service_class} to receive #{method}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
# rubocop:enable Metrics/BlockLength
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servus
|
|
4
|
+
# Testing utilities for Servus services.
|
|
5
|
+
#
|
|
6
|
+
# This module provides helpers for extracting example values from JSON schemas
|
|
7
|
+
# to use in tests, making it easier to create test fixtures without manually
|
|
8
|
+
# maintaining separate factory files.
|
|
9
|
+
#
|
|
10
|
+
# @see Servus::Testing::ExampleBuilders
|
|
11
|
+
# @see Servus::Testing::ExampleExtractor
|
|
12
|
+
# @see Servus::Testing::Matchers
|
|
13
|
+
module Testing
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require_relative 'testing/example_extractor'
|
|
18
|
+
require_relative 'testing/example_builders'
|
|
19
|
+
require_relative 'testing/matchers'
|
data/lib/servus/version.rb
CHANGED
data/lib/servus.rb
CHANGED
|
@@ -25,6 +25,12 @@ require_relative 'servus/support/validator'
|
|
|
25
25
|
require_relative 'servus/support/errors'
|
|
26
26
|
require_relative 'servus/support/rescuer'
|
|
27
27
|
|
|
28
|
+
# Events
|
|
29
|
+
require_relative 'servus/events/errors'
|
|
30
|
+
require_relative 'servus/events/bus'
|
|
31
|
+
require_relative 'servus/events/emitter'
|
|
32
|
+
require_relative 'servus/event_handler'
|
|
33
|
+
|
|
28
34
|
# Core
|
|
29
35
|
require_relative 'servus/version'
|
|
30
36
|
require_relative 'servus/base'
|