model-to-schema 0.1.0

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