graphiti-openapi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.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
|