esquema 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|