apigen 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -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: []