verse-schema 1.1.1 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6608dcff00a7bef2d8313866053ac7d37d82933d9b597052a222f0bcad693f91
4
- data.tar.gz: dfcecf7d576b599b3c39d552cfbbf082ceda4707f0ec532d94d4a0db3dbfa620
3
+ metadata.gz: ac21990f8abf487af0be8d14959c40595c2327cdbbda87a4bc15d53cfcf87a36
4
+ data.tar.gz: 102efefd85e3181bda88e337672a810c54bfafe4c623ae825972b7e82e6bc689
5
5
  SHA512:
6
- metadata.gz: d3bc6f97bf7b683d4e45ee578af5a1cd57375d56f54161a93ed3b725c2f783cbfedb2583fa9dc6470e68243bdaa3cda93f703dc82df4a63bd912d386695933bc
7
- data.tar.gz: 49c7ae54ea24d5a4e7a8304e1361f70feb81f68a4993beddc153fabd1c88fa1d63e5eb5bd5bbcb41eea83bd863f7188bcfc73cdf31d98c9a0b08b32d749c0ecc
6
+ metadata.gz: 1e464f3fcbaa0d77b69d81f7001c80ecb7efba9298476aab84f16390bbda8481c1f37c3c7a5d80e11d9de31f7064c67e9db1448b545d4b927079d331ea939b3d
7
+ data.tar.gz: 89cfcaa93e6813a5ffab8834e49002ebf3791a42a64a5457444a0fa4ba941412583ea1925e5ba7c607e7bfcd3bd16628437f9c954b6710f6296f400b9b0d3be7
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.4.3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 1.2
2
+
3
+ - Add `Verse::Schema::Json.from` method to convert a Verse schema
4
+ to JSON Schema format (note: doesn't work the other way around yet)
5
+
1
6
  ## 1.1
2
7
 
3
8
  - Add `strict` mode to `validate` which will raise an error if the input has
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ gemspec
7
7
 
8
8
  # Development dependencies
9
9
  group :development do
10
- gem "prism", "~> 0.19.0", require: false
10
+ gem "prism", "~> 1.5.2", require: false
11
11
  end
12
12
 
13
13
  gem "rake", "~> 13.0"
@@ -18,6 +18,6 @@ gem "relaxed-rubocop"
18
18
  gem "rubocop", "~> 1.21"
19
19
  gem "simplecov"
20
20
 
21
- gem "ruby-prof"
21
+ gem "ruby-prof", "~> 1.7.2"
22
22
 
23
23
  gem "rspec", "~> 3.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- verse-schema (1.1.1)
4
+ verse-schema (1.2.0)
5
5
  attr_chainable (~> 1.0)
6
6
 
7
7
  GEM
@@ -9,6 +9,7 @@ GEM
9
9
  specs:
10
10
  ast (2.4.2)
11
11
  attr_chainable (1.0.0)
12
+ base64 (0.3.0)
12
13
  bootsnap (1.18.3)
13
14
  msgpack (~> 1.2)
14
15
  coderay (1.1.3)
@@ -22,7 +23,7 @@ GEM
22
23
  parser (3.3.0.5)
23
24
  ast (~> 2.4.1)
24
25
  racc
25
- prism (0.19.0)
26
+ prism (1.5.2)
26
27
  pry (0.14.2)
27
28
  coderay (~> 1.1)
28
29
  method_source (~> 1.0)
@@ -58,7 +59,8 @@ GEM
58
59
  unicode-display_width (>= 2.4.0, < 3.0)
59
60
  rubocop-ast (1.31.2)
60
61
  parser (>= 3.3.0.4)
61
- ruby-prof (1.7.1)
62
+ ruby-prof (1.7.2)
63
+ base64
62
64
  ruby-progressbar (1.13.0)
63
65
  simplecov (0.22.0)
64
66
  docile (~> 1.1)
@@ -73,13 +75,13 @@ PLATFORMS
73
75
 
74
76
  DEPENDENCIES
75
77
  bootsnap (~> 1.16)
76
- prism (~> 0.19.0)
78
+ prism (~> 1.5.2)
77
79
  pry
78
80
  rake (~> 13.0)
79
81
  relaxed-rubocop
80
82
  rspec (~> 3.0)
81
83
  rubocop (~> 1.21)
82
- ruby-prof
84
+ ruby-prof (~> 1.7.2)
83
85
  simplecov
84
86
  verse-schema!
85
87
 
data/README.md CHANGED
@@ -123,6 +123,11 @@ These examples are extracted directly from the gem's specs, ensuring they are ac
123
123
  - [Polymorphic Schema](#polymorphic-schema)
124
124
 
125
125
 
126
+ - [JSON Schema Generation](#json-schema-generation)
127
+
128
+ - [Generating JSON Schema](#generating-json-schema)
129
+
130
+
126
131
 
127
132
 
128
133
  ## 1. Basic Usage
@@ -952,7 +957,7 @@ it "demonstrates reusable rules defined with Verse::Schema.rule" do
952
957
  is_positive = Verse::Schema.rule("must be positive") { |value| value > 0 }
953
958
 
954
959
  # Define another reusable rule
955
- is_even = Verse::Schema.rule("must be even") { |value| value.even? }
960
+ is_even = Verse::Schema.rule("must be even", &:even?)
956
961
 
957
962
  # Create a schema that uses the reusable rules
958
963
  schema = Verse::Schema.define do
@@ -1666,7 +1671,6 @@ it "demonstrates a polymorphic schema" do
1666
1671
  # 3. Define a builder schema. The best way to do this is to use the
1667
1672
  # scalar type:
1668
1673
  builder_schema = Verse::Schema.scalar(Hash).transform do |input, error_builder|
1669
-
1670
1674
  type = input[:type]
1671
1675
 
1672
1676
  if type.respond_to?(:to_sym)
@@ -1677,14 +1681,14 @@ it "demonstrates a polymorphic schema" do
1677
1681
  end
1678
1682
 
1679
1683
  schema = case type
1680
- when :facebook
1681
- facebook_schema
1682
- when :google
1683
- google_schema
1684
- else
1685
- error_builder.add(:type, "invalid type")
1686
- stop
1687
- end
1684
+ when :facebook
1685
+ facebook_schema
1686
+ when :google
1687
+ google_schema
1688
+ else
1689
+ error_builder.add(:type, "invalid type")
1690
+ stop
1691
+ end
1688
1692
 
1689
1693
  # Validate the input against the selected schema
1690
1694
  result = schema.validate(input, error_builder:)
@@ -1753,9 +1757,9 @@ it "demonstrates a polymorphic schema" do
1753
1757
  expect(invalid_result.success?).to be false
1754
1758
  # The errors are collected
1755
1759
  expect(invalid_result.errors).to eq({
1756
- :"events.0.url" => ["is required"],
1757
- :"events.1.location" => ["is required"],
1758
- :"events.2.type" => ["invalid type"]
1760
+ "events.0.url": ["is required"],
1761
+ "events.1.location": ["is required"],
1762
+ "events.2.type": ["invalid type"]
1759
1763
  })
1760
1764
  end
1761
1765
 
@@ -1763,6 +1767,47 @@ end
1763
1767
 
1764
1768
 
1765
1769
 
1770
+ ## JSON Schema Generation
1771
+
1772
+
1773
+ ### Generating JSON Schema
1774
+
1775
+
1776
+ ```ruby
1777
+ it "converts a simple schema to a valid JSON schema" do
1778
+ schema = Verse::Schema.define do
1779
+ field(:name, String).meta(description: "The name of the user")
1780
+ field(:age, Integer)
1781
+ end
1782
+
1783
+ json_schema = Verse::Schema::Json.from(schema)
1784
+ puts JSON.pretty_generate(json_schema)
1785
+
1786
+ # The output of the `to_json` method will be a valid JSON schema hash:
1787
+ #
1788
+ # {
1789
+ # "type": "object",
1790
+ # "properties": {
1791
+ # "name": {
1792
+ # "type": "string",
1793
+ # "description": "The name of the user"
1794
+ # },
1795
+ # "age": {
1796
+ # "type": "integer"
1797
+ # }
1798
+ # },
1799
+ # "required": [
1800
+ # "name",
1801
+ # "age"
1802
+ # ],
1803
+ # "additionalProperties": false
1804
+ # }
1805
+ end
1806
+
1807
+ ```
1808
+
1809
+
1810
+
1766
1811
 
1767
1812
  ## License
1768
1813
 
@@ -62,13 +62,11 @@ module Verse
62
62
  if !child_type.is_a?(Base) && parent_type.is_a?(Scalar)
63
63
  # Check if the primitive class is compatible with any of the Scalar's values
64
64
  parent_type.values.any? do |scalar_value|
65
- begin
66
- # Use standard Ruby `<=` for comparison
67
- child_type <= scalar_value
68
- rescue TypeError
69
- # Handle cases where <= is not defined between types
70
- false
71
- end
65
+ # Use standard Ruby `<=` for comparison
66
+ child_type <= scalar_value
67
+ rescue TypeError
68
+ # Handle cases where <= is not defined between types
69
+ false
72
70
  end
73
71
  else
74
72
  # Use the existing `<=` operator defined on schema types (Scalar, Struct, etc.)
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verse
4
+ module Schema
5
+ module Json
6
+ # rubocop:disable Lint/HashCompareByIdentity
7
+ # @param schema [Verse::Schema::Base] The schema to convert to JSON schema
8
+ # @return [Hash] The JSON schema
9
+ def self.from(schema)
10
+ definitions = {}
11
+ # directly build the root schema, don't use a ref.
12
+ output = _build_schema(schema, registry: {}, definitions: definitions)
13
+
14
+ if definitions.any?
15
+ output[:"$defs"] = definitions
16
+ end
17
+
18
+ output
19
+ end
20
+
21
+ def self._from_schema(schema, registry:, definitions:)
22
+ return { "$ref": registry[schema.object_id] } if registry.key?(schema.object_id)
23
+
24
+ if schema.is_a?(Verse::Schema::Struct)
25
+ # Register the schema to handle recursion
26
+ # and give it a name.
27
+ # The name is based on the class name, or the object_id if the class is anonymous.
28
+ name = :"Schema#{schema.object_id}"
29
+ ref = "#/$defs/#{name}"
30
+
31
+ registry[schema.object_id] = ref
32
+
33
+ # if it's the root schema, don't create a definition, just build it.
34
+ built_schema = _build_schema(schema, registry:, definitions:)
35
+
36
+ definitions[name] = built_schema
37
+
38
+ return { "$ref": ref }
39
+ end
40
+
41
+ _build_schema(schema, registry:, definitions:)
42
+ end
43
+
44
+ def self._build_schema(schema, registry:, definitions:)
45
+ case schema
46
+ when Verse::Schema::Struct
47
+ properties = schema.fields.each_with_object({}) do |field_obj, obj|
48
+ next if field_obj.type.is_a?(Verse::Schema::Selector)
49
+
50
+ obj[field_obj.name] = begin
51
+ output = _from_schema(field_obj.type, registry:, definitions:)
52
+ desc = field_obj.opts.dig(:meta, :description)
53
+
54
+ output[:description] = desc if desc
55
+
56
+ default = field_obj.opts[:default]
57
+
58
+ if default && !default.is_a?(Proc)
59
+ output[:default] = default
60
+ end
61
+
62
+ output
63
+ end
64
+ end
65
+
66
+ required_fields = schema.fields.select(&:required?).map(&:name)
67
+
68
+ json = {
69
+ type: "object",
70
+ properties: properties
71
+ }
72
+ json[:required] = required_fields if required_fields.any?
73
+ json[:additionalProperties] = schema.extra_fields?
74
+
75
+ # Handle selectors
76
+ schema.fields.each do |field_obj|
77
+ next unless field_obj.type.is_a?(Verse::Schema::Selector)
78
+
79
+ discriminator = field_obj.opts[:over]
80
+ json[:properties][field_obj.name] = { type: "object" }
81
+
82
+ selector_keys = field_obj.type.values.keys
83
+ if !selector_keys.include?(:__else__)
84
+ json[:properties][discriminator][:enum] = selector_keys.map(&:to_s)
85
+ end
86
+
87
+ json[:allOf] = field_obj.type.values.map do |key, sub_schema|
88
+ next if key == :__else__
89
+
90
+ {
91
+ if: {
92
+ properties: { discriminator.to_sym => { const: key.to_s } }
93
+ },
94
+ then: {
95
+ properties: {
96
+ field_obj.name => _from_schema(sub_schema, registry:, definitions:)
97
+ }
98
+ }
99
+ }
100
+ end.compact
101
+ end
102
+
103
+ json
104
+ when Verse::Schema::Collection
105
+ items = if schema.values.length > 1
106
+ { anyOf: schema.values.map { |v| _from_schema(v, registry:, definitions:) } }
107
+ else
108
+ _from_schema(schema.values.first, registry:, definitions:)
109
+ end
110
+
111
+ {
112
+ type: "array",
113
+ items: items
114
+ }
115
+ when Verse::Schema::Dictionary
116
+ additional_properties = if schema.values.length > 1
117
+ { anyOf: schema.values.map { |v| _from_schema(v, registry:, definitions:) } }
118
+ else
119
+ _from_schema(schema.values.first, registry:, definitions:)
120
+ end
121
+ {
122
+ type: "object",
123
+ additionalProperties: additional_properties
124
+ }
125
+ when Verse::Schema::Scalar
126
+ {
127
+ anyOf: schema.values.map { |v| _from_schema(v, registry:, definitions:) }
128
+ }
129
+ when Verse::Schema::Selector
130
+ # This should not be reached directly for a valid schema with `over`
131
+ raise "Selector schema must be used within a Struct with `over` option."
132
+ when String.singleton_class, Symbol.singleton_class
133
+ { type: "string" }
134
+ when Integer.singleton_class
135
+ { type: "integer" }
136
+ when Float.singleton_class, Numeric.singleton_class
137
+ { type: "number" }
138
+ when TrueClass.singleton_class, FalseClass.singleton_class
139
+ { type: "boolean" }
140
+ when Time.singleton_class
141
+ { type: "string", format: "date-time" }
142
+ when NilClass.singleton_class, nil
143
+ { type: "null" }
144
+ when Array
145
+ case schema.length
146
+ when 0
147
+ { type: "null" }
148
+ when 1
149
+ _from_schema(schema.first, registry:, definitions:)
150
+ else
151
+ { anyOf: schema.map { |v| _from_schema(v, registry:, definitions:) } }
152
+ end
153
+ else
154
+ raise "Unknown type #{schema.inspect}"
155
+ end
156
+ end
157
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Lint/HashCompareByIdentity
158
+ end
159
+ end
160
+ end
@@ -64,45 +64,43 @@ module Verse
64
64
  end
65
65
  end
66
66
 
67
- def <=(other)
68
- # 1. Identical check: Is it the exact same object?
69
- return true if other == self
70
-
71
- # 2. Check if inheriting from another Scalar:
72
- # Use the existing inherit? method which correctly handles Scalar-to-Scalar inheritance.
73
- # (inherit? implicitly checks `other.is_a?(Scalar)`)
74
- return true if inherit?(other)
75
-
76
- # 3. NEW: Check compatibility with non-Scalar types:
77
- # If 'other' is not a Scalar, check if any type *wrapped* by this Scalar
78
- # is a subtype of 'other'. This handles `Scalar<Integer> <= Integer`.
79
- # We rely on the `<=` operator of the wrapped types themselves.
80
- @values.any? do |wrapped_type|
81
- begin
82
- # Use standard Ruby `<=` for comparison.
83
- # This works for Class <= Class (e.g., Integer <= Integer, Integer <= Numeric)
84
- # and potentially for SchemaType <= SchemaType if defined.
85
- wrapped_type <= other
86
- rescue TypeError
87
- # Handle cases where <= is not defined between wrapped_type and other
88
- false
67
+ def <=(other)
68
+ # 1. Identical check: Is it the exact same object?
69
+ return true if other == self
70
+
71
+ # 2. Check if inheriting from another Scalar:
72
+ # Use the existing inherit? method which correctly handles Scalar-to-Scalar inheritance.
73
+ # (inherit? implicitly checks `other.is_a?(Scalar)`)
74
+ return true if inherit?(other)
75
+
76
+ # 3. NEW: Check compatibility with non-Scalar types:
77
+ # If 'other' is not a Scalar, check if any type *wrapped* by this Scalar
78
+ # is a subtype of 'other'. This handles `Scalar<Integer> <= Integer`.
79
+ # We rely on the `<=` operator of the wrapped types themselves.
80
+ @values.any? do |wrapped_type|
81
+ # Use standard Ruby `<=` for comparison.
82
+ # This works for Class <= Class (e.g., Integer <= Integer, Integer <= Numeric)
83
+ # and potentially for SchemaType <= SchemaType if defined.
84
+ wrapped_type <= other
85
+ rescue TypeError
86
+ # Handle cases where <= is not defined between wrapped_type and other
87
+ false
88
+ end
89
89
  end
90
- end
91
- end
92
90
 
93
- def <(other)
94
- other != self && self <= other
95
- end
91
+ def <(other)
92
+ other != self && self <= other
93
+ end
96
94
 
97
- # rubocop:disable Style/InverseMethods
98
- def >(other)
99
- !(self <= other)
100
- end
95
+ # rubocop:disable Style/InverseMethods
96
+ def >(other)
97
+ !(self <= other)
98
+ end
101
99
 
102
- def >=(other)
103
- other <= self
104
- end
105
- # rubocop:enable Style/InverseMethods
100
+ def >=(other)
101
+ other <= self
102
+ end
103
+ # rubocop:enable Style/InverseMethods
106
104
 
107
105
  # Aggregation of two schemas.
108
106
  def +(other)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Verse
4
4
  module Schema
5
- VERSION = "1.1.1"
5
+ VERSION = "1.2.0"
6
6
  end
7
7
  end
data/lib/verse/schema.rb CHANGED
@@ -9,6 +9,7 @@ module Verse
9
9
  require_relative "schema/base"
10
10
  require_relative "schema/coalescer"
11
11
  require_relative "schema/post_processor"
12
+ require_relative "schema/json"
12
13
 
13
14
  def define(from = nil, &block)
14
15
  if from
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verse-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yacine Petitprez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: attr_chainable
@@ -33,6 +33,7 @@ files:
33
33
  - ".rspec"
34
34
  - ".rubocop-https---relaxed-ruby-style-rubocop-yml"
35
35
  - ".rubocop.yml"
36
+ - ".ruby-version"
36
37
  - CHANGELOG.md
37
38
  - Gemfile
38
39
  - Gemfile.lock
@@ -51,6 +52,7 @@ files:
51
52
  - lib/verse/schema/field.rb
52
53
  - lib/verse/schema/field/ext.rb
53
54
  - lib/verse/schema/invalid_schema_error.rb
55
+ - lib/verse/schema/json.rb
54
56
  - lib/verse/schema/optionable.rb
55
57
  - lib/verse/schema/post_processor.rb
56
58
  - lib/verse/schema/result.rb
@@ -78,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
80
  - !ruby/object:Gem::Version
79
81
  version: '0'
80
82
  requirements: []
81
- rubygems_version: 3.6.3
83
+ rubygems_version: 3.6.7
82
84
  specification_version: 4
83
85
  summary: Consume input schema and output coalesced version of them
84
86
  test_files: []