versponse 0.1.0 → 0.2.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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -6
  3. data/CHANGELOG.md +59 -1
  4. data/CODE_OF_CONDUCT.md +1 -1
  5. data/LICENSE.txt +1 -1
  6. data/README.md +674 -20
  7. data/Rakefile +26 -3
  8. data/lib/versponse/base/class_methods.rb +180 -0
  9. data/lib/versponse/base/dsl.rb +231 -0
  10. data/lib/versponse/base.rb +35 -0
  11. data/lib/versponse/builder.rb +423 -0
  12. data/lib/versponse/cache.rb +47 -0
  13. data/lib/versponse/callable.rb +74 -0
  14. data/lib/versponse/class_policies.rb +15 -0
  15. data/lib/versponse/collapse_if_plan.rb +26 -0
  16. data/lib/versponse/collections.rb +49 -0
  17. data/lib/versponse/components.rb +37 -0
  18. data/lib/versponse/condition.rb +33 -0
  19. data/lib/versponse/configuration.rb +261 -0
  20. data/lib/versponse/const_as_enum.rb +39 -0
  21. data/lib/versponse/context_override/call.rb +27 -0
  22. data/lib/versponse/context_override/merge.rb +23 -0
  23. data/lib/versponse/context_override.rb +25 -0
  24. data/lib/versponse/context_plan.rb +24 -0
  25. data/lib/versponse/deep_copy.rb +27 -0
  26. data/lib/versponse/deep_freeze.rb +20 -0
  27. data/lib/versponse/discriminated_plan.rb +26 -0
  28. data/lib/versponse/error_policy/recovery.rb +68 -0
  29. data/lib/versponse/error_policy.rb +97 -0
  30. data/lib/versponse/expansion.rb +46 -0
  31. data/lib/versponse/extractors/block.rb +24 -0
  32. data/lib/versponse/extractors/fallback.rb +27 -0
  33. data/lib/versponse/extractors/itself.rb +21 -0
  34. data/lib/versponse/extractors/key.rb +28 -0
  35. data/lib/versponse/extractors/path.rb +28 -0
  36. data/lib/versponse/extractors.rb +23 -0
  37. data/lib/versponse/field_order.rb +23 -0
  38. data/lib/versponse/field_type.rb +88 -0
  39. data/lib/versponse/formatters.rb +38 -0
  40. data/lib/versponse/gem_version.rb +6 -0
  41. data/lib/versponse/inline_validation/builder.rb +129 -0
  42. data/lib/versponse/inline_validation/state.rb +16 -0
  43. data/lib/versponse/inline_validation.rb +86 -0
  44. data/lib/versponse/internals.rb +553 -0
  45. data/lib/versponse/json_value.rb +22 -0
  46. data/lib/versponse/lazy_plan.rb +30 -0
  47. data/lib/versponse/naming.rb +68 -0
  48. data/lib/versponse/object_schema.rb +25 -0
  49. data/lib/versponse/open_api/builder.rb +159 -0
  50. data/lib/versponse/open_api/component_set.rb +33 -0
  51. data/lib/versponse/open_api/declarations.rb +90 -0
  52. data/lib/versponse/open_api/document.rb +187 -0
  53. data/lib/versponse/open_api/ref_check.rb +89 -0
  54. data/lib/versponse/open_api/rswag_specs.rb +98 -0
  55. data/lib/versponse/open_api/sources.rb +72 -0
  56. data/lib/versponse/open_api/values.rb +29 -0
  57. data/lib/versponse/open_api.rb +12 -0
  58. data/lib/versponse/output_validator.rb +82 -0
  59. data/lib/versponse/pattern.rb +38 -0
  60. data/lib/versponse/plan.rb +104 -0
  61. data/lib/versponse/properties/array.rb +56 -0
  62. data/lib/versponse/properties/base.rb +325 -0
  63. data/lib/versponse/properties/collection.rb +135 -0
  64. data/lib/versponse/properties/const.rb +46 -0
  65. data/lib/versponse/properties/container.rb +51 -0
  66. data/lib/versponse/properties/enum.rb +93 -0
  67. data/lib/versponse/properties/field.rb +43 -0
  68. data/lib/versponse/properties/item_one_of.rb +19 -0
  69. data/lib/versponse/properties/object.rb +32 -0
  70. data/lib/versponse/properties/one_of.rb +176 -0
  71. data/lib/versponse/properties/reference.rb +133 -0
  72. data/lib/versponse/references.rb +162 -0
  73. data/lib/versponse/registry.rb +124 -0
  74. data/lib/versponse/schema_context.rb +69 -0
  75. data/lib/versponse/schema_merge.rb +24 -0
  76. data/lib/versponse/selection/scope.rb +36 -0
  77. data/lib/versponse/selection.rb +92 -0
  78. data/lib/versponse/selection_schema.rb +101 -0
  79. data/lib/versponse/steps/checked.rb +43 -0
  80. data/lib/versponse/steps/checked_read.rb +46 -0
  81. data/lib/versponse/steps/checked_read_formatted.rb +32 -0
  82. data/lib/versponse/steps/checked_value.rb +48 -0
  83. data/lib/versponse/steps/const.rb +44 -0
  84. data/lib/versponse/steps/nested.rb +98 -0
  85. data/lib/versponse/steps/one_of.rb +62 -0
  86. data/lib/versponse/steps/read.rb +48 -0
  87. data/lib/versponse/steps/read_formatted.rb +28 -0
  88. data/lib/versponse/steps/value.rb +52 -0
  89. data/lib/versponse/supported_versions.rb +61 -0
  90. data/lib/versponse/validator/all.rb +32 -0
  91. data/lib/versponse/validator/array_check.rb +48 -0
  92. data/lib/versponse/validator/boolean_check.rb +16 -0
  93. data/lib/versponse/validator/both.rb +24 -0
  94. data/lib/versponse/validator/class_check.rb +25 -0
  95. data/lib/versponse/validator/closed_object_check.rb +78 -0
  96. data/lib/versponse/validator/combinator.rb +40 -0
  97. data/lib/versponse/validator/compiler.rb +146 -0
  98. data/lib/versponse/validator/constant.rb +33 -0
  99. data/lib/versponse/validator/date_time_check.rb +21 -0
  100. data/lib/versponse/validator/format_check.rb +49 -0
  101. data/lib/versponse/validator/integer_check.rb +16 -0
  102. data/lib/versponse/validator/nullable_check.rb +22 -0
  103. data/lib/versponse/validator/number_check.rb +58 -0
  104. data/lib/versponse/validator/object_check.rb +90 -0
  105. data/lib/versponse/validator/ref.rb +38 -0
  106. data/lib/versponse/validator/root.rb +31 -0
  107. data/lib/versponse/validator/string_check.rb +48 -0
  108. data/lib/versponse/validator/string_keys.rb +21 -0
  109. data/lib/versponse/validator/type_check.rb +84 -0
  110. data/lib/versponse/validator/value_check.rb +36 -0
  111. data/lib/versponse/validator.rb +10 -0
  112. data/lib/versponse/variant_dispatcher.rb +35 -0
  113. data/lib/versponse/version.rb +179 -1
  114. data/lib/versponse/version_resolver.rb +27 -0
  115. data/lib/versponse/versions.rb +116 -0
  116. data/lib/versponse/view.rb +126 -0
  117. data/lib/versponse/wrapper.rb +53 -0
  118. data/lib/versponse.rb +165 -3
  119. metadata +151 -12
  120. data/sig/versponse.rbs +0 -4
data/lib/versponse.rb CHANGED
@@ -1,8 +1,170 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "versponse/version"
3
+ require "zeitwerk"
4
+ require "json"
5
+ require "time"
4
6
 
7
+ require_relative "versponse/gem_version"
8
+
9
+ loader = Zeitwerk::Loader.new
10
+ loader.tag = File.basename(__FILE__, ".rb")
11
+ loader.ignore("#{File.dirname(__FILE__)}/versponse/gem_version.rb")
12
+ loader.inflector.inflect("dsl" => "DSL", "open_api" => "OpenAPI", "json_value" => "JSONValue")
13
+ loader.push_dir(File.dirname(__FILE__))
14
+ loader.setup
15
+
16
+ # Versponse serializes objects to JSON-ready hashes with a versioned DSL
17
+ # and generates an OpenAPI schema for every response, version and view.
18
+ #
19
+ # @example
20
+ # class UserResponse < Versponse::Base
21
+ # version "2025-09" do
22
+ # field :id, type: :string
23
+ # field :name, type: :string
24
+ # end
25
+ # end
26
+ #
27
+ # UserResponse.serialize(user, version: "2025-09") # => {"id" => "u_1", "name" => "Ann"}
5
28
  module Versponse
6
- class Error < StandardError; end
7
- # Your code goes here...
29
+ # Base error for everything Versponse raises
30
+ Error = Class.new(StandardError)
31
+
32
+ # Raised when the DSL is used incorrectly
33
+ DefinitionError = Class.new(Error)
34
+
35
+ # Raised when to_validation_schema(property:) names an unknown property
36
+ PropertyNotFoundError = Class.new(Error)
37
+
38
+ # Raised when no version matches the requested one
39
+ VersionNotFoundError = Class.new(Error)
40
+
41
+ # Raised when a view does not exist in the resolved version
42
+ ViewNotFoundError = Class.new(Error)
43
+
44
+ # Raised when reading a value from the serialized object fails
45
+ class ExtractionError < Error
46
+ # @return [String] the plan where extraction failed, like "UserResponse[2025-09/default]"
47
+ attr_reader :label
48
+ # @return [Array<String>] output keys from the plan down to the failing value
49
+ attr_reader :keys
50
+ # @return [String] message of the original error
51
+ attr_reader :detail
52
+
53
+ # @param label [String] plan label
54
+ # @param keys [Array<String>] output keys leading to the failing value
55
+ # @param detail [String] message of the original error
56
+ def initialize(label:, keys:, detail:)
57
+ @label = label
58
+ @keys = keys
59
+ @detail = detail
60
+ super("#{[label, *keys].join(".")}: #{detail}")
61
+ end
62
+ end
63
+
64
+ # Raised when validate_output is on and the output does not match its schema
65
+ class InvalidOutputError < Error
66
+ # @return [Array<Hash>] errors with :pointer, :type, :message and :details
67
+ attr_reader :errors
68
+
69
+ # @param message [String] error message
70
+ # @param errors [Array<Hash>] validation errors
71
+ def initialize(message, errors:)
72
+ super(message)
73
+ @errors = errors
74
+ end
75
+ end
76
+
77
+ # Raised when expand: names paths that match no expandable association (config.unknown_expand = :raise)
78
+ class InvalidExpandError < Error
79
+ # @return [Array<String>] the unknown paths
80
+ attr_reader :paths
81
+
82
+ # @param paths [Array<String>] unknown paths
83
+ def initialize(paths:)
84
+ @paths = paths
85
+ super("Unknown expand paths: #{paths.map(&:inspect).join(", ")}")
86
+ end
87
+ end
88
+
89
+ # Raised when fields: names unknown paths, or paths below associations the render doesn't expand
90
+ # (config.unknown_fields = :raise)
91
+ class InvalidFieldsError < Error
92
+ # @return [Array<String>] the invalid paths
93
+ attr_reader :paths
94
+
95
+ # @param paths [Array<String>] invalid paths
96
+ # @param reasons [Hash{String => String}] why each path is invalid
97
+ def initialize(paths:, reasons:)
98
+ @paths = paths
99
+ super("Invalid fields paths: #{paths.map { |path| "#{path.inspect} (#{reasons.fetch(path)})" }.join(", ")}")
100
+ end
101
+ end
102
+
103
+ class << self
104
+ # @return [Versponse::Configuration] global configuration
105
+ def configuration
106
+ @configuration ||= Configuration.new
107
+ end
108
+
109
+ # @yieldparam configuration [Versponse::Configuration]
110
+ # @return [void]
111
+ def configure
112
+ yield(configuration)
113
+ end
114
+
115
+ # Clears compiled plans and schemas of every response class (lazily, on each class's next use).
116
+ # Setting a configuration value that changes output calls it already.
117
+ #
118
+ # @return [void]
119
+ def reset_caches!
120
+ Registry.clear_canonical
121
+ @cache_generation = cache_generation + 1
122
+ end
123
+
124
+ # @return [Integer] how many times the caches were reset; each class compares it and clears its own caches when behind
125
+ # @api private
126
+ def cache_generation
127
+ @cache_generation ||= 0
128
+ end
129
+
130
+ # @param version [String] requested version
131
+ # @param classes [Array<Class>] response classes (default: every loaded one; eager load your app first)
132
+ # @return [Hash{String => Hash}] components/schemas for an OpenAPI document
133
+ # @raise [Versponse::DefinitionError] when two components share a name
134
+ def components(version:, classes: Registry.classes)
135
+ Components.call(version:, classes:)
136
+ end
137
+
138
+ # Compiles every plan and schema of every supported version and view, surfacing definition errors at boot.
139
+ # A class with supported_versions compiles the names its range serves, such as "preview" for a
140
+ # min: "preview" class whose only block is 2025-09.
141
+ #
142
+ # @param classes [Array<Class>] response classes (default: every loaded one)
143
+ # @return [Array<Class>] the classes
144
+ # @raise [Versponse::DefinitionError] when a supported_versions max sorts before every declared version
145
+ def eager_load!(classes: Registry.classes)
146
+ classes.each do |klass|
147
+ klass.versponse.supported_version_names.each do |version|
148
+ klass.versponse.resolve(version).view_names.each do |view|
149
+ klass.versponse.plan(version:, view:)
150
+ klass.to_schema(version:, view:)
151
+ klass.to_validation_schema(version:, view:)
152
+ end
153
+ end
154
+ check_served_references(klass)
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ # Nested classes resolve each request on their own, so a reference can break at a version the class
161
+ # serves but doesn't declare; check every registered one it serves
162
+ def check_served_references(klass)
163
+ Registry.version_names.each do |version|
164
+ klass.versponse.check_references!(version)
165
+ rescue VersionNotFoundError
166
+ next
167
+ end
168
+ end
169
+ end
8
170
  end
metadata CHANGED
@@ -1,17 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: versponse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Petr Hlavicka
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- description: ''
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: zeitwerk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.7'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.7'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json_schemer
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.4'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.4'
40
+ description: Versponse describes API responses with a Ruby DSL shared with verquest,
41
+ one definition per API version, with blueprinter-style views. It renders objects
42
+ to JSON-ready hashes and generates an OpenAPI schema for every response, version
43
+ and view.
13
44
  email:
14
- - petr@hlavicka.cz
45
+ - yes@petr.codes
15
46
  executables: []
16
47
  extensions: []
17
48
  extra_rdoc_files: []
@@ -23,15 +54,123 @@ files:
23
54
  - README.md
24
55
  - Rakefile
25
56
  - lib/versponse.rb
57
+ - lib/versponse/base.rb
58
+ - lib/versponse/base/class_methods.rb
59
+ - lib/versponse/base/dsl.rb
60
+ - lib/versponse/builder.rb
61
+ - lib/versponse/cache.rb
62
+ - lib/versponse/callable.rb
63
+ - lib/versponse/class_policies.rb
64
+ - lib/versponse/collapse_if_plan.rb
65
+ - lib/versponse/collections.rb
66
+ - lib/versponse/components.rb
67
+ - lib/versponse/condition.rb
68
+ - lib/versponse/configuration.rb
69
+ - lib/versponse/const_as_enum.rb
70
+ - lib/versponse/context_override.rb
71
+ - lib/versponse/context_override/call.rb
72
+ - lib/versponse/context_override/merge.rb
73
+ - lib/versponse/context_plan.rb
74
+ - lib/versponse/deep_copy.rb
75
+ - lib/versponse/deep_freeze.rb
76
+ - lib/versponse/discriminated_plan.rb
77
+ - lib/versponse/error_policy.rb
78
+ - lib/versponse/error_policy/recovery.rb
79
+ - lib/versponse/expansion.rb
80
+ - lib/versponse/extractors.rb
81
+ - lib/versponse/extractors/block.rb
82
+ - lib/versponse/extractors/fallback.rb
83
+ - lib/versponse/extractors/itself.rb
84
+ - lib/versponse/extractors/key.rb
85
+ - lib/versponse/extractors/path.rb
86
+ - lib/versponse/field_order.rb
87
+ - lib/versponse/field_type.rb
88
+ - lib/versponse/formatters.rb
89
+ - lib/versponse/gem_version.rb
90
+ - lib/versponse/inline_validation.rb
91
+ - lib/versponse/inline_validation/builder.rb
92
+ - lib/versponse/inline_validation/state.rb
93
+ - lib/versponse/internals.rb
94
+ - lib/versponse/json_value.rb
95
+ - lib/versponse/lazy_plan.rb
96
+ - lib/versponse/naming.rb
97
+ - lib/versponse/object_schema.rb
98
+ - lib/versponse/open_api.rb
99
+ - lib/versponse/open_api/builder.rb
100
+ - lib/versponse/open_api/component_set.rb
101
+ - lib/versponse/open_api/declarations.rb
102
+ - lib/versponse/open_api/document.rb
103
+ - lib/versponse/open_api/ref_check.rb
104
+ - lib/versponse/open_api/rswag_specs.rb
105
+ - lib/versponse/open_api/sources.rb
106
+ - lib/versponse/open_api/values.rb
107
+ - lib/versponse/output_validator.rb
108
+ - lib/versponse/pattern.rb
109
+ - lib/versponse/plan.rb
110
+ - lib/versponse/properties/array.rb
111
+ - lib/versponse/properties/base.rb
112
+ - lib/versponse/properties/collection.rb
113
+ - lib/versponse/properties/const.rb
114
+ - lib/versponse/properties/container.rb
115
+ - lib/versponse/properties/enum.rb
116
+ - lib/versponse/properties/field.rb
117
+ - lib/versponse/properties/item_one_of.rb
118
+ - lib/versponse/properties/object.rb
119
+ - lib/versponse/properties/one_of.rb
120
+ - lib/versponse/properties/reference.rb
121
+ - lib/versponse/references.rb
122
+ - lib/versponse/registry.rb
123
+ - lib/versponse/schema_context.rb
124
+ - lib/versponse/schema_merge.rb
125
+ - lib/versponse/selection.rb
126
+ - lib/versponse/selection/scope.rb
127
+ - lib/versponse/selection_schema.rb
128
+ - lib/versponse/steps/checked.rb
129
+ - lib/versponse/steps/checked_read.rb
130
+ - lib/versponse/steps/checked_read_formatted.rb
131
+ - lib/versponse/steps/checked_value.rb
132
+ - lib/versponse/steps/const.rb
133
+ - lib/versponse/steps/nested.rb
134
+ - lib/versponse/steps/one_of.rb
135
+ - lib/versponse/steps/read.rb
136
+ - lib/versponse/steps/read_formatted.rb
137
+ - lib/versponse/steps/value.rb
138
+ - lib/versponse/supported_versions.rb
139
+ - lib/versponse/validator.rb
140
+ - lib/versponse/validator/all.rb
141
+ - lib/versponse/validator/array_check.rb
142
+ - lib/versponse/validator/boolean_check.rb
143
+ - lib/versponse/validator/both.rb
144
+ - lib/versponse/validator/class_check.rb
145
+ - lib/versponse/validator/closed_object_check.rb
146
+ - lib/versponse/validator/combinator.rb
147
+ - lib/versponse/validator/compiler.rb
148
+ - lib/versponse/validator/constant.rb
149
+ - lib/versponse/validator/date_time_check.rb
150
+ - lib/versponse/validator/format_check.rb
151
+ - lib/versponse/validator/integer_check.rb
152
+ - lib/versponse/validator/nullable_check.rb
153
+ - lib/versponse/validator/number_check.rb
154
+ - lib/versponse/validator/object_check.rb
155
+ - lib/versponse/validator/ref.rb
156
+ - lib/versponse/validator/root.rb
157
+ - lib/versponse/validator/string_check.rb
158
+ - lib/versponse/validator/string_keys.rb
159
+ - lib/versponse/validator/type_check.rb
160
+ - lib/versponse/validator/value_check.rb
161
+ - lib/versponse/variant_dispatcher.rb
26
162
  - lib/versponse/version.rb
27
- - sig/versponse.rbs
28
- homepage: https://github.com/CiTroNaK/versponse
163
+ - lib/versponse/version_resolver.rb
164
+ - lib/versponse/versions.rb
165
+ - lib/versponse/view.rb
166
+ - lib/versponse/wrapper.rb
167
+ homepage: https://github.com/verquest/versponse
29
168
  licenses:
30
169
  - MIT
31
170
  metadata:
32
- homepage_uri: https://github.com/CiTroNaK/versponse
33
- source_code_uri: https://github.com/CiTroNaK/versponse
34
- changelog_uri: https://github.com/CiTroNaK/versponse/blob/main/CHANGELOG.md
171
+ source_code_uri: https://github.com/verquest/versponse
172
+ changelog_uri: https://github.com/verquest/versponse/blob/main/CHANGELOG.md
173
+ rubygems_mfa_required: 'true'
35
174
  rdoc_options: []
36
175
  require_paths:
37
176
  - lib
@@ -39,14 +178,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
39
178
  requirements:
40
179
  - - ">="
41
180
  - !ruby/object:Gem::Version
42
- version: 3.1.0
181
+ version: '3.2'
43
182
  required_rubygems_version: !ruby/object:Gem::Requirement
44
183
  requirements:
45
184
  - - ">="
46
185
  - !ruby/object:Gem::Version
47
186
  version: '0'
48
187
  requirements: []
49
- rubygems_version: 3.6.7
188
+ rubygems_version: 4.0.17
50
189
  specification_version: 4
51
- summary: ''
190
+ summary: Versioned JSON response serializer with views and OpenAPI schemas
52
191
  test_files: []
data/sig/versponse.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Versponse
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end