servus 0.7.0 → 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.
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'did_you_mean'
4
+
5
+ module Servus
6
+ # Registry of reusable JSON Schema fragments, and the entry point for
7
+ # compiling a schema that references them.
8
+ #
9
+ # Servus services and events declare their contracts inline via the +schema+
10
+ # DSL. That keeps a service's inputs and outputs visible in the file that
11
+ # implements it. The cost of inline-only declaration is duplication: the same
12
+ # +amount+ or +timestamp+ shape gets copy-pasted across every service that
13
+ # touches it.
14
+ #
15
+ # Registered fragments close that gap without giving up explicitness. A
16
+ # fragment is registered under a key, and services reference into it with a
17
+ # standard JSON Schema +$ref+. A service that references a shared type is
18
+ # still explicitly declaring that type — it just names it once.
19
+ #
20
+ # @example Registering a fragment
21
+ # Servus::Schema.register('core', {
22
+ # '$defs' => {
23
+ # 'amount' => { 'type' => 'integer', 'minimum' => 0 }
24
+ # }
25
+ # })
26
+ #
27
+ # @example Referencing it from a service
28
+ # class Treasury::TransferGold::Service < Servus::Base
29
+ # schema arguments: {
30
+ # type: 'object',
31
+ # required: ['gold_dragons'],
32
+ # properties: {
33
+ # gold_dragons: { '$ref' => '#/core/$defs/amount' }
34
+ # }
35
+ # }
36
+ # end
37
+ #
38
+ # Lookups never return nil. An unregistered key raises {UnknownKeyError} at
39
+ # the point of use, because the alternative — silently skipping validation for
40
+ # a service that appears to declare a contract — is the worst failure mode
41
+ # this system has.
42
+ #
43
+ # @see Servus::Schema::Compiler
44
+ # @see Servus::Base.schema
45
+ module Schema
46
+ @registry = {}.freeze
47
+ @cache = Cache.new
48
+ @mutex = Mutex.new
49
+
50
+ class << self
51
+ # Memoized ref resolutions and the generation counter derived from them.
52
+ #
53
+ # @return [Servus::Schema::Cache]
54
+ # @api private
55
+ attr_reader :cache
56
+
57
+ # Monotonic counter bumped whenever the registry changes.
58
+ #
59
+ # Consumers memoize compiled schemas alongside the generation they were
60
+ # compiled under, and recompile when it moves. That makes registry
61
+ # updates propagate without any explicit dependency tracking.
62
+ #
63
+ # @return [Integer]
64
+ def generation = cache.generation
65
+
66
+ # Registers a reusable schema fragment under +key+.
67
+ #
68
+ # Re-registering an equal value is a silent no-op, so calling this from a
69
+ # Rails +to_prepare+ block is safe. Re-registering a *different* value
70
+ # replaces it, logs an override, and bumps {generation} — which
71
+ # invalidates every compiled schema that referenced it.
72
+ #
73
+ # @param key [String, Symbol] the fragment key, referenced as +#/<key>/...+
74
+ # @param fragment [Hash] the schema fragment
75
+ # @return [ActiveSupport::HashWithIndifferentAccess] the normalized fragment
76
+ # @raise [InvalidKeyError] if the key is blank or contains a +/+
77
+ # @raise [ArgumentError] if the fragment is not a Hash
78
+ #
79
+ # @example
80
+ # Servus::Schema.register('core', { '$defs' => { 'id' => { 'type' => 'integer' } } })
81
+ def register(key, fragment)
82
+ key = normalize_key(key)
83
+ normalized = normalize_fragment(key, fragment)
84
+
85
+ cache.invalidate! if store(key, normalized)
86
+
87
+ normalized
88
+ end
89
+
90
+ # Returns a registered fragment, or a definition within one.
91
+ #
92
+ # Given no path, returns the whole fragment. Given path segments, walks
93
+ # them as literal keys — the same addressing a +$ref+ uses, so
94
+ # +fetch(key, *path)+ reads exactly what +ref(key, *path)+ points at.
95
+ #
96
+ # A missing path raises rather than returning nil. Reaching for the
97
+ # fragment and calling +dig+ would return nil on a typo, which is the
98
+ # silent failure this registry exists to prevent.
99
+ #
100
+ # Fragments are returned as authored, with any +$ref+s intact. Use
101
+ # {compile} to resolve them.
102
+ #
103
+ # @param key [String, Symbol] the fragment key
104
+ # @param path [Array<String, Symbol>] segments to walk within the fragment
105
+ # @return [ActiveSupport::HashWithIndifferentAccess, Object] the frozen fragment or definition
106
+ # @raise [UnknownKeyError] if nothing is registered under +key+
107
+ # @raise [RefNotFoundError] if the path is not present in the fragment
108
+ #
109
+ # @example
110
+ # Servus::Schema.fetch('core')
111
+ # Servus::Schema.fetch('core', '$defs', 'amount')
112
+ def fetch(key, *path)
113
+ key = key.to_s
114
+ fragment = @registry.fetch(key) { raise UnknownKeyError.for(key, available: @registry.keys) }
115
+
116
+ Path.walk(fragment, key, path.map(&:to_s))
117
+ end
118
+
119
+ # Returns a fragment, or a definition within one, with all +$ref+s resolved.
120
+ #
121
+ # The compiled counterpart to {fetch}: same addressing, but the result is
122
+ # self-contained and ready to validate against. This is usually what
123
+ # application code outside a service wants — a controller validating a
124
+ # request body, a serializer checking a response shape.
125
+ #
126
+ # Results are memoized, so asking repeatedly for the same address is cheap.
127
+ #
128
+ # @param key [String, Symbol] the fragment key
129
+ # @param path [Array<String, Symbol>] segments to walk within the fragment
130
+ # @return [Hash, Object] the compiled fragment or definition
131
+ # @raise [UnknownKeyError] if nothing is registered under +key+
132
+ # @raise [RefNotFoundError] if the path is not present in the fragment
133
+ # @raise [Error] if a ref within it cannot be resolved
134
+ #
135
+ # @example
136
+ # Servus::Schema.resolve('endpoints::trades::create', '$defs', 'request')
137
+ # # => { "type" => "object", "properties" => { "price" => { "type" => "integer" } } }
138
+ def resolve(key, *path)
139
+ pointer = ref(key, *path)
140
+
141
+ compile(pointer, context: "schema #{pointer['$ref']}")
142
+ end
143
+
144
+ # Compiles every registered fragment, resolving all +$ref+s.
145
+ #
146
+ # Returns a hash of key to compiled fragment, mirroring the registry's own
147
+ # shape so keys stay addressable and the result serializes straight to
148
+ # JSON. Useful for producing a single schema asset for an API description,
149
+ # a docs build, client codegen, or a CI freshness check.
150
+ #
151
+ # @return [Hash{String => Hash}] every fragment, refs resolved
152
+ # @raise [Error] if any fragment contains a ref that cannot be resolved
153
+ #
154
+ # @example
155
+ # File.write('schema.json', JSON.pretty_generate(Servus::Schema.compile_all))
156
+ def compile_all
157
+ keys.to_h { |key| [key, compile(fetch(key), context: "schema fragment #{key.inspect}")] }
158
+ end
159
+
160
+ # @return [Array<String>] registered keys, sorted
161
+ def keys
162
+ @registry.keys.sort
163
+ end
164
+
165
+ # Builds a +$ref+ pointing at a registered fragment.
166
+ #
167
+ # Prefer this over hand-writing ref strings — it is typo-proof in the
168
+ # separator and prefix, which are the parts people get wrong.
169
+ #
170
+ # @param key [String, Symbol] the fragment key
171
+ # @param path [Array<String, Symbol>] segments to walk within the fragment
172
+ # @return [Hash] a +$ref+ hash
173
+ #
174
+ # @example
175
+ # Servus::Schema.ref('core', '$defs', 'amount')
176
+ # # => { "$ref" => "#/core/$defs/amount" }
177
+ def ref(key, *path)
178
+ { '$ref' => "#/#{[key, *path].map(&:to_s).join('/')}" }
179
+ end
180
+
181
+ # Compiles a schema, replacing every +$ref+ with the fragment it names.
182
+ #
183
+ # @param schema [Hash, nil] the authored schema
184
+ # @param context [String, nil] label used in error messages, e.g.
185
+ # "Treasury::TransferGold::Service arguments schema"
186
+ # @return [Hash, nil] the compiled schema, or nil if +schema+ was nil
187
+ # @raise [Error] if any ref cannot be resolved
188
+ def compile(schema, context: nil)
189
+ return nil if schema.nil?
190
+
191
+ Compiler.new(context: context).compile(schema)
192
+ end
193
+
194
+ # Clears the registry. Intended for test suites.
195
+ #
196
+ # @return [void]
197
+ def reset!
198
+ restore({}.freeze)
199
+ end
200
+
201
+ # Captures the registry state so a test can restore it afterwards.
202
+ #
203
+ # @return [Hash] an opaque snapshot for {restore}
204
+ # @api private
205
+ def snapshot
206
+ @registry
207
+ end
208
+
209
+ # Restores a snapshot taken by {snapshot}.
210
+ #
211
+ # @param snapshot [Hash]
212
+ # @return [void]
213
+ # @api private
214
+ def restore(snapshot)
215
+ @mutex.synchronize { @registry = snapshot }
216
+ cache.invalidate!
217
+ end
218
+
219
+ private
220
+
221
+ # Writes a normalized fragment into the registry.
222
+ #
223
+ # @param key [String]
224
+ # @param normalized [Hash] the normalized fragment
225
+ # @return [Boolean] whether the registry actually changed
226
+ # @api private
227
+ def store(key, normalized)
228
+ @mutex.synchronize do
229
+ existing = @registry[key]
230
+ return false if existing == normalized
231
+
232
+ Support::Logger.log_schema_override(key) if existing
233
+ @registry = @registry.merge(key => normalized).freeze
234
+ end
235
+
236
+ true
237
+ end
238
+
239
+ # @param key [String, Symbol]
240
+ # @return [String]
241
+ # @raise [InvalidKeyError]
242
+ # @api private
243
+ def normalize_key(key)
244
+ key = key.to_s
245
+
246
+ raise InvalidKeyError, 'schema fragment key cannot be blank' if key.strip.empty?
247
+
248
+ if key.include?('/')
249
+ raise InvalidKeyError, "schema fragment key #{key.inspect} cannot contain '/' — " \
250
+ 'it is the separator in $ref paths, so the key would be unreferenceable'
251
+ end
252
+
253
+ key
254
+ end
255
+
256
+ # @param key [String] the key, for the error message
257
+ # @param fragment [Hash]
258
+ # @return [ActiveSupport::HashWithIndifferentAccess] deeply frozen
259
+ # @raise [ArgumentError] if the fragment is not a Hash
260
+ # @api private
261
+ def normalize_fragment(key, fragment)
262
+ unless fragment.is_a?(Hash)
263
+ raise ArgumentError, "schema fragment for #{key.inspect} must be a Hash, got #{fragment.class}"
264
+ end
265
+
266
+ deep_freeze(fragment.deep_dup.with_indifferent_access)
267
+ end
268
+
269
+ # @param value [Object]
270
+ # @return [Object] the same value, frozen through nested hashes and arrays
271
+ # @api private
272
+ def deep_freeze(value)
273
+ case value
274
+ when Hash then value.each_value { |v| deep_freeze(v) }.freeze
275
+ when Array then value.each { |v| deep_freeze(v) }.freeze
276
+ else value.freeze
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -94,6 +94,16 @@ module Servus
94
94
  logger.error("#{service_class.name} uncaught exception: #{exception.class} - #{exception.message}")
95
95
  end
96
96
 
97
+ # Logs that a registered schema fragment was replaced with a different value.
98
+ #
99
+ # Expected during development reloads. Outside of that it usually means
100
+ # two libraries are claiming the same fragment key.
101
+ #
102
+ # @param key [String] The schema fragment key being overridden
103
+ def self.log_schema_override(key)
104
+ logger.warn("Schema fragment #{key.inspect} was already registered with a different value; replacing it.")
105
+ end
106
+
97
107
  # Filters parameters for logging based on the configured filter list.
98
108
  #
99
109
  # @param params [Hash] The parameters to filter
@@ -2,42 +2,46 @@
2
2
 
3
3
  module Servus
4
4
  module Support
5
- # Handles JSON Schema validation for service arguments and results.
5
+ # Validates service arguments and results, and event payloads, against the
6
+ # JSON schemas declared with the +schema+ DSL.
6
7
  #
7
- # The Validator class provides automatic validation of service inputs and outputs
8
- # against JSON Schema definitions. Schemas can be defined as inline constants
9
- # (ARGUMENTS_SCHEMA, RESULT_SCHEMA) or as external JSON files.
8
+ # Arguments are validated before +call+ runs, so a service body can trust
9
+ # the shape of its inputs. Result data is validated after it returns, so a
10
+ # service that stops honouring its own contract fails loudly rather than
11
+ # passing the wrong shape to its callers. Both raise
12
+ # {Servus::Support::Errors::ValidationError}, which signals a bug — in the
13
+ # caller for arguments, in the service itself for results — and is not
14
+ # meant to be rescued.
10
15
  #
11
- # @example Inline schema validation
16
+ # Schemas come from the +schema+ DSL and nowhere else. The class-level
17
+ # readers resolve any +$ref+s against {Servus::Schema}, so what arrives
18
+ # here is always a self-contained schema.
19
+ #
20
+ # @example
12
21
  # class MyService < Servus::Base
13
- # ARGUMENTS_SCHEMA = {
14
- # type: "object",
15
- # required: ["user_id"],
16
- # properties: {
17
- # user_id: { type: "integer" }
18
- # }
19
- # }
22
+ # schema arguments: { type: 'object', required: ['user_id'] }
20
23
  # end
21
24
  #
22
- # @example File-based schema validation
23
- # # app/schemas/services/my_service/arguments.json
24
- # # { "type": "object", "required": ["user_id"], ... }
25
- #
25
+ # @see Servus::Base.schema
26
+ # @see Servus::Schema
26
27
  # @see https://json-schema.org/specification.html
27
28
  class Validator
29
+ # Schema kinds that may be requested from {.load_schema}.
30
+ #
31
+ # @api private
32
+ SCHEMA_TYPES = %w[arguments result failure payload].freeze
33
+
28
34
  # @api private
29
35
  @schema_cache = {}
30
36
 
31
- # Validates service arguments against the ARGUMENTS_SCHEMA.
32
- #
33
- # Checks arguments against either an inline ARGUMENTS_SCHEMA constant or
34
- # a file-based schema at app/schemas/services/namespace/arguments.json.
35
- # Validation is skipped if no schema is defined.
37
+ # Validates service arguments against the service's arguments schema.
36
38
  #
37
39
  # @param service_class [Class] the service class being validated
38
40
  # @param args [Hash] keyword arguments passed to the service
39
41
  # @return [Boolean] true if validation passes
40
42
  # @raise [Servus::Support::Errors::ValidationError] if arguments fail validation
43
+ # @raise [Servus::Support::Errors::SchemaRequiredError] if no schema is
44
+ # declared and +require_service_arguments_schema+ is enabled
41
45
  #
42
46
  # @example
43
47
  # Validator.validate_arguments!(MyService, { user_id: 123 })
@@ -48,11 +52,7 @@ module Servus
48
52
  enforce_schema_presence!(schema, service_class, :require_service_arguments_schema)
49
53
  return true unless schema
50
54
 
51
- validate_data_against_schema!(
52
- args,
53
- schema,
54
- "Invalid arguments for #{service_class.name}"
55
- )
55
+ validate_data_against_schema!(args, schema, "Invalid arguments for #{service_class.name}")
56
56
 
57
57
  true
58
58
  end
@@ -102,7 +102,7 @@ module Servus
102
102
  end
103
103
  end
104
104
 
105
- # Validates event payload against the Event class's payload schema.
105
+ # Validates an event payload against the event's payload schema.
106
106
  #
107
107
  # @param event_class [Class] the Event subclass
108
108
  # @param payload [Hash] the event payload to validate
@@ -114,7 +114,7 @@ module Servus
114
114
  #
115
115
  # @api private
116
116
  def self.validate_event_payload!(event_class, payload)
117
- schema = event_class.payload_schema
117
+ schema = load_schema(event_class, 'payload')
118
118
  enforce_schema_presence!(schema, event_class, :require_event_payload_schema)
119
119
  return true unless schema
120
120
 
@@ -127,50 +127,36 @@ module Servus
127
127
  true
128
128
  end
129
129
 
130
- # Loads and caches a schema for a service.
131
- #
132
- # Implements a three-tier lookup strategy:
133
- # 1. Check for schema defined via DSL method (service_class.arguments_schema/result_schema)
134
- # 2. Check for inline constant (ARGUMENTS_SCHEMA or RESULT_SCHEMA)
135
- # 3. Fall back to JSON file in app/schemas/services/namespace/type.json
130
+ # Returns a class's compiled schema of the given kind.
136
131
  #
137
- # Schemas are cached after first load for performance.
132
+ # Cached per class and kind. The underlying compilation is also memoized
133
+ # on the class itself and rebuilds when {Servus::Schema} changes, so this
134
+ # cache exists to skip the lookup, not to hold compilation results.
138
135
  #
139
- # @param service_class [Class] the service class
140
- # @param type [String] schema type ("arguments", "result", or "failure")
141
- # @return [Hash, nil] the schema hash, or nil if no schema found
136
+ # @param klass [Class] a {Servus::Base} or {Servus::Event} subclass
137
+ # @param type [String, Symbol] one of {SCHEMA_TYPES}
138
+ # @return [Hash, nil] the compiled schema, or nil if none is declared
139
+ # @raise [ArgumentError] if +type+ is not a known schema kind
142
140
  #
143
141
  # @api private
144
- # rubocop:disable Metrics/MethodLength
145
- def self.load_schema(service_class, type)
146
- # Get service path based on class name (e.g., "process_payment" from "Servus::ProcessPayment::Service")
147
- service_namespace = parse_service_namespace(service_class)
148
- schema_path = Servus.config.schema_path_for(service_namespace, type)
149
-
150
- # Return from cache if available
151
- return @schema_cache[schema_path] if @schema_cache.key?(schema_path)
142
+ def self.load_schema(klass, type)
143
+ type = type.to_s
152
144
 
153
- # Check for DSL-defined schema first
154
- dsl_schema = case type
155
- when 'arguments' then service_class.arguments_schema
156
- when 'result' then service_class.result_schema
157
- when 'failure' then service_class.failure_schema
158
- end
145
+ unless SCHEMA_TYPES.include?(type)
146
+ raise ArgumentError, "unknown schema type #{type.inspect}. Valid: #{SCHEMA_TYPES.join(', ')}."
147
+ end
159
148
 
160
- inline_schema_constant_name = "#{service_class}::#{type.upcase}_SCHEMA"
161
- inline_schema_constant = if Object.const_defined?(inline_schema_constant_name)
162
- Object.const_get(inline_schema_constant_name)
163
- end
149
+ key = [klass, type]
150
+ return @schema_cache[key] if @schema_cache.key?(key)
164
151
 
165
- @schema_cache[schema_path] = fetch_schema_from_sources(dsl_schema, inline_schema_constant, schema_path)
166
- @schema_cache[schema_path]
152
+ @schema_cache[key] = klass.public_send(:"#{type}_schema")
167
153
  end
168
- # rubocop:enable Metrics/MethodLength
169
154
 
170
155
  # Clears the schema cache.
171
156
  #
172
- # Useful in development when schema files are modified, or in tests
173
- # to ensure fresh schema loading between test cases.
157
+ # Useful in tests, and in development after changing a schema. Registry
158
+ # changes invalidate compiled schemas on their own, so this is rarely
159
+ # needed in application code.
174
160
  #
175
161
  # @return [Hash] empty hash
176
162
  #
@@ -184,7 +170,7 @@ module Servus
184
170
 
185
171
  # Returns the current schema cache.
186
172
  #
187
- # @return [Hash] cache mapping schema paths to loaded schemas
173
+ # @return [Hash] cache mapping [class, type] pairs to compiled schemas
188
174
  # @api private
189
175
  def self.cache
190
176
  @schema_cache
@@ -206,12 +192,12 @@ module Servus
206
192
  raise Servus::Base::ValidationError, "#{message_prefix}: #{errors.join(', ')}"
207
193
  end
208
194
 
209
- # Returns the schema if present. Raises if absent and the config flag is enabled.
195
+ # Raises if a schema is absent and the corresponding config flag is on.
210
196
  #
211
197
  # @param schema [Hash, nil] the loaded schema
212
198
  # @param klass [Class] the service or Event class
213
199
  # @param config_flag [Symbol] the config method to check
214
- # @return [Hash, nil] the schema
200
+ # @return [Hash, nil] the schema, unchanged
215
201
  # @raise [Servus::Support::Errors::SchemaRequiredError] if schema is nil and enforcement is enabled
216
202
  #
217
203
  # @api private
@@ -223,49 +209,6 @@ module Servus
223
209
  raise Servus::Support::Errors::SchemaRequiredError,
224
210
  "#{klass.name} schema missing! #{config_flag} is set to true."
225
211
  end
226
-
227
- # Fetches schema from DSL, inline constant, or file.
228
- #
229
- # Implements the schema resolution precedence:
230
- # 1. DSL-defined schema (if provided)
231
- # 2. Inline constant (if provided)
232
- # 3. File at schema_path (if exists)
233
- # 4. nil (no schema found)
234
- #
235
- # @param dsl_schema [Hash, nil] schema from DSL method (e.g., schema arguments: Hash)
236
- # @param inline_schema_constant [Hash, nil] inline schema constant (e.g., ARGUMENTS_SCHEMA)
237
- # @param schema_path [String] file path to external schema JSON
238
- # @return [Hash, nil] schema with indifferent access, or nil if not found
239
- #
240
- # @api private
241
- def self.fetch_schema_from_sources(dsl_schema, inline_schema_constant, schema_path)
242
- if dsl_schema
243
- dsl_schema.with_indifferent_access
244
- elsif inline_schema_constant
245
- inline_schema_constant.with_indifferent_access
246
- elsif File.exist?(schema_path)
247
- JSON.load_file(schema_path).with_indifferent_access
248
- end
249
- end
250
-
251
- # Converts service class name to file path namespace.
252
- #
253
- # Transforms a class name like "Services::ProcessPayment::Service" into
254
- # "services/process_payment" for locating schema files.
255
- #
256
- # @param service_class [Class] the service class
257
- # @return [String] underscored namespace path
258
- #
259
- # @example
260
- # parse_service_namespace(Services::ProcessPayment::Service)
261
- # # => "services/process_payment"
262
- #
263
- # @api private
264
- def self.parse_service_namespace(service_class)
265
- service_class.name.split('::')[..-2].map do |s|
266
- s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
267
- end.join('/')
268
- end
269
212
  end
270
213
  end
271
214
  end
@@ -286,13 +286,11 @@ module Servus
286
286
  end
287
287
  end
288
288
 
289
- # Loads schema from service class using Validator.
289
+ # Loads a schema from a service class using the Validator.
290
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
291
+ # The schema returned is compiled, so +example+ and +examples+ values
292
+ # inside +$ref+'d fragments are visible to extraction — a shared fragment
293
+ # can carry its own examples and every service referencing it gets them.
296
294
  #
297
295
  # @param service_class [Class] The service class
298
296
  # @param schema_type [Symbol] Either :arguments or :result
@@ -94,12 +94,7 @@ end
94
94
  # Matcher for asserting schema presence on a service or Event class
95
95
  RSpec::Matchers.define :have_schema do |schema_type|
96
96
  match do |klass|
97
- if schema_type.to_s == 'payload'
98
- !klass.payload_schema.nil?
99
- else
100
- Servus::Support::Validator.clear_cache!
101
- !Servus::Support::Validator.load_schema(klass, schema_type.to_s).nil?
102
- end
97
+ !Servus::Support::Validator.load_schema(klass, schema_type.to_s).nil?
103
98
  end
104
99
 
105
100
  failure_message do |klass|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Servus
4
- VERSION = '0.7.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/servus.rb CHANGED
@@ -5,6 +5,8 @@ require 'json-schema'
5
5
  require 'active_support'
6
6
  require 'active_support/core_ext/class/attribute'
7
7
  require 'active_support/core_ext/hash/indifferent_access'
8
+ require 'active_support/core_ext/object/deep_dup'
9
+ require 'active_support/core_ext/string/inflections'
8
10
  # Servus namespace
9
11
  module Servus; end
10
12
 
@@ -17,6 +19,15 @@ require_relative 'servus/railtie' if defined?(Rails::Railtie)
17
19
  # Config
18
20
  require_relative 'servus/config'
19
21
 
22
+ # Schema
23
+ require_relative 'servus/schema/errors'
24
+ require_relative 'servus/schema/cache'
25
+ require_relative 'servus/schema/path'
26
+ require_relative 'servus/schema/ref'
27
+ require_relative 'servus/schema/declaration'
28
+ require_relative 'servus/schema/compiler'
29
+ require_relative 'servus/schema'
30
+
20
31
  # Support
21
32
  require_relative 'servus/support/logger'
22
33
  require_relative 'servus/support/data_object'
@@ -28,6 +39,7 @@ require_relative 'servus/support/lockdown'
28
39
  require_relative 'servus/support/message_resolver'
29
40
 
30
41
  # Events
42
+ require_relative 'servus/events/errors'
31
43
  require_relative 'servus/events/bus'
32
44
  require_relative 'servus/events/emitter'
33
45
  require_relative 'servus/event'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-15 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -68,8 +68,6 @@ files:
68
68
  - lib/generators/servus/guard/templates/guard.rb.erb
69
69
  - lib/generators/servus/guard/templates/guard_spec.rb.erb
70
70
  - lib/generators/servus/service/service_generator.rb
71
- - lib/generators/servus/service/templates/arguments.json.erb
72
- - lib/generators/servus/service/templates/result.json.erb
73
71
  - lib/generators/servus/service/templates/service.rb.erb
74
72
  - lib/generators/servus/service/templates/service_spec.rb.erb
75
73
  - lib/servus.rb
@@ -79,6 +77,7 @@ files:
79
77
  - lib/servus/events/bus.rb
80
78
  - lib/servus/events/class_router.rb
81
79
  - lib/servus/events/emitter.rb
80
+ - lib/servus/events/errors.rb
82
81
  - lib/servus/events/invocation.rb
83
82
  - lib/servus/events/router.rb
84
83
  - lib/servus/extensions/async/call.rb
@@ -97,6 +96,13 @@ files:
97
96
  - lib/servus/guards/truthy_guard.rb
98
97
  - lib/servus/helpers/controller_helpers.rb
99
98
  - lib/servus/railtie.rb
99
+ - lib/servus/schema.rb
100
+ - lib/servus/schema/cache.rb
101
+ - lib/servus/schema/compiler.rb
102
+ - lib/servus/schema/declaration.rb
103
+ - lib/servus/schema/errors.rb
104
+ - lib/servus/schema/path.rb
105
+ - lib/servus/schema/ref.rb
100
106
  - lib/servus/support/data_object.rb
101
107
  - lib/servus/support/errors.rb
102
108
  - lib/servus/support/lockdown.rb
@@ -127,7 +133,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
133
  requirements:
128
134
  - - ">="
129
135
  - !ruby/object:Gem::Version
130
- version: 3.0.0
136
+ version: 3.2.0
131
137
  required_rubygems_version: !ruby/object:Gem::Requirement
132
138
  requirements:
133
139
  - - ">="