esquema 0.1.1 → 0.1.2
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/.rubocop.yml +4 -1
- data/CHANGELOG.md +9 -1
- data/README.md +8 -2
- data/lib/esquema/builder.rb +73 -28
- data/lib/esquema/configuration.rb +2 -1
- data/lib/esquema/keyword_validator.rb +98 -0
- data/lib/esquema/model.rb +4 -0
- data/lib/esquema/property.rb +185 -26
- data/lib/esquema/schema_enhancer.rb +55 -30
- data/lib/esquema/type_caster.rb +16 -4
- data/lib/esquema/version.rb +1 -1
- data/lib/esquema/virtual_column.rb +46 -0
- data/lib/esquema.rb +7 -1
- data/lib/generators/esquema/install/install_generator.rb +1 -0
- data/sorbet/config +4 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/activemodel.rbi +89 -0
- data/sorbet/rbi/annotations/activerecord.rbi +92 -0
- data/sorbet/rbi/annotations/activesupport.rbi +421 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/activemodel@7.1.3.rbi +8 -0
- data/sorbet/rbi/gems/activerecord@7.1.3.rbi +8 -0
- data/sorbet/rbi/gems/activesupport@7.1.3.rbi +192 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
- data/sorbet/rbi/gems/base64@0.2.0.rbi +8 -0
- data/sorbet/rbi/gems/bigdecimal@3.1.6.rbi +8 -0
- data/sorbet/rbi/gems/byebug@11.1.3.rbi +3606 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.2.3.rbi +8 -0
- data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +8 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1130 -0
- data/sorbet/rbi/gems/drb@2.2.0.rbi +1272 -0
- data/sorbet/rbi/gems/erubi@1.12.0.rbi +145 -0
- data/sorbet/rbi/gems/i18n@1.14.1.rbi +8 -0
- data/sorbet/rbi/gems/json@2.7.1.rbi +1553 -0
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
- data/sorbet/rbi/gems/minitest@5.22.2.rbi +8 -0
- data/sorbet/rbi/gems/mutex_m@0.2.0.rbi +8 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sorbet/rbi/gems/parallel@1.24.0.rbi +280 -0
- data/sorbet/rbi/gems/parser@3.3.0.5.rbi +5472 -0
- data/sorbet/rbi/gems/prettier_print@1.2.1.rbi +951 -0
- data/sorbet/rbi/gems/prism@0.24.0.rbi +31040 -0
- data/sorbet/rbi/gems/pry-byebug@3.10.1.rbi +1150 -0
- data/sorbet/rbi/gems/pry@0.14.2.rbi +10075 -0
- data/sorbet/rbi/gems/racc@1.7.3.rbi +157 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
- data/sorbet/rbi/gems/rake@13.1.0.rbi +3027 -0
- data/sorbet/rbi/gems/rbi@0.1.9.rbi +3006 -0
- data/sorbet/rbi/gems/regexp_parser@2.9.0.rbi +3771 -0
- data/sorbet/rbi/gems/rexml@3.2.6.rbi +4781 -0
- data/sorbet/rbi/gems/rspec-core@3.13.0.rbi +10978 -0
- data/sorbet/rbi/gems/rspec-expectations@3.13.0.rbi +8153 -0
- data/sorbet/rbi/gems/rspec-mocks@3.13.0.rbi +5340 -0
- data/sorbet/rbi/gems/rspec-support@3.13.0.rbi +1629 -0
- data/sorbet/rbi/gems/rspec@3.13.0.rbi +82 -0
- data/sorbet/rbi/gems/rubocop-ast@1.30.0.rbi +7006 -0
- data/sorbet/rbi/gems/rubocop@1.60.2.rbi +57383 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
- data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +8 -0
- data/sorbet/rbi/gems/spoom@1.2.4.rbi +3777 -0
- data/sorbet/rbi/gems/sqlite3@1.7.2.rbi +1691 -0
- data/sorbet/rbi/gems/syntax_tree@6.2.0.rbi +23133 -0
- data/sorbet/rbi/gems/tapioca@0.12.0.rbi +3510 -0
- data/sorbet/rbi/gems/thor@1.3.0.rbi +4345 -0
- data/sorbet/rbi/gems/timeout@0.4.1.rbi +142 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +8 -0
- data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +65 -0
- data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
- data/sorbet/rbi/gems/yard@0.9.34.rbi +18219 -0
- data/sorbet/rbi/todo.rbi +20 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- metadata +72 -10
- data/esquema.gemspec +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a31e08f199e964d003d99fe4e8e03b62dd86231a48d29053fec1fef03ce06f8
|
|
4
|
+
data.tar.gz: 20f4e5198607e7bba751bc61f1b4b0666f4468f19879ac44fdbdf2ee8542675b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c621671e563866f1c9d0216b4561f135dc4d8c66c3921b18876600fedaa4c37417c6b2a12ee12c15ec9cdf6cbabbac6fe910bed3820d193a5395412a94aa975
|
|
7
|
+
data.tar.gz: 69a1b9637e854a0ae3f5cc7730c37af8e4fce0170432bab567fb6bc079d7914dc256346e4f1d35c9e9691ed801ef7effe062d098df9387c04136d08047a9f56c
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.1.2] - 2024-02-22
|
|
2
|
+
- Removed Rails dependency and added ActiveRecord dependency.
|
|
3
|
+
- Added documentation for `virtual_property` and improved overall README.md
|
|
4
|
+
- Only supporting Ruby 3.2 and above for now.
|
|
5
|
+
- Added support for `virtual_property`. This allows you to define JSON schema properties that don't have a corresponding AR attribute.
|
|
6
|
+
- Performance improvements.
|
|
7
|
+
- Improved error handling and added more tests.
|
|
8
|
+
- Improved documentation and added more examples.
|
|
9
|
+
- Added additional support for schema validation keywords. Example: `minLength`, `maxLength`, `pattern`, `format`, `multipleOf`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `enum`, `const`, `items`, `additionalItems`, `contains`, `minItems`, `maxItems`, `uniqueItems`, `propertyNames`, `minProperties`, `maxProperties`, `required`, `dependencies`, `patternProperties`, `allOf`, `anyOf`, `oneOf`, `not`.
|
|
2
10
|
|
|
3
11
|
## [0.1.0] - 2024-02-14
|
|
4
12
|
|
data/README.md
CHANGED
|
@@ -4,10 +4,10 @@ Esquema is a Ruby library for JSON Schema generation from ActiveRecord models.
|
|
|
4
4
|
|
|
5
5
|
Esquema was designed with the following assumptions:
|
|
6
6
|
|
|
7
|
-
- An ActiveRecord model represents a JSON object.
|
|
7
|
+
- An ActiveRecord model represents a JSON Schema object.
|
|
8
8
|
- The JSON object properties are a representation of the model's attributes.
|
|
9
9
|
- The JSON Schema property types are inferred from the model's attribute types.
|
|
10
|
-
- The model associations (has_many, belongs_to, etc.) are represented as subschemas
|
|
10
|
+
- The model associations (has_many, belongs_to, etc.) are represented as subschemas or nested schema objects.
|
|
11
11
|
- You can customize the generated schema by using the configuration file or the `enhance_schema` method.
|
|
12
12
|
|
|
13
13
|
Example Use:
|
|
@@ -82,12 +82,18 @@ class User < ApplicationRecord
|
|
|
82
82
|
model_description "A user of the system"
|
|
83
83
|
property :name, description: "The user's name", title: "Full Name"
|
|
84
84
|
property :group, enum: [1, 2, 3], default: 1, description: "The user's group"
|
|
85
|
+
property :email, description: "The user's email", format: "email"
|
|
86
|
+
virtual_property :age, type: "integer", minimum: 18, maximum: 100, description: "The user's age"
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
```
|
|
88
90
|
|
|
89
91
|
In the example above, the `enhance_schema` method is used to add a description to the model, change the title of the `name` property and add a description. It adds an enum, default value and a description to the `group` property.
|
|
90
92
|
|
|
93
|
+
Use the `property` keyword for the existing model attributes. In other words the symbol passed to the `property` method must be a column in the table that the model represents. Property does not accept a `type` argument, as the type is inferred from the column type.
|
|
94
|
+
|
|
95
|
+
Use the `virtual_property` keyword for properties that are not columns in the table that the model represents. Virtual properties require a `type` argument, as the type cannot be inferred.
|
|
96
|
+
|
|
91
97
|
|
|
92
98
|
## Development
|
|
93
99
|
|
data/lib/esquema/builder.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "property"
|
|
4
|
+
require_relative "virtual_column"
|
|
4
5
|
|
|
5
6
|
module Esquema
|
|
7
|
+
# The Builder class is responsible for building a schema for an ActiveRecord model.
|
|
6
8
|
class Builder
|
|
7
9
|
attr_reader :model, :required_properties
|
|
8
10
|
|
|
@@ -14,26 +16,57 @@ module Esquema
|
|
|
14
16
|
@required_properties = []
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
# Builds the schema for the ActiveRecord model.
|
|
20
|
+
#
|
|
21
|
+
# @return [Hash] The built schema.
|
|
17
22
|
def build_schema
|
|
18
|
-
|
|
23
|
+
@build_schema ||= {
|
|
19
24
|
title: build_title,
|
|
20
25
|
description: build_description,
|
|
21
|
-
type:
|
|
26
|
+
type: build_type,
|
|
22
27
|
properties: build_properties,
|
|
23
28
|
required: required_properties
|
|
24
|
-
}
|
|
29
|
+
}.compact
|
|
30
|
+
end
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
# @return [Hash] The schema for the ActiveRecord model.
|
|
33
|
+
def schema
|
|
34
|
+
build_schema
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# Builds the properties for the schema.
|
|
38
|
+
#
|
|
39
|
+
# @return [Hash] The built properties.
|
|
29
40
|
def build_properties
|
|
30
41
|
add_properties_from_columns
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
add_properties_from_associations
|
|
43
|
+
add_virtual_properties
|
|
34
44
|
@properties
|
|
35
45
|
end
|
|
36
46
|
|
|
47
|
+
# Builds the type for the schema.
|
|
48
|
+
#
|
|
49
|
+
# @return [String] The built type.
|
|
50
|
+
def build_type
|
|
51
|
+
model.respond_to?(:type) ? model.type : "object"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Adds virtual properties to the schema.
|
|
57
|
+
def add_virtual_properties
|
|
58
|
+
return unless schema_enhancements[:properties]
|
|
59
|
+
|
|
60
|
+
virtual_properties = schema_enhancements[:properties].select { |_k, v| v[:virtual] }
|
|
61
|
+
required_properties.concat(virtual_properties.keys)
|
|
62
|
+
|
|
63
|
+
virtual_properties.each do |property_name, options|
|
|
64
|
+
virtual_col = VirtualColumn.new(property_name, options)
|
|
65
|
+
@properties[property_name] = Property.new(virtual_col, options)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Adds properties from columns to the schema.
|
|
37
70
|
def add_properties_from_columns
|
|
38
71
|
columns.each do |property|
|
|
39
72
|
next if property.name.end_with?("_id") && config.exclude_foreign_keys?
|
|
@@ -44,57 +77,66 @@ module Esquema
|
|
|
44
77
|
end
|
|
45
78
|
end
|
|
46
79
|
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
# Adds properties from associations to the schema.
|
|
81
|
+
def add_properties_from_associations
|
|
82
|
+
associations.each do |association|
|
|
49
83
|
next if config.exclude_associations?
|
|
50
84
|
|
|
51
85
|
@properties[association.name] ||= Property.new(association)
|
|
52
86
|
end
|
|
53
87
|
end
|
|
54
88
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
klass = association.klass.name.constantize
|
|
60
|
-
@properties[association.name] ||= self.class.new(klass).build_schema
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
89
|
+
# Retrieves the columns of the model.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::Column>] The columns of the model.
|
|
64
92
|
def columns
|
|
65
93
|
model.columns.reject { |c| excluded_column?(c.name) }
|
|
66
94
|
end
|
|
67
95
|
|
|
96
|
+
# Retrieves the enhancement options for a property.
|
|
97
|
+
#
|
|
98
|
+
# @param property_name [Symbol] The name of the property.
|
|
99
|
+
# @return [Hash] The enhancement options for the property.
|
|
68
100
|
def enhancement_for(property_name)
|
|
69
|
-
schema_enhancements
|
|
101
|
+
schema_enhancements.dig(:properties, property_name.to_sym) || {}
|
|
70
102
|
end
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
# Retrieves the associations of the model.
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<ActiveRecord::Reflection::AssociationReflection>] The associations of the model.
|
|
107
|
+
def associations
|
|
108
|
+
return [] unless model.respond_to?(:reflect_on_all_associations)
|
|
75
109
|
|
|
76
|
-
|
|
77
|
-
model.reflect_on_all_associations(:has_one)
|
|
110
|
+
model.reflect_on_all_associations
|
|
78
111
|
end
|
|
79
112
|
|
|
113
|
+
# Checks if a column is excluded.
|
|
114
|
+
#
|
|
115
|
+
# @param column_name [String] The name of the column.
|
|
116
|
+
# @return [Boolean] True if the column is excluded, false otherwise.
|
|
80
117
|
def excluded_column?(column_name)
|
|
81
118
|
raise ArgumentError, "Column name must be a string" unless column_name.is_a? String
|
|
82
119
|
|
|
83
120
|
config.excluded_columns.include?(column_name.to_sym)
|
|
84
121
|
end
|
|
85
122
|
|
|
123
|
+
# Builds the title for the schema.
|
|
124
|
+
#
|
|
125
|
+
# @return [String] The built title.
|
|
86
126
|
def build_title
|
|
87
127
|
schema_enhancements[:model_title].presence || model.name.demodulize.humanize
|
|
88
128
|
end
|
|
89
129
|
|
|
130
|
+
# Builds the description for the schema.
|
|
131
|
+
#
|
|
132
|
+
# @return [String] The built description.
|
|
90
133
|
def build_description
|
|
91
134
|
schema_enhancements[:model_description].presence
|
|
92
135
|
end
|
|
93
136
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
# Retrieves the schema enhancements for the model.
|
|
138
|
+
#
|
|
139
|
+
# @return [Hash] The schema enhancements.
|
|
98
140
|
def schema_enhancements
|
|
99
141
|
if model.respond_to?(:schema_enhancements)
|
|
100
142
|
model.schema_enhancements
|
|
@@ -103,6 +145,9 @@ module Esquema
|
|
|
103
145
|
end
|
|
104
146
|
end
|
|
105
147
|
|
|
148
|
+
# Retrieves the Esquema configuration.
|
|
149
|
+
#
|
|
150
|
+
# @return [Esquema::Configuration] The Esquema configuration.
|
|
106
151
|
def config
|
|
107
152
|
Esquema.configuration
|
|
108
153
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Esquema
|
|
3
|
+
module Esquema # rubocop:disable Style/Documentation
|
|
4
|
+
# The Configuration module provides configuration options for the gem.
|
|
4
5
|
class Configuration
|
|
5
6
|
attr_accessor :exclude_associations, :exclude_foreign_keys, :excluded_columns
|
|
6
7
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esquema
|
|
4
|
+
# The KeywordValidator module provides functionality for validating schema keyword values.
|
|
5
|
+
# There are three types of keyword values that must be validated against the type of the
|
|
6
|
+
# property they are associated with:
|
|
7
|
+
# - default: The default value for a property.
|
|
8
|
+
# - enum: The allowed values for a property.
|
|
9
|
+
# - const: The constant value for a property.
|
|
10
|
+
module KeywordValidator
|
|
11
|
+
# The valid options for a property.
|
|
12
|
+
VALID_OPTIONS = %i[type title description maxLength minLength pattern maxItems minItems
|
|
13
|
+
maxProperties minProperties properties additionalProperties dependencies
|
|
14
|
+
enum format multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
|
|
15
|
+
const allOf anyOf oneOf not default items uniqueItems virtual].freeze
|
|
16
|
+
|
|
17
|
+
# Hash containing type validators for different data types.
|
|
18
|
+
TYPE_VALIDATORS = {
|
|
19
|
+
string: ->(value) { value.is_a?(String) },
|
|
20
|
+
integer: ->(value) { value.is_a?(Integer) },
|
|
21
|
+
number: ->(value) { value.is_a?(Numeric) },
|
|
22
|
+
boolean: ->(value) { [true, false].include?(value) },
|
|
23
|
+
array: ->(value) { value.is_a?(Array) },
|
|
24
|
+
object: ->(value) { value.is_a?(Hash) },
|
|
25
|
+
null: ->(value) { value.nil? },
|
|
26
|
+
date: ->(value) { value.is_a?(Date) },
|
|
27
|
+
datetime: ->(value) { value.is_a?(DateTime) },
|
|
28
|
+
time: ->(value) { value.is_a?(Time) }
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# Validates a property based on its type and options.
|
|
32
|
+
#
|
|
33
|
+
# @param property_name [Symbol] The name of the property being validated.
|
|
34
|
+
# @param type [Symbol] The type of the property.
|
|
35
|
+
# @param options [Hash] The options for the property.
|
|
36
|
+
# @option options [Object] :default The default value for the property.
|
|
37
|
+
# @option options [Array] :enum The allowed values for the property.
|
|
38
|
+
# @option options [Object] :const The constant value for the property.
|
|
39
|
+
# @raise [ArgumentError] If the options are not in the VALID_OPTIONS constant.
|
|
40
|
+
# @raise [ArgumentError] If the property name is not a symbol.
|
|
41
|
+
# @raise [ArgumentError] If the property type is not a symbol.
|
|
42
|
+
# @raise [ArgumentError] If the type is unknown.
|
|
43
|
+
def self.validate!(property_name, type, options) # rubocop:disable Metrics/AbcSize
|
|
44
|
+
options.assert_valid_keys(VALID_OPTIONS)
|
|
45
|
+
raise ArgumentError, "Property must be a symbol" unless property_name.is_a?(Symbol)
|
|
46
|
+
raise ArgumentError, "Property type must be a symbol" unless type.is_a?(Symbol)
|
|
47
|
+
raise ArgumentError, "Unknown type #{type}" unless TYPE_VALIDATORS.key?(type)
|
|
48
|
+
|
|
49
|
+
validate_default(property_name, type, options[:default]) if options.key?(:default)
|
|
50
|
+
validate_enum(property_name, type, options[:enum]) if options.key?(:enum)
|
|
51
|
+
validate_const(property_name, type, options[:const]) if options.key?(:const)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Validates the default value for a property.
|
|
55
|
+
#
|
|
56
|
+
# @param property_name [Symbol] The name of the property being validated.
|
|
57
|
+
# @param type [Symbol] The type of the property.
|
|
58
|
+
# @param default [Object] The default value for the property.
|
|
59
|
+
def self.validate_default(property_name, type, default)
|
|
60
|
+
validate_value!(property_name, type, default, "default")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Validates the allowed values for a property.
|
|
64
|
+
#
|
|
65
|
+
# @param property_name [Symbol] The name of the property being validated.
|
|
66
|
+
# @param type [Symbol] The type of the property.
|
|
67
|
+
# @param enum [Array] The allowed values for the property.
|
|
68
|
+
# @raise [ArgumentError] If the enum is not an array.
|
|
69
|
+
def self.validate_enum(property_name, type, enum)
|
|
70
|
+
raise ArgumentError, "Enum for #{property_name} is not an array" unless enum.is_a?(Array)
|
|
71
|
+
|
|
72
|
+
enum.each { |value| validate_value!(property_name, type, value, "enum") }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Validates the constant value for a property.
|
|
76
|
+
#
|
|
77
|
+
# @param property_name [Symbol] The name of the property being validated.
|
|
78
|
+
# @param type [Symbol] The type of the property.
|
|
79
|
+
# @param const [Object] The constant value for the property.
|
|
80
|
+
def self.validate_const(property_name, type, const)
|
|
81
|
+
validate_value!(property_name, type, const, "const")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Validates a value based on its type and keyword.
|
|
85
|
+
#
|
|
86
|
+
# @param property_name [Symbol] The name of the property being validated.
|
|
87
|
+
# @param type [Symbol] The type of the property.
|
|
88
|
+
# @param value [Object] The value to be validated.
|
|
89
|
+
# @param keyword [String] The keyword being validated (e.g., "default", "enum").
|
|
90
|
+
# @raise [ArgumentError] If the value does not match the type.
|
|
91
|
+
def self.validate_value!(property_name, type, value, keyword)
|
|
92
|
+
validator = TYPE_VALIDATORS[type]
|
|
93
|
+
return if validator.call(value)
|
|
94
|
+
|
|
95
|
+
raise ArgumentError, "#{keyword.capitalize} value for #{property_name} does not match type #{type}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/esquema/model.rb
CHANGED
|
@@ -5,20 +5,24 @@ require_relative "builder"
|
|
|
5
5
|
require_relative "schema_enhancer"
|
|
6
6
|
|
|
7
7
|
module Esquema
|
|
8
|
+
# The Esquema module provides functionality for building JSON schemas.
|
|
8
9
|
module Model
|
|
9
10
|
extend ActiveSupport::Concern
|
|
10
11
|
|
|
11
12
|
included do
|
|
13
|
+
# Returns the JSON schema for the model.
|
|
12
14
|
def self.json_schema
|
|
13
15
|
Esquema::Builder.new(self).build_schema.to_json
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
# Enhances the schema using the provided block.
|
|
16
19
|
def self.enhance_schema(&block)
|
|
17
20
|
schema_enhancements
|
|
18
21
|
enhancer = SchemaEnhancer.new(self, @schema_enhancements)
|
|
19
22
|
enhancer.instance_eval(&block)
|
|
20
23
|
end
|
|
21
24
|
|
|
25
|
+
# Returns the schema enhancements.
|
|
22
26
|
def self.schema_enhancements
|
|
23
27
|
@schema_enhancements ||= {}
|
|
24
28
|
end
|
data/lib/esquema/property.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "type_caster"
|
|
4
4
|
module Esquema
|
|
5
|
-
|
|
5
|
+
# Represents a property in the Esquema schema.
|
|
6
|
+
class Property # rubocop:disable Metrics/ClassLength
|
|
7
|
+
# Mapping of database types to JSON types.
|
|
6
8
|
DB_TO_JSON_TYPE_MAPPINGS = {
|
|
7
9
|
date: "date",
|
|
8
10
|
datetime: "date-time",
|
|
@@ -11,69 +13,226 @@ module Esquema
|
|
|
11
13
|
text: "string",
|
|
12
14
|
integer: "integer",
|
|
13
15
|
float: "number",
|
|
16
|
+
number: "number",
|
|
14
17
|
decimal: "number",
|
|
15
18
|
boolean: "boolean",
|
|
16
19
|
array: "array",
|
|
17
20
|
object: "object"
|
|
18
21
|
}.freeze
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
NUMERIC_CONSTRAINT_KEYWORDS = %i[minimum maximum exclusiveMinimum exclusiveMaximum multipleOf].freeze
|
|
24
|
+
STRING_CONSTRAINT_KEYWORDS = %i[maxLength minLength pattern format].freeze
|
|
25
|
+
ARRAY_CONSTRAINT_KEYWORDS = %i[maxItems minItems uniqueItems items].freeze
|
|
26
|
+
OBJECT_CONSTRAINT_KEYWORDS = %i[maxProperties minProperties properties additionalProperties dependencies].freeze
|
|
27
|
+
LOGICAL_KEYWORDS = %i[allOf anyOf oneOf not].freeze
|
|
28
|
+
GENERIC_KEYWORDS = %i[type default title description enum const].freeze
|
|
29
|
+
|
|
30
|
+
KEYWORDS = (
|
|
31
|
+
NUMERIC_CONSTRAINT_KEYWORDS +
|
|
32
|
+
STRING_CONSTRAINT_KEYWORDS +
|
|
33
|
+
ARRAY_CONSTRAINT_KEYWORDS +
|
|
34
|
+
OBJECT_CONSTRAINT_KEYWORDS +
|
|
35
|
+
LOGICAL_KEYWORDS +
|
|
36
|
+
GENERIC_KEYWORDS
|
|
37
|
+
).freeze
|
|
38
|
+
|
|
39
|
+
FORMAT_OPTIONS = %i[date-time date time email hostname ipv4 ipv6 uri uuid uri-reference uri-template].freeze
|
|
40
|
+
|
|
41
|
+
attr_reader :object, :options
|
|
42
|
+
|
|
43
|
+
# Initializes a new Property instance.
|
|
44
|
+
#
|
|
45
|
+
# @param object [Object] The object to build the property for.
|
|
46
|
+
# An object can be any of the following instance types:
|
|
47
|
+
# An ActiveRecord column. Example: ActiveRecord::ConnectionAdapters::SQLite3::Column
|
|
48
|
+
# An ActiveRecord association reflection. Example: ActiveRecord::Reflection::BelongsToReflection
|
|
49
|
+
# An Esquema virtual column. Example: Esquema::VirtualColumn
|
|
50
|
+
# @param options [Hash] Additional options for the property.
|
|
51
|
+
# @raise [ArgumentError] If the property does not have a name.
|
|
52
|
+
def initialize(object, options = {})
|
|
53
|
+
raise ArgumentError, "property must have a name" unless object.respond_to?(:name)
|
|
54
|
+
|
|
55
|
+
@object = object
|
|
28
56
|
@options = options
|
|
29
57
|
end
|
|
30
58
|
|
|
59
|
+
# Converts the Property instance to a JSON representation.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] The JSON representation of the Property.
|
|
31
62
|
def as_json
|
|
32
|
-
|
|
33
|
-
value = send("build_#{property}")
|
|
63
|
+
KEYWORDS.each_with_object({}) do |property, hash|
|
|
64
|
+
value = send("build_#{property.downcase}")
|
|
34
65
|
next if value.nil? || (value.is_a?(String) && value.empty?)
|
|
35
66
|
|
|
36
67
|
hash[property] = value
|
|
37
68
|
end.compact
|
|
38
69
|
end
|
|
39
70
|
|
|
71
|
+
# Builds the title attribute for the Property.
|
|
72
|
+
#
|
|
73
|
+
# @return [String] The title attribute.
|
|
40
74
|
def build_title
|
|
41
|
-
options[:title].presence ||
|
|
75
|
+
options[:title].presence || object.name.to_s.humanize
|
|
42
76
|
end
|
|
43
77
|
|
|
78
|
+
# Builds the default attribute for the Property.
|
|
79
|
+
#
|
|
80
|
+
# @return [Object, nil] The default attribute.
|
|
44
81
|
def build_default
|
|
45
|
-
return unless
|
|
82
|
+
return unless object.respond_to?(:default)
|
|
46
83
|
|
|
47
|
-
default_value =
|
|
84
|
+
default_value = object.default || options[:default].presence
|
|
48
85
|
|
|
49
|
-
@default = TypeCaster.cast(
|
|
86
|
+
@default = TypeCaster.cast(object.type, default_value) unless default_value.nil?
|
|
50
87
|
end
|
|
51
88
|
|
|
89
|
+
# Builds the type attribute for the Property.
|
|
90
|
+
#
|
|
91
|
+
# @return [String, nil] The type attribute.
|
|
52
92
|
def build_type
|
|
53
|
-
return DB_TO_JSON_TYPE_MAPPINGS[:array] if
|
|
54
|
-
|
|
55
|
-
@type = DB_TO_JSON_TYPE_MAPPINGS[property.type]
|
|
56
|
-
end
|
|
93
|
+
return DB_TO_JSON_TYPE_MAPPINGS[:array] if object.try(:collection?)
|
|
57
94
|
|
|
58
|
-
|
|
59
|
-
return unless property.respond_to?(:item_type)
|
|
95
|
+
return unless object.respond_to?(:type)
|
|
60
96
|
|
|
61
|
-
@
|
|
97
|
+
@type = DB_TO_JSON_TYPE_MAPPINGS[object.type]
|
|
62
98
|
end
|
|
63
99
|
|
|
100
|
+
# Builds the description attribute for the Property.
|
|
101
|
+
#
|
|
102
|
+
# @return [String, nil] The description attribute.
|
|
64
103
|
def build_description
|
|
65
104
|
options[:description]
|
|
66
105
|
end
|
|
67
106
|
|
|
107
|
+
# Builds the items attribute for the Property.
|
|
108
|
+
#
|
|
109
|
+
# @return [Hash, nil] The items attribute.
|
|
68
110
|
def build_items
|
|
69
|
-
return unless
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
111
|
+
return unless object.try(:collection?)
|
|
112
|
+
|
|
113
|
+
case object.type
|
|
114
|
+
when :array
|
|
115
|
+
{ type: DB_TO_JSON_TYPE_MAPPINGS[object.item_type] }
|
|
116
|
+
else
|
|
117
|
+
Builder.new(object.klass).build_schema
|
|
118
|
+
end
|
|
73
119
|
end
|
|
74
120
|
|
|
121
|
+
# Builds the enum attribute for the Property.
|
|
122
|
+
#
|
|
123
|
+
# @return [Array, nil] The enum attribute.
|
|
75
124
|
def build_enum
|
|
76
125
|
options[:enum]
|
|
77
126
|
end
|
|
127
|
+
|
|
128
|
+
def build_minimum
|
|
129
|
+
options[:minimum]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_maximum
|
|
133
|
+
options[:maximum]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_exclusiveminimum
|
|
137
|
+
options[:exclusiveMinimum]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_exclusivemaximum
|
|
141
|
+
options[:exclusiveMaximum]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_multipleof
|
|
145
|
+
options[:multipleof]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_maxlength
|
|
149
|
+
options[:maxLength]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def build_minlength
|
|
153
|
+
options[:minLength]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def build_pattern
|
|
157
|
+
options[:pattern]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def build_format
|
|
161
|
+
options[:format]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def build_maxitems # rubocop:disable Metrics/AbcSize
|
|
165
|
+
raise ArgumentError, "maxItems must be an integer" if options[:maxItems] && !options[:maxItems].is_a?(Integer)
|
|
166
|
+
|
|
167
|
+
if options[:maxItems]&.negative?
|
|
168
|
+
raise ArgumentError,
|
|
169
|
+
"maxItems must be a non-negative integer"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if options[:maxItems] && options[:type] != :array
|
|
173
|
+
raise ArgumentError, "maxItems must be use for array type properties only."
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
options[:maxItems]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_minitems # rubocop:disable Metrics/AbcSize
|
|
180
|
+
raise ArgumentError, "minItems must be an integer" if options[:minItems] && !options[:minItems].is_a?(Integer)
|
|
181
|
+
|
|
182
|
+
if options[:minItems]&.negative?
|
|
183
|
+
raise ArgumentError,
|
|
184
|
+
"minItems must be a non-negative integer"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if options[:minItems] && options[:type] != :array
|
|
188
|
+
raise ArgumentError, "minItems must be use for array type properties only."
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
options[:minItems]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_uniqueitems
|
|
195
|
+
options[:uniqueItems]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build_properties
|
|
199
|
+
options[:properties]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def build_maxproperties
|
|
203
|
+
options[:maxProperties]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def build_minproperties
|
|
207
|
+
options[:minProperties]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def build_additionalproperties
|
|
211
|
+
options[:additionalProperties]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def build_dependencies
|
|
215
|
+
options[:dependencies]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_allof
|
|
219
|
+
options[:allOf]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def build_anyof
|
|
223
|
+
options[:anyOf]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def build_oneof
|
|
227
|
+
options[:oneOf]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def build_not
|
|
231
|
+
options[:not]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_const
|
|
235
|
+
options[:const]
|
|
236
|
+
end
|
|
78
237
|
end
|
|
79
238
|
end
|