Recommended way of accessing single remote records? #11

Open
coneybeare opened this Issue Jan 4, 2013 · 10 comments

Projects

None yet

2 participants

@coneybeare
Contributor

I have a class Foo which has_many_remote Bars.

class Foo
  has_many_remote :bars, :path => "/foos/:id/bars"
end

This works great for collections, i.e., I can call @foo.bars and remotely will connect to the external api and populate an array of bars.

What is the recommended way to fetch a specific Bar, that also has the Foo's scope? In other words, I wish to call:

  /foos/1/bars/2

Currently, doing something like @foo.bars.find(2) produces the correct end-result, but fetches the collection then does the find in ruby. This could take forever if there are hundreds of bars associated with this foo. How can I make this call to get the specified bar resource only?

@coneybeare
Contributor

I guess the ideal markup would be @foo.bars(2) which currently returns the same result as @foo.bars

@coneybeare
Contributor

This actually is a tough problem to solve it seems. I think there needs to be some intelligent method of chaining calls as well, because what about wanting to get a drill down object such as

/foos/1/bars/2/bazzes/3/goobs/4/zaps/5

The only option currently is to

class Foo
  has_many_remote :zaps, :path => `/foos/1/zaps`
end

... then find over all zaps in this foo, which may be millions.

@mnoble
Contributor
mnoble commented Jan 4, 2013

I think we'd need a lazy evaluation type setup like ARel does with queries. Something that builds the request incrementally then fires it off on access (iteration, etc.).

Also, I'm not sure how this would look API-wise.

foo.bars(2).bazzes(3).zaps(5)

would be my immediate thought, but that's getting further away from the ActiveRecord-esque API I wanted for this.

It could use where kind of like this:

foo.zaps.where(bar_id: 2, baz_id: 3, goobs_id: 4)

but then you have the problem of ordering. You'd have to specify the order in which to construct the URI. That seems weird.

I'm not quite sure what the best way to do this is, but I agree it'd be very nice and useful to support it.

@coneybeare
Contributor

Perhaps it is simply a matter of naming symbols uniquely? Lets say you have this:

class Foo
  has_many_remote :bars, :path => "/foos/:foo_id/bars"
  has_many_remote :bazzes, :path => "/foos/:foo_id/bars/:bar_id/bazzes"
  has_many_remote :goobs, :path => "/foos/:foo_id/bars/:bar_id/bazzes/:baz_id/goobs"
  has_many_remote :zaps, :path => "/foos/:foo_id/bars/:bar_id/bazzes/:baz_id/goobs/:goob_id/zaps"
end

Upon setup, the has_many_remote sets up these associations detecting if there is more than one symbol there. It requires that each symbol after the first include a passed object to call the id method on, then substitutes it in order.

Then it can be called like:

@foo.bars => get all bars associated with this foo, as normal
@foo.bazzes(@bar) => plug the `foo_id` var as normal, plug the `bar_id` var with @bar.id
@foo.goobs(@bar, @baz) => plug the `foo_id` var as normal, plug the `bar_id` var with @bar.id, plug the `baz_id` with @baz.id
@foo.zaps(@bar, @baz, @zap) => plug the `foo_id` var as normal, plug the `bar_id` var with @bar.id, plug the `baz_id` with @baz.id, @zap.id

This is similar to your second solution, but easier to implement (albeit less powerful) by defining the structure of the call beforehand.

The second half of this problem, getting a specific has_many item instead of a collection, might be addressed by adding an integer as the last item in these calls, such that:

has_many_remote :bars, :path => "/foos/:foo_id/bars(:/bar_id)"
@foo.bars(9) => get the bar with id = 9 by replacing the `(/:bar_id)`. If the integer is not passed, the `(/:bar_id)` is ignored for the collections call.

has_many_remote :bazzes, :path => "/foos/:foo_id/bars/:bar_id/bazzes(:/baz_id)"
@foo.bazzes(@bar, 9) => get the baz with id = 9 by replacing the `(/:baz_id)`. If the integer is not passed, the `(/:baz_id)` is ignored for the collections call.
...and so on...

Thoughts?

@coneybeare
Contributor

Or, thinking of this from another angle, perhaps the markup should be in the local class as to what it needs to fetch.

class Foo
  has_many_remote :bars
  has_many_remote :bazzes
  has_many_remote :goobs
  has_many_remote :zaps
end

class Bar
  remote_collection_uri Proc {|foo| "/foo/#{foo.id}/bars" }
  remote_singular_uri Proc {|foo, id| "/foo/#{foo.id}/bars/#{id}" }
end

class Baz
  remote_collection_uri Proc {|foo, bar| "/foo/#{foo.id}/bars/#{bar.id}/foos" }
  remote_singular_uri Proc {|foo, bar, id| "/foo/#{foo.id}/bars/#{bar.id}/foos/#{id}" }
end

Then when calling, we can use a find()-like syntax with :all and :single. If :all is found as the first arg, then the remote_collection_uri proc is called with the remaining arguments. If :single is found, then the remote_singular_uri is called

@foo.bars(:all) => remote_collection_uri called on Bar, with @foo as the yield arg.
@foo.bars(:single, 9) => remote_singular_uri called on Bar, with @foo as the first yield arg, and 9 as the second.

@foo.bazzes(:all, @bar) => remote_collection_uri called on Baz, with @foo as the first yield arg, @bar as the second
@foo.bazzes(:single, @bar, 9) => remote_singular_uri called on Baz, with @foo as the first yield arg, @bar as the second, and 9 as the third

The markup could be the same for existing calls such that @foo.bars still calls the same method using the :path argument defined in Foo.

@coneybeare
Contributor

Another possible way is to leverage the existing find method you have on Remotely::Model.

# Retreive a single object. Combines `uri` and `id` to determine
      # the URI to use.
      #
      # @param [Fixnum] id The `id` of the resource.
      #
      # @example Find the User with id=1
      #   User.find(1)
      #
      # @return [Remotely::Model] Single model object.
      #
      def find(id)
        get URL(uri, id)
      end

Right now you have it only accepting a single arg, and a static uri. What if the model's uri method allowed Procs to fill the required scope info?

def Bar
  # uri '/bars'
  # => behaves as normal because it is a String class.

  uri Proc.new { |id| "/foos/#{current_foo.id}/bars/#{id}" }
  # => this proc is evaluated at runtime with the id to generate the uri string for the URL call
end

Bar.find(9) => get URL("/foos/42/bars/9")

This seems like the easiest approach to solve the problem. Starting from this approach, there are some complications that may arise such as the Bar proc not having access to the objects needed. I still think that it should be the callers responsibility to scope the model, perhaps in a where call like you mentioned earlier:

Bar.where(id: 9, baz_id: 3, goobs_id: 4)

The where call would be handled differently in case of a uri String of Proc class. If a String, we could do a gsub replacement for the uri's symbols. If a proc, we could have it be defined to take 2 arguments (id and a scope Hash), we could then pull the id out from the hash, and pass them both into the proc for the class to determine it's order. I would be happy to implement something like this if you agree on the basic approach. Thoughts?

@coneybeare
Contributor

I found a gem which does query chaining that we could use as a guide if we go down that route. Only about 100 loc. We could integrate something similar that was also a bit smarter about param substitution.

@mnoble
Contributor
mnoble commented Jan 5, 2013

Yea, that's pretty much what I was thinking. Something where query methods (find, where, etc) and association methods return some proxy object (a la ActiveRecord::Relation). Then on access it would actually fetch the resource.

foo = Foo.find(1)

baz = foo.bars.find(2).bazzes.find(3)
#<ResourceProxy:0x00 uri="/foos/1/bars/2/bazzes/3">

baz.name
#=> Actually fetches the resource

I like the chained find just because it encourages your models to be more atomic. By that I mean, each model only has to specify it's specific portion of the URI. Then the associations define the resource hierarchy. Also, the caller would define the order in which they should appear in the URI as well as the id for each resource.

I think if we can do that cleanly it would be pretty awesome.

@coneybeare
Contributor

I'm gonna mull it over a bit. I decided to try implementing this in parallel with ActiveResource because I noticed I was asking for a bunch of stuff that it supported out of the box. I may end up going that route instead, but I have yet to decide completely.

Yea, that's pretty much what I was thinking. Something where query methods (find, where, etc) and association methods return some proxy object (a la ActiveRecord::Relation). Then on access it would actually fetch the resource.

foo = Foo.find(1)

baz = foo.bars.find(2).bazzes.find(3)
#<ResourceProxy:0x00 uri="/foos/1/bars/2/bazzes/3">

baz.name
#=> Actually fetches the resource
I like the chained find just because it encourages your models to be more atomic. By that I mean, each model only has to specify it's specific portion of the URI. Then the associations define the resource hierarchy. Also, the caller would define the order in which they should appear in the URI as well as the id for each resource.

I think if we can do that cleanly it would be pretty awesome.


Reply to this email directly or view it on GitHub.

@mnoble
Contributor
mnoble commented Jan 6, 2013

Cool. Yea, this was originally written with a very specific set of rules based on our own applications, so it's lacking a lot of the things ActiveResource offered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment