graphiti-openapi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +10 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +7 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +16 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.md +45 -0
  10. data/Rakefile +20 -0
  11. data/app/assets/packs/api.js +27 -0
  12. data/app/controllers/graphiti/open_api/application_controller.rb +7 -0
  13. data/app/controllers/graphiti/open_api/specifications_controller.rb +21 -0
  14. data/app/helpers/graphiti/open_api/application_helper.rb +6 -0
  15. data/app/models/graphiti/open_api/action.rb +130 -0
  16. data/app/models/graphiti/open_api/attribute.rb +40 -0
  17. data/app/models/graphiti/open_api/endpoint.rb +72 -0
  18. data/app/models/graphiti/open_api/functions.rb +16 -0
  19. data/app/models/graphiti/open_api/generator.rb +174 -0
  20. data/app/models/graphiti/open_api/parameters.rb +30 -0
  21. data/app/models/graphiti/open_api/relationship.rb +65 -0
  22. data/app/models/graphiti/open_api/resource.rb +322 -0
  23. data/app/models/graphiti/open_api/schema.rb +32 -0
  24. data/app/models/graphiti/open_api/source.rb +27 -0
  25. data/app/models/graphiti/open_api/struct.rb +12 -0
  26. data/app/models/graphiti/open_api/type.rb +38 -0
  27. data/app/models/graphiti/open_api/types.rb +10 -0
  28. data/app/views/graphiti/open_api/specifications/index.html.erb +6 -0
  29. data/app/views/graphiti/open_api/specifications/index.yaml.erb +1 -0
  30. data/app/views/layouts/graphiti/open_api/application.html.erb +12 -0
  31. data/bin/console +6 -0
  32. data/bin/rails +26 -0
  33. data/bin/setup +8 -0
  34. data/bin/webpack +20 -0
  35. data/config/openapi.yml +66 -0
  36. data/config/routes.rb +5 -0
  37. data/graphiti-openapi.gemspec +48 -0
  38. data/lib/graphiti-openapi.rb +1 -0
  39. data/lib/graphiti/open_api.rb +15 -0
  40. data/lib/graphiti/open_api/engine.rb +16 -0
  41. data/lib/graphiti/open_api/version.rb +5 -0
  42. data/lib/tasks/graphiti_openapi.rake +33 -0
  43. data/lib/templates/installer.rb +20 -0
  44. 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