Skip to content

allow to send fields and/or includes on create/update resource #285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ results = Article.includes(:author, :comments => :author).find(1)

# should not have to make additional requests to the server
authors = results.map(&:author)

# makes POST request to /articles?include=author,comments.author
article = Article.new(title: 'New one').request_includes(:author, :comments => :author)
article.save

# makes PATCH request to /articles/1?include=author,comments.author
article = Article.find(1)
article.title = 'Changed'
article.request_includes(:author, :comments => :author)
article.save

# request includes will be cleared if response is successful
# to avoid this `keep_request_params` class attribute can be used
Article.keep_request_params = true

# to clear request_includes use
article.reset_request_includes!
```

## Sparse Fieldsets
Expand All @@ -217,6 +234,24 @@ article.created_at
# or you can use fieldsets from multiple resources
# makes request to /articles?fields[articles]=title,body&fields[comments]=tag
article = Article.select("title", "body",{comments: 'tag'}).first

# makes POST request to /articles?fields[articles]=title,body&fields[comments]=tag
article = Article.new(title: 'New one').request_select(:title, :body, comments: 'tag')
article.save

# makes PATCH request to /articles/1?fields[articles]=title,body&fields[comments]=tag
article = Article.find(1)
article.title = 'Changed'
article.request_select(:title, :body, comments: 'tag')
article.save

# request fields will be cleared if response is successful
# to avoid this `keep_request_params` class attribute can be used
Article.keep_request_params = true

# to clear request fields use
article.reset_request_select!(:comments) # to clear for comments
article.reset_request_select! # to clear for all fields
```

## Sorting
Expand Down
1 change: 1 addition & 0 deletions lib/json_api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module JsonApiClient
autoload :Paginating, 'json_api_client/paginating'
autoload :Parsers, 'json_api_client/parsers'
autoload :Query, 'json_api_client/query'
autoload :RequestParams, 'json_api_client/request_params'
autoload :Resource, 'json_api_client/resource'
autoload :ResultSet, 'json_api_client/result_set'
autoload :Schema, 'json_api_client/schema'
Expand Down
6 changes: 4 additions & 2 deletions lib/json_api_client/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ def delete(middleware)
faraday.builder.delete(middleware)
end

def run(request_method, path, params = {}, headers = {})
faraday.send(request_method, path, params, headers)
def run(request_method, path, params: nil, headers: {}, body: nil)
faraday.run_request(request_method, path, body, headers) do |request|
request.params.update(params) if params
end
end

end
Expand Down
17 changes: 1 addition & 16 deletions lib/json_api_client/query/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,22 +159,7 @@ def select_params
end

def parse_related_links(*tables)
tables.map do |table|
case table
when Hash
table.map do |k, v|
parse_related_links(*v).map do |sub|
"#{k}.#{sub}"
end
end
when Array
table.map do |v|
parse_related_links(*v)
end
else
key_formatter.format(table)
end
end.flatten
Utils.parse_includes(klass, *tables)
end

def parse_orders(*args)
Expand Down
22 changes: 13 additions & 9 deletions lib/json_api_client/query/requestor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,39 @@ def initialize(klass)
# expects a record
def create(record)
request(:post, klass.path(record.attributes), {
data: record.as_json_api
body: { data: record.as_json_api },
params: record.request_params.to_params
})
end

def update(record)
request(:patch, resource_path(record.attributes), {
data: record.as_json_api
body: { data: record.as_json_api },
params: record.request_params.to_params
})
end

def get(params = {})
path = resource_path(params)
params.delete(klass.primary_key)
request(:get, path, params)
request(:get, path, params: params)
end

def destroy(record)
request(:delete, resource_path(record.attributes), {})
request(:delete, resource_path(record.attributes))
end

def linked(path)
request(:get, path, {})
request(:get, path)
end

def custom(method_name, options, params)
path = resource_path(params)
params.delete(klass.primary_key)
path = File.join(path, method_name.to_s)

request(options.fetch(:request_method, :get), path, params)
request_method = options.fetch(:request_method, :get).to_sym
query_params, body_params = [:get, :delete].include?(request_method) ? [params, nil] : [nil, params]
request(request_method, path, params: query_params, body: body_params)
end

protected
Expand All @@ -56,8 +59,9 @@ def resource_path(parameters)
end
end

def request(type, path, params)
klass.parser.parse(klass, connection.run(type, path, params, klass.custom_headers))
def request(type, path, params: nil, body: nil)
response = connection.run(type, path, params: params, body: body, headers: klass.custom_headers)
klass.parser.parse(klass, response)
end

end
Expand Down
57 changes: 57 additions & 0 deletions lib/json_api_client/request_params.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module JsonApiClient
class RequestParams
attr_reader :klass, :includes, :fields

def initialize(klass, includes: [], fields: {})
@klass = klass
@includes = includes
@fields = fields
end

def add_includes(includes)
Utils.parse_includes(klass, *includes).each do |name|
name = name.to_sym
self.includes.push(name) unless self.includes.include?(name)
end
end

def reset_includes!
@includes = []
end

def set_fields(type, field_names)
self.fields[type.to_sym] = field_names.map(&:to_sym)
end

def remove_fields(type)
self.fields.delete(type.to_sym)
end

def field_types
self.fields.keys
end

def clear
reset_includes!
@fields = {}
end

def to_params
return nil if field_types.empty? && includes.empty?
parsed_fields.merge(parsed_includes)
end

private

def parsed_includes
return {} if includes.empty?
{include: includes.join(",")}
end

def parsed_fields
return {} if field_types.empty?
{fields: fields.map { |type, names| [type, names.join(",")] }.to_h}
end

end
end
35 changes: 34 additions & 1 deletion lib/json_api_client/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class Resource

attr_accessor :last_result_set,
:links,
:relationships
:relationships,
:request_params
class_attribute :site,
:primary_key,
:parser,
Expand All @@ -31,6 +32,8 @@ class Resource
:associations,
:json_key_format,
:route_format,
:request_params_class,
:keep_request_params,
instance_accessor: false
self.primary_key = :id
self.parser = Parsers::Parser
Expand All @@ -43,6 +46,8 @@ class Resource
self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
self.requestor_class = Query::Requestor
self.associations = []
self.request_params_class = RequestParams
self.keep_request_params = false

#:underscored_key, :camelized_key, :dasherized_key, or custom
self.json_key_format = :underscored_key
Expand Down Expand Up @@ -318,6 +323,7 @@ def initialize(params = {})
set_attribute(association.attr_name, params[association.attr_name.to_s])
end
end
self.request_params = self.class.request_params_class.new(self.class)
end

# Set the current attributes and try to save them
Expand Down Expand Up @@ -416,12 +422,14 @@ def save
false
else
self.errors.clear if self.errors
self.request_params.clear unless self.class.keep_request_params
mark_as_persisted!
if updated = last_result_set.first
self.attributes = updated.attributes
self.links.attributes = updated.links.attributes
self.relationships.attributes = updated.relationships.attributes
clear_changes_information
self.relationships.clear_changes_information
end
true
end
Expand All @@ -445,6 +453,31 @@ def inspect
"#<#{self.class.name}:@attributes=#{attributes.inspect}>"
end

def request_includes(*includes)
self.request_params.add_includes(includes)
self
end

def reset_request_includes!
self.request_params.reset_includes!
self
end

def request_select(*fields)
fields_by_type = fields.extract_options!
fields_by_type[type.to_sym] = fields if fields.any?
fields_by_type.each do |field_type, field_names|
self.request_params.set_fields(field_type, field_names)
end
self
end

def reset_request_select!(*resource_types)
resource_types = self.request_params.field_types if resource_types.empty?
resource_types.each { |resource_type| self.request_params.remove_fields(resource_type) }
self
end

protected

def method_missing(method, *args)
Expand Down
21 changes: 20 additions & 1 deletion lib/json_api_client/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,24 @@ def self.compute_type(klass, type_name)
raise NameError, "uninitialized constant #{candidates.first}"
end

def self.parse_includes(klass, *tables)
tables.map do |table|
case table
when Hash
table.map do |k, v|
parse_includes(klass, *v).map do |sub|
"#{k}.#{sub}"
end
end
when Array
table.map do |v|
parse_includes(klass, *v)
end
else
klass.key_formatter.format(table)
end
end.flatten
end

end
end
end
64 changes: 63 additions & 1 deletion test/unit/creation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,68 @@ def test_can_create_with_new_record_and_save
assert_equal "1", article.id
end

def test_can_create_with_includes_and_fields
stub_request(:post, "http://example.com/articles")
.with(
headers: { content_type: "application/vnd.api+json", accept: "application/vnd.api+json" },
query: { include: 'comments,author.comments', fields: { articles: 'title', authors: 'name' } },
body: {
data: {
type: "articles",
attributes: {
title: "Rails is Omakase"
}
}
}.to_json
).to_return(
headers: { content_type: "application/vnd.api+json" },
body: {
data: {
type: "articles",
id: "1",
attributes: {
title: "Rails is Omakase"
},
relationships: {
comments: {
data: [
{
id: "2",
type: "comments"
}
]
},
author: {
data: nil
}
}
},
included: [
{
id: "2",
type: "comments",
attributes: {
body: "it is isn't it ?"
}
}
]
}.to_json
)
article = Article.new({
title: "Rails is Omakase"
})
article.request_includes(:comments, author: :comments).
request_select(:title, authors: [:name])

assert article.save
assert article.persisted?
assert_equal "1", article.id
assert_nil article.author
assert_equal 1, article.comments.size
assert_equal "2", article.comments.first.id
assert_equal "it is isn't it ?", article.comments.first.body
end

def test_can_create_with_links
article = Article.new({
title: "Rails is Omakase"
Expand Down Expand Up @@ -132,7 +194,7 @@ def test_can_create_with_new_record_with_relationships_and_save

end

def test_correct_create_with_nil_attirbute_value
def test_correct_create_with_nil_attribute_value
stub_request(:post, "http://example.com/authors")
.with(headers: {
content_type: "application/vnd.api+json",
Expand Down
Loading