graphiti-openapi 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +16 -0
- data/MIT-LICENSE +20 -0
- data/README.md +45 -0
- data/Rakefile +20 -0
- data/app/assets/packs/api.js +27 -0
- data/app/controllers/graphiti/open_api/application_controller.rb +7 -0
- data/app/controllers/graphiti/open_api/specifications_controller.rb +21 -0
- data/app/helpers/graphiti/open_api/application_helper.rb +6 -0
- data/app/models/graphiti/open_api/action.rb +130 -0
- data/app/models/graphiti/open_api/attribute.rb +40 -0
- data/app/models/graphiti/open_api/endpoint.rb +72 -0
- data/app/models/graphiti/open_api/functions.rb +16 -0
- data/app/models/graphiti/open_api/generator.rb +174 -0
- data/app/models/graphiti/open_api/parameters.rb +30 -0
- data/app/models/graphiti/open_api/relationship.rb +65 -0
- data/app/models/graphiti/open_api/resource.rb +322 -0
- data/app/models/graphiti/open_api/schema.rb +32 -0
- data/app/models/graphiti/open_api/source.rb +27 -0
- data/app/models/graphiti/open_api/struct.rb +12 -0
- data/app/models/graphiti/open_api/type.rb +38 -0
- data/app/models/graphiti/open_api/types.rb +10 -0
- data/app/views/graphiti/open_api/specifications/index.html.erb +6 -0
- data/app/views/graphiti/open_api/specifications/index.yaml.erb +1 -0
- data/app/views/layouts/graphiti/open_api/application.html.erb +12 -0
- data/bin/console +6 -0
- data/bin/rails +26 -0
- data/bin/setup +8 -0
- data/bin/webpack +20 -0
- data/config/openapi.yml +66 -0
- data/config/routes.rb +5 -0
- data/graphiti-openapi.gemspec +48 -0
- data/lib/graphiti-openapi.rb +1 -0
- data/lib/graphiti/open_api.rb +15 -0
- data/lib/graphiti/open_api/engine.rb +16 -0
- data/lib/graphiti/open_api/version.rb +5 -0
- data/lib/tasks/graphiti_openapi.rake +33 -0
- data/lib/templates/installer.rb +20 -0
- metadata +353 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
require "graphiti/open_api"
|
2
|
+
require_relative "struct"
|
3
|
+
require_relative "action"
|
4
|
+
|
5
|
+
module Graphiti::OpenAPI
|
6
|
+
class EndpointData < Struct
|
7
|
+
attribute :actions, Types::Hash.map(Types::Symbol, ActionData)
|
8
|
+
|
9
|
+
def actions
|
10
|
+
Actions.load(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
memoize :actions
|
14
|
+
end
|
15
|
+
|
16
|
+
class Endpoint < EndpointData
|
17
|
+
attribute :schema, Types::Any
|
18
|
+
attribute :path, Types::Coercible::String
|
19
|
+
|
20
|
+
def resource_path
|
21
|
+
File.join(path.to_s, "{id}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def paths
|
25
|
+
{
|
26
|
+
path => {
|
27
|
+
parameters: parameters,
|
28
|
+
}.merge(collection_actions.map(&:operation).inject(&:merge)),
|
29
|
+
resource_path => {
|
30
|
+
parameters: [{'$ref': "#/components/parameters/#{resource.type}_id"}] + parameters,
|
31
|
+
}.merge(resource_actions.map(&:operation).inject(&:merge)),
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def parameters
|
36
|
+
[].tap do |parameters|
|
37
|
+
parameters << {'$ref': "#/components/parameters/#{type}_include"} if resource.relationships?
|
38
|
+
parameters << {'$ref': "#/components/parameters/#{type}_sort"}
|
39
|
+
parameters << {'$ref': "#/components/parameters/#{type}_fields"}
|
40
|
+
resource.relationships.values.map do |relationship|
|
41
|
+
relationship.resources.each do |resource|
|
42
|
+
parameters << {'$ref': "#/components/parameters/#{resource.type}_fields"}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end.uniq
|
46
|
+
end
|
47
|
+
|
48
|
+
def resource
|
49
|
+
resource_actions.first.resource
|
50
|
+
end
|
51
|
+
|
52
|
+
def_instance_delegators :resource, :type
|
53
|
+
|
54
|
+
def resource_actions
|
55
|
+
actions.reject(&:collection?)
|
56
|
+
end
|
57
|
+
|
58
|
+
def collection_actions
|
59
|
+
actions.select(&:collection?)
|
60
|
+
end
|
61
|
+
|
62
|
+
memoize :resource_path, :paths, :parameters, :resource, :resource_actions, :collection_actions
|
63
|
+
end
|
64
|
+
|
65
|
+
class Endpoints < Hash
|
66
|
+
def self.load(schema, data: schema.__attributes__[:endpoints])
|
67
|
+
data.each_with_object({}) do |(path, data), result|
|
68
|
+
result[path] = Endpoint.new(data.to_hash.merge(schema: schema, path: path))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "graphiti/open_api"
|
2
|
+
require "transproc"
|
3
|
+
require "transproc/recursion"
|
4
|
+
|
5
|
+
module Graphiti::OpenAPI
|
6
|
+
module Functions
|
7
|
+
extend Transproc::Registry
|
8
|
+
|
9
|
+
import Transproc::HashTransformations
|
10
|
+
import Transproc::Recursion
|
11
|
+
|
12
|
+
def self.deep_reject_keys(hash, keys)
|
13
|
+
t(:hash_recursion, t(:reject_keys, keys))[hash]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require "graphiti/open_api"
|
2
|
+
require "forwardable"
|
3
|
+
require "dry/core/memoizable"
|
4
|
+
require_relative "functions"
|
5
|
+
require_relative "schema"
|
6
|
+
|
7
|
+
module Graphiti::OpenAPI
|
8
|
+
class Generator
|
9
|
+
extend Forwardable
|
10
|
+
include Dry::Core::Memoizable
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
root: Rails.root,
|
14
|
+
schema: root.join("public#{ApplicationResource.endpoint_namespace}").join("schema.json"),
|
15
|
+
jsonapi: root.join("public/schemas/jsonapi.json"),
|
16
|
+
template: root.join("config/openapi.yml"))
|
17
|
+
@root = Pathname(root)
|
18
|
+
@schema_path = schema
|
19
|
+
@jsonapi_path = jsonapi
|
20
|
+
@template_path = template
|
21
|
+
end
|
22
|
+
|
23
|
+
# @!attribute [r] root
|
24
|
+
# @return [Pathname]
|
25
|
+
# @!attribute [r] schema
|
26
|
+
# @return [{String => Source}]
|
27
|
+
attr_reader :root
|
28
|
+
|
29
|
+
def schema
|
30
|
+
Schema.new(schema_source.data)
|
31
|
+
end
|
32
|
+
|
33
|
+
def_instance_delegators :schema,
|
34
|
+
:endpoints, :resources, :types,
|
35
|
+
:resource
|
36
|
+
|
37
|
+
def schema_source(path = @schema_path)
|
38
|
+
Source.load(path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def template_source(path = @template_path)
|
42
|
+
Source.load(path, parse: YAML.method(:safe_load))
|
43
|
+
end
|
44
|
+
|
45
|
+
def paths
|
46
|
+
@paths ||=
|
47
|
+
endpoints.values.map(&:paths).inject(&:merge)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_openapi(format: :json)
|
51
|
+
template = template_source.data
|
52
|
+
data = {
|
53
|
+
openapi: "3.0.1",
|
54
|
+
servers: [{url: Rails.application.routes.default_url_options[:host], description: "#{Rails.env} server"}],
|
55
|
+
tags: tags,
|
56
|
+
paths: paths,
|
57
|
+
components: {
|
58
|
+
schemas: schemas,
|
59
|
+
parameters: parameters,
|
60
|
+
requestBodies: request_bodies,
|
61
|
+
responses: responses,
|
62
|
+
links: links,
|
63
|
+
},
|
64
|
+
}
|
65
|
+
specification = Functions[:deep_merge][template, data]
|
66
|
+
case format
|
67
|
+
when :json
|
68
|
+
specification
|
69
|
+
when :yaml
|
70
|
+
json = specification.to_json
|
71
|
+
JSON.parse(json).to_yaml
|
72
|
+
else
|
73
|
+
raise ArgumentError, "Unknown format: `#{format.inspect}`"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def tags
|
80
|
+
resources.values.map do |resource|
|
81
|
+
{
|
82
|
+
name: resource.type,
|
83
|
+
description: "#{resource.human} operations",
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def request_bodies
|
89
|
+
resources.values.each_with_object({}) do |resource, result|
|
90
|
+
result[resource.type] = {
|
91
|
+
required: true,
|
92
|
+
content: {
|
93
|
+
"application/vnd.api+json" => {schema: {"$ref": "#/components/schemas/#{resource.type}_request"}},
|
94
|
+
# "application/xml" => {schema: {"$ref": "#/components/schemas/#{resource.type}_request"}},
|
95
|
+
},
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def responses
|
101
|
+
resources.values.map(&:to_responses).inject(&:merge).compact
|
102
|
+
end
|
103
|
+
|
104
|
+
def links
|
105
|
+
resources.values.map(&:to_links).inject(&:merge).compact
|
106
|
+
end
|
107
|
+
|
108
|
+
def schemas
|
109
|
+
{types: {type: :string, enum: resources.values.map(&:type)}}
|
110
|
+
.merge(resources.values.map(&:to_schema).inject(&:merge))
|
111
|
+
.merge(jsonapi_definitions)
|
112
|
+
end
|
113
|
+
|
114
|
+
def parameters
|
115
|
+
resources.values.map(&:to_parameters).inject(&:merge)
|
116
|
+
end
|
117
|
+
|
118
|
+
REWRITE_JSONAPI_SCHEMA = -> (text) do
|
119
|
+
text
|
120
|
+
.gsub("/definitions/", "/components/schemas/jsonapi_")
|
121
|
+
.gsub(%r'"type": "null"', '"nullable": true')
|
122
|
+
# .gsub(%r',\s*{\s*"type": "null"\s*}\s*', ', "nullable": true')
|
123
|
+
end
|
124
|
+
|
125
|
+
PROCESS_JSONAPI_SCHEMA = Source::DEFAULT_PROCESS >>
|
126
|
+
# Reject unsupported schema properties
|
127
|
+
Functions[:deep_reject_keys,
|
128
|
+
%i[$schema additionalItems const contains dependencies id $id patternProperties propertyNames]
|
129
|
+
]
|
130
|
+
|
131
|
+
PREFIX_JSONAPI_DEFINITIONS = Functions[:map_keys, -> (key) { "jsonapi_#{key}" }]
|
132
|
+
|
133
|
+
def jsonapi_source(path = @jsonapi_path)
|
134
|
+
Source.load(path, rewrite: REWRITE_JSONAPI_SCHEMA, process: PROCESS_JSONAPI_SCHEMA)
|
135
|
+
end
|
136
|
+
|
137
|
+
def jsonapi_definitions
|
138
|
+
defs = jsonapi_source.data[:definitions]
|
139
|
+
defs[:jsonapi][:properties][:version][:example] = "1.0"
|
140
|
+
|
141
|
+
# Provide meaningful linkages in examples
|
142
|
+
variants = defs[:relationshipToOne].delete(:anyOf)
|
143
|
+
variants = variants.keep_if { |item| item[:$ref] !~ /empty/ }
|
144
|
+
defs[:relationshipToOne][:oneOf] = variants + [nullable: true]
|
145
|
+
|
146
|
+
# Provide real types and id examples
|
147
|
+
%i[resource linkage].each do |schema|
|
148
|
+
defs[schema][:properties][:id] = {type: :string, example: rand(100).to_s}
|
149
|
+
defs[schema][:properties][:type] = {'$ref': "#/components/schemas/types"}
|
150
|
+
end
|
151
|
+
|
152
|
+
# Use real resources in examples
|
153
|
+
defs[:resource] = {
|
154
|
+
oneOf: resources.values.map { |resource| {'$ref': "#/components/schemas/#{resource.type}_resource"} },
|
155
|
+
}
|
156
|
+
|
157
|
+
# Fix OpenAPI and JSON Schema differences
|
158
|
+
defs[:relationshipLinks][:properties][:self].delete(:description)
|
159
|
+
|
160
|
+
# Hide meta & links
|
161
|
+
%i[meta links relationshipLinks].each do |schema|
|
162
|
+
original = defs.delete(schema)
|
163
|
+
defs[schema] = {oneOf: [{nullable: true}, original]}
|
164
|
+
end
|
165
|
+
|
166
|
+
# Remove unused elements
|
167
|
+
%i[attributes empty].each { |schema| defs.delete(schema) }
|
168
|
+
|
169
|
+
PREFIX_JSONAPI_DEFINITIONS[defs]
|
170
|
+
end
|
171
|
+
|
172
|
+
memoize :jsonapi_source, :jsonapi_definitions, :schema_source, :schema, :template_source
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "graphiti/open_api"
|
2
|
+
|
3
|
+
module Graphiti::OpenAPI
|
4
|
+
module Parameters
|
5
|
+
def parameter(name, desc: nil, **options)
|
6
|
+
options.merge(name: name).tap do |parameter|
|
7
|
+
parameter[:description] = desc if desc
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def query_parameter(name, **options)
|
12
|
+
parameter(name, in: :query, **options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def path_parameter(name, required: true, **options)
|
16
|
+
parameter(name, in: :path, required: required, **options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def array_enum(enum, type: :string, uniq: true)
|
20
|
+
{
|
21
|
+
type: :array,
|
22
|
+
items: {
|
23
|
+
type: type,
|
24
|
+
enum: enum,
|
25
|
+
uniqueItems: uniq,
|
26
|
+
},
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "graphiti/open_api"
|
2
|
+
require_relative "struct"
|
3
|
+
|
4
|
+
module Graphiti::OpenAPI
|
5
|
+
class RelationshipData < Struct
|
6
|
+
attribute :type, Types::String
|
7
|
+
attribute :description, Types::String.optional
|
8
|
+
attribute? :resource, Types::String.optional
|
9
|
+
attribute? :resources, Types.Array(Types::String).default([])
|
10
|
+
end
|
11
|
+
|
12
|
+
class Relationship < RelationshipData
|
13
|
+
attribute :origin, Types::Any
|
14
|
+
attribute :name, Types::Symbol
|
15
|
+
|
16
|
+
def_instance_delegator :origin, :schema
|
17
|
+
|
18
|
+
def resource
|
19
|
+
return unless __attributes__[:resource]
|
20
|
+
schema.resources[__attributes__[:resource]]
|
21
|
+
end
|
22
|
+
|
23
|
+
def resources
|
24
|
+
return [resource] if __attributes__[:resources].empty?
|
25
|
+
__attributes__[:resources].map { |resource| schema.resources[resource] }
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_schema
|
29
|
+
{
|
30
|
+
name => {
|
31
|
+
type: :object,
|
32
|
+
properties: {
|
33
|
+
links: {"$ref": "#/components/schemas/jsonapi_relationshipLinks"},
|
34
|
+
data: {'$ref': "#/components/schemas/#{jsonapi_relationship}"},
|
35
|
+
meta: {"$ref": "#/components/schemas/jsonapi_meta"},
|
36
|
+
},
|
37
|
+
},
|
38
|
+
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def jsonapi_relationship
|
43
|
+
case type
|
44
|
+
when /belongs_to/, "has_one"
|
45
|
+
"jsonapi_relationshipToOne"
|
46
|
+
else
|
47
|
+
"jsonapi_relationshipToMany"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
memoize :resource, :resources
|
52
|
+
end
|
53
|
+
|
54
|
+
class Relationships < Hash
|
55
|
+
def self.load(resource, data = resource.__attributes__[:relationships])
|
56
|
+
data.each_with_object(new) do |(name, data), result|
|
57
|
+
result[name] = Relationship.new(data.to_hash.merge(name: name, origin: resource))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def resources
|
62
|
+
values.map(&:resources).flatten.uniq.compact
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,322 @@
|
|
1
|
+
require "graphiti/open_api"
|
2
|
+
require "active_model"
|
3
|
+
require_relative "struct"
|
4
|
+
require_relative "attribute"
|
5
|
+
require_relative "relationship"
|
6
|
+
|
7
|
+
module Graphiti::OpenAPI
|
8
|
+
class ResourceData < Struct
|
9
|
+
attribute :name, Types::String
|
10
|
+
attribute :type, Types::String
|
11
|
+
attribute :description, Types::String.optional
|
12
|
+
attribute :attributes, Types::Hash.map(Types::Symbol, AttributeData)
|
13
|
+
attribute :extra_attributes, Types::Hash.map(Types::Symbol, AttributeData)
|
14
|
+
attribute :sorts, Types::Hash.map(Types::Symbol, Types::Hash)
|
15
|
+
attribute :filters, Types::Hash.map(Types::Symbol, Types::Hash)
|
16
|
+
attribute :relationships, Types::Hash.map(Types::Symbol, RelationshipData)
|
17
|
+
|
18
|
+
def relationships
|
19
|
+
Relationships.load(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def relationships?
|
23
|
+
relationships.any?
|
24
|
+
end
|
25
|
+
|
26
|
+
memoize :relationships
|
27
|
+
end
|
28
|
+
|
29
|
+
class Resource < ResourceData
|
30
|
+
include Parameters
|
31
|
+
|
32
|
+
attribute :schema, Types::Any
|
33
|
+
|
34
|
+
def model_name
|
35
|
+
ActiveModel::Name.new(self.class, nil, name.gsub(/Resource/, ""))
|
36
|
+
end
|
37
|
+
|
38
|
+
def_instance_delegators :model_name, :human, :singular, :plural
|
39
|
+
|
40
|
+
def plural_human(**options)
|
41
|
+
human(**options).pluralize
|
42
|
+
end
|
43
|
+
|
44
|
+
def attributes
|
45
|
+
Attributes.load(self)
|
46
|
+
end
|
47
|
+
|
48
|
+
def extra_attributes
|
49
|
+
Attributes.load(self, __attributes__[:extra_attributes])
|
50
|
+
end
|
51
|
+
|
52
|
+
def all_attributes
|
53
|
+
attributes.merge(extra_attributes)
|
54
|
+
end
|
55
|
+
|
56
|
+
def resource_attributes
|
57
|
+
all_attributes.except(:id).values
|
58
|
+
end
|
59
|
+
|
60
|
+
def readable_attributes
|
61
|
+
all_attributes.values.select(&:readable)
|
62
|
+
end
|
63
|
+
|
64
|
+
def sortable_attributes
|
65
|
+
sortable_attribute_names = sorts.keys
|
66
|
+
all_attributes.values.select { |attribute| sortable_attribute_names.include?(attribute.name) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def writable_attributes
|
70
|
+
all_attributes.values.select(&:writable)
|
71
|
+
end
|
72
|
+
|
73
|
+
def query_parameters
|
74
|
+
[].tap do |result|
|
75
|
+
result << query_include_parameter
|
76
|
+
result << query_fields_parameter
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def query_fields_parameter
|
81
|
+
schema = {
|
82
|
+
description: "#{human} readable attributes list",
|
83
|
+
type: :array,
|
84
|
+
items: {'$ref': "#/components/schemas/#{type}_readable_attribute"},
|
85
|
+
uniqueItems: true,
|
86
|
+
}
|
87
|
+
|
88
|
+
query_parameter("fields[#{type}]",
|
89
|
+
desc: "[Include only specified fields of #{human} in response](https://jsonapi.org/format/#fetching-sparse-fieldsets)",
|
90
|
+
schema: schema,
|
91
|
+
explode: false)
|
92
|
+
end
|
93
|
+
|
94
|
+
def query_include_parameter
|
95
|
+
return unless relationships?
|
96
|
+
|
97
|
+
query_parameter(:include, desc: "[Include related resources](https://jsonapi.org/format/#fetching-includes)", schema: {'$ref': "#/components/schemas/#{type}_related"}, explode: false)
|
98
|
+
end
|
99
|
+
|
100
|
+
def query_sort_parameter(relationships: false)
|
101
|
+
return unless sorts.any?
|
102
|
+
orderings = sorts.keys.map { |id| %W[#{id} -#{id}] }.flatten
|
103
|
+
if relationships
|
104
|
+
relationships.each do |name, relationship|
|
105
|
+
relationship.resources.each do |resource|
|
106
|
+
orderings += resource.sorts.keys.map { |id| %W[#{name}.#{id} -#{name}.#{id}] }.flatten
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
query_parameter(:sort,
|
111
|
+
desc: "[Sort #{model_name.plural} according to one or more criteria](https://jsonapi.org/format/#fetching-sorting)\n\n" \
|
112
|
+
"You should not include both ascending `id` and descending `-id` fields the same time\n\n",
|
113
|
+
schema: {"$ref" => "#/components/schemas/#{type}_sortable_attributes_list"}, explode: false)
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_parameters
|
117
|
+
{
|
118
|
+
"#{type}_id": path_parameter(:id, schema: {type: :string}, desc: "ID of the resource"),
|
119
|
+
"#{type}_include": query_include_parameter,
|
120
|
+
"#{type}_fields": query_fields_parameter,
|
121
|
+
"#{type}_sort": query_sort_parameter,
|
122
|
+
}.keep_if { |name, value| value }
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_schema
|
126
|
+
attributes_schema
|
127
|
+
.merge(relationships_schema)
|
128
|
+
.merge(resource_schema)
|
129
|
+
.merge(response_schema)
|
130
|
+
.merge(request_schema)
|
131
|
+
.merge(attribute_schemas)
|
132
|
+
end
|
133
|
+
|
134
|
+
def attributes_schema
|
135
|
+
{
|
136
|
+
type => {
|
137
|
+
type: :object,
|
138
|
+
description: "#{human} attributes",
|
139
|
+
properties: resource_attributes.map(&:to_property).inject(&:merge),
|
140
|
+
additionalProperties: false,
|
141
|
+
},
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
def relationships_schema
|
146
|
+
schema_name = "#{type}_relationships"
|
147
|
+
return {schema_name => {'$ref': "#/components/schemas/jsonapi_relationships"}} unless relationships?
|
148
|
+
{
|
149
|
+
schema_name => {
|
150
|
+
type: :object,
|
151
|
+
properties: relationships.values.map(&:to_schema).inject(&:merge),
|
152
|
+
additionalProperties: false,
|
153
|
+
},
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
def resource_schema
|
158
|
+
{
|
159
|
+
"#{type}_resource" => {
|
160
|
+
type: :object,
|
161
|
+
properties: {
|
162
|
+
id: {type: :string, example: rand(100).to_s},
|
163
|
+
type: {type: :string, enum: [type]},
|
164
|
+
attributes: {'$ref': "#/components/schemas/#{type}"},
|
165
|
+
relationships: {'$ref': "#/components/schemas/#{type}_relationships"},
|
166
|
+
links: {'$ref': "#/components/schemas/jsonapi_links"},
|
167
|
+
},
|
168
|
+
additionalProperties: false,
|
169
|
+
},
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
def response_schema
|
174
|
+
{
|
175
|
+
"#{type}_single" => {
|
176
|
+
type: :object,
|
177
|
+
allOf: [
|
178
|
+
{'$ref': "#/components/schemas/jsonapi_success"},
|
179
|
+
{
|
180
|
+
type: :object,
|
181
|
+
properties: {
|
182
|
+
data: {'$ref': "#/components/schemas/#{type}_resource"},
|
183
|
+
},
|
184
|
+
},
|
185
|
+
],
|
186
|
+
},
|
187
|
+
"#{type}_collection" => {
|
188
|
+
type: :object,
|
189
|
+
allOf: [
|
190
|
+
{'$ref': "#/components/schemas/jsonapi_success"},
|
191
|
+
{
|
192
|
+
type: :object,
|
193
|
+
properties: {
|
194
|
+
data: {
|
195
|
+
type: :array,
|
196
|
+
items: {"$ref" => "#/components/schemas/#{type}_resource"},
|
197
|
+
},
|
198
|
+
},
|
199
|
+
},
|
200
|
+
],
|
201
|
+
},
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
def request_schema
|
206
|
+
{
|
207
|
+
"#{type}_request" => {
|
208
|
+
type: :object,
|
209
|
+
properties: {
|
210
|
+
data: {'$ref': "#/components/schemas/#{type}_resource"},
|
211
|
+
},
|
212
|
+
# xml: {name: :data},
|
213
|
+
},
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
def attribute_schemas
|
218
|
+
types = {
|
219
|
+
"#{type}_readable_attribute" => {
|
220
|
+
description: "#{human} readable attributes",
|
221
|
+
type: :string,
|
222
|
+
enum: readable_attributes.map(&:name),
|
223
|
+
},
|
224
|
+
"#{type}_sortable_attributes_list" => {
|
225
|
+
description: "#{human} sortable attributes",
|
226
|
+
type: :array,
|
227
|
+
items: {
|
228
|
+
type: :string,
|
229
|
+
enum: sortable_attribute_names.map { |name| %W[#{name} -#{name}] }.flatten,
|
230
|
+
},
|
231
|
+
uniqueItems: true,
|
232
|
+
},
|
233
|
+
"#{type}_related" => {
|
234
|
+
description: "#{human} relationships available for inclusion",
|
235
|
+
type: :array,
|
236
|
+
items: {type: :string},
|
237
|
+
uniqueItems: true,
|
238
|
+
},
|
239
|
+
|
240
|
+
}
|
241
|
+
if relationship_names.any?
|
242
|
+
types["#{type}_related"][:items][:enum] = relationship_names
|
243
|
+
else
|
244
|
+
types["#{type}_related"][:nullable] = true
|
245
|
+
end
|
246
|
+
types
|
247
|
+
end
|
248
|
+
|
249
|
+
def to_responses
|
250
|
+
{
|
251
|
+
"#{type}_200" => {
|
252
|
+
description: "OK: #{human} resource",
|
253
|
+
content: {
|
254
|
+
"application/vnd.api+json" => {schema: {"$ref": "#/components/schemas/#{type}_single"}},
|
255
|
+
# "application/xml" => {schema: {"$ref": "#/components/schemas/#{type}_single"}},
|
256
|
+
},
|
257
|
+
links: link_refs,
|
258
|
+
},
|
259
|
+
"#{type}_200_collection" => {
|
260
|
+
description: "OK: #{plural_human} collection",
|
261
|
+
content: {
|
262
|
+
"application/vnd.api+json" => {schema: {"$ref": "#/components/schemas/#{type}_collection"}},
|
263
|
+
# "application/xml" => {schema: {"$ref": "#/components/schemas/#{type}_collection"}},
|
264
|
+
},
|
265
|
+
},
|
266
|
+
"#{type}_201" => {
|
267
|
+
description: "Created",
|
268
|
+
content: {
|
269
|
+
"application/vnd.api+json" => {schema: {"$ref": "#/components/schemas/#{type}_single"}},
|
270
|
+
# "application/xml" => {schema: {"$ref": "#/components/schemas/#{type}_single"}},
|
271
|
+
},
|
272
|
+
links: link_refs,
|
273
|
+
},
|
274
|
+
}
|
275
|
+
end
|
276
|
+
|
277
|
+
def to_links
|
278
|
+
%i[get update delete].inject({}) do |result, method|
|
279
|
+
operation_id = "#{method}_#{model_name.singular}".camelize(:lower)
|
280
|
+
result.merge(
|
281
|
+
"#{operation_id}Id": {
|
282
|
+
operationId: operation_id,
|
283
|
+
parameters: {id: "$response.body#/data/id"},
|
284
|
+
},
|
285
|
+
)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
memoize :model_name, :attributes, :query_parameters, :to_schema, :to_responses, :to_links
|
290
|
+
|
291
|
+
private
|
292
|
+
|
293
|
+
def link_refs
|
294
|
+
to_links.keys.inject({}) { |result, link| result.merge(link => {'$ref': "#/components/links/#{link}"}) }
|
295
|
+
end
|
296
|
+
|
297
|
+
def relationship_names
|
298
|
+
relationships.keys
|
299
|
+
end
|
300
|
+
|
301
|
+
def sortable_attribute_names
|
302
|
+
sortable_attributes.map(&:name)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
class Resources < Hash
|
307
|
+
# @param [<ResourceData>]
|
308
|
+
def self.load(schema, data = schema.__attributes__[:resources])
|
309
|
+
data.each_with_object(new) do |resource, result|
|
310
|
+
result[resource.name] = Resource.new(resource.to_hash.merge(schema: schema))
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def by_model(model)
|
315
|
+
fetch("#{model}Resource")
|
316
|
+
end
|
317
|
+
|
318
|
+
def by_type(type)
|
319
|
+
values.detect { |resource| resource.type = type }
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|