transducer 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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +214 -0
- data/exe/transducer +6 -0
- data/lib/transducer/cli.rb +61 -0
- data/lib/transducer/formatter.rb +176 -0
- data/lib/transducer/generator.rb +147 -0
- data/lib/transducer/parser.rb +76 -0
- data/lib/transducer/templates/default.md.erb +61 -0
- data/lib/transducer/version.rb +5 -0
- data/lib/transducer.rb +14 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ec4069db4028d341c58598e45bdefdd0749e92092e4362b491845f350dc7e3df
|
|
4
|
+
data.tar.gz: 6cc2eeca6343cdcf575f76f1405bbe10066be7d6b1934193ed3b8b10215b1ff8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c2ecf6366f6121751bf0bf2a160264b087b6bb592f6f92a802f7b46acf0040fbd52f9068de01f97197ed566f1dc954c38d8fe718db51bd71f46d5d6300e5a17a
|
|
7
|
+
data.tar.gz: 4d1480b86c0134a6155c29a625ffccc49fcb833c64ab96cddf42f13d41bdce553a082c5cf454744351c1b7cec577e5ed86c9ea9e42611c1950d3bc0ee9ac7a75
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yudai Takada
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Transducer
|
|
2
|
+
|
|
3
|
+
Generate Markdown documentation from OpenAPI specifications.
|
|
4
|
+
|
|
5
|
+
Transducer is a Ruby gem that reads OpenAPI YAML specifications (versions 3.0.x and 3.1.x) and generates well-formatted, readable Markdown documentation.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'transducer'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ gem install transducer
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Command Line
|
|
30
|
+
|
|
31
|
+
Generate Markdown documentation from an OpenAPI specification:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ transducer generate input.yaml -o output.md
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
- `-o, --output`: Output file path (default: `docs/openapi.md`)
|
|
39
|
+
- `-t, --template`: Custom ERB template file path
|
|
40
|
+
|
|
41
|
+
#### Using Custom Templates
|
|
42
|
+
|
|
43
|
+
You can customize the output format using ERB templates:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
$ transducer generate input.yaml --template=custom.md.erb --output=api.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Template variables available:
|
|
50
|
+
- `data`: Parsed OpenAPI specification hash
|
|
51
|
+
- `formatter`: Formatter instance for formatting endpoints, parameters, schemas, etc.
|
|
52
|
+
|
|
53
|
+
Example custom template:
|
|
54
|
+
|
|
55
|
+
```erb
|
|
56
|
+
# <%= data['info']['title'] %>
|
|
57
|
+
|
|
58
|
+
<% data['paths']&.each do |path, methods| %>
|
|
59
|
+
<% methods.each do |method, details| %>
|
|
60
|
+
### <%= method.upcase %> <%= path %>
|
|
61
|
+
<%= formatter.format_endpoint(path, method, details) %>
|
|
62
|
+
<% end %>
|
|
63
|
+
<% end %>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Display version information:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
$ transducer version
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Display help:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
$ transducer help
|
|
76
|
+
$ transducer help generate
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Programmatic Usage
|
|
80
|
+
|
|
81
|
+
You can also use Transducer programmatically in your Ruby code:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
require 'transducer'
|
|
85
|
+
|
|
86
|
+
# Parse OpenAPI specification
|
|
87
|
+
parser = Transducer::Parser.new('path/to/openapi.yaml')
|
|
88
|
+
data = parser.parse
|
|
89
|
+
|
|
90
|
+
# Generate Markdown with default template
|
|
91
|
+
generator = Transducer::Generator.new(data)
|
|
92
|
+
markdown = generator.generate
|
|
93
|
+
|
|
94
|
+
# Or use a custom template
|
|
95
|
+
generator = Transducer::Generator.new(data, template_path: 'custom.md.erb')
|
|
96
|
+
markdown = generator.generate
|
|
97
|
+
|
|
98
|
+
# Write to file
|
|
99
|
+
generator.to_file('output.md')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Output Format
|
|
103
|
+
|
|
104
|
+
The generated Markdown documentation includes:
|
|
105
|
+
|
|
106
|
+
- API Title and Description: From the `info` section
|
|
107
|
+
- Version and Base URL: API version and server URL
|
|
108
|
+
- Table of Contents: Automatically generated with links to all sections
|
|
109
|
+
- Endpoints: All API endpoints with:
|
|
110
|
+
- HTTP method and path
|
|
111
|
+
- Description
|
|
112
|
+
- Parameters (path, query, header)
|
|
113
|
+
- Request body schema and examples
|
|
114
|
+
- Response codes with schemas and examples
|
|
115
|
+
- Schemas: Component schemas with:
|
|
116
|
+
- Type information
|
|
117
|
+
- Property descriptions
|
|
118
|
+
- Required fields
|
|
119
|
+
- Examples
|
|
120
|
+
|
|
121
|
+
### Example Output
|
|
122
|
+
|
|
123
|
+
```markdown
|
|
124
|
+
# Simple API
|
|
125
|
+
|
|
126
|
+
A simple API for testing
|
|
127
|
+
|
|
128
|
+
| | |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Version | 1.0.0 |
|
|
131
|
+
| Base URL | https://api.example.com/v1 |
|
|
132
|
+
|
|
133
|
+
## Table of Contents
|
|
134
|
+
|
|
135
|
+
- [Endpoints](#endpoints)
|
|
136
|
+
- [GET /users](#get-users)
|
|
137
|
+
- [POST /users](#post-users)
|
|
138
|
+
- [Schemas](#schemas)
|
|
139
|
+
- [User](#user)
|
|
140
|
+
|
|
141
|
+
## Endpoints
|
|
142
|
+
|
|
143
|
+
### GET /users
|
|
144
|
+
|
|
145
|
+
Returns a list of all users
|
|
146
|
+
|
|
147
|
+
**Parameters**:
|
|
148
|
+
|
|
149
|
+
| Name | Location | Type | Required | Description |
|
|
150
|
+
|------|----------|------|----------|-------------|
|
|
151
|
+
| limit | query | integer | No | Maximum number of users to return |
|
|
152
|
+
|
|
153
|
+
**Responses**:
|
|
154
|
+
|
|
155
|
+
#### 200 OK
|
|
156
|
+
|
|
157
|
+
Successful response
|
|
158
|
+
...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Supported OpenAPI Versions
|
|
162
|
+
|
|
163
|
+
- OpenAPI 3.0.x
|
|
164
|
+
- OpenAPI 3.1.x
|
|
165
|
+
|
|
166
|
+
OpenAPI 2.0 (Swagger) is not currently supported.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
|
171
|
+
|
|
172
|
+
Run tests:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
$ bundle exec rspec
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Run RuboCop:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
$ bundle exec rubocop
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Run all checks (tests + RuboCop):
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
$ bundle exec rake
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
To install this gem onto your local machine:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
$ bundle exec rake install
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
To release a new version:
|
|
197
|
+
|
|
198
|
+
1. Update the version number in `lib/transducer/version.rb`
|
|
199
|
+
2. Update `CHANGELOG.md`
|
|
200
|
+
3. Run `bundle exec rake release`
|
|
201
|
+
|
|
202
|
+
## Contributing
|
|
203
|
+
|
|
204
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/transducer.
|
|
205
|
+
|
|
206
|
+
1. Fork it
|
|
207
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
208
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
209
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
210
|
+
5. Create new Pull Request
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/transducer
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Transducer
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
def self.exit_on_failure?
|
|
8
|
+
true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc 'generate INPUT', 'Generate Markdown documentation from OpenAPI specification'
|
|
12
|
+
method_option :output, aliases: '-o', type: :string, default: 'docs/openapi.md',
|
|
13
|
+
desc: 'Output file path for the generated Markdown'
|
|
14
|
+
method_option :template, aliases: '-t', type: :string,
|
|
15
|
+
desc: 'Custom ERB template file path'
|
|
16
|
+
def generate(input_file)
|
|
17
|
+
unless File.exist?(input_file)
|
|
18
|
+
error "Input file not found: #{input_file}"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
output_file = options[:output]
|
|
23
|
+
template_path = options[:template]
|
|
24
|
+
|
|
25
|
+
parser = Parser.new(input_file)
|
|
26
|
+
data = parser.parse
|
|
27
|
+
|
|
28
|
+
generator = Generator.new(data, template_path: template_path)
|
|
29
|
+
generator.to_file(output_file)
|
|
30
|
+
|
|
31
|
+
success "Documentation generated successfully: #{output_file}"
|
|
32
|
+
rescue FileError => e
|
|
33
|
+
error "File error: #{e.message}"
|
|
34
|
+
exit 1
|
|
35
|
+
rescue ParseError => e
|
|
36
|
+
error "Parse error: #{e.message}"
|
|
37
|
+
exit 2
|
|
38
|
+
rescue ValidationError => e
|
|
39
|
+
error "Validation error: #{e.message}"
|
|
40
|
+
exit 3
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
error "Unexpected error: #{e.message}"
|
|
43
|
+
exit 4
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc 'version', 'Display version information'
|
|
47
|
+
def version
|
|
48
|
+
puts "Transducer version #{Transducer::VERSION}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def success(message)
|
|
54
|
+
puts "✓ #{message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def error(message)
|
|
58
|
+
warn "✗ #{message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Transducer
|
|
4
|
+
class Formatter
|
|
5
|
+
def format_endpoint(path, method, details)
|
|
6
|
+
output = []
|
|
7
|
+
output << "### #{method.upcase} #{path}"
|
|
8
|
+
output << ''
|
|
9
|
+
output << details['description'] if details['description']
|
|
10
|
+
output << ''
|
|
11
|
+
output << format_parameters(details['parameters']) if details['parameters']
|
|
12
|
+
output << format_request_body(details['requestBody']) if details['requestBody']
|
|
13
|
+
output << format_responses(details['responses']) if details['responses']
|
|
14
|
+
output.join("\n")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def format_parameters(parameters)
|
|
18
|
+
return '' if parameters.nil? || parameters.empty?
|
|
19
|
+
|
|
20
|
+
output = []
|
|
21
|
+
output << '**Parameters**:'
|
|
22
|
+
output << ''
|
|
23
|
+
output << '| Name | Location | Type | Required | Description |'
|
|
24
|
+
output << '|------|----------|------|----------|-------------|'
|
|
25
|
+
|
|
26
|
+
parameters.each do |param|
|
|
27
|
+
name = param['name'] || 'N/A'
|
|
28
|
+
location = param['in'] || 'N/A'
|
|
29
|
+
type = extract_type(param['schema'])
|
|
30
|
+
required = param['required'] ? 'Yes' : 'No'
|
|
31
|
+
description = param['description'] || ''
|
|
32
|
+
output << "| #{name} | #{location} | #{type} | #{required} | #{description} |"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
output << ''
|
|
36
|
+
output.join("\n")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format_request_body(request_body)
|
|
40
|
+
return '' unless request_body
|
|
41
|
+
|
|
42
|
+
output = []
|
|
43
|
+
output << '**Request Body**:'
|
|
44
|
+
output << ''
|
|
45
|
+
|
|
46
|
+
if request_body['description']
|
|
47
|
+
output << request_body['description']
|
|
48
|
+
output << ''
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
request_body['content']&.each do |content_type, details|
|
|
52
|
+
output << "**Content-Type**: `#{content_type}`"
|
|
53
|
+
output << ''
|
|
54
|
+
output << format_schema(details['schema']) if details['schema']
|
|
55
|
+
next unless details['example']
|
|
56
|
+
|
|
57
|
+
output << '**Example**:'
|
|
58
|
+
output << ''
|
|
59
|
+
output << '```json'
|
|
60
|
+
output << JSON.pretty_generate(details['example'])
|
|
61
|
+
output << '```'
|
|
62
|
+
output << ''
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
output.join("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def format_responses(responses)
|
|
69
|
+
return '' unless responses
|
|
70
|
+
|
|
71
|
+
output = []
|
|
72
|
+
output << '**Responses**:'
|
|
73
|
+
output << ''
|
|
74
|
+
|
|
75
|
+
responses.each do |status_code, response|
|
|
76
|
+
output << "#### #{status_code} #{status_name(status_code)}"
|
|
77
|
+
output << ''
|
|
78
|
+
output << response['description'] if response['description']
|
|
79
|
+
output << ''
|
|
80
|
+
|
|
81
|
+
next unless response['content']
|
|
82
|
+
|
|
83
|
+
response['content'].each do |content_type, details|
|
|
84
|
+
output << "**Content-Type**: `#{content_type}`"
|
|
85
|
+
output << ''
|
|
86
|
+
if details['schema']
|
|
87
|
+
output << '**Schema**:'
|
|
88
|
+
output << ''
|
|
89
|
+
output << '```json'
|
|
90
|
+
output << JSON.pretty_generate(details['schema'])
|
|
91
|
+
output << '```'
|
|
92
|
+
output << ''
|
|
93
|
+
end
|
|
94
|
+
next unless details['example']
|
|
95
|
+
|
|
96
|
+
output << '**Example**:'
|
|
97
|
+
output << ''
|
|
98
|
+
output << '```json'
|
|
99
|
+
output << JSON.pretty_generate(details['example'])
|
|
100
|
+
output << '```'
|
|
101
|
+
output << ''
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
output.join("\n")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def format_schema(schema)
|
|
109
|
+
return '' unless schema
|
|
110
|
+
|
|
111
|
+
output = []
|
|
112
|
+
|
|
113
|
+
if schema['$ref']
|
|
114
|
+
ref_name = schema['$ref'].split('/').last
|
|
115
|
+
output << "**Schema**: `#{ref_name}`"
|
|
116
|
+
output << ''
|
|
117
|
+
return output.join("\n")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
output << "**Type**: #{schema['type']}" if schema['type']
|
|
121
|
+
output << ''
|
|
122
|
+
|
|
123
|
+
if schema['properties']
|
|
124
|
+
output << '**Properties**:'
|
|
125
|
+
output << ''
|
|
126
|
+
output << '| Name | Type | Required | Description |'
|
|
127
|
+
output << '|------|------|----------|-------------|'
|
|
128
|
+
|
|
129
|
+
required_fields = schema['required'] || []
|
|
130
|
+
schema['properties'].each do |prop_name, prop_details|
|
|
131
|
+
type = extract_type(prop_details)
|
|
132
|
+
is_required = required_fields.include?(prop_name) ? 'Yes' : 'No'
|
|
133
|
+
description = prop_details['description'] || ''
|
|
134
|
+
output << "| #{prop_name} | #{type} | #{is_required} | #{description} |"
|
|
135
|
+
end
|
|
136
|
+
output << ''
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
output.join("\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def extract_type(schema)
|
|
145
|
+
return 'N/A' unless schema
|
|
146
|
+
|
|
147
|
+
if schema['$ref']
|
|
148
|
+
schema['$ref'].split('/').last
|
|
149
|
+
elsif schema['type']
|
|
150
|
+
type = schema['type']
|
|
151
|
+
if type == 'array' && schema['items']
|
|
152
|
+
item_type = extract_type(schema['items'])
|
|
153
|
+
"array[#{item_type}]"
|
|
154
|
+
else
|
|
155
|
+
type
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
'object'
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def status_name(code)
|
|
163
|
+
names = {
|
|
164
|
+
'200' => 'OK',
|
|
165
|
+
'201' => 'Created',
|
|
166
|
+
'204' => 'No Content',
|
|
167
|
+
'400' => 'Bad Request',
|
|
168
|
+
'401' => 'Unauthorized',
|
|
169
|
+
'403' => 'Forbidden',
|
|
170
|
+
'404' => 'Not Found',
|
|
171
|
+
'500' => 'Internal Server Error'
|
|
172
|
+
}
|
|
173
|
+
names[code.to_s] || ''
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'erb'
|
|
5
|
+
|
|
6
|
+
module Transducer
|
|
7
|
+
class Generator
|
|
8
|
+
attr_reader :data, :formatter, :template_path
|
|
9
|
+
|
|
10
|
+
def initialize(parsed_data, template_path: nil)
|
|
11
|
+
@data = parsed_data
|
|
12
|
+
@formatter = Formatter.new
|
|
13
|
+
@template_path = template_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate
|
|
17
|
+
if @template_path
|
|
18
|
+
render_template
|
|
19
|
+
else
|
|
20
|
+
generate_default
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_file(output_path)
|
|
25
|
+
require 'fileutils'
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
27
|
+
|
|
28
|
+
File.write(output_path, generate)
|
|
29
|
+
rescue Errno::EACCES
|
|
30
|
+
raise FileError, "Permission denied: #{output_path}"
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
raise FileError, "Failed to write file: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def render_template
|
|
38
|
+
raise FileError, "Template file not found: #{@template_path}" unless File.exist?(@template_path)
|
|
39
|
+
|
|
40
|
+
template_content = File.read(@template_path)
|
|
41
|
+
erb = ERB.new(template_content, trim_mode: '-')
|
|
42
|
+
erb.result(binding)
|
|
43
|
+
rescue Errno::EACCES
|
|
44
|
+
raise FileError, "Permission denied reading template: #{@template_path}"
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
raise FileError, "Failed to render template: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def generate_default
|
|
50
|
+
output = []
|
|
51
|
+
output << generate_header
|
|
52
|
+
output << generate_table_of_contents
|
|
53
|
+
output << generate_endpoints
|
|
54
|
+
output << generate_schemas
|
|
55
|
+
output.compact.join("\n\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def generate_header
|
|
59
|
+
info = @data['info']
|
|
60
|
+
output = []
|
|
61
|
+
output << "# #{info['title']}"
|
|
62
|
+
output << ''
|
|
63
|
+
output << info['description'] if info['description']
|
|
64
|
+
output << ''
|
|
65
|
+
output << '| | |'
|
|
66
|
+
output << '|---|---|'
|
|
67
|
+
output << "| Version | #{info['version']} |"
|
|
68
|
+
|
|
69
|
+
output << "| Base URL | #{@data['servers'][0]['url']} |" if @data['servers'] && !@data['servers'].empty?
|
|
70
|
+
|
|
71
|
+
output.join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def generate_table_of_contents
|
|
75
|
+
output = []
|
|
76
|
+
output << '## Table of Contents'
|
|
77
|
+
output << ''
|
|
78
|
+
output << '- [Endpoints](#endpoints)'
|
|
79
|
+
|
|
80
|
+
@data['paths']&.each do |path, methods|
|
|
81
|
+
methods.each_key do |method|
|
|
82
|
+
next if method.start_with?('$')
|
|
83
|
+
|
|
84
|
+
anchor = "#{method}-#{path}".downcase.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-')
|
|
85
|
+
output << " - [#{method.upcase} #{path}](##{anchor})"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if @data['components'] && @data['components']['schemas']
|
|
90
|
+
output << '- [Schemas](#schemas)'
|
|
91
|
+
@data['components']['schemas'].each_key do |schema_name|
|
|
92
|
+
anchor = schema_name.downcase.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-')
|
|
93
|
+
output << " - [#{schema_name}](##{anchor})"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
output.join("\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def generate_endpoints
|
|
101
|
+
return nil unless @data['paths']
|
|
102
|
+
|
|
103
|
+
output = []
|
|
104
|
+
output << '## Endpoints'
|
|
105
|
+
output << ''
|
|
106
|
+
|
|
107
|
+
@data['paths'].each do |path, methods|
|
|
108
|
+
methods.each do |method, details|
|
|
109
|
+
next if method.start_with?('$')
|
|
110
|
+
|
|
111
|
+
output << @formatter.format_endpoint(path, method, details)
|
|
112
|
+
output << ''
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
output.join("\n")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def generate_schemas
|
|
120
|
+
return nil unless @data['components'] && @data['components']['schemas']
|
|
121
|
+
|
|
122
|
+
output = []
|
|
123
|
+
output << '## Schemas'
|
|
124
|
+
output << ''
|
|
125
|
+
|
|
126
|
+
@data['components']['schemas'].each do |schema_name, schema|
|
|
127
|
+
output << "### #{schema_name}"
|
|
128
|
+
output << ''
|
|
129
|
+
output << schema['description'] if schema['description']
|
|
130
|
+
output << ''
|
|
131
|
+
output << @formatter.format_schema(schema)
|
|
132
|
+
output << ''
|
|
133
|
+
|
|
134
|
+
next unless schema['example']
|
|
135
|
+
|
|
136
|
+
output << '**Example**:'
|
|
137
|
+
output << ''
|
|
138
|
+
output << '```json'
|
|
139
|
+
output << JSON.pretty_generate(schema['example'])
|
|
140
|
+
output << '```'
|
|
141
|
+
output << ''
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
output.join("\n")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'psych'
|
|
4
|
+
|
|
5
|
+
module Transducer
|
|
6
|
+
class Parser
|
|
7
|
+
attr_reader :file_path, :data, :errors
|
|
8
|
+
|
|
9
|
+
def initialize(file_path)
|
|
10
|
+
@file_path = file_path
|
|
11
|
+
@data = nil
|
|
12
|
+
@errors = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse
|
|
16
|
+
raise FileError, "File not found: #{file_path}" unless File.exist?(file_path)
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
@data = Psych.safe_load_file(file_path, permitted_classes: [Symbol, Date, Time])
|
|
20
|
+
rescue Psych::SyntaxError => e
|
|
21
|
+
raise ParseError, "YAML syntax error at line #{e.line}: #{e.message}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
validate_openapi_spec
|
|
25
|
+
@data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def valid?
|
|
29
|
+
@errors.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def validate_openapi_spec
|
|
35
|
+
@errors = []
|
|
36
|
+
|
|
37
|
+
unless @data.is_a?(Hash)
|
|
38
|
+
@errors << 'OpenAPI specification must be a hash'
|
|
39
|
+
raise ValidationError, @errors.join(', ')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
validate_openapi_version
|
|
43
|
+
validate_info
|
|
44
|
+
validate_paths
|
|
45
|
+
|
|
46
|
+
raise ValidationError, @errors.join(', ') unless valid?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_openapi_version
|
|
50
|
+
unless @data['openapi']
|
|
51
|
+
@errors << 'Missing required field: openapi'
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
version = @data['openapi'].to_s
|
|
56
|
+
return if version.start_with?('3.0') || version.start_with?('3.1')
|
|
57
|
+
|
|
58
|
+
@errors << "Unsupported OpenAPI version: #{version}. Only 3.0.x and 3.1.x are supported."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_info
|
|
62
|
+
unless @data['info']
|
|
63
|
+
@errors << 'Missing required field: info'
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
info = @data['info']
|
|
68
|
+
@errors << 'Missing required field: info.title' unless info['title']
|
|
69
|
+
@errors << 'Missing required field: info.version' unless info['version']
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_paths
|
|
73
|
+
@errors << 'Missing required field: paths' unless @data['paths']
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# <%= data['info']['title'] %>
|
|
2
|
+
|
|
3
|
+
<% if data['info']['description'] %>
|
|
4
|
+
<%= data['info']['description'] %>
|
|
5
|
+
|
|
6
|
+
<% end %>
|
|
7
|
+
**Version**: <%= data['info']['version'] %>
|
|
8
|
+
<% if data['servers'] && !data['servers'].empty? %>
|
|
9
|
+
**Base URL**: <%= data['servers'][0]['url'] %>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Endpoints](#endpoints)
|
|
15
|
+
<% data['paths']&.each do |path, methods| %>
|
|
16
|
+
<% methods.each_key do |method| %>
|
|
17
|
+
<% next if method.start_with?('$') %>
|
|
18
|
+
<% anchor = "#{method}-#{path}".downcase.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-') %>
|
|
19
|
+
- [<%= method.upcase %> <%= path %>](#<%= anchor %>)
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% if data['components'] && data['components']['schemas'] %>
|
|
23
|
+
- [Schemas](#schemas)
|
|
24
|
+
<% data['components']['schemas'].each_key do |schema_name| %>
|
|
25
|
+
<% anchor = schema_name.downcase.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-') %>
|
|
26
|
+
- [<%= schema_name %>](#<%= anchor %>)
|
|
27
|
+
<% end %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
## Endpoints
|
|
31
|
+
|
|
32
|
+
<% data['paths']&.each do |path, methods| %>
|
|
33
|
+
<% methods.each do |method, details| %>
|
|
34
|
+
<% next if method.start_with?('$') %>
|
|
35
|
+
<%= formatter.format_endpoint(path, method, details) %>
|
|
36
|
+
|
|
37
|
+
<% end %>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% if data['components'] && data['components']['schemas'] %>
|
|
40
|
+
|
|
41
|
+
## Schemas
|
|
42
|
+
|
|
43
|
+
<% data['components']['schemas'].each do |schema_name, schema| %>
|
|
44
|
+
### <%= schema_name %>
|
|
45
|
+
|
|
46
|
+
<% if schema['description'] %>
|
|
47
|
+
<%= schema['description'] %>
|
|
48
|
+
|
|
49
|
+
<% end %>
|
|
50
|
+
<%= formatter.format_schema(schema) %>
|
|
51
|
+
|
|
52
|
+
<% if schema['example'] %>
|
|
53
|
+
**Example**:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
<%= JSON.pretty_generate(schema['example']) %>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
<% end %>
|
|
60
|
+
<% end %>
|
|
61
|
+
<% end %>
|
data/lib/transducer.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'transducer/version'
|
|
4
|
+
require_relative 'transducer/parser'
|
|
5
|
+
require_relative 'transducer/formatter'
|
|
6
|
+
require_relative 'transducer/generator'
|
|
7
|
+
require_relative 'transducer/cli'
|
|
8
|
+
|
|
9
|
+
module Transducer
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
class ParseError < Error; end
|
|
12
|
+
class ValidationError < Error; end
|
|
13
|
+
class FileError < Error; end
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: transducer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
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: psych
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: thor
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.3'
|
|
40
|
+
description: A Ruby gem that reads OpenAPI YAML specifications and generates well-formatted
|
|
41
|
+
Markdown documentation
|
|
42
|
+
email:
|
|
43
|
+
- t.yudai92@gmail.com
|
|
44
|
+
executables:
|
|
45
|
+
- transducer
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- exe/transducer
|
|
53
|
+
- lib/transducer.rb
|
|
54
|
+
- lib/transducer/cli.rb
|
|
55
|
+
- lib/transducer/formatter.rb
|
|
56
|
+
- lib/transducer/generator.rb
|
|
57
|
+
- lib/transducer/parser.rb
|
|
58
|
+
- lib/transducer/templates/default.md.erb
|
|
59
|
+
- lib/transducer/version.rb
|
|
60
|
+
homepage: https://github.com/ydah/transducer
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/ydah/transducer
|
|
65
|
+
source_code_uri: https://github.com/ydah/transducer
|
|
66
|
+
changelog_uri: https://github.com/ydah/transducer/blob/main/CHANGELOG.md
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 3.0.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.6.9
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: Generate Markdown documentation from OpenAPI specifications
|
|
85
|
+
test_files: []
|