oas_core 0.0.1

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +70 -0
  4. data/Rakefile +12 -0
  5. data/lib/oas_core/builders/content_builder.rb +44 -0
  6. data/lib/oas_core/builders/operation_builder.rb +94 -0
  7. data/lib/oas_core/builders/parameter_builder.rb +31 -0
  8. data/lib/oas_core/builders/parameters_builder.rb +46 -0
  9. data/lib/oas_core/builders/path_item_builder.rb +26 -0
  10. data/lib/oas_core/builders/request_body_builder.rb +45 -0
  11. data/lib/oas_core/builders/response_builder.rb +42 -0
  12. data/lib/oas_core/builders/responses_builder.rb +92 -0
  13. data/lib/oas_core/builders/specification_builder.rb +24 -0
  14. data/lib/oas_core/configuration.rb +143 -0
  15. data/lib/oas_core/json_schema_generator.rb +134 -0
  16. data/lib/oas_core/oas_route.rb +24 -0
  17. data/lib/oas_core/spec/components.rb +99 -0
  18. data/lib/oas_core/spec/contact.rb +20 -0
  19. data/lib/oas_core/spec/hashable.rb +41 -0
  20. data/lib/oas_core/spec/info.rb +68 -0
  21. data/lib/oas_core/spec/license.rb +20 -0
  22. data/lib/oas_core/spec/media_type.rb +26 -0
  23. data/lib/oas_core/spec/operation.rb +28 -0
  24. data/lib/oas_core/spec/parameter.rb +39 -0
  25. data/lib/oas_core/spec/path_item.rb +27 -0
  26. data/lib/oas_core/spec/paths.rb +28 -0
  27. data/lib/oas_core/spec/reference.rb +18 -0
  28. data/lib/oas_core/spec/request_body.rb +23 -0
  29. data/lib/oas_core/spec/response.rb +22 -0
  30. data/lib/oas_core/spec/responses.rb +27 -0
  31. data/lib/oas_core/spec/server.rb +19 -0
  32. data/lib/oas_core/spec/specable.rb +56 -0
  33. data/lib/oas_core/spec/specification.rb +34 -0
  34. data/lib/oas_core/spec/tag.rb +20 -0
  35. data/lib/oas_core/string.rb +13 -0
  36. data/lib/oas_core/utils.rb +94 -0
  37. data/lib/oas_core/version.rb +5 -0
  38. data/lib/oas_core/yard/example_tag.rb +14 -0
  39. data/lib/oas_core/yard/oas_core_factory.rb +166 -0
  40. data/lib/oas_core/yard/parameter_tag.rb +16 -0
  41. data/lib/oas_core/yard/request_body_example_tag.rb +13 -0
  42. data/lib/oas_core/yard/request_body_tag.rb +17 -0
  43. data/lib/oas_core/yard/response_example_tag.rb +14 -0
  44. data/lib/oas_core/yard/response_tag.rb +15 -0
  45. data/lib/oas_core.rb +88 -0
  46. metadata +120 -0
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module OasCore
6
+ # The JsonSchemaGenerator module provides methods to transform string representations
7
+ # of data types into JSON schema formats.
8
+ module JsonSchemaGenerator
9
+ # Processes a string representing a data type and converts it into a JSON schema.
10
+ #
11
+ # @param str [String] The string representation of a data type.
12
+ # @return [Hash] A hash containing the required flag and the JSON schema.
13
+ def self.process_string(str)
14
+ parsed = parse_type(str)
15
+ {
16
+ required: parsed[:required],
17
+ json_schema: to_json_schema(parsed)
18
+ }
19
+ end
20
+
21
+ # Parses a string representing a data type and determines its JSON schema type.
22
+ #
23
+ # @param str [String] The string representation of a data type.
24
+ # @return [Hash] A hash containing the type, whether it's required, and any additional properties.
25
+ def self.parse_type(str)
26
+ required = str.start_with?('!')
27
+ type = str.sub(/^!/, '').strip
28
+
29
+ case type
30
+ when /^Hash\{(.+)\}$/i
31
+ { type: :object, required:, properties: parse_object_properties(::Regexp.last_match(1)) }
32
+ when /^Array<(.+)>$/i
33
+ { type: :array, required:, items: parse_type(::Regexp.last_match(1)) }
34
+ else
35
+ { type: type.downcase.to_sym, required: }
36
+ end
37
+ end
38
+
39
+ # Parses the properties of an object type from a string.
40
+ #
41
+ # @param str [String] The string representation of the object's properties.
42
+ # @return [Hash] A hash where keys are property names and values are their JSON schema types.
43
+ def self.parse_object_properties(str)
44
+ properties = {}
45
+ stack = []
46
+ current_key = ''
47
+ current_value = ''
48
+
49
+ str.each_char.with_index do |char, index|
50
+ case char
51
+ when '{', '<'
52
+ stack.push(char)
53
+ current_value += char
54
+ when '}', '>'
55
+ stack.pop
56
+ current_value += char
57
+ when ','
58
+ if stack.empty?
59
+ properties[current_key.strip.to_sym] = parse_type(current_value.strip)
60
+ current_key = ''
61
+ current_value = ''
62
+ else
63
+ current_value += char
64
+ end
65
+ when ':'
66
+ if stack.empty?
67
+ current_key = current_value
68
+ current_value = ''
69
+ else
70
+ current_value += char
71
+ end
72
+ else
73
+ current_value += char
74
+ end
75
+
76
+ if index == str.length - 1 && !current_key.empty?
77
+ properties[current_key.strip.to_sym] =
78
+ parse_type(current_value.strip)
79
+ end
80
+ end
81
+
82
+ properties
83
+ end
84
+
85
+ # Converts a parsed data type into a JSON schema format.
86
+ #
87
+ # @param parsed [Hash] The parsed data type hash.
88
+ # @return [Hash] The JSON schema representation of the parsed data type.
89
+ def self.to_json_schema(parsed)
90
+ case parsed[:type]
91
+ when :object
92
+ schema = {
93
+ type: 'object',
94
+ properties: {}
95
+ }
96
+ required_props = []
97
+ parsed[:properties].each do |key, value|
98
+ schema[:properties][key] = to_json_schema(value)
99
+ required_props << key.to_s if value[:required]
100
+ end
101
+ schema[:required] = required_props unless required_props.empty?
102
+ schema
103
+ when :array
104
+ {
105
+ type: 'array',
106
+ items: to_json_schema(parsed[:items])
107
+ }
108
+ when nil
109
+ parsed
110
+ else
111
+ ruby_type_to_json_schema_type(parsed[:type])
112
+ end
113
+ end
114
+
115
+ # Converts a Ruby data type into its corresponding JSON schema type.
116
+ #
117
+ # @param type [Symbol, String] The Ruby data type.
118
+ # @return [Hash, String] The JSON schema type or a hash with additional format information.
119
+ def self.ruby_type_to_json_schema_type(type)
120
+ case type.to_s.downcase
121
+ when 'string' then { type: 'string' }
122
+ when 'integer' then { type: 'integer' }
123
+ when 'float' then { type: 'float' }
124
+ when 'boolean' then { type: 'boolean' }
125
+ when 'array' then { type: 'array' }
126
+ when 'hash' then { type: 'hash' }
127
+ when 'nil' then { type: 'null' }
128
+ when 'date' then { type: 'string', format: 'date' }
129
+ when 'datetime' then { type: 'string', format: 'date-time' }
130
+ else type.to_s.downcase
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ class OasRoute
5
+ # TODO: Check what variables are in use and remove the ones that are not.
6
+ attr_accessor :controller_class, :controller_action, :controller, :controller_path, :method_name, :verb, :path,
7
+ :docstring, :source_string
8
+ attr_writer :tags
9
+
10
+ def initialize(attributes = {})
11
+ attributes.each { |key, value| send("#{key}=", value) }
12
+ end
13
+
14
+ def path_params
15
+ @path.to_s.scan(/:(\w+)/).flatten.reject! { |e| e == 'format' }
16
+ end
17
+
18
+ def tags(name = nil)
19
+ return @tags if name.nil?
20
+
21
+ @tags.select { |tag| tag.tag_name.to_s == name.to_s }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Components
6
+ include Specable
7
+
8
+ attr_accessor :schemas, :parameters, :security_schemes, :request_bodies, :responses, :headers, :examples, :links,
9
+ :callbacks
10
+
11
+ def initialize(specification)
12
+ @specification = specification
13
+ @schemas = {}
14
+ @parameters = {}
15
+ @security_schemes = OasCore.config.security_schemas
16
+ @request_bodies = {}
17
+ @responses = {}
18
+ @headers = {}
19
+ @examples = {}
20
+ @links = {}
21
+ @callbacks = {}
22
+ end
23
+
24
+ def oas_fields
25
+ %i[request_bodies examples responses schemas parameters security_schemes]
26
+ end
27
+
28
+ def add_response(response)
29
+ key = response.hash_key
30
+ @responses[key] = response unless @responses.key? key
31
+
32
+ response_reference(key)
33
+ end
34
+
35
+ def add_parameter(parameter)
36
+ key = parameter.hash_key
37
+ @parameters[key] = parameter unless @parameters.key? key
38
+
39
+ parameter_reference(key)
40
+ end
41
+
42
+ def add_request_body(request_body)
43
+ key = request_body.hash_key
44
+ @request_bodies[key] = request_body unless @request_bodies.key? key
45
+
46
+ request_body_reference(key)
47
+ end
48
+
49
+ def add_schema(schema)
50
+ key = nil
51
+ if OasCore.config.use_model_names
52
+ if schema[:type] == 'array'
53
+ arr_schema = schema[:items]
54
+ arr_key = arr_schema['title']
55
+ key = "#{arr_key}List" unless arr_key.nil?
56
+ else
57
+ key = schema['title']
58
+ end
59
+ end
60
+
61
+ key = Hashable.generate_hash(schema) if key.nil?
62
+
63
+ @schemas[key] = schema if @schemas[key].nil?
64
+ schema_reference(key)
65
+ end
66
+
67
+ def add_example(example)
68
+ key = Hashable.generate_hash(example)
69
+ @examples[key] = example if @examples[key].nil?
70
+
71
+ example_reference(key)
72
+ end
73
+
74
+ def create_reference(type, name)
75
+ "#/components/#{type}/#{name}"
76
+ end
77
+
78
+ def schema_reference(name)
79
+ Reference.new(create_reference('schemas', name))
80
+ end
81
+
82
+ def response_reference(name)
83
+ Reference.new(create_reference('responses', name))
84
+ end
85
+
86
+ def parameter_reference(name)
87
+ Reference.new(create_reference('parameters', name))
88
+ end
89
+
90
+ def example_reference(name)
91
+ Reference.new(create_reference('examples', name))
92
+ end
93
+
94
+ def request_body_reference(name)
95
+ Reference.new(create_reference('requestBodies', name))
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Contact
6
+ include Specable
7
+ attr_accessor :name, :url, :email
8
+
9
+ def initialize(**kwargs)
10
+ @name = kwargs[:name] || ''
11
+ @url = kwargs[:url] || ''
12
+ @email = kwargs[:email] || ''
13
+ end
14
+
15
+ def oas_fields
16
+ %i[name url email]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ require 'digest'
6
+
7
+ module Hashable
8
+ def hash_key
9
+ Hashable.generate_hash(hash_representation)
10
+ end
11
+
12
+ def hash_representation
13
+ public_instance_variables.sort.to_h { |var| [var, instance_variable_get(var)] }
14
+ end
15
+
16
+ def self.generate_hash(obj)
17
+ Digest::MD5.hexdigest(hash_representation_recursive(obj).to_s)
18
+ end
19
+
20
+ def public_instance_variables
21
+ instance_variables.select do |var|
22
+ method_name = var.to_s.delete('@')
23
+ respond_to?(method_name) || respond_to?("#{method_name}=")
24
+ end
25
+ end
26
+
27
+ def self.hash_representation_recursive(obj)
28
+ case obj
29
+ when Hash
30
+ obj.transform_values { |v| hash_representation_recursive(v) }
31
+ when Array
32
+ obj.map { |v| hash_representation_recursive(v) }
33
+ when Hashable
34
+ obj.hash_representation
35
+ else
36
+ obj
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Info
6
+ include Specable
7
+ attr_accessor :title, :summary, :description, :terms_of_service, :contact, :license, :version
8
+
9
+ def initialize(**kwargs)
10
+ @title = kwargs[:title] || default_title
11
+ @summary = kwargs[:summary] || default_summary
12
+ @description = kwargs[:description] || default_description
13
+ @terms_of_service = kwargs[:terms_of_service] || ''
14
+ @contact = Spec::Contact.new
15
+ @license = Spec::License.new
16
+ @version = kwargs[:version] || '0.0.1'
17
+ end
18
+
19
+ def oas_fields
20
+ %i[title summary description terms_of_service contact license version]
21
+ end
22
+
23
+ def default_title
24
+ "OasCore #{VERSION}"
25
+ end
26
+
27
+ def default_summary
28
+ 'OasCore: Automatic Interactive API Documentation for Rails'
29
+ end
30
+
31
+ def default_description
32
+ "# Welcome to OasCore
33
+
34
+ OasCore automatically generates interactive documentation for your Rails APIs using the OpenAPI Specification 3.1 (OAS 3.1) and displays it with a nice UI.
35
+
36
+ ## Getting Started
37
+
38
+ You've successfully mounted the OasCore engine. This default documentation is based on your routes and automatically gathered information.
39
+
40
+ ## Enhancing Your Documentation
41
+
42
+ To customize and enrich your API documentation:
43
+
44
+ 1. Generate an initializer file:
45
+
46
+ ```
47
+ rails generate oas_core:config
48
+ ```
49
+ 2. Edit the created `config/initializers/oas_core.rb` file to override default settings and add project-specific information.
50
+
51
+ 3. Use Yard tags in your controller methods to provide detailed API endpoint descriptions.
52
+
53
+ ## Features
54
+
55
+ - Automatic OAS 3.1 document generation
56
+ - [RapiDoc](https://github.com/rapi-doc/RapiDoc) integration for interactive exploration
57
+ - Minimal setup required for basic documentation
58
+ - Extensible through configuration and Yard tags
59
+
60
+ Explore your API documentation and enjoy the power of OasCore!
61
+
62
+ For more information and advanced usage, visit the [OasCore GitHub repository](https://github.com/a-chacon/oas_core).
63
+
64
+ "
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class License
6
+ include Specable
7
+
8
+ attr_accessor :name, :url
9
+
10
+ def initialize(**kwargs)
11
+ @name = kwargs[:name] || 'GPL 3.0'
12
+ @url = kwargs[:url] || 'https://www.gnu.org/licenses/gpl-3.0.html#license-text'
13
+ end
14
+
15
+ def oas_fields
16
+ %i[name url]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class MediaType
6
+ include Specable
7
+
8
+ attr_accessor :schema, :example, :examples, :encoding
9
+
10
+ # Initializes a new MediaType object.
11
+ #
12
+ # @param schema [Hash] the schema of the media type.
13
+ # @param kwargs [Hash] additional keyword arguments.
14
+ def initialize(specification)
15
+ @specification = specification
16
+ @schema = {}
17
+ @example = {}
18
+ @examples = {}
19
+ end
20
+
21
+ def oas_fields
22
+ %i[schema example examples encoding]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Operation
6
+ include Specable
7
+
8
+ attr_accessor :specification, :tags, :summary, :description, :operation_id, :parameters, :request_body,
9
+ :responses, :security
10
+
11
+ def initialize(specification)
12
+ @specification = specification
13
+ @summary = ''
14
+ @operation_id = ''
15
+ @tags = []
16
+ @description = @summary
17
+ @parameters = []
18
+ @request_body = {}
19
+ @responses = Spec::Responses.new(specification)
20
+ @security = []
21
+ end
22
+
23
+ def oas_fields
24
+ %i[tags summary description operation_id parameters request_body responses security]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Parameter
6
+ include Specable
7
+ include Hashable
8
+
9
+ STYLE_DEFAULTS = { query: 'form', path: 'simple', header: 'simple', cookie: 'form' }.freeze
10
+
11
+ attr_accessor :name, :style, :description, :required, :schema
12
+ attr_reader :in
13
+
14
+ def initialize(specification)
15
+ @specification = specification
16
+ @name = ''
17
+ @in = ''
18
+ @description = ''
19
+ @required = false
20
+ @style = ''
21
+ @schema = { type: 'string' }
22
+ end
23
+
24
+ def in=(value)
25
+ @in = value
26
+ @style = STYLE_DEFAULTS[@in.to_sym]
27
+ @required = true if value == 'path'
28
+ end
29
+
30
+ def required?
31
+ @in == 'path'
32
+ end
33
+
34
+ def oas_fields
35
+ %i[name in description required schema style]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class PathItem
6
+ include Specable
7
+ attr_reader :get, :post, :put, :patch, :delete
8
+
9
+ def initialize(specification)
10
+ @specification = specification
11
+ @get = nil
12
+ @post = nil
13
+ @put = nil
14
+ @patch = nil
15
+ @delete = nil
16
+ end
17
+
18
+ def add_operation(http_method, operation)
19
+ instance_variable_set("@#{http_method}", operation)
20
+ end
21
+
22
+ def oas_fields
23
+ OasCore.config.http_verbs
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Paths
6
+ include Specable
7
+
8
+ attr_accessor :path_items
9
+
10
+ def initialize(specification)
11
+ @specification = specification
12
+ @path_items = {}
13
+ end
14
+
15
+ def add_path(path, oas_routes)
16
+ @path_items[path] = Builders::PathItemBuilder.new(@specification).with_oas_routes(oas_routes).build
17
+ end
18
+
19
+ def to_spec
20
+ paths_hash = {}
21
+ @path_items.each do |path, path_object|
22
+ paths_hash[path] = path_object.to_spec
23
+ end
24
+ paths_hash
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Reference
6
+ include Specable
7
+ attr_reader :ref
8
+
9
+ def initialize(ref)
10
+ @ref = ref
11
+ end
12
+
13
+ def to_spec
14
+ { '$ref' => @ref }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class RequestBody
6
+ include Specable
7
+ include Hashable
8
+
9
+ attr_accessor :description, :content, :required
10
+
11
+ def initialize(specification)
12
+ @specification = specification
13
+ @description = ''
14
+ @content = {} # a hash with media type objects
15
+ @required = false
16
+ end
17
+
18
+ def oas_fields
19
+ %i[description content required]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Response
6
+ include Specable
7
+ include Hashable
8
+
9
+ attr_accessor :code, :description, :content
10
+
11
+ def initialize(specification)
12
+ @specification = specification
13
+ @description = ''
14
+ @content = {} # Hash with {content: MediaType}
15
+ end
16
+
17
+ def oas_fields
18
+ %i[description content]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Responses
6
+ include Specable
7
+ attr_accessor :responses
8
+
9
+ def initialize(specification)
10
+ @specification = specification
11
+ @responses = {}
12
+ end
13
+
14
+ def add_response(response)
15
+ @responses[response.code] = @specification.components.add_response(response)
16
+ end
17
+
18
+ def to_spec
19
+ spec = {}
20
+ @responses.each do |key, value|
21
+ spec[key] = value.to_spec
22
+ end
23
+ spec
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OasCore
4
+ module Spec
5
+ class Server
6
+ include Specable
7
+ attr_accessor :url, :description
8
+
9
+ def initialize(url:, description:)
10
+ @url = url
11
+ @description = description
12
+ end
13
+
14
+ def oas_fields
15
+ %i[url description]
16
+ end
17
+ end
18
+ end
19
+ end