esquema 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +9 -1
  4. data/README.md +8 -2
  5. data/lib/esquema/builder.rb +73 -28
  6. data/lib/esquema/configuration.rb +2 -1
  7. data/lib/esquema/keyword_validator.rb +98 -0
  8. data/lib/esquema/model.rb +4 -0
  9. data/lib/esquema/property.rb +185 -26
  10. data/lib/esquema/schema_enhancer.rb +55 -30
  11. data/lib/esquema/type_caster.rb +16 -4
  12. data/lib/esquema/version.rb +1 -1
  13. data/lib/esquema/virtual_column.rb +46 -0
  14. data/lib/esquema.rb +7 -1
  15. data/lib/generators/esquema/install/install_generator.rb +1 -0
  16. data/sorbet/config +4 -0
  17. data/sorbet/rbi/annotations/.gitattributes +1 -0
  18. data/sorbet/rbi/annotations/activemodel.rbi +89 -0
  19. data/sorbet/rbi/annotations/activerecord.rbi +92 -0
  20. data/sorbet/rbi/annotations/activesupport.rbi +421 -0
  21. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  22. data/sorbet/rbi/gems/.gitattributes +1 -0
  23. data/sorbet/rbi/gems/activemodel@7.1.3.rbi +8 -0
  24. data/sorbet/rbi/gems/activerecord@7.1.3.rbi +8 -0
  25. data/sorbet/rbi/gems/activesupport@7.1.3.rbi +192 -0
  26. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  27. data/sorbet/rbi/gems/base64@0.2.0.rbi +8 -0
  28. data/sorbet/rbi/gems/bigdecimal@3.1.6.rbi +8 -0
  29. data/sorbet/rbi/gems/byebug@11.1.3.rbi +3606 -0
  30. data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
  31. data/sorbet/rbi/gems/concurrent-ruby@1.2.3.rbi +8 -0
  32. data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +8 -0
  33. data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1130 -0
  34. data/sorbet/rbi/gems/drb@2.2.0.rbi +1272 -0
  35. data/sorbet/rbi/gems/erubi@1.12.0.rbi +145 -0
  36. data/sorbet/rbi/gems/i18n@1.14.1.rbi +8 -0
  37. data/sorbet/rbi/gems/json@2.7.1.rbi +1553 -0
  38. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
  39. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  40. data/sorbet/rbi/gems/minitest@5.22.2.rbi +8 -0
  41. data/sorbet/rbi/gems/mutex_m@0.2.0.rbi +8 -0
  42. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  43. data/sorbet/rbi/gems/parallel@1.24.0.rbi +280 -0
  44. data/sorbet/rbi/gems/parser@3.3.0.5.rbi +5472 -0
  45. data/sorbet/rbi/gems/prettier_print@1.2.1.rbi +951 -0
  46. data/sorbet/rbi/gems/prism@0.24.0.rbi +31040 -0
  47. data/sorbet/rbi/gems/pry-byebug@3.10.1.rbi +1150 -0
  48. data/sorbet/rbi/gems/pry@0.14.2.rbi +10075 -0
  49. data/sorbet/rbi/gems/racc@1.7.3.rbi +157 -0
  50. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  51. data/sorbet/rbi/gems/rake@13.1.0.rbi +3027 -0
  52. data/sorbet/rbi/gems/rbi@0.1.9.rbi +3006 -0
  53. data/sorbet/rbi/gems/regexp_parser@2.9.0.rbi +3771 -0
  54. data/sorbet/rbi/gems/rexml@3.2.6.rbi +4781 -0
  55. data/sorbet/rbi/gems/rspec-core@3.13.0.rbi +10978 -0
  56. data/sorbet/rbi/gems/rspec-expectations@3.13.0.rbi +8153 -0
  57. data/sorbet/rbi/gems/rspec-mocks@3.13.0.rbi +5340 -0
  58. data/sorbet/rbi/gems/rspec-support@3.13.0.rbi +1629 -0
  59. data/sorbet/rbi/gems/rspec@3.13.0.rbi +82 -0
  60. data/sorbet/rbi/gems/rubocop-ast@1.30.0.rbi +7006 -0
  61. data/sorbet/rbi/gems/rubocop@1.60.2.rbi +57383 -0
  62. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  63. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +8 -0
  64. data/sorbet/rbi/gems/spoom@1.2.4.rbi +3777 -0
  65. data/sorbet/rbi/gems/sqlite3@1.7.2.rbi +1691 -0
  66. data/sorbet/rbi/gems/syntax_tree@6.2.0.rbi +23133 -0
  67. data/sorbet/rbi/gems/tapioca@0.12.0.rbi +3510 -0
  68. data/sorbet/rbi/gems/thor@1.3.0.rbi +4345 -0
  69. data/sorbet/rbi/gems/timeout@0.4.1.rbi +142 -0
  70. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +8 -0
  71. data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +65 -0
  72. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
  73. data/sorbet/rbi/gems/yard@0.9.34.rbi +18219 -0
  74. data/sorbet/rbi/todo.rbi +20 -0
  75. data/sorbet/tapioca/config.yml +13 -0
  76. data/sorbet/tapioca/require.rb +4 -0
  77. metadata +72 -10
  78. data/esquema.gemspec +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8324bb76d58095f92b42debd64164b92ea4b710ed6e4a8245727f0f37738ecc
4
- data.tar.gz: cff598345220aa3ca29a30cf3020f27f4bcc093a660137203a564ec2adf2318a
3
+ metadata.gz: 4a31e08f199e964d003d99fe4e8e03b62dd86231a48d29053fec1fef03ce06f8
4
+ data.tar.gz: 20f4e5198607e7bba751bc61f1b4b0666f4468f19879ac44fdbdf2ee8542675b
5
5
  SHA512:
6
- metadata.gz: b875bf9a00edbf26515c261e96e512cf8b940a48a613d35d1b461380c96528777c8a4522403d2ae1a907876c4e635033e453bec2356d708f554105c9533aeb3b
7
- data.tar.gz: cdfcc607f90a1695d96aacbc0af3a07e646843a6bd0ebd221adf27105356fd61e6d75e1946826c893fedfde48990788547caf9ebbae522785f665c7b5154d186
6
+ metadata.gz: 7c621671e563866f1c9d0216b4561f135dc4d8c66c3921b18876600fedaa4c37417c6b2a12ee12c15ec9cdf6cbabbac6fe910bed3820d193a5395412a94aa975
7
+ data.tar.gz: 69a1b9637e854a0ae3f5cc7730c37af8e4fce0170432bab567fb6bc079d7914dc256346e4f1d35c9e9691ed801ef7effe062d098df9387c04136d08047a9f56c
data/.rubocop.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 3.2
3
+ Exclude:
4
+ - 'spec/support/matchers/**'
5
+ - 'vendor/**/*'
3
6
 
4
7
  Style/StringLiterals:
5
8
  Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
- ## [Unreleased]
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 in the JSON Schema.
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
 
@@ -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
- schema = {
23
+ @build_schema ||= {
19
24
  title: build_title,
20
25
  description: build_description,
21
- type: "object",
26
+ type: build_type,
22
27
  properties: build_properties,
23
28
  required: required_properties
24
- }
29
+ }.compact
30
+ end
25
31
 
26
- schema.compact
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
- add_properties_from_has_many_associations
32
- add_properties_from_has_one_associations
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
- def add_properties_from_has_many_associations
48
- has_many_associations.each do |association|
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
- def add_properties_from_has_one_associations
56
- has_one_associations.each do |association|
57
- next if config.exclude_associations?
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&.dig(:properties, property_name.to_sym) || {}
101
+ schema_enhancements.dig(:properties, property_name.to_sym) || {}
70
102
  end
71
103
 
72
- def has_many_associations
73
- model.reflect_on_all_associations(:has_many)
74
- end
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
- def has_one_associations
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
- def name
95
- model.name
96
- end
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
@@ -2,7 +2,9 @@
2
2
 
3
3
  require_relative "type_caster"
4
4
  module Esquema
5
- class Property
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
- ATTRS = %i[type default title description item_type items enum].freeze
21
- attr_accessor(*ATTRS)
22
- attr_reader :property, :options
23
-
24
- def initialize(property, options = {})
25
- raise ArgumentError, "property must have a name" unless property.respond_to?(:name)
26
-
27
- @property = property
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
- ATTRS.each_with_object({}) do |property, hash|
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 || property.name.to_s.humanize
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 property.respond_to?(:default)
82
+ return unless object.respond_to?(:default)
46
83
 
47
- default_value = property.default || options[:default].presence
84
+ default_value = object.default || options[:default].presence
48
85
 
49
- @default = TypeCaster.cast(property.type, default_value) unless default_value.nil?
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 property.try(:collection?)
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
- def build_item_type
59
- return unless property.respond_to?(:item_type)
95
+ return unless object.respond_to?(:type)
60
96
 
61
- @item_type = property.item_type if property.type == :array
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 property.try(:collection?)
70
-
71
- class_name = property.class_name.constantize
72
- @items = Builder.new(class_name).build_schema
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