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
@@ -1,26 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "date"
2
4
  require "bigdecimal"
5
+ require_relative "keyword_validator"
3
6
 
4
7
  module Esquema
8
+ # The SchemaEnhancer class is responsible for enhancing the schema of a model.
5
9
  class SchemaEnhancer
6
- VALID_OPTIONS = %i[title description maxLength minLength pattern maxItems minItems uniqueItems
7
- maxProperties minProperties properties additionalProperties dependencies
8
- enum format multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
9
- const allOf anyOf oneOf not default].freeze
10
-
11
- TYPE_MAPPINGS = {
12
- date: Date,
13
- datetime: DateTime,
14
- time: Time,
15
- string: String,
16
- text: String,
17
- integer: Integer,
18
- float: Float,
19
- decimal: BigDecimal,
20
- boolean: [TrueClass, FalseClass],
21
- array: Array,
22
- object: Object
23
- }.freeze
24
10
  attr_reader :model
25
11
 
26
12
  def initialize(model, schema_enhancements)
@@ -28,38 +14,77 @@ module Esquema
28
14
  @model = model
29
15
  end
30
16
 
17
+ # Sets the description for the model.
18
+ #
19
+ # @param description [String] The description of the model.
31
20
  def model_description(description)
32
21
  @schema_enhancements[:model_description] = description
33
22
  end
34
23
 
24
+ # Sets the title for the model.
25
+ #
26
+ # @param title [String] The title of the model.
35
27
  def model_title(title)
36
28
  @schema_enhancements[:model_title] = title
37
29
  end
38
30
 
31
+ # Adds a property to the schema.
32
+ #
33
+ # @param name [Symbol] The name of the property.
34
+ # @param options [Hash] Additional options for the property.
39
35
  def property(name, options = {})
40
- db_type = model.type_for_attribute(name).type
41
- klass_type = Array(TYPE_MAPPINGS[db_type])
36
+ validate_property_as_attribute_for(name, options)
42
37
 
43
- validate_default_value(options[:default], klass_type, db_type)
44
- validate_enum_values(options[:enum], klass_type, db_type)
38
+ type = resolve_type(name, options)
39
+
40
+ KeywordValidator.validate!(name, type, options)
45
41
 
46
- options.assert_valid_keys(VALID_OPTIONS)
47
42
  @schema_enhancements[:properties] ||= {}
48
43
  @schema_enhancements[:properties][name] = options
49
44
  end
50
45
 
51
- private
46
+ # Adds a virtual property to the schema.
47
+ #
48
+ # @param name [Symbol] The name of the virtual property.
49
+ # @param options [Hash] Additional options for the virtual property.
50
+ def virtual_property(name, options = {})
51
+ options[:virtual] = true
52
+ property(name, options)
53
+ end
52
54
 
53
- def validate_default_value(default_value, klass_type, db_type)
54
- return unless default_value.present? && !klass_type.include?(default_value.class)
55
+ private
55
56
 
56
- raise ArgumentError, "Default value must be of type #{db_type}"
57
+ # Resolves the type of a property.
58
+ #
59
+ # @param name [Symbol] The name of the property.
60
+ # @param options [Hash] Additional options for the property.
61
+ # @return [Symbol] The resolved type of the property.
62
+ def resolve_type(name, options = {})
63
+ if options[:virtual] == true
64
+ options[:type]
65
+ else
66
+ model.type_for_attribute(name).type
67
+ end
57
68
  end
58
69
 
59
- def validate_enum_values(enum_values, klass_type, db_type)
60
- return unless enum_values.present? && !enum_values.all? { |value| klass_type.include?(value.class) }
70
+ # Retrieves the valid properties for the model.
71
+ #
72
+ # @return [Array<Symbol>] The valid properties for the model.
73
+ def valid_properties
74
+ @valid_properties ||= begin
75
+ properties = model.column_names + model.reflect_on_all_associations.map(&:name)
76
+ properties.map(&:to_sym)
77
+ end
78
+ end
61
79
 
62
- raise ArgumentError, "Enum values must be of type #{db_type}"
80
+ # Validates that a property is a valid attribute for the model.
81
+ #
82
+ # @param prop_name [Symbol] The name of the property.
83
+ # @param options [Hash] Additional options for the property.
84
+ # @raise [ArgumentError] If the property is not a valid attribute for the model.
85
+ def validate_property_as_attribute_for(prop_name, options = {})
86
+ return if options[:virtual] == true
87
+ raise ArgumentError, "`#{prop_name}` is not a model attribute." unless valid_properties.include?(prop_name.to_sym)
63
88
  end
64
89
  end
65
90
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Esquema
4
- module TypeCaster
5
- def self.cast(type, value) # rubocop:disable Metrics/MethodLength
4
+ module TypeCaster # rubocop:disable Style/Documentation
5
+ def self.cast(type, value) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
6
6
  case type
7
7
  when :string, :text
8
8
  value.to_s
@@ -18,14 +18,26 @@ module Esquema
18
18
  rescue StandardError
19
19
  nil
20
20
  end
21
+ when :number
22
+ if value.to_s.include?(".")
23
+ begin
24
+ Float(value)
25
+ rescue StandardError
26
+ nil
27
+ end
28
+ else
29
+ begin
30
+ Integer(value)
31
+ rescue StandardError
32
+ nil
33
+ end
34
+ end
21
35
  when :boolean
22
36
  case value
23
37
  when true, "true", "1", 1
24
38
  true
25
39
  when false, "false", "0", 0
26
40
  false
27
- else
28
- nil # or handle as desired
29
41
  end
30
42
  when :array
31
43
  Array(value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Esquema
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esquema
4
+ class VirtualColumn # rubocop:disable Style/Documentation
5
+ def initialize(property_name, options = {})
6
+ @property_name = property_name
7
+ @options = options
8
+ end
9
+
10
+ def name
11
+ @property_name.to_s
12
+ end
13
+
14
+ def class_name
15
+ @property_name.to_s.classify
16
+ end
17
+
18
+ def type
19
+ @options[:type]
20
+ end
21
+
22
+ def item_type
23
+ @options.dig(:items, :type)
24
+ end
25
+
26
+ def default
27
+ @options[:default]
28
+ end
29
+
30
+ def title
31
+ @options[:title]
32
+ end
33
+
34
+ def description
35
+ @options[:description]
36
+ end
37
+
38
+ def columns
39
+ []
40
+ end
41
+
42
+ def collection?
43
+ @options[:type] == :array
44
+ end
45
+ end
46
+ end
data/lib/esquema.rb CHANGED
@@ -3,6 +3,12 @@
3
3
  require_relative "esquema/version"
4
4
  require_relative "esquema/configuration"
5
5
  require_relative "esquema/model"
6
+ require_relative "esquema/schema_enhancer"
7
+ require_relative "esquema/keyword_validator"
8
+ require_relative "esquema/builder"
9
+ require_relative "esquema/property"
10
+ require_relative "esquema/type_caster"
11
+ require_relative "esquema/virtual_column"
6
12
 
7
- module Esquema
13
+ module Esquema # rubocop:disable Style/Documentation
8
14
  end
@@ -4,6 +4,7 @@ require "rails/generators"
4
4
 
5
5
  module Esquema
6
6
  module Generators
7
+ # This generator is responsible for installing the Esquema gem.
7
8
  class InstallGenerator < Rails::Generators::Base
8
9
  source_root File.expand_path("templates", __dir__)
9
10
 
data/sorbet/config ADDED
@@ -0,0 +1,4 @@
1
+ --dir
2
+ .
3
+ --ignore=tmp/
4
+ --ignore=vendor/
@@ -0,0 +1 @@
1
+ **/*.rbi linguist-vendored=true
@@ -0,0 +1,89 @@
1
+ # typed: true
2
+
3
+ # DO NOT EDIT MANUALLY
4
+ # This file was pulled from a central RBI files repository.
5
+ # Please run `bin/tapioca annotations` to update it.
6
+
7
+ class ActiveModel::Errors
8
+ Elem = type_member { { fixed: ActiveModel::Error } }
9
+
10
+ sig { params(attribute: T.any(Symbol, String)).returns(T::Array[String]) }
11
+ def [](attribute); end
12
+
13
+ sig { params(attribute: T.any(Symbol, String), type: T.untyped, options: T.untyped).returns(ActiveModel::Error) }
14
+ def add(attribute, type = :invalid, **options); end
15
+
16
+ sig { params(attribute: T.any(Symbol, String), type: T.untyped, options: T.untyped).returns(T::Boolean) }
17
+ def added?(attribute, type = :invalid, options = {}); end
18
+
19
+ sig { params(options: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
20
+ def as_json(options = nil); end
21
+
22
+ sig { returns(T::Array[Symbol]) }
23
+ def attribute_names; end
24
+
25
+ sig { params(attribute: T.any(Symbol, String), type: T.untyped, options: T.untyped).returns(T.nilable(T::Array[String])) }
26
+ def delete(attribute, type = nil, **options); end
27
+
28
+ sig { returns(T::Hash[Symbol, T::Array[T::Hash[Symbol, T.untyped]]]) }
29
+ def details; end
30
+
31
+ sig { returns(T::Array[Elem]) }
32
+ def errors; end
33
+
34
+ sig { params(attribute: T.any(Symbol, String), message: String).returns(String) }
35
+ def full_message(attribute, message); end
36
+
37
+ sig { returns(T::Array[String]) }
38
+ def full_messages; end
39
+
40
+ sig { params(attribute: T.any(Symbol, String)).returns(T::Array[String]) }
41
+ def full_messages_for(attribute); end
42
+
43
+ sig { params(attribute: T.any(Symbol, String), type: T.untyped, options: T.untyped).returns(String) }
44
+ def generate_message(attribute, type = :invalid, options = {}); end
45
+
46
+ sig { returns(T::Hash[Symbol, T::Array[ActiveModel::Error]]) }
47
+ def group_by_attribute; end
48
+
49
+ sig { params(attribute: T.any(Symbol, String)).returns(T::Boolean) }
50
+ def has_key?(attribute); end
51
+
52
+ sig { params(error: ActiveModel::Error, override_options: T.untyped).returns(T::Array[ActiveModel::Error]) }
53
+ def import(error, override_options = {}); end
54
+
55
+ sig { params(attribute: T.any(Symbol, String)).returns(T::Boolean) }
56
+ def include?(attribute); end
57
+
58
+ sig { params(attribute: T.any(Symbol, String)).returns(T::Boolean) }
59
+ def key?(attribute); end
60
+
61
+ sig { params(other: T.untyped).returns(T::Array[ActiveModel::Error]) }
62
+ def merge!(other); end
63
+
64
+ sig { returns(T::Hash[Symbol, T::Array[String]]) }
65
+ def messages; end
66
+
67
+ sig { params(attribute: T.any(Symbol, String)).returns(T::Array[String]) }
68
+ def messages_for(attribute); end
69
+
70
+ sig { returns(T::Array[Elem]) }
71
+ def objects; end
72
+
73
+ sig { params(attribute: T.any(Symbol, String), type: T.untyped).returns(T::Boolean) }
74
+ def of_kind?(attribute, type = :invalid); end
75
+
76
+ sig { returns(T::Array[String]) }
77
+ def to_a; end
78
+
79
+ sig { params(full_messages: T.untyped).returns(T::Hash[Symbol, T::Array[String]]) }
80
+ def to_hash(full_messages = false); end
81
+
82
+ sig { params(attribute: T.any(Symbol, String), type: T.untyped, options: T.untyped).returns(T::Array[ActiveModel::Error]) }
83
+ def where(attribute, type = nil, **options); end
84
+ end
85
+
86
+ module ActiveModel::Validations
87
+ sig { returns(ActiveModel::Errors) }
88
+ def errors; end
89
+ end
@@ -0,0 +1,92 @@
1
+ # typed: true
2
+
3
+ # DO NOT EDIT MANUALLY
4
+ # This file was pulled from a central RBI files repository.
5
+ # Please run `bin/tapioca annotations` to update it.
6
+
7
+ class ActiveRecord::Schema
8
+ sig { params(info: T::Hash[T.untyped, T.untyped], blk: T.proc.bind(ActiveRecord::Schema).void).void }
9
+ def self.define(info = nil, &blk); end
10
+ end
11
+
12
+ class ActiveRecord::Migration
13
+ # @shim: Methods on migration are delegated to `SchemaStatements` using `method_missing`
14
+ include ActiveRecord::ConnectionAdapters::SchemaStatements
15
+
16
+ # @shim: Methods on migration are delegated to `DatabaseStatements` using `method_missing`
17
+ include ActiveRecord::ConnectionAdapters::DatabaseStatements
18
+ end
19
+
20
+ class ActiveRecord::Base
21
+ sig { returns(FalseClass) }
22
+ def blank?; end
23
+
24
+ # @shim: since `present?` is always true, `presence` always returns `self`
25
+ sig { returns(T.self_type) }
26
+ def presence; end
27
+
28
+ sig { returns(TrueClass) }
29
+ def present?; end
30
+
31
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
32
+ def self.after_initialize(*args, **options, &block); end
33
+
34
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
35
+ def self.after_find(*args, **options, &block); end
36
+
37
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
38
+ def self.after_touch(*args, **options, &block); end
39
+
40
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
41
+ def self.before_validation(*args, **options, &block); end
42
+
43
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
44
+ def self.after_validation(*args, **options, &block); end
45
+
46
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
47
+ def self.before_save(*args, **options, &block); end
48
+
49
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
50
+ def self.around_save(*args, **options, &block); end
51
+
52
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
53
+ def self.after_save(*args, **options, &block); end
54
+
55
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
56
+ def self.before_create(*args, **options, &block); end
57
+
58
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
59
+ def self.around_create(*args, **options, &block); end
60
+
61
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
62
+ def self.after_create(*args, **options, &block); end
63
+
64
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
65
+ def self.before_update(*args, **options, &block); end
66
+
67
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
68
+ def self.around_update(*args, **options, &block); end
69
+
70
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
71
+ def self.after_update(*args, **options, &block); end
72
+
73
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
74
+ def self.before_destroy(*args, **options, &block); end
75
+
76
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
77
+ def self.around_destroy(*args, **options, &block); end
78
+
79
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
80
+ def self.after_destroy(*args, **options, &block); end
81
+
82
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
83
+ def self.after_commit(*args, **options, &block); end
84
+
85
+ sig { params(args: T.untyped, options: T.untyped, block: T.nilable(T.proc.bind(T.attached_class).params(record: T.attached_class).void)).void }
86
+ def self.after_rollback(*args, **options, &block); end
87
+ end
88
+
89
+ class ActiveRecord::Relation
90
+ sig { returns(T::Boolean) }
91
+ def blank?; end
92
+ end