Skip to content
This repository has been archived by the owner on Oct 19, 2021. It is now read-only.

[JSON-API] Add support for "include" query #95

Merged
merged 1 commit into from May 20, 2015
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 5 additions & 4 deletions rakelib/shared.rake
Expand Up @@ -7,10 +7,11 @@ require 'yard'
def mutant_task(_gem)
require 'mutant'
task :mutant do
pattern = ENV.fetch('PATTERN', 'Yaks*')
opts = ENV.fetch('MUTANT_OPTS', '').split(' ')
args = %w[-Ilib -ryaks --use rspec --score 100] + opts + [pattern]
result = Mutant::CLI.run(args)
pattern = ENV.fetch('PATTERN', 'Yaks*')
opts = ENV.fetch('MUTANT_OPTS', '').split(' ')
requires = %w[-ryaks -ryaks/behaviour/optional_includes]
args = %w[-Ilib --use rspec --score 100] + requires + opts + [pattern]
result = Mutant::CLI.run(args)
raise unless result == Mutant::CLI::EXIT_SUCCESS
end
end
Expand Down
37 changes: 37 additions & 0 deletions yaks/README.md
Expand Up @@ -552,6 +552,43 @@ yaks = Yaks.new do
end
```

Yaks also has support for respecting the `include` query parameter (e.g.
`include=author,comments`), which is a behaviour you can include in
your mappers:

```ruby
require "yaks/behaviour/optional_includes"

class PostMapper < Yaks::Mapper
include Yaks::Behaviour::OptionalIncludes

has_one :author
has_many :comments
end

# ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix ataru, try adding this in ataru_setup.rb at top level:

User = Struct.new

this in the Setup module:

def user
  User.new
end

and this in the README here:

yaks = Yaks.new

and run ataru locally with ataru check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I didn't know how to run Ataru. Instead of adding anything to ataru_setup.rb, I reused the existing objects.

Btw, how do you run RuboCop locally? I tried bundle exec rake rubocop from both yaks/ and top-level directory, and it says that the task is missing.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bundle exec rake rubocop from the top level should work. Does it show up when you do bundle exec rake -T?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actaully, I meant to say that from the top-level it works, but it throws a lot of offenses unrelated to my files, so it doesn't seem like the same Rake task that is run on Travis.

yaks = Yaks.new
yaks.call(post, env: rack_env)
```

Now all your associations will be included only if specified in the `include`
query. Note that you need to pass the Rack env to Yaks, and that you need to
explicitly require `yaks/behaviour/optional_includes`. If you want some
associations to always be included regardless of the `include` query parameter,
just specify `:if` that returns true:

```ruby
require "yaks/behaviour/optional_includes"

class PostMapper < Yaks::Mapper
include Yaks::Behaviour::OptionalIncludes

has_one :author
has_many :comments, if: ->{ true }
end
```

### Collection+JSON

Collection+JSON has support for write templates. To use them, the `:template`
Expand Down
29 changes: 29 additions & 0 deletions yaks/lib/yaks/behaviour/optional_includes.rb
@@ -0,0 +1,29 @@
require "rack/utils"

module Yaks
module Behaviour
module OptionalIncludes
RACK_KEY = "yaks.optional_includes".freeze

def associations
super.select do |association|
association.if != Undefined || include_association?(association)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that also include it even if we have if: ->{ false } for example?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would at this stage, map_associations handles evaluating the if and taking the association out

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@groyoh Yes, but this is exactly what I want. I want that if someone specifies :if, that include_association? checking is ignored. Because I want it to mean that their overriding this behaviour for that association. If someone specifies :if -> { false }, that should mean that they never want this association included (in which case of course they wouldn't even put it there).

end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what do we think about merging only if the key doesn't already exist in the options. That way we could always or never include a particular sub-resource simply by putting something in the config for :if

Can you see any issues with that approach?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new implementation proposed by @plexus it's no longer valid to implement it this way. However, I think my original implementation had the downside of overriding the :if, and I think the user would want to keep OptionalInclude's :if and still add their own.

Now we just need to think how to specify these options. @plexus, any ideas?

end

private

def include_association?(association)
includes = env.fetch(RACK_KEY) do
query_string = env.fetch("QUERY_STRING", nil)
query = Rack::Utils.parse_query(query_string)
env[RACK_KEY] = query.fetch("include", "").split(",").map { |r| r.split(".") }
end

includes.any? do |relationship|
relationship[mapper_stack.size].eql?(association.name.to_s)
end
end
end
end
end
63 changes: 63 additions & 0 deletions yaks/spec/unit/yaks/behaviour/optional_includes_spec.rb
@@ -0,0 +1,63 @@
require "yaks/behaviour/optional_includes"

RSpec.describe Yaks::Behaviour::OptionalIncludes do
include_context 'yaks context'

subject(:mapper) { mapper_class.new(yaks_context) }
let(:resource) { mapper.call(instance) }

let(:mapper_class) do
Class.new(Yaks::Mapper).tap do |mapper_class|
mapper_class.send :include, Yaks::Behaviour::OptionalIncludes
mapper_class.type "user"
mapper_class.has_many :posts, mapper: post_mapper_class
mapper_class.has_one :account, mapper: account_mapper_class
end
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also work with just passing the class body to Class.new, no?

    Class.new(Yaks::Mapper) do 
      include Yaks::Behaviour::OptionalIncludes
      type "user"
      has_many :posts, mapper: post_mapper_class
      has_one :account, mapper: account_mapper_class
    end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost would, but then post_mapper_class and account_mapper_class aren't available, because self changes. That's the reason why I chose this less pretty approach.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok, then it's fine. I was wondering if there was a reason

end
let(:post_mapper_class) do
Class.new(Yaks::Mapper).tap do |mapper_class|
mapper_class.type "post"
mapper_class.has_many :comments, mapper: comment_mapper_class
end
end
let(:account_mapper_class) { Class.new(Yaks::Mapper) { type "account" } }
let(:comment_mapper_class) { Class.new(Yaks::Mapper) { type "comment" } }

let(:instance) { fake(posts: [fake(comments: [fake])], account: fake) }

it "includes the associations" do
rack_env["QUERY_STRING"] = "include=posts.comments,account"

expect(resource.type).to eq "user"
expect(resource.subresources[0].type).to eq "post"
expect(resource.subresources[0].members[0].type).to eq "post"
expect(resource.subresources[0].members[0].subresources[0].type).to eq "comment"
expect(resource.subresources[0].members[0].subresources[0].members[0].type).to eq "comment"
expect(resource.subresources[1].type).to eq "account"
end

it "excludes associations not specified in the QUERY_STRING" do
rack_env["QUERY_STRING"] = "include=posts"

expect(resource.subresources.count).to eq 1
end

it "doesn't include the associations when QUERY_STRING is empty" do
expect(resource.type).to eq "user"
expect(resource.subresources).to be_empty
end

it "allows :if to override the query parameter checking" do
mapper_class.has_one :account, mapper: account_mapper_class, if: true

expect(resource.subresources.count).to eq 1
end

it "caches parsing of the query parameter" do
rack_env["QUERY_STRING"] = "include=posts"
expect(mapper.call(instance).subresources.count).to eq 1

rack_env["QUERY_STRING"] = nil
expect(mapper.call(instance).subresources.count).to eq 1
end
end