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.
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