model-to-schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +13 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +111 -0
  8. data/Rakefile +12 -0
  9. data/esquema.gemspec +38 -0
  10. data/lib/esquema/builder.rb +155 -0
  11. data/lib/esquema/configuration.rb +34 -0
  12. data/lib/esquema/keyword_validator.rb +98 -0
  13. data/lib/esquema/model.rb +31 -0
  14. data/lib/esquema/property.rb +238 -0
  15. data/lib/esquema/schema_enhancer.rb +90 -0
  16. data/lib/esquema/type_caster.rb +53 -0
  17. data/lib/esquema/version.rb +5 -0
  18. data/lib/esquema/virtual_column.rb +46 -0
  19. data/lib/esquema.rb +14 -0
  20. data/lib/generators/esquema/install/install_generator.rb +16 -0
  21. data/lib/generators/esquema/install/templates/esquema_initializer.rb +22 -0
  22. data/sorbet/config +4 -0
  23. data/sorbet/rbi/annotations/.gitattributes +1 -0
  24. data/sorbet/rbi/annotations/activemodel.rbi +89 -0
  25. data/sorbet/rbi/annotations/activerecord.rbi +92 -0
  26. data/sorbet/rbi/annotations/activesupport.rbi +421 -0
  27. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  28. data/sorbet/rbi/gems/.gitattributes +1 -0
  29. data/sorbet/rbi/gems/activemodel@7.1.3.rbi +8 -0
  30. data/sorbet/rbi/gems/activerecord@7.1.3.rbi +8 -0
  31. data/sorbet/rbi/gems/activesupport@7.1.3.rbi +192 -0
  32. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  33. data/sorbet/rbi/gems/base64@0.2.0.rbi +8 -0
  34. data/sorbet/rbi/gems/bigdecimal@3.1.6.rbi +8 -0
  35. data/sorbet/rbi/gems/byebug@11.1.3.rbi +3606 -0
  36. data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
  37. data/sorbet/rbi/gems/concurrent-ruby@1.2.3.rbi +8 -0
  38. data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +8 -0
  39. data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1130 -0
  40. data/sorbet/rbi/gems/drb@2.2.0.rbi +1272 -0
  41. data/sorbet/rbi/gems/erubi@1.12.0.rbi +145 -0
  42. data/sorbet/rbi/gems/i18n@1.14.1.rbi +8 -0
  43. data/sorbet/rbi/gems/json@2.7.1.rbi +1553 -0
  44. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
  45. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  46. data/sorbet/rbi/gems/minitest@5.22.2.rbi +8 -0
  47. data/sorbet/rbi/gems/mutex_m@0.2.0.rbi +8 -0
  48. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  49. data/sorbet/rbi/gems/parallel@1.24.0.rbi +280 -0
  50. data/sorbet/rbi/gems/parser@3.3.0.5.rbi +5472 -0
  51. data/sorbet/rbi/gems/prettier_print@1.2.1.rbi +951 -0
  52. data/sorbet/rbi/gems/prism@0.24.0.rbi +31040 -0
  53. data/sorbet/rbi/gems/pry-byebug@3.10.1.rbi +1150 -0
  54. data/sorbet/rbi/gems/pry@0.14.2.rbi +10075 -0
  55. data/sorbet/rbi/gems/racc@1.7.3.rbi +157 -0
  56. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  57. data/sorbet/rbi/gems/rake@13.1.0.rbi +3027 -0
  58. data/sorbet/rbi/gems/rbi@0.1.9.rbi +3006 -0
  59. data/sorbet/rbi/gems/regexp_parser@2.9.0.rbi +3771 -0
  60. data/sorbet/rbi/gems/rexml@3.2.6.rbi +4781 -0
  61. data/sorbet/rbi/gems/rspec-core@3.13.0.rbi +10978 -0
  62. data/sorbet/rbi/gems/rspec-expectations@3.13.0.rbi +8153 -0
  63. data/sorbet/rbi/gems/rspec-mocks@3.13.0.rbi +5340 -0
  64. data/sorbet/rbi/gems/rspec-support@3.13.0.rbi +1629 -0
  65. data/sorbet/rbi/gems/rspec@3.13.0.rbi +82 -0
  66. data/sorbet/rbi/gems/rubocop-ast@1.30.0.rbi +7006 -0
  67. data/sorbet/rbi/gems/rubocop@1.60.2.rbi +57383 -0
  68. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  69. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +8 -0
  70. data/sorbet/rbi/gems/spoom@1.2.4.rbi +3777 -0
  71. data/sorbet/rbi/gems/sqlite3@1.7.2.rbi +1691 -0
  72. data/sorbet/rbi/gems/syntax_tree@6.2.0.rbi +23133 -0
  73. data/sorbet/rbi/gems/tapioca@0.12.0.rbi +3510 -0
  74. data/sorbet/rbi/gems/thor@1.3.0.rbi +4345 -0
  75. data/sorbet/rbi/gems/timeout@0.4.1.rbi +142 -0
  76. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +8 -0
  77. data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +65 -0
  78. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
  79. data/sorbet/rbi/gems/yard@0.9.34.rbi +18219 -0
  80. data/sorbet/rbi/todo.rbi +20 -0
  81. data/sorbet/tapioca/config.yml +13 -0
  82. data/sorbet/tapioca/require.rb +4 -0
  83. metadata +176 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92f331264f95271e7f464eadf17836ae1bfc3ef7d968b0f4c7e09c0694375200
4
+ data.tar.gz: ab9a5582eee8f567802129b7248aaf1aface3970363d79112e7ad5e071d8396e
5
+ SHA512:
6
+ metadata.gz: a6253ecb98c34f70ab1c9d8f00613bdf40cc55656a8f29dc37d45f95d211408718e437f7b1f74f81145def1aeb25185bf85270f0a7ed8887b5a4e8b44d50dd30
7
+ data.tar.gz: 71110299132f10d628f717ee65c90d772da6790f0f8b0711541a714653c50a35f8928ddc6e5dec756a433e9a9de2d2be546f04c29d849f7d823bb03351c1a5fa
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ Exclude:
4
+ - "spec/support/matchers/**"
5
+ - "vendor/**/*"
6
+
7
+ Style/StringLiterals:
8
+ Enabled: true
9
+ EnforcedStyle: double_quotes
10
+
11
+ Style/StringLiteralsInInterpolation:
12
+ Enabled: true
13
+ EnforcedStyle: double_quotes
14
+
15
+ Layout/LineLength:
16
+ Max: 120
17
+
18
+ Metrics/BlockLength:
19
+ Enabled: false
20
+
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
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`.
10
+
11
+ ## [0.1.0] - 2024-02-14
12
+
13
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Sergio Bayona
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # Esquema
2
+
3
+ Esquema is a Ruby library for JSON Schema generation from ActiveRecord models.
4
+
5
+ Esquema was designed with the following assumptions:
6
+
7
+ - An ActiveRecord model represents a JSON Schema object.
8
+ - The JSON object properties are a representation of the model's attributes.
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 or nested schema objects.
11
+ - You can customize the generated schema by using the configuration file or the `enhance_schema` method.
12
+
13
+ Example Use:
14
+
15
+ ```ruby
16
+ class User < ApplicationRecord
17
+ include Esquema::Model
18
+
19
+ # Assuming the User db table has the following columns:
20
+ # column :name, :string
21
+ # column :email, :string
22
+
23
+ end
24
+ end
25
+ ```
26
+
27
+ Calling `User.json_schema` will return the JSON Schema for the User model:
28
+
29
+ ```json
30
+ {
31
+ "title": "User model",
32
+ "type": "object",
33
+ "properties": {
34
+ "id": {
35
+ "type": "integer"
36
+ },
37
+ "name": {
38
+ "type": "string"
39
+ },
40
+ "email": {
41
+ "type": "string"
42
+ }
43
+ },
44
+ "required:": [
45
+ "name",
46
+ "email"
47
+ ]
48
+ }
49
+ ```
50
+
51
+ ## Installation
52
+
53
+ install the gem by executing:
54
+
55
+ $ gem install esquema
56
+
57
+
58
+ Run the following command to install the gem and generate the configuration file:
59
+
60
+ ```bash
61
+ rails generate esquema:install
62
+ ```
63
+
64
+ This will generate a configuration file at:
65
+
66
+ <rails_app>/config/initializer/esquema.rb
67
+
68
+
69
+ ## Usage
70
+
71
+ Simply include the `Esquema::Model` module in your ActiveRecord model and call the `json_schema` method to generate the JSON Schema for the model.
72
+
73
+ There are multiple ways to customize the generated schema:
74
+ - You can exclude columns, foreign keys, and associations from the schema. See the <rails_project>/config/initializer/esquema.rb configuration for more details.
75
+ - For more complex customizations, you can use the `enhance_schema` method to modify the schema directly on the AR model. Here is an example:
76
+
77
+ ```ruby
78
+ class User < ApplicationRecord
79
+ include Esquema::Model
80
+
81
+ enhance_schema do
82
+ model_description "A user of the system"
83
+ property :name, description: "The user's name", title: "Full Name"
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"
87
+ end
88
+ end
89
+ ```
90
+
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.
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
+
97
+
98
+ ## Development
99
+
100
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/esquema.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
111
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/esquema.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/esquema/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "model-to-schema"
7
+ spec.version = Esquema::VERSION
8
+ spec.authors = ["Sergio Bayona"]
9
+ spec.email = ["bayona.sergio@gmail.com"]
10
+
11
+ spec.summary = "Generate json-schema from ActiveRecord models."
12
+ spec.description = "Generate json-schema from ActiveRecord models."
13
+ spec.homepage = "https://github.com/sergiobayona/esquema"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/sergiobayona/esquema"
21
+ spec.metadata["changelog_uri"] = "https://github.com/sergiobayona/esquema/blob/main/CHANGELOG.md"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (File.expand_path(f) == __FILE__) ||
28
+ f.start_with?(*%w[bin/ spec/ .git .github Gemfile])
29
+ end
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_dependency "activerecord", "~> 7.0"
36
+ spec.add_development_dependency "pry-byebug", "~> 3.10", ">= 3.10.1"
37
+ spec.add_development_dependency "rake", "~> 13.0"
38
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "property"
4
+ require_relative "virtual_column"
5
+
6
+ module Esquema
7
+ # The Builder class is responsible for building a schema for an ActiveRecord model.
8
+ class Builder
9
+ attr_reader :model, :required_properties
10
+
11
+ def initialize(model)
12
+ raise ArgumentError, "Class is not an ActiveRecord model" unless model.ancestors.include? ActiveRecord::Base
13
+
14
+ @model = model
15
+ @properties = {}
16
+ @required_properties = []
17
+ end
18
+
19
+ # Builds the schema for the ActiveRecord model.
20
+ #
21
+ # @return [Hash] The built schema.
22
+ def build_schema
23
+ @build_schema ||= {
24
+ title: build_title,
25
+ description: build_description,
26
+ type: build_type,
27
+ properties: build_properties,
28
+ required: required_properties
29
+ }.compact
30
+ end
31
+
32
+ # @return [Hash] The schema for the ActiveRecord model.
33
+ def schema
34
+ build_schema
35
+ end
36
+
37
+ # Builds the properties for the schema.
38
+ #
39
+ # @return [Hash] The built properties.
40
+ def build_properties
41
+ add_properties_from_columns
42
+ add_properties_from_associations
43
+ add_virtual_properties
44
+ @properties
45
+ end
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.
70
+ def add_properties_from_columns
71
+ columns.each do |property|
72
+ next if property.name.end_with?("_id") && config.exclude_foreign_keys?
73
+
74
+ required_properties << property.name
75
+ options = enhancement_for(property.name)
76
+ @properties[property.name] ||= Property.new(property, options)
77
+ end
78
+ end
79
+
80
+ # Adds properties from associations to the schema.
81
+ def add_properties_from_associations
82
+ associations.each do |association|
83
+ next if config.exclude_associations?
84
+
85
+ @properties[association.name] ||= Property.new(association)
86
+ end
87
+ end
88
+
89
+ # Retrieves the columns of the model.
90
+ #
91
+ # @return [Array<ActiveRecord::ConnectionAdapters::Column>] The columns of the model.
92
+ def columns
93
+ model.columns.reject { |c| excluded_column?(c.name) }
94
+ end
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.
100
+ def enhancement_for(property_name)
101
+ schema_enhancements.dig(:properties, property_name.to_sym) || {}
102
+ end
103
+
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)
109
+
110
+ model.reflect_on_all_associations
111
+ end
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.
117
+ def excluded_column?(column_name)
118
+ raise ArgumentError, "Column name must be a string" unless column_name.is_a? String
119
+
120
+ config.excluded_columns.include?(column_name.to_sym)
121
+ end
122
+
123
+ # Builds the title for the schema.
124
+ #
125
+ # @return [String] The built title.
126
+ def build_title
127
+ schema_enhancements[:model_title].presence || model.name.demodulize.humanize
128
+ end
129
+
130
+ # Builds the description for the schema.
131
+ #
132
+ # @return [String] The built description.
133
+ def build_description
134
+ schema_enhancements[:model_description].presence
135
+ end
136
+
137
+ # Retrieves the schema enhancements for the model.
138
+ #
139
+ # @return [Hash] The schema enhancements.
140
+ def schema_enhancements
141
+ if model.respond_to?(:schema_enhancements)
142
+ model.schema_enhancements
143
+ else
144
+ {}
145
+ end
146
+ end
147
+
148
+ # Retrieves the Esquema configuration.
149
+ #
150
+ # @return [Esquema::Configuration] The Esquema configuration.
151
+ def config
152
+ Esquema.configuration
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esquema # rubocop:disable Style/Documentation
4
+ # The Configuration module provides configuration options for the gem.
5
+ class Configuration
6
+ attr_accessor :exclude_associations, :exclude_foreign_keys, :excluded_columns
7
+
8
+ def initialize
9
+ reset
10
+ end
11
+
12
+ def reset
13
+ @exclude_associations = false
14
+ @exclude_foreign_keys = true
15
+ @excluded_columns = []
16
+ end
17
+
18
+ def exclude_foreign_keys?
19
+ exclude_foreign_keys
20
+ end
21
+
22
+ def exclude_associations?
23
+ exclude_associations
24
+ end
25
+ end
26
+
27
+ def self.configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ def self.configure
32
+ yield(configuration)
33
+ end
34
+ end
@@ -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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require_relative "builder"
5
+ require_relative "schema_enhancer"
6
+
7
+ module Esquema
8
+ # The Esquema module provides functionality for building JSON schemas.
9
+ module Model
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ # Returns the JSON schema for the model.
14
+ def self.json_schema
15
+ Esquema::Builder.new(self).build_schema.to_json
16
+ end
17
+
18
+ # Enhances the schema using the provided block.
19
+ def self.enhance_schema(&block)
20
+ schema_enhancements
21
+ enhancer = SchemaEnhancer.new(self, @schema_enhancements)
22
+ enhancer.instance_eval(&block)
23
+ end
24
+
25
+ # Returns the schema enhancements.
26
+ def self.schema_enhancements
27
+ @schema_enhancements ||= {}
28
+ end
29
+ end
30
+ end
31
+ end