apigen 0.0.6
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/lib/apigen/formats/jsonschema.rb +83 -0
- data/lib/apigen/formats/openapi.rb +182 -0
- data/lib/apigen/formats/swagger.rb +165 -0
- data/lib/apigen/model.rb +236 -0
- data/lib/apigen/rest.rb +271 -0
- data/lib/apigen/util.rb +23 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d490568fbf42afb3f2c5ee580739f02bf13403444c0c852004d44b9dc6a1c209
|
4
|
+
data.tar.gz: be1468d137a7efcee80ab31f496a7ffe74bd7eb09cdcf7514e8d569a79789d15
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4f55e4d77ef95e85938b51306fa0f0cc09ec5d6371eff815d50033760bed8a792d525bd2e7439dbec4eb324022ca88c176cd30b9674db2920d5bfd29a4e3ce4e
|
7
|
+
data.tar.gz: 6b2fc1dfc6ab532aa69ceb4d0c6d2c6143d7604bfd0002fe1317209ef245842e12fc620a508ea72edc69049307f43a455048f26836f812c327c354e38a32de42
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Apigen
|
6
|
+
module Formats
|
7
|
+
module JsonSchema
|
8
|
+
##
|
9
|
+
# JSON Schema Draft 7 generator.
|
10
|
+
module Draft7
|
11
|
+
class << self
|
12
|
+
def generate(api)
|
13
|
+
JSON.pretty_generate definitions(api)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def definitions(api)
|
19
|
+
{
|
20
|
+
'$schema' => 'http://json-schema.org/draft-07/schema#',
|
21
|
+
'definitions' => api.models.map { |key, model| [key.to_s, schema(api, model.type, model.description, model.example)] }.to_h
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def schema(api, type, description = nil, example = nil)
|
26
|
+
schema = schema_without_description(api, type)
|
27
|
+
schema['description'] = description unless description.nil?
|
28
|
+
schema['example'] = example unless example.nil?
|
29
|
+
schema
|
30
|
+
end
|
31
|
+
|
32
|
+
def schema_without_description(api, type)
|
33
|
+
case type
|
34
|
+
when Apigen::ObjectType
|
35
|
+
object_schema(api, type)
|
36
|
+
when Apigen::ArrayType
|
37
|
+
array_schema(api, type)
|
38
|
+
when Apigen::OptionalType
|
39
|
+
raise 'OptionalType fields are only supported within object types.'
|
40
|
+
when :string
|
41
|
+
{
|
42
|
+
'type' => 'string'
|
43
|
+
}
|
44
|
+
when :int32
|
45
|
+
{
|
46
|
+
'type' => 'integer',
|
47
|
+
'format' => 'int32'
|
48
|
+
}
|
49
|
+
when :bool
|
50
|
+
{
|
51
|
+
'type' => 'boolean'
|
52
|
+
}
|
53
|
+
else
|
54
|
+
return { '$ref' => "#/definitions/#{type}" } if api.models.key? type
|
55
|
+
raise "Unsupported type: #{type}."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def object_schema(api, object_type)
|
60
|
+
{
|
61
|
+
'type' => 'object',
|
62
|
+
'properties' => object_type.properties.map { |name, property| object_property(api, name, property) }.to_h,
|
63
|
+
'required' => object_type.properties.reject { |_name, property| property.type.is_a? Apigen::OptionalType }.map { |name, _property| name.to_s }
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def object_property(api, name, property)
|
68
|
+
# A property is never optional, because we specify which are required on the schema itself.
|
69
|
+
actual_type = property.type.is_a?(Apigen::OptionalType) ? property.type.type : property.type
|
70
|
+
[name.to_s, schema(api, actual_type, property.description, property.example)]
|
71
|
+
end
|
72
|
+
|
73
|
+
def array_schema(api, array_type)
|
74
|
+
{
|
75
|
+
'type' => 'array',
|
76
|
+
'items' => schema(api, array_type.type)
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Apigen
|
6
|
+
module Formats
|
7
|
+
module OpenAPI
|
8
|
+
##
|
9
|
+
# OpenAPI 3 generator.
|
10
|
+
module V3
|
11
|
+
class << self
|
12
|
+
def generate(api)
|
13
|
+
# TODO: Allow overriding any of the hardcoded elements.
|
14
|
+
{
|
15
|
+
'openapi' => '3.0.0',
|
16
|
+
'info' => {
|
17
|
+
'version' => '1.0.0',
|
18
|
+
'title' => 'API',
|
19
|
+
'description' => api.description,
|
20
|
+
'termsOfService' => '',
|
21
|
+
'contact' => {
|
22
|
+
'name' => ''
|
23
|
+
},
|
24
|
+
'license' => {
|
25
|
+
'name' => ''
|
26
|
+
}
|
27
|
+
},
|
28
|
+
'servers' => [
|
29
|
+
{
|
30
|
+
'url' => 'http://localhost'
|
31
|
+
}
|
32
|
+
],
|
33
|
+
'paths' => paths(api),
|
34
|
+
'components' => {
|
35
|
+
'schemas' => definitions(api)
|
36
|
+
}
|
37
|
+
}.to_yaml
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def paths(api)
|
43
|
+
hash = {}
|
44
|
+
api.endpoints.each do |endpoint|
|
45
|
+
parameters = []
|
46
|
+
parameters.concat(endpoint.path_parameters.properties.map { |name, property| path_parameter(api, name, property) })
|
47
|
+
parameters.concat(endpoint.query_parameters.properties.map { |name, property| query_parameter(api, name, property) })
|
48
|
+
responses = endpoint.outputs.map { |output| response(api, output) }.to_h
|
49
|
+
operation = {
|
50
|
+
'operationId' => endpoint.name.to_s,
|
51
|
+
'parameters' => parameters,
|
52
|
+
'responses' => responses
|
53
|
+
}
|
54
|
+
operation['description'] = endpoint.description unless endpoint.description.nil?
|
55
|
+
operation['requestBody'] = input(api, endpoint.input) if endpoint.input
|
56
|
+
hash[endpoint.path] ||= {}
|
57
|
+
hash[endpoint.path][endpoint.method.to_s] = operation
|
58
|
+
end
|
59
|
+
hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def path_parameter(api, name, property)
|
63
|
+
parameter = {
|
64
|
+
'in' => 'path',
|
65
|
+
'name' => name.to_s,
|
66
|
+
'required' => true,
|
67
|
+
'schema' => schema(api, property.type)
|
68
|
+
}
|
69
|
+
parameter['description'] = property.description unless property.description.nil?
|
70
|
+
parameter['example'] = property.example unless property.example.nil?
|
71
|
+
parameter
|
72
|
+
end
|
73
|
+
|
74
|
+
def query_parameter(api, name, property)
|
75
|
+
optional = property.type.is_a?(Apigen::OptionalType)
|
76
|
+
actual_type = optional ? property.type.type : property.type
|
77
|
+
parameter = {
|
78
|
+
'in' => 'query',
|
79
|
+
'name' => name.to_s,
|
80
|
+
'required' => !optional,
|
81
|
+
'schema' => schema(api, actual_type)
|
82
|
+
}
|
83
|
+
parameter['description'] = property.description unless property.description.nil?
|
84
|
+
parameter['example'] = property.example unless property.example.nil?
|
85
|
+
parameter
|
86
|
+
end
|
87
|
+
|
88
|
+
def input(api, property)
|
89
|
+
parameter = {
|
90
|
+
'required' => true,
|
91
|
+
'content' => {
|
92
|
+
'application/json' => {
|
93
|
+
'schema' => schema(api, property.type)
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
parameter['description'] = property.description unless property.description.nil?
|
98
|
+
parameter['example'] = property.example unless property.example.nil?
|
99
|
+
parameter
|
100
|
+
end
|
101
|
+
|
102
|
+
def response(api, output)
|
103
|
+
response = {}
|
104
|
+
response['description'] = output.description unless output.description.nil?
|
105
|
+
response['example'] = output.example unless output.example.nil?
|
106
|
+
if output.type != :void
|
107
|
+
response['content'] = {
|
108
|
+
'application/json' => {
|
109
|
+
'schema' => schema(api, output.type)
|
110
|
+
}
|
111
|
+
}
|
112
|
+
end
|
113
|
+
[output.status.to_s, response]
|
114
|
+
end
|
115
|
+
|
116
|
+
def definitions(api)
|
117
|
+
hash = {}
|
118
|
+
api.models.each do |key, model|
|
119
|
+
hash[key.to_s] = schema(api, model.type, model.description, model.example)
|
120
|
+
end
|
121
|
+
hash
|
122
|
+
end
|
123
|
+
|
124
|
+
def schema(api, type, description = nil, example = nil)
|
125
|
+
schema = schema_without_description(api, type)
|
126
|
+
schema['description'] = description unless description.nil?
|
127
|
+
schema['example'] = example unless example.nil?
|
128
|
+
schema
|
129
|
+
end
|
130
|
+
|
131
|
+
def schema_without_description(api, type)
|
132
|
+
case type
|
133
|
+
when Apigen::ObjectType
|
134
|
+
object_schema(api, type)
|
135
|
+
when Apigen::ArrayType
|
136
|
+
array_schema(api, type)
|
137
|
+
when Apigen::OptionalType
|
138
|
+
raise 'OptionalType fields are only supported within object types.'
|
139
|
+
when :string
|
140
|
+
{
|
141
|
+
'type' => 'string'
|
142
|
+
}
|
143
|
+
when :int32
|
144
|
+
{
|
145
|
+
'type' => 'integer',
|
146
|
+
'format' => 'int32'
|
147
|
+
}
|
148
|
+
when :bool
|
149
|
+
{
|
150
|
+
'type' => 'boolean'
|
151
|
+
}
|
152
|
+
else
|
153
|
+
return { '$ref' => "#/components/schemas/#{type}" } if api.models.key? type
|
154
|
+
raise "Unsupported type: #{type}."
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def object_schema(api, object_type)
|
159
|
+
{
|
160
|
+
'type' => 'object',
|
161
|
+
'properties' => object_type.properties.map { |name, property| object_property(api, name, property) }.to_h,
|
162
|
+
'required' => object_type.properties.reject { |_name, property| property.type.is_a? Apigen::OptionalType }.map { |name, _property| name.to_s }
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def object_property(api, name, property)
|
167
|
+
# A property is never optional, because we specify which are required on the schema itself.
|
168
|
+
actual_type = property.type.is_a?(Apigen::OptionalType) ? property.type.type : property.type
|
169
|
+
[name.to_s, schema(api, actual_type, property.description, property.example)]
|
170
|
+
end
|
171
|
+
|
172
|
+
def array_schema(api, array_type)
|
173
|
+
{
|
174
|
+
'type' => 'array',
|
175
|
+
'items' => schema(api, array_type.type)
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Apigen
|
6
|
+
module Formats
|
7
|
+
module Swagger
|
8
|
+
##
|
9
|
+
# Swagger 2 (aka OpenAPI 2) generator.
|
10
|
+
module V2
|
11
|
+
class << self
|
12
|
+
def generate(api)
|
13
|
+
# TODO: Allow overriding any of the hardcoded elements.
|
14
|
+
{
|
15
|
+
'swagger' => '2.0',
|
16
|
+
'info' => {
|
17
|
+
'version' => '1.0.0',
|
18
|
+
'title' => 'API',
|
19
|
+
'description' => api.description,
|
20
|
+
'termsOfService' => '',
|
21
|
+
'contact' => {
|
22
|
+
'name' => ''
|
23
|
+
},
|
24
|
+
'license' => {
|
25
|
+
'name' => ''
|
26
|
+
}
|
27
|
+
},
|
28
|
+
'host' => 'localhost',
|
29
|
+
'basePath' => '/',
|
30
|
+
'schemes' => %w[
|
31
|
+
http
|
32
|
+
https
|
33
|
+
],
|
34
|
+
'consumes' => [
|
35
|
+
'application/json'
|
36
|
+
],
|
37
|
+
'produces' => [
|
38
|
+
'application/json'
|
39
|
+
],
|
40
|
+
'paths' => paths(api),
|
41
|
+
'definitions' => definitions(api)
|
42
|
+
}.to_yaml
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def paths(api)
|
48
|
+
hash = {}
|
49
|
+
api.endpoints.each do |endpoint|
|
50
|
+
parameters = []
|
51
|
+
parameters.concat(endpoint.path_parameters.properties.map { |name, property| path_parameter(api, name, property) })
|
52
|
+
parameters.concat(endpoint.query_parameters.properties.map { |name, property| query_parameter(api, name, property) })
|
53
|
+
parameters << input_parameter(api, endpoint.input) if endpoint.input
|
54
|
+
responses = endpoint.outputs.map { |output| response(api, output) }.to_h
|
55
|
+
hash[endpoint.path] ||= {}
|
56
|
+
hash[endpoint.path][endpoint.method.to_s] = {
|
57
|
+
'parameters' => parameters,
|
58
|
+
'responses' => responses
|
59
|
+
}
|
60
|
+
hash[endpoint.path][endpoint.method.to_s]['description'] = endpoint.description unless endpoint.description.nil?
|
61
|
+
end
|
62
|
+
hash
|
63
|
+
end
|
64
|
+
|
65
|
+
def path_parameter(api, name, property)
|
66
|
+
{
|
67
|
+
'in' => 'path',
|
68
|
+
'name' => name.to_s,
|
69
|
+
'required' => true
|
70
|
+
}.merge(schema(api, property.type, property.description, property.example))
|
71
|
+
end
|
72
|
+
|
73
|
+
def query_parameter(api, name, property)
|
74
|
+
optional = property.type.is_a?(Apigen::OptionalType)
|
75
|
+
actual_type = optional ? property.type.type : property.type
|
76
|
+
{
|
77
|
+
'in' => 'query',
|
78
|
+
'name' => name.to_s,
|
79
|
+
'required' => !optional
|
80
|
+
}.merge(schema(api, actual_type, property.description, property.example))
|
81
|
+
end
|
82
|
+
|
83
|
+
def input_parameter(api, property)
|
84
|
+
parameter = {
|
85
|
+
'name' => 'input',
|
86
|
+
'in' => 'body',
|
87
|
+
'required' => true,
|
88
|
+
'schema' => schema(api, property.type)
|
89
|
+
}
|
90
|
+
parameter['description'] = property.description unless property.description.nil?
|
91
|
+
parameter['example'] = property.example unless property.example.nil?
|
92
|
+
parameter
|
93
|
+
end
|
94
|
+
|
95
|
+
def response(api, output)
|
96
|
+
response = {}
|
97
|
+
response['description'] = output.description unless output.description.nil?
|
98
|
+
response['example'] = output.example unless output.example.nil?
|
99
|
+
response['schema'] = schema(api, output.type) if output.type != :void
|
100
|
+
[output.status.to_s, response]
|
101
|
+
end
|
102
|
+
|
103
|
+
def definitions(api)
|
104
|
+
api.models.map { |key, model| [key.to_s, schema(api, model.type, model.description, model.example)] }.to_h
|
105
|
+
end
|
106
|
+
|
107
|
+
def schema(api, type, description = nil, example = nil)
|
108
|
+
schema = schema_without_description(api, type)
|
109
|
+
schema['description'] = description unless description.nil?
|
110
|
+
schema['example'] = example unless example.nil?
|
111
|
+
schema
|
112
|
+
end
|
113
|
+
|
114
|
+
def schema_without_description(api, type)
|
115
|
+
case type
|
116
|
+
when Apigen::ObjectType
|
117
|
+
object_schema(api, type)
|
118
|
+
when Apigen::ArrayType
|
119
|
+
array_schema(api, type)
|
120
|
+
when Apigen::OptionalType
|
121
|
+
raise 'OptionalType fields are only supported within object types.'
|
122
|
+
when :string
|
123
|
+
{
|
124
|
+
'type' => 'string'
|
125
|
+
}
|
126
|
+
when :int32
|
127
|
+
{
|
128
|
+
'type' => 'integer',
|
129
|
+
'format' => 'int32'
|
130
|
+
}
|
131
|
+
when :bool
|
132
|
+
{
|
133
|
+
'type' => 'boolean'
|
134
|
+
}
|
135
|
+
else
|
136
|
+
return { '$ref' => "#/definitions/#{type}" } if api.models.key? type
|
137
|
+
raise "Unsupported type: #{type}."
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def object_schema(api, object_type)
|
142
|
+
{
|
143
|
+
'type' => 'object',
|
144
|
+
'properties' => object_type.properties.map { |name, property| object_property(api, name, property) }.to_h,
|
145
|
+
'required' => object_type.properties.reject { |_name, property| property.type.is_a? Apigen::OptionalType }.map { |name, _property| name.to_s }
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def object_property(api, name, property)
|
150
|
+
# A property is never optional, because we specify which are required on the schema itself.
|
151
|
+
actual_type = property.type.is_a?(Apigen::OptionalType) ? property.type.type : property.type
|
152
|
+
[name.to_s, schema(api, actual_type, property.description, property.example)]
|
153
|
+
end
|
154
|
+
|
155
|
+
def array_schema(api, array_type)
|
156
|
+
{
|
157
|
+
'type' => 'array',
|
158
|
+
'items' => schema(api, array_type.type)
|
159
|
+
}
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/apigen/model.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require_relative './util'
|
5
|
+
|
6
|
+
module Apigen
|
7
|
+
PRIMARY_TYPES = Set.new %i[string int32 bool void]
|
8
|
+
|
9
|
+
##
|
10
|
+
# ModelRegistry is where all model definitions are stored.
|
11
|
+
class ModelRegistry
|
12
|
+
attr_reader :models
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@models = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def model(name, &block)
|
19
|
+
model = Apigen::Model.new name
|
20
|
+
raise 'You must pass a block when calling `model`.' unless block_given?
|
21
|
+
model.instance_eval(&block)
|
22
|
+
@models[model.name] = model
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate
|
26
|
+
@models.each do |_key, model|
|
27
|
+
model.validate self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_type(type)
|
32
|
+
if type.is_a? Symbol
|
33
|
+
raise "Unknown type :#{type}." unless @models.key?(type) || PRIMARY_TYPES.include?(type)
|
34
|
+
elsif type.is_a?(ObjectType) || type.is_a?(ArrayType) || type.is_a?(OptionalType)
|
35
|
+
type.validate self
|
36
|
+
else
|
37
|
+
raise "Cannot process type #{type.class.name}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
@models.map do |key, model|
|
43
|
+
"#{key}: #{model}"
|
44
|
+
end.join "\n"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Model represents a data model with a specific name, e.g. "User" with an object type.
|
50
|
+
class Model
|
51
|
+
attr_reader :name
|
52
|
+
attribute_setter_getter :description
|
53
|
+
attribute_setter_getter :example
|
54
|
+
|
55
|
+
def initialize(name)
|
56
|
+
@name = name
|
57
|
+
@type = nil
|
58
|
+
@description = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def type(shape = nil, &block)
|
62
|
+
return @type unless shape
|
63
|
+
@type = Model.type shape, &block
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.type(shape, &block)
|
67
|
+
if shape.to_s.end_with? '?'
|
68
|
+
shape = shape[0..-2].to_sym
|
69
|
+
optional = true
|
70
|
+
else
|
71
|
+
optional = false
|
72
|
+
end
|
73
|
+
case shape
|
74
|
+
when :object
|
75
|
+
object = ObjectType.new
|
76
|
+
object.instance_eval(&block)
|
77
|
+
type = object
|
78
|
+
when :array
|
79
|
+
array = ArrayType.new
|
80
|
+
array.instance_eval(&block)
|
81
|
+
type = array
|
82
|
+
when :optional
|
83
|
+
optional = OptionalType.new
|
84
|
+
optional.instance_eval(&block)
|
85
|
+
type = optional
|
86
|
+
else
|
87
|
+
type = shape
|
88
|
+
end
|
89
|
+
optional ? OptionalType.new(type) : type
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate(model_registry)
|
93
|
+
raise 'One of the models is missing a name.' unless @name
|
94
|
+
raise "Use `type :model_type [block]` to assign a type to :#{@name}." unless @type
|
95
|
+
model_registry.check_type @type
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
@type.to_s
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# ObjectType represents an object type, with specific fields.
|
105
|
+
class ObjectType
|
106
|
+
attr_reader :properties
|
107
|
+
|
108
|
+
def initialize
|
109
|
+
@properties = {}
|
110
|
+
end
|
111
|
+
|
112
|
+
# rubocop:disable Style/MethodMissingSuper
|
113
|
+
def method_missing(field_name, *args, &block)
|
114
|
+
raise "Field :#{field_name} is defined multiple times." if @properties.key? field_name
|
115
|
+
field_type = args[0]
|
116
|
+
field_description = args[1]
|
117
|
+
block_called = false
|
118
|
+
if block_given?
|
119
|
+
block_wrapper = lambda do
|
120
|
+
block_called = true
|
121
|
+
yield
|
122
|
+
end
|
123
|
+
end
|
124
|
+
property = ObjectProperty.new(
|
125
|
+
Apigen::Model.type(field_type, &block_wrapper),
|
126
|
+
field_description
|
127
|
+
)
|
128
|
+
property.instance_eval(&block) if block_given? && !block_called
|
129
|
+
@properties[field_name] = property
|
130
|
+
end
|
131
|
+
# rubocop:enable Style/MethodMissingSuper
|
132
|
+
|
133
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
def validate(model_registry)
|
138
|
+
@properties.each do |_key, property|
|
139
|
+
model_registry.check_type property.type
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def to_s
|
144
|
+
repr ''
|
145
|
+
end
|
146
|
+
|
147
|
+
def repr(indent)
|
148
|
+
repr = '{'
|
149
|
+
@properties.each do |key, property|
|
150
|
+
type_repr = if property.type.respond_to? :repr
|
151
|
+
property.type.repr(indent + ' ')
|
152
|
+
else
|
153
|
+
property.type.to_s
|
154
|
+
end
|
155
|
+
repr += "\n#{indent} #{key}: #{type_repr}"
|
156
|
+
end
|
157
|
+
repr += "\n#{indent}}"
|
158
|
+
repr
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# ObjectProperty is a specific property in an ObjectType.
|
164
|
+
class ObjectProperty
|
165
|
+
attr_reader :type
|
166
|
+
attribute_setter_getter :description
|
167
|
+
attribute_setter_getter :example
|
168
|
+
|
169
|
+
def initialize(type, description = nil)
|
170
|
+
@type = type
|
171
|
+
@description = description
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# ArrayType represents an array type, with a given item type.
|
177
|
+
class ArrayType
|
178
|
+
def initialize(type = nil)
|
179
|
+
@type = type
|
180
|
+
end
|
181
|
+
|
182
|
+
def type(item_type = nil, &block)
|
183
|
+
return @type unless item_type
|
184
|
+
@type = Apigen::Model.type item_type, &block
|
185
|
+
end
|
186
|
+
|
187
|
+
def validate(model_registry)
|
188
|
+
raise 'Use `type [typename]` to specify the type of types in an array.' unless @type
|
189
|
+
model_registry.check_type @type
|
190
|
+
end
|
191
|
+
|
192
|
+
def to_s
|
193
|
+
repr ''
|
194
|
+
end
|
195
|
+
|
196
|
+
def repr(indent)
|
197
|
+
type_repr = if @type.respond_to? :repr
|
198
|
+
@type.repr indent
|
199
|
+
else
|
200
|
+
@type.to_s
|
201
|
+
end
|
202
|
+
"ArrayType<#{type_repr}>"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# OptionalType represents a type whose value may be absent.
|
208
|
+
class OptionalType
|
209
|
+
def initialize(type = nil)
|
210
|
+
@type = type
|
211
|
+
end
|
212
|
+
|
213
|
+
def type(item_type = nil, &block)
|
214
|
+
return @type unless item_type
|
215
|
+
@type = Apigen::Model.type item_type, &block
|
216
|
+
end
|
217
|
+
|
218
|
+
def validate(model_registry)
|
219
|
+
raise 'Use `type [typename]` to specify an optional type.' unless @type
|
220
|
+
model_registry.check_type @type
|
221
|
+
end
|
222
|
+
|
223
|
+
def to_s
|
224
|
+
repr ''
|
225
|
+
end
|
226
|
+
|
227
|
+
def repr(indent)
|
228
|
+
type_repr = if @type.respond_to? :repr
|
229
|
+
@type.repr indent
|
230
|
+
else
|
231
|
+
@type.to_s
|
232
|
+
end
|
233
|
+
"OptionalType<#{type_repr}>"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
data/lib/apigen/rest.rb
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './model'
|
4
|
+
require_relative './util'
|
5
|
+
|
6
|
+
PATH_PARAMETER_REGEX = /\{(\w+)\}/
|
7
|
+
|
8
|
+
module Apigen
|
9
|
+
##
|
10
|
+
# Rest contains what you need to declare a REST-ish API.
|
11
|
+
module Rest
|
12
|
+
##
|
13
|
+
# Declares an API.
|
14
|
+
def self.api(&block)
|
15
|
+
api = Api.new
|
16
|
+
api.instance_eval(&block)
|
17
|
+
api.validate
|
18
|
+
api
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Api is a self-contained definition of a REST API, includings its endpoints and data types.
|
23
|
+
class Api
|
24
|
+
attr_reader :endpoints
|
25
|
+
attribute_setter_getter :description
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@description = ''
|
29
|
+
@endpoints = []
|
30
|
+
@model_registry = Apigen::ModelRegistry.new
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Declares a specific endpoint.
|
35
|
+
def endpoint(name, &block)
|
36
|
+
endpoint = Endpoint.new name
|
37
|
+
@endpoints << endpoint
|
38
|
+
endpoint.instance_eval(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Declares a data model.
|
43
|
+
def model(name, &block)
|
44
|
+
@model_registry.model name, &block
|
45
|
+
end
|
46
|
+
|
47
|
+
def models
|
48
|
+
@model_registry.models
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate
|
52
|
+
@model_registry.validate
|
53
|
+
@endpoints.each do |e|
|
54
|
+
e.validate @model_registry
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
repr = "Endpoints:\n\n"
|
60
|
+
repr += @endpoints.map(&:to_s).join "\n"
|
61
|
+
repr += "\n\nTypes:\n\n"
|
62
|
+
repr += @model_registry.to_s
|
63
|
+
repr
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Endpoint is a definition of a specific endpoint in the API, e.g. /users with GET method.
|
69
|
+
class Endpoint
|
70
|
+
attribute_setter_getter :name
|
71
|
+
attribute_setter_getter :description
|
72
|
+
attr_reader :outputs
|
73
|
+
attr_reader :path_parameters
|
74
|
+
attr_reader :query_parameters
|
75
|
+
|
76
|
+
def initialize(name)
|
77
|
+
@name = name
|
78
|
+
@method = nil
|
79
|
+
@path = nil
|
80
|
+
@path_parameters = Apigen::ObjectType.new
|
81
|
+
@query_parameters = Apigen::ObjectType.new
|
82
|
+
@input = nil
|
83
|
+
@outputs = []
|
84
|
+
@description = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Declares the HTTP method.
|
89
|
+
def method(method = nil)
|
90
|
+
return @method unless method
|
91
|
+
case method
|
92
|
+
when :get, :post, :put, :delete
|
93
|
+
@method = method
|
94
|
+
else
|
95
|
+
raise "Unknown HTTP method :#{method}."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Declares the endpoint path relative to the host.
|
101
|
+
def path(path = nil, &block)
|
102
|
+
return @path unless path
|
103
|
+
@path = path
|
104
|
+
if PATH_PARAMETER_REGEX.match path
|
105
|
+
set_path_parameters(path, &block)
|
106
|
+
elsif block_given?
|
107
|
+
raise 'A path block was provided but no URL parameter was found.'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Declares query parameters.
|
113
|
+
def query(&block)
|
114
|
+
raise 'A block must be passed to define query fields.' unless block_given?
|
115
|
+
@query_parameters.instance_eval(&block)
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Declares the input type of an endpoint.
|
120
|
+
def input(&block)
|
121
|
+
return @input unless block_given?
|
122
|
+
@input = Input.new
|
123
|
+
@input.instance_eval(&block)
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Declares the output of an endpoint for a given status code.
|
128
|
+
def output(name, &block)
|
129
|
+
output = Output.new name
|
130
|
+
@outputs << output
|
131
|
+
output.instance_eval(&block)
|
132
|
+
output
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate(model_registry)
|
136
|
+
validate_properties
|
137
|
+
validate_input(model_registry)
|
138
|
+
validate_path_parameters(model_registry)
|
139
|
+
validate_outputs(model_registry)
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_s
|
143
|
+
repr = "#{@name}: #{@input}"
|
144
|
+
@outputs.each do |output|
|
145
|
+
repr += "\n-> #{output}"
|
146
|
+
end
|
147
|
+
repr
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def set_path_parameters(path, &block)
|
153
|
+
block = {} unless block_given?
|
154
|
+
@path_parameters.instance_eval(&block)
|
155
|
+
expected_parameters = path.scan(PATH_PARAMETER_REGEX).map { |parameter, _| parameter.to_sym }
|
156
|
+
ensure_parameters_all_defined(expected_parameters)
|
157
|
+
end
|
158
|
+
|
159
|
+
def ensure_parameters_all_defined(expected_parameters)
|
160
|
+
expected_parameters.each do |parameter|
|
161
|
+
raise "Path parameter :#{parameter} in path #{@path} is not defined." unless @path_parameters.properties.key? parameter
|
162
|
+
end
|
163
|
+
@path_parameters.properties.each do |parameter, _type|
|
164
|
+
raise "Parameter :#{parameter} does not appear in path #{@path}." unless expected_parameters.include? parameter
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def validate_properties
|
169
|
+
raise 'One of the endpoints is missing a name.' unless @name
|
170
|
+
raise "Use `method :get/post/put/delete` to set an HTTP method for :#{@name}." unless @method
|
171
|
+
raise "Use `path \"/some/path\"` to assign a path to :#{@name}." unless @path
|
172
|
+
end
|
173
|
+
|
174
|
+
def validate_input(model_registry)
|
175
|
+
case @method
|
176
|
+
when :put, :post
|
177
|
+
raise "Use `input { type :typename }` to assign an input type to :#{@name}." unless @input
|
178
|
+
@input.validate(model_registry)
|
179
|
+
when :get
|
180
|
+
raise "Endpoint :#{@name} with method GET cannot accept an input payload." if @input
|
181
|
+
when :delete
|
182
|
+
raise "Endpoint :#{@name} with method DELETE cannot accept an input payload." if @input
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def validate_path_parameters(model_registry)
|
187
|
+
@path_parameters.validate model_registry
|
188
|
+
end
|
189
|
+
|
190
|
+
def validate_outputs(model_registry)
|
191
|
+
raise "Endpoint :#{@name} does not declare any outputs" if @outputs.empty?
|
192
|
+
@outputs.each do |output|
|
193
|
+
output.validate model_registry
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# Input is the request body expected by an API endpoint.
|
200
|
+
class Input
|
201
|
+
attribute_setter_getter :description
|
202
|
+
attribute_setter_getter :example
|
203
|
+
|
204
|
+
def initialize
|
205
|
+
@type = nil
|
206
|
+
@description = nil
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Declares the input type.
|
211
|
+
def type(type = nil, &block)
|
212
|
+
return @type unless type
|
213
|
+
@type = Apigen::Model.type type, &block
|
214
|
+
end
|
215
|
+
|
216
|
+
def validate(model_registry)
|
217
|
+
validate_properties
|
218
|
+
model_registry.check_type @type
|
219
|
+
end
|
220
|
+
|
221
|
+
def to_s
|
222
|
+
@type.to_s
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def validate_properties
|
228
|
+
raise 'Use `type :typename` to assign a type to the input.' unless @type
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
##
|
233
|
+
# Output is the response type associated with a specific status code for an API endpoint.
|
234
|
+
class Output
|
235
|
+
attribute_setter_getter :status
|
236
|
+
attribute_setter_getter :description
|
237
|
+
attribute_setter_getter :example
|
238
|
+
|
239
|
+
def initialize(name)
|
240
|
+
@name = name
|
241
|
+
@status = nil
|
242
|
+
@type = nil
|
243
|
+
@description = nil
|
244
|
+
end
|
245
|
+
|
246
|
+
##
|
247
|
+
# Declares the output type.
|
248
|
+
def type(type = nil, &block)
|
249
|
+
return @type unless type
|
250
|
+
@type = Apigen::Model.type type, &block
|
251
|
+
end
|
252
|
+
|
253
|
+
def validate(model_registry)
|
254
|
+
validate_properties
|
255
|
+
model_registry.check_type @type
|
256
|
+
end
|
257
|
+
|
258
|
+
def to_s
|
259
|
+
"#{@name} #{@status} #{@type}"
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
def validate_properties
|
265
|
+
raise 'One of the outputs is missing a name.' unless @name
|
266
|
+
raise "Use `status [code]` to assign a status code to :#{@name}." unless @status
|
267
|
+
raise "Use `type :typename` to assign a type to :#{@name}." unless @type
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
data/lib/apigen/util.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Creates a setter/getter method for :attribute.
|
5
|
+
#
|
6
|
+
# attribute_setter_getter :name
|
7
|
+
# is equivalent to:
|
8
|
+
# def name value = nil
|
9
|
+
# if value.nil?
|
10
|
+
# @name
|
11
|
+
# else
|
12
|
+
# @name = value
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
def attribute_setter_getter(attribute)
|
16
|
+
define_method attribute.to_s.to_sym do |value = nil|
|
17
|
+
if value.nil?
|
18
|
+
instance_variable_get "@#{attribute}"
|
19
|
+
else
|
20
|
+
instance_variable_set "@#{attribute}", value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apigen
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Francois Wouts
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A simple DSL to generate OpenAPI and/or JSON Schema definitions in Ruby.
|
14
|
+
email: f@zenc.io
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/apigen/formats/jsonschema.rb
|
20
|
+
- lib/apigen/formats/openapi.rb
|
21
|
+
- lib/apigen/formats/swagger.rb
|
22
|
+
- lib/apigen/model.rb
|
23
|
+
- lib/apigen/rest.rb
|
24
|
+
- lib/apigen/util.rb
|
25
|
+
homepage: https://rubygems.org/gems/apigen
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 2.7.6
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: OpenAPI spec generator
|
49
|
+
test_files: []
|