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.
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