rails_routes_to_openapi 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fded852e3d8520a0ca3e59b04afa46d637185933027c8d960872f29c28c3bc6d
4
+ data.tar.gz: 8e3e5578b948e57a89a6ae82be630240fa55a7a35efc92a855013297bfa6e244
5
+ SHA512:
6
+ metadata.gz: f00dfdbe5d465f5069718d5c10f281ce8cdde15afae559101e2739437e3a7b1038f83c56e0b538cdeb18008ea674d2e43e1d6efe904835787a84432e27d662c4
7
+ data.tar.gz: f86f5e5a6a07647fcb3e7af9315f74595be110e4019791d1591eec865b0434e8de84673780af97e795a67addb60177c966dc56040a9d427e09a6aa5d397f3723
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Converting a Rails Routes Output into a Postman Collection
2
+
3
+ In an effort to learn a little bit about Ruby and Rails, I decided a fun project would be to attempt generating a YAML file that aligns with the OpenAPI v3.1.0 specification. This file can then be imported into Postman so you can test Rails routes without manually using the Rails console or the consuming frontend application.
4
+
5
+ ## Quick Start
6
+
7
+ 1. Add the gem to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'rails_routes_to_openapi'
11
+ ```
12
+
13
+ 2. Install dependencies:
14
+
15
+ ```shell
16
+ bundle install
17
+ ```
18
+
19
+ 3. Run the conversion task:
20
+
21
+ ```shell
22
+ bundle exec rake routes_to_openapi:convert
23
+ ```
24
+
25
+ This task will internally run `rails routes` on your Rails application and generate an OpenAPI YAML file named similar to `openapi_v3.1_YYYYMMDDHHMMSS.yml` in the project root.
26
+
27
+ > [!WARNING] > **Tip:** Ensure you run the task within a Rails project context so that it can properly pull the routes.
28
+
29
+ ## Workflow Overview
30
+
31
+ 1. **Routes Processing**
32
+ The conversion workflow starts by reading the output of `rails routes`, splitting it into lines, and filtering out header information. A helper module parses each line into a structured `RouteInfo` object that captures the HTTP verb, URI pattern, and the associated controller action.
33
+
34
+ 2. **OpenAPI Generation**
35
+ The parsed routes are then converted into an OpenAPI compliant hash. The conversion process does the following:
36
+
37
+ - It adapts URL parameters (e.g., turning `/a/b/:param/d` into `/a/b/{param}`).
38
+ - For non-GET verbs, it attaches a default request body schema.
39
+ - Finally, it merges the above details into an OpenAPI compliant hash which is then dumped into YAML format.
40
+
41
+ 3. **Output**
42
+ The generated YAML file contains:
43
+ - OpenAPI version information.
44
+ - Descriptive metadata (title, description, version, etc.).
45
+ - A list of servers.
46
+ - A paths list that maps each route to its HTTP verb and summary (derived from the controller action).
47
+
48
+ ## Example OpenAPI YAML Output
49
+
50
+ Below is an abbreviated snippet of what the output might look like:
51
+
52
+ ```yaml
53
+ openapi: 3.1.0
54
+ info:
55
+ title: Rails Routes to OpenAPI
56
+ description: Generate an OpenAPI v3.1 YAML file for Postman import
57
+ version: "1.1.0"
58
+ servers:
59
+ - url: "https://api.example.com"
60
+ description: "Base URL for the API"
61
+ paths:
62
+ "/":
63
+ get:
64
+ summary: "home#show"
65
+ "/path/to/authorize/native":
66
+ get:
67
+ summary: "doorkeeper/authorizations#show"
68
+ "/path/to/authorize":
69
+ get:
70
+ summary: "doorkeeper/authorizations#new"
71
+ delete:
72
+ summary: "doorkeeper/authorizations#destroy"
73
+ requestBody:
74
+ required: true
75
+ content:
76
+ "application/json":
77
+ schema:
78
+ type: object
79
+ properties:
80
+ id:
81
+ type: string
82
+ format: string
83
+ ```
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require "rake"
2
+ require_relative "lib/rails_routes_to_openapi"
3
+
4
+ namespace :routes_to_openapi do
5
+ desc "Convert Rails routes to OpenAPI YAML"
6
+ task :convert do
7
+ routes_output = IO.popen("rails routes") { |io| io.read }
8
+
9
+ # Check if the output contains expected routes information
10
+ if routes_output.include?("Usage:") || routes_output.strip.empty?
11
+ puts "Failed to retrieve routes. Please ensure you are running this task within a Rails project."
12
+ exit 1
13
+ end
14
+
15
+ begin
16
+ RailsRoutesToOpenAPI.convert(routes_output)
17
+ puts 'OpenAPI v3 YAML file generated successfully.'
18
+ rescue => e
19
+ puts "Failed to generate OpenAPI YAML file."
20
+ puts e.message
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ require "dry-struct"
2
+ require_relative "../types/index"
3
+
4
+ module Structs
5
+ module OpenAPI
6
+ class Base < Dry::Struct
7
+ attribute :openapi, Types::String
8
+ attribute :info, Types::Hash.schema(
9
+ title: Types::Strict::String,
10
+ description: Types::Strict::String,
11
+ version: Types::String
12
+ )
13
+ attribute :servers, Types::Array.of(
14
+ Types::Hash.schema(
15
+ url: Types::Strict::String,
16
+ description: Types::Strict::String
17
+ )
18
+ )
19
+ attribute :paths, Types::Strict::Hash
20
+ end
21
+
22
+ class VerbWithRequestBody < Dry::Struct
23
+ attribute :summary, Types::Strict::String
24
+ attribute :requestBody, Types::Hash.schema(
25
+ required: Types::Strict::Bool,
26
+ content: Types::Hash.schema(
27
+ "application/json": Types::Hash.schema(
28
+ schema: Types::Hash.schema(
29
+ type: Types::Strict::String,
30
+ properties: Types::Hash.schema(
31
+ id: Types::Hash.schema(
32
+ type: Types::Strict::String,
33
+ format: Types::Strict::String
34
+ )
35
+ )
36
+ )
37
+ )
38
+ )
39
+ )
40
+ end
41
+
42
+ class Verb < Dry::Struct
43
+ attribute :summary, Types::Strict::String
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ require "dry-struct"
2
+ require_relative "../types/index"
3
+
4
+ module Structs
5
+ class RouteInfo < Dry::Struct
6
+ attribute :verb, Types::Strict::String
7
+ attribute :uri_pattern, Types::Strict::String
8
+ attribute :controller_action, Types::Strict::String
9
+
10
+ attr_writer :uri_pattern
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Types
2
+ include Dry.Types()
3
+ end
@@ -0,0 +1,46 @@
1
+ require_relative "../structs/rails_route_info"
2
+ module Util
3
+ class FileHelpers
4
+ class << self
5
+ # Takes routes output and returns RouteInfo based on each line
6
+ # for REST verb, uri_pattern and associated controller_action
7
+ def parse(routes_output)
8
+ split_file_data = split(routes_output)
9
+ filtered_file_data = filter_header(split_file_data)
10
+ create_route_info(filtered_file_data)
11
+ end
12
+
13
+ # Returns string array for each line
14
+ def split(file_content)
15
+ file_content.split(/\n/)
16
+ rescue
17
+ raise "Failed to split file content"
18
+ end
19
+
20
+ # Filter out the header line
21
+ def filter_header(file_content_array)
22
+ # Assuming the first line is the header
23
+ file_content_array.drop(1)
24
+ rescue
25
+ raise "Failed to filter header"
26
+ end
27
+
28
+ # Accepts string array from file line
29
+ def create_route_info(file_data)
30
+ # Use regex to split each line into prefix, verb, uri_pattern, and controller_action
31
+ file_data.flat_map { |line|
32
+ next if line.empty?
33
+ match = line.match(/^\s*(\S*)\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)?\s+(\S+)\s+(.+)\s*$/)
34
+ if match
35
+ prefix, verb, uri_pattern, controller_action = match.captures
36
+ # Split the verb by '|' if it exists, otherwise default to 'GET'
37
+ verbs = verb&.split('|') || ['GET']
38
+ verbs.map { |v| Structs::RouteInfo.new(verb: v, uri_pattern: uri_pattern, controller_action: controller_action) }
39
+ end
40
+ }.compact
41
+ rescue
42
+ raise "Failed to map line to RouteInfo"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,76 @@
1
+ require "deep_merge"
2
+ require_relative "../structs/open_api"
3
+
4
+ module Util
5
+ class OpenAPIHelpers
6
+ class << self
7
+ # Takes an array of RouteInfo types and generates the OpenAPI v3 spec
8
+ # to write into YAML and export to a file.
9
+ def convert(route_info)
10
+ param_friendly_route_info = convert_params_to_compatible_format(route_info)
11
+ paths_hash = write_paths_hash(param_friendly_route_info)
12
+ write_open_api_compliant_hash(paths_hash)
13
+ end
14
+
15
+ def convert_params_to_compatible_format(route_info)
16
+ route_info.map { |info|
17
+ # Remove (.:format) from uri_pattern
18
+ update_string = info.uri_pattern.gsub(/\(\.:format\)/, '')
19
+
20
+ # Turn something like /a/b/:param/d to /a/b/{param} for OpenAPI standards
21
+ update_string = update_string.split("/").map { |uri|
22
+ uri[0] == ":" ? "{#{uri[1..]}}" : uri
23
+ }.join("/")
24
+
25
+ Structs::RouteInfo.new(verb: info.verb, controller_action: info.controller_action, uri_pattern: update_string)
26
+ }
27
+ rescue
28
+ raise 'Could not convert params from :param to {param} format'
29
+ end
30
+
31
+ # ! To be honest, I feel like all of this should be cleaned up.
32
+ # Even if it is just that I need a better formatter to make it read easier.
33
+ # I feel like the following four methods are a bit wtf really.
34
+ def get_request_body_schema
35
+ {type: "object", properties: {id: {type: "string", format: "string"}}}
36
+ end
37
+
38
+ def get_verb_with_req_body(info)
39
+ Structs::OpenAPI::VerbWithRequestBody.new(summary: info.controller_action, requestBody: {required: true, content: {"application/json": {schema: get_request_body_schema}}}).attributes
40
+ end
41
+
42
+ def get_verb(info)
43
+ Structs::OpenAPI::Verb.new(summary: info.controller_action).attributes
44
+ end
45
+
46
+ def write_paths_hash(route_info)
47
+ paths_hash = {}
48
+ route_info.each do |info|
49
+ # Simplify the GET part - assume all other REST verbs require a request body
50
+ if info.verb == "GET"
51
+ paths_hash.deep_merge!({info.uri_pattern => {info.verb.downcase => get_verb(info)}})
52
+ else
53
+ paths_hash.deep_merge!({info.uri_pattern => {info.verb.downcase => get_verb_with_req_body(info)}})
54
+ end
55
+ end
56
+ paths_hash
57
+ rescue
58
+ raise 'Could not write paths for hash'
59
+ end
60
+
61
+ def write_open_api_compliant_hash(paths_hash)
62
+ info = {
63
+ title: "Rails Routes to OpenAPI",
64
+ description: "Generate an OpenAPI v3.1 YAML file for Postman import",
65
+ version: "1.1.0",
66
+ }
67
+ servers = [{url: "https://api.example.com", description: "Base URL for the API"}]
68
+
69
+ # This ends up the final hash to write to disk.
70
+ Structs::OpenAPI::Base.new(openapi: "3.1.0", info: info, servers: servers, paths: paths_hash).attributes
71
+ rescue
72
+ raise 'Could not write OpenAPI compliant hash'
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,34 @@
1
+ require "yaml"
2
+ require_relative "rails_routes_to_openapi/util/file_helpers"
3
+ require_relative "rails_routes_to_openapi/util/open_api_helpers"
4
+
5
+ module RailsRoutesToOpenAPI
6
+ class ::Hash
7
+ def deep_stringify_keys
8
+ each_with_object({}) do |(k, v), h|
9
+ h[k.to_s] = case v
10
+ when Hash
11
+ v.deep_stringify_keys
12
+ when Array
13
+ v.map { |i| i.is_a?(Hash) ? i.deep_stringify_keys : i }
14
+ else
15
+ v
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.convert(routes_output)
22
+ route_info = Util::FileHelpers.parse(routes_output)
23
+ yaml_format = Util::OpenAPIHelpers.convert(route_info).to_hash.deep_stringify_keys.to_yaml
24
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
25
+ output_file = "openapi_v3.1_#{timestamp}.yml"
26
+
27
+ File.open(output_file, "w") { |file| file.write(yaml_format) }
28
+ end
29
+ end
30
+
31
+ # Load Rake tasks if Rails is defined
32
+ if defined?(Rails)
33
+ Dir[File.join(__dir__, 'tasks/**/*.rake')].each { |ext| load ext }
34
+ end
@@ -0,0 +1,23 @@
1
+ require "rake"
2
+ require_relative "../rails_routes_to_openapi"
3
+
4
+ namespace :routes_to_openapi do
5
+ desc "Convert Rails routes to OpenAPI YAML"
6
+ task :convert do
7
+ routes_output = IO.popen("rails routes") { |io| io.read }
8
+
9
+ # Check if the output contains expected routes information
10
+ if routes_output.include?("Usage:") || routes_output.strip.empty?
11
+ puts "Failed to retrieve routes. Please ensure you are running this task within a Rails project."
12
+ exit 1
13
+ end
14
+
15
+ begin
16
+ RailsRoutesToOpenAPI.convert(routes_output)
17
+ puts 'OpenAPI v3 YAML file generated successfully.'
18
+ rescue => e
19
+ puts "Failed to generate OpenAPI YAML file."
20
+ puts e.message
21
+ end
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_routes_to_openapi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Camel Chang
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-02-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-types
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-struct
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yaml
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: deep_merge
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: A gem to convert Rails routes to OpenAPI YAML for Postman import
70
+ email:
71
+ - a556622821@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - README.md
77
+ - Rakefile
78
+ - lib/rails_routes_to_openapi.rb
79
+ - lib/rails_routes_to_openapi/structs/open_api.rb
80
+ - lib/rails_routes_to_openapi/structs/rails_route_info.rb
81
+ - lib/rails_routes_to_openapi/types/index.rb
82
+ - lib/rails_routes_to_openapi/util/file_helpers.rb
83
+ - lib/rails_routes_to_openapi/util/open_api_helpers.rb
84
+ - lib/tasks/routes_to_openapi.rake
85
+ homepage: https://github.com/camel2243/rails-gen-openapi
86
+ licenses:
87
+ - Apache-2.0
88
+ metadata: {}
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 3.4.10
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Convert Rails routes to OpenAPI YAML
108
+ test_files: []