openapi_blocks 0.2.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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +86 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +304 -0
  6. data/README.pt-BR.md +495 -0
  7. data/Rakefile +12 -0
  8. data/app/controllers/openapi_blocks/spec_controller.rb +110 -0
  9. data/config/routes.rb +7 -0
  10. data/lib/openapi_blocks/base.rb +52 -0
  11. data/lib/openapi_blocks/builder.rb +23 -0
  12. data/lib/openapi_blocks/cache.rb +28 -0
  13. data/lib/openapi_blocks/configuration/contact_builder.rb +23 -0
  14. data/lib/openapi_blocks/configuration/info_builder.rb +42 -0
  15. data/lib/openapi_blocks/configuration/license_builder.rb +19 -0
  16. data/lib/openapi_blocks/configuration/security_builder.rb +33 -0
  17. data/lib/openapi_blocks/configuration/server_builder.rb +19 -0
  18. data/lib/openapi_blocks/configuration/servers_builder.rb +21 -0
  19. data/lib/openapi_blocks/configuration.rb +55 -0
  20. data/lib/openapi_blocks/engine.rb +13 -0
  21. data/lib/openapi_blocks/file_watcher.rb +42 -0
  22. data/lib/openapi_blocks/middleware.rb +54 -0
  23. data/lib/openapi_blocks/operation_builder.rb +57 -0
  24. data/lib/openapi_blocks/railtie.rb +19 -0
  25. data/lib/openapi_blocks/routing/extractor.rb +187 -0
  26. data/lib/openapi_blocks/routing/operation.rb +45 -0
  27. data/lib/openapi_blocks/schema/extractor.rb +103 -0
  28. data/lib/openapi_blocks/schema/types.rb +52 -0
  29. data/lib/openapi_blocks/schema/validator.rb +86 -0
  30. data/lib/openapi_blocks/spec/components.rb +67 -0
  31. data/lib/openapi_blocks/spec/document.rb +47 -0
  32. data/lib/openapi_blocks/spec/paths.rb +17 -0
  33. data/lib/openapi_blocks/version.rb +5 -0
  34. data/lib/openapi_blocks.rb +35 -0
  35. data/sig/openapi_blocks.rbs +4 -0
  36. metadata +177 -0
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types"
4
+
5
+ module OpenapiBlocks
6
+ module Schema
7
+ class Extractor # rubocop:disable Style/Documentation
8
+ IGNORED_COLUMNS = %w[
9
+ password_digest
10
+ encrypted_password
11
+ reset_password_token
12
+ reset_password_sent_at
13
+ remember_created_at
14
+ confirmation_token
15
+ confirmed_at
16
+ confirmation_sent_at
17
+ unconfirmed_email
18
+ failed_attempts
19
+ unlock_token
20
+ locked_at
21
+ ].freeze
22
+
23
+ def initialize(openapi_class)
24
+ @openapi_class = openapi_class
25
+ @model = openapi_class.model
26
+ @ignored = Array(openapi_class._ignored) + IGNORED_COLUMNS
27
+ end
28
+
29
+ def extract
30
+ properties = {}
31
+
32
+ column_properties.each { |name, schema| properties[name] = schema }
33
+ virtual_properties.each { |name, schema| properties[name] = schema }
34
+ association_properties.each { |name, schema| properties[name] = schema }
35
+
36
+ {
37
+ type: "object",
38
+ required: required_columns,
39
+ properties: properties
40
+ }.compact
41
+ end
42
+
43
+ private
44
+
45
+ def column_properties
46
+ @model.columns.each_with_object({}) do |column, hash|
47
+ next if @ignored.include?(column.name)
48
+
49
+ hash[column.name] = Types.map(column.sql_type_metadata.type)
50
+ end
51
+ end
52
+
53
+ def virtual_properties
54
+ Array(@openapi_class._virtual_attributes).each_with_object({}) do |attr, hash|
55
+ name = attr.delete(:name)
56
+ options = attr
57
+
58
+ hash[name.to_s] = build_virtual_property(options)
59
+ end
60
+ end
61
+
62
+ def association_properties
63
+ Array(@openapi_class._associations).each_with_object({}) do |assoc, hash|
64
+ name = assoc[:name]
65
+ type = assoc[:type]
66
+ input = assoc.fetch(:input, true)
67
+ ref = { "$ref" => "#/components/schemas/#{name.to_s.classify}" }
68
+
69
+ schema = type == :array ? { type: "array", items: ref } : ref
70
+ schema[:readOnly] = true unless input
71
+
72
+ hash[name.to_s] = schema
73
+ end
74
+ end
75
+
76
+ def required_columns # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
77
+ association_names = Array(@openapi_class._associations).map { |a| a[:name].to_s }
78
+
79
+ required = @model.validators.each_with_object([]) do |validator, arr|
80
+ next unless validator.is_a?(ActiveModel::Validations::PresenceValidator)
81
+
82
+ validator.attributes.each do |attr|
83
+ next if @ignored.include?(attr.to_s)
84
+ next if association_names.include?(attr.to_s)
85
+
86
+ arr << attr.to_s
87
+ end
88
+ end
89
+
90
+ required.empty? ? nil : required
91
+ end
92
+
93
+ def build_virtual_property(options)
94
+ property = {}
95
+ property[:type] = options[:type].to_s if options[:type]
96
+ property[:format] = options[:format].to_s if options[:format]
97
+ property[:description] = options[:description] if options[:description]
98
+ property[:readOnly] = options[:read_only] if options[:read_only]
99
+ property
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiBlocks
4
+ module Schema
5
+ module Types # rubocop:disable Style/Documentation
6
+ MAPPING = {
7
+ # Inteiros
8
+ "integer" => { type: "integer", format: "int32" },
9
+ "bigint" => { type: "integer", format: "int64" },
10
+ "smallint" => { type: "integer" },
11
+
12
+ # Decimais
13
+ "float" => { type: "number", format: "float" },
14
+ "decimal" => { type: "number", format: "double" },
15
+
16
+ # Texto
17
+ "string" => { type: "string" },
18
+ "text" => { type: "string" },
19
+ "citext" => { type: "string" },
20
+
21
+ # Booleano
22
+ "boolean" => { type: "boolean" },
23
+
24
+ # Datas e horas
25
+ "date" => { type: "string", format: "date" },
26
+ "datetime" => { type: "string", format: "date-time" },
27
+ "timestamp" => { type: "string", format: "date-time" },
28
+ "time" => { type: "string", format: "time" },
29
+
30
+ # UUID
31
+ "uuid" => { type: "string", format: "uuid" },
32
+
33
+ # JSON
34
+ "json" => { type: "object" },
35
+ "jsonb" => { type: "object" },
36
+
37
+ # Binário
38
+ "binary" => { type: "string", format: "binary" },
39
+
40
+ # Arrays (PostgreSQL)
41
+ "string[]" => { type: "array", items: { type: "string" } },
42
+ "integer[]" => { type: "array", items: { type: "integer" } }
43
+ }.freeze
44
+
45
+ DEFAULT = { type: "string" }.freeze
46
+
47
+ def self.map(ar_type)
48
+ MAPPING.fetch(ar_type.to_s, DEFAULT)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiBlocks
4
+ module Schema
5
+ class Validator # rubocop:disable Style/Documentation
6
+ def initialize(model)
7
+ @model = model
8
+ end
9
+
10
+ def extract
11
+ @model.validators.each_with_object({}) do |validator, hash|
12
+ validator.attributes.each do |attribute|
13
+ hash[attribute.to_s] ||= {}
14
+ hash[attribute.to_s].merge!(convert(validator))
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def convert(validator) # rubocop:disable Metrics/MethodLength
22
+ case validator
23
+ in ActiveModel::Validations::LengthValidator
24
+ convert_length(validator)
25
+ in ActiveModel::Validations::NumericalityValidator
26
+ convert_numericality(validator)
27
+ in ActiveModel::Validations::InclusionValidator
28
+ convert_inclusion(validator)
29
+ in ActiveModel::Validations::FormatValidator
30
+ convert_format(validator)
31
+ else
32
+ {}
33
+ end
34
+ end
35
+
36
+ def convert_length(validator)
37
+ options = validator.options
38
+ result = {}
39
+
40
+ result[:minLength] = options[:minimum] if options[:minimum]
41
+ result[:maxLength] = options[:maximum] if options[:maximum]
42
+ result[:minLength] = options[:is] if options[:is]
43
+ result[:maxLength] = options[:is] if options[:is]
44
+
45
+ result
46
+ end
47
+
48
+ def convert_numericality(validator) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
49
+ options = validator.options
50
+ result = {}
51
+
52
+ result[:minimum] = options[:greater_than] + 1 if options[:greater_than]
53
+ result[:minimum] = options[:greater_than_or_equal_to] if options[:greater_than_or_equal_to]
54
+ result[:maximum] = options[:less_than] - 1 if options[:less_than]
55
+ result[:maximum] = options[:less_than_or_equal_to] if options[:less_than_or_equal_to]
56
+ result[:multipleOf] = options[:other_than] if options[:other_than]
57
+ result[:exclusiveMinimum] = options[:greater_than] if options[:greater_than]
58
+ result[:exclusiveMaximum] = options[:less_than] if options[:less_than]
59
+
60
+ result
61
+ end
62
+
63
+ def convert_inclusion(validator)
64
+ options = validator.options
65
+ return {} unless options[:in]
66
+
67
+ { enum: Array(options[:in]) }
68
+ end
69
+
70
+ def convert_format(validator)
71
+ options = validator.options
72
+ return {} unless options[:with]
73
+
74
+ regexp = options[:with]
75
+
76
+ if regexp.source == URI::MailTo::EMAIL_REGEXP.source
77
+ { format: "email" }
78
+ elsif regexp.source =~ /url|uri/i
79
+ { format: "uri" }
80
+ else
81
+ { pattern: regexp.source }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../schema/extractor"
4
+ require_relative "../schema/validator"
5
+
6
+ module OpenapiBlocks
7
+ module Spec
8
+ class Components # rubocop:disable Style/Documentation
9
+ INPUT_IGNORED_PROPERTIES = %w[id created_at updated_at deleted_at].freeze
10
+
11
+ def initialize(openapi_classes)
12
+ @openapi_classes = openapi_classes
13
+ end
14
+
15
+ def build # rubocop:disable Metrics/AbcSize
16
+ schemas = @openapi_classes.each_with_object({}) do |klass, hash|
17
+ schema_name = klass.model.name
18
+ extractor = Schema::Extractor.new(klass)
19
+ validator = Schema::Validator.new(klass.model)
20
+
21
+ schema = extractor.extract
22
+ schema[:properties] = merge_validations(schema[:properties], validator.extract)
23
+
24
+ hash[schema_name] = schema
25
+ hash["#{schema_name}Input"] = build_input(schema, klass)
26
+ end
27
+
28
+ { schemas: schemas }
29
+ end
30
+
31
+ private
32
+
33
+ def build_input(schema, openapi_class) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
34
+ read_only_virtuals = Array(openapi_class._virtual_attributes)
35
+ .select { |attr| attr[:read_only] == true }
36
+ .map { |attr| attr[:name].to_s }
37
+
38
+ input_properties = schema[:properties].reject do |name, property|
39
+ INPUT_IGNORED_PROPERTIES.include?(name.to_s) ||
40
+ read_only_virtuals.include?(name.to_s) ||
41
+ property[:readOnly] == true
42
+ end
43
+
44
+ {
45
+ type: "object",
46
+ required: filter_required(schema[:required], input_properties),
47
+ properties: input_properties
48
+ }.compact
49
+ end
50
+
51
+ def filter_required(required, input_properties)
52
+ return nil if required.blank?
53
+
54
+ filtered = required.select { |r| input_properties.key?(r) || input_properties.key?(r.to_sym) }
55
+ filtered.empty? ? nil : filtered
56
+ end
57
+
58
+ def merge_validations(properties, validations)
59
+ return properties if properties.blank?
60
+
61
+ properties.each_with_object({}) do |(name, schema), hash|
62
+ hash[name] = schema.merge(validations.fetch(name, {}))
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "components"
4
+ require_relative "paths"
5
+
6
+ module OpenapiBlocks
7
+ module Spec
8
+ class Document # rubocop:disable Style/Documentation
9
+ def initialize(openapi_classes)
10
+ @openapi_classes = openapi_classes
11
+ end
12
+
13
+ def build # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
14
+ config = OpenapiBlocks.configuration
15
+ components = Components.new(@openapi_classes).build
16
+ security = config.security
17
+
18
+ components[:securitySchemes] = security.to_h if security&.schemes&.any?
19
+
20
+ paths = Paths.new.build
21
+
22
+ doc = {
23
+ openapi: config.openapi_version,
24
+ info: config.info.to_h,
25
+ servers: config.to_h[:servers],
26
+ paths: paths,
27
+ components: components,
28
+ tags: build_tags_from_paths(paths)
29
+ }
30
+
31
+ doc[:security] = security.schemes.keys.map { |s| { s => [] } } if security&.schemes&.any?
32
+
33
+ doc
34
+ end
35
+
36
+ private
37
+
38
+ def build_tags_from_paths(paths)
39
+ names = paths.values.flat_map do |operations|
40
+ operations.values.flat_map { |op| Array(op[:tags]) }
41
+ end
42
+
43
+ names.uniq.map { |n| { name: n } }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../routing/extractor"
4
+
5
+ module OpenapiBlocks
6
+ module Spec
7
+ class Paths # rubocop:disable Style/Documentation
8
+ def initialize(app = Rails.application)
9
+ @app = app
10
+ end
11
+
12
+ def build
13
+ Routing::Extractor.new(@app).extract
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiBlocks
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require "active_record"
5
+
6
+ require_relative "openapi_blocks/version"
7
+ require_relative "openapi_blocks/configuration"
8
+ require_relative "openapi_blocks/cache"
9
+ require_relative "openapi_blocks/file_watcher"
10
+ require_relative "openapi_blocks/middleware"
11
+ require_relative "openapi_blocks/schema/types"
12
+ require_relative "openapi_blocks/schema/extractor"
13
+ require_relative "openapi_blocks/schema/validator"
14
+ require_relative "openapi_blocks/routing/operation"
15
+ require_relative "openapi_blocks/routing/extractor"
16
+ require_relative "openapi_blocks/spec/components"
17
+ require_relative "openapi_blocks/spec/paths"
18
+ require_relative "openapi_blocks/spec/document"
19
+ require_relative "openapi_blocks/builder"
20
+ require_relative "openapi_blocks/base"
21
+ require_relative "openapi_blocks/engine"
22
+ require_relative "openapi_blocks/railtie"
23
+ require_relative "openapi_blocks/operation_builder"
24
+
25
+ module OpenapiBlocks # rubocop:disable Style/Documentation
26
+ class Error < StandardError; end
27
+
28
+ def self.configuration
29
+ @configuration ||= Configuration.new
30
+ end
31
+
32
+ def self.configure
33
+ yield configuration
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ module OpenapiBlocks
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openapi_blocks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Caio Santos
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: actionpack
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activemodel
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: activerecord
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '7.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '7.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: railties
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '7.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '7.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec-rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '6.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '6.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: sqlite3
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.1'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.1'
110
+ description: Generates OpenAPI specs automatically from ActiveRecord models, ActiveModel
111
+ validations and Rails routes, inspired by ActiveModel::Serializer.
112
+ email:
113
+ - caio.francelinosena@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - CHANGELOG.md
119
+ - CODE_OF_CONDUCT.md
120
+ - LICENSE.txt
121
+ - README.md
122
+ - README.pt-BR.md
123
+ - Rakefile
124
+ - app/controllers/openapi_blocks/spec_controller.rb
125
+ - config/routes.rb
126
+ - lib/openapi_blocks.rb
127
+ - lib/openapi_blocks/base.rb
128
+ - lib/openapi_blocks/builder.rb
129
+ - lib/openapi_blocks/cache.rb
130
+ - lib/openapi_blocks/configuration.rb
131
+ - lib/openapi_blocks/configuration/contact_builder.rb
132
+ - lib/openapi_blocks/configuration/info_builder.rb
133
+ - lib/openapi_blocks/configuration/license_builder.rb
134
+ - lib/openapi_blocks/configuration/security_builder.rb
135
+ - lib/openapi_blocks/configuration/server_builder.rb
136
+ - lib/openapi_blocks/configuration/servers_builder.rb
137
+ - lib/openapi_blocks/engine.rb
138
+ - lib/openapi_blocks/file_watcher.rb
139
+ - lib/openapi_blocks/middleware.rb
140
+ - lib/openapi_blocks/operation_builder.rb
141
+ - lib/openapi_blocks/railtie.rb
142
+ - lib/openapi_blocks/routing/extractor.rb
143
+ - lib/openapi_blocks/routing/operation.rb
144
+ - lib/openapi_blocks/schema/extractor.rb
145
+ - lib/openapi_blocks/schema/types.rb
146
+ - lib/openapi_blocks/schema/validator.rb
147
+ - lib/openapi_blocks/spec/components.rb
148
+ - lib/openapi_blocks/spec/document.rb
149
+ - lib/openapi_blocks/spec/paths.rb
150
+ - lib/openapi_blocks/version.rb
151
+ - sig/openapi_blocks.rbs
152
+ homepage: https://github.com/evotechbuilder/openapi_blocks
153
+ licenses:
154
+ - MIT
155
+ metadata:
156
+ homepage_uri: https://github.com/evotechbuilder/openapi_blocks
157
+ source_code_uri: https://github.com/evotechbuilder/openapi_blocks
158
+ changelog_uri: https://github.com/evotechbuilder/openapi_blocks/blob/main/CHANGELOG.md
159
+ rubygems_mfa_required: 'true'
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: 3.2.0
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubygems_version: 4.0.9
175
+ specification_version: 4
176
+ summary: DSL to generate OpenAPI 3.0/3.1 documentation for Rails applications
177
+ test_files: []