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.
@@ -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: []