Skip to content

Commit

Permalink
Merge pull request #17 from gadabout/feature/pagination
Browse files Browse the repository at this point in the history
Add pagination and collections.
  • Loading branch information
Chris Dosé committed Nov 15, 2016
2 parents 10e5953 + 6a34d2c commit 7f9c024
Show file tree
Hide file tree
Showing 21 changed files with 420 additions and 159 deletions.
86 changes: 68 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ store = Lurch::Store.new("http://example.com/api")
GET individual resources from the server by id:

```ruby
person = store.from(:people).find(1)
#=> #<Lurch::Resource[Person] id: 1, name: "Bob">
person = store.from(:people).find("1")
#=> #<Lurch::Resource[Person] id: "1", name: "Bob">
```

Or GET all of them at once:

```ruby
people = store.from(:people).all
#=> [#<Lurch::Resource[Person] id: 1, name: "Bob">, #<Lurch::Resource[Person] id: 2, name: "Alice">]
#=> [#<Lurch::Resource[Person] id: "1", name: "Bob">, #<Lurch::Resource[Person] id: "2", name: "Alice">]
```

`Lurch::Resource` objects have easy accessors for all attributes returned from the server:
Expand Down Expand Up @@ -68,7 +68,7 @@ To update an existing resource, create a changeset from the resource, then PATCH
```ruby
changeset = Lurch::Changeset.new(person, name: "Robert")
store.save(changeset)
#=> #<Lurch::Resource[Person] id: 1, name: "Robert">
#=> #<Lurch::Resource[Person] id: "1", name: "Robert">
```

Existing references to the resource will be updated:
Expand All @@ -85,7 +85,7 @@ To create new resources, first create a changeset, then POST it to the server us
```ruby
changeset = Lurch::Changeset.new(:person, name: "Carol")
new_person = store.insert(changeset)
#=> #<Lurch::Resource[Person] id: 3, name: "Carol">
#=> #<Lurch::Resource[Person] id: "3", name: "Carol">
```

## Filtering
Expand All @@ -94,45 +94,45 @@ You can add filters to your request if your server supports them:

```ruby
people = store.from(:people).filter(name: "Alice").all
#=> [#<Lurch::Resource[Person] id: 2, name: "Alice">]
#=> [#<Lurch::Resource[Person] id: "2", name: "Alice">]
```

## Relationships

Lurch can fetch *has-many* and *has-one* relationships from the server when they are provided as *related links*:

```ruby
person = store.from(:people).find(1)
person = store.from(:people).find("1")

person.hobbies
#=> #<Lurch::Relationship link: "/people/1/hobbies" not loaded>
#=> #<Lurch::Relationship::Linked href: "http://example.com/api/people/1/friends">
person.hobbies.fetch
#=> [#<Lurch::Resource[Hobby] id: 1, name: "Cryptography">, ...]
#=> #<Lurch::Collection[Hobby] size: 2, pages: 1>

person.best_friend
#=> #<Lurch::Relationship link: "/people/2" not loaded>
#=> #<Lurch::Relationship::Linked href: "http://example.com/api/people/1/best-friend">
person.best_friend.fetch
#=> #<Lurch::Resource[Person] id: 2, name: "Alice">
#=> #<Lurch::Resource[Person] id: "2", name: "Alice">
```

If the server provides the relationships as *resource identifiers* instead of links, you can get some information about the relationships without having to load them:

```ruby
person = store.from(:people).find(1)
person = store.from(:people).find("1")

person.hobbies
#=> [#<Lurch::Resource[Hobby] id: 1, not loaded>, ...]
#=> [#<Lurch::Resource[Hobby] id: "1", not loaded>, ...]
person.hobbies.count
#=> 3
person.hobbies.map(&id)
#=> [1, 2, 3]
#=> ["1", "2", "3"]
person.hobbies.map(&:name)
#=> Lurch::Errors::ResourceNotLoaded: Resource (Hobby) not loaded, try calling #fetch first.

person.best_friend
#=> #<Lurch::Resource[Person] id: 2, not loaded>
#=> #<Lurch::Resource[Person] id: "2", not loaded>
person.best_friend.id
#=> 2
#=> "2"
person.best_friend.name
#=> Lurch::Errors::ResourceNotLoaded: Resource (Person) not loaded, try calling #fetch first.
```
Expand All @@ -141,17 +141,67 @@ Regardless of what kind of relationship it is, it can be fetched from the server

```ruby
person.best_friend.id
#=> 2
#=> "2"
person.best_friend.loaded?
#=> false
person.best_friend.fetch
#=> #<Lurch::Resource[Person] id: 2, name: "Alice">
#=> #<Lurch::Resource[Person] id: "2", name: "Alice">
person.best_friend.loaded?
#=> true
person.best_friend.name
#=> "Alice"
```

## Pagination

Lurch supports traversing and requesting paginated results if the server implements pagination:

```ruby
people = store.from(:people).all
#=> #<Lurch::Collection[Person] size: 1000, pages: 100>
```

If the server responded with meta data about the resources, you can get some information about them without loading them all:

```ruby
people.size
#=> 1000
people.page_count
#=> 100
```

*NOTE: This data comes from the top-level `meta` key in the jsonapi response document. It assumes by default the keys are "record-count" and "page-count" respectively, but can be configured in the store.*

To request a specific page, use the pagination query methods:

```ruby
people = store.from(:people).page(12).per(50).all
#=> #<Lurch::Collection[Person] size: 1000, pages: 20>
```

If you'd like to traverse the whole set, you can do that using the collection enumerator or the page enumerator:

```ruby
people.map(&:name)
# ...many HTTP requests later...
#=> ["Summer Brakus", "Katharina Orn", "Mr. Angus Hickle", "Collin Lowe PhD", "Kaylie Larson", ...]

people.each_page.map(&:size)
# ...many HTTP requests later...
#=> [10, 10, 10, 10, ...]
```

*NOTE: These enumerators can cause many HTTP requests to the server, since when it runs out of the first page of resources, it will automatically request the next page to continue.*

*TIP: Don't use `#count` on a collection to get its size. Use `#size` instead. `#count` causes the entire collection to be traversed, whereas `#size` will try and get the information from the collection meta data.*

You can also just get the resources from the current page as an array:

```ruby
people.resources
#=> [#<Lurch::Resource[Person] id: "2", name: "Summer Brakus", email: "summerb2b@kiehnhirthe.info", twitter: "@summerb2b">, ...]
```

## Authentication

You can add an *Authorization* header to all your requests by configuring the store:
Expand Down
6 changes: 5 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
* ~~Configurable HTTP adapter (don't assume faraday with typhoeus)~~
* [x] Configurable field and path adapters (dasherized vs underscored vs ~~camelcased~~)
* [x] Configurable pluralization/singularization (don't assume urls and types are always plural)
* [ ] Handle paginated results
* [x] Handle paginated results
* [ ] Allow arbitrary headers
* [ ] Allow arbitrary query params
* [ ] Singleton resources
* [ ] Handle links better?
* [ ] >= 90% test coverage
* [ ] Yardoc documentation
* [ ] Release 0.1.0 open source
5 changes: 5 additions & 0 deletions lib/lurch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
require "lurch/errors/resource_not_loaded"

require "lurch/stored_resource"
require "lurch/paginator"
require "lurch/collection"
require "lurch/relationship"
require "lurch/relationship/linked"
require "lurch/relationship/has_one"
require "lurch/relationship/has_many"
require "lurch/resource"

require "lurch/uri_builder"
Expand Down
103 changes: 103 additions & 0 deletions lib/lurch/collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
module Lurch
class Collection
include Enumerable

attr_reader :resources

def initialize(resources, paginator)
@resources = resources
@paginator = paginator
end

def each(&block)
block_given? ? enum.each(&block) : enum
end

def each_page(&block)
block_given? ? page_enum.each(&block) : page_enum
end

def size
@paginator ? @paginator.record_count : @resources.size
end

def page_size
@resources.size
end

def page_count
@paginator.page_count if @paginator
end

def next_collection
return @next_collection if defined?(@next_collection)
@next_collection = @paginator ? @paginator.next_collection : nil
end

def prev_collection
return @prev_collection if defined?(@prev_collection)
@prev_collection = @paginator ? @paginator.prev_collection : nil
end

def first_collection
return @first_collection if defined?(@first_collection)
@first_collection = @paginator ? @paginator.first_collection : nil
end

def last_collection
return @last_collection if defined?(@last_collection)
@last_collection = @paginator ? @paginator.last_collection : nil
end

def next?
@paginator ? @paginator.next? : false
end

def prev?
@paginator ? @paginator.prev? : false
end

def first?
@paginator ? @paginator.first? : false
end

def last?
@paginator ? @paginator.last? : false
end

def inspect
suffix = @resources.first ? "[#{Inflector.classify(@resources.first.type)}]" : ""
inspection = size ? ["size: #{size}"] : []
inspection << ["pages: #{page_count}"] if page_count
"#<#{self.class}#{suffix} #{inspection.join(', ')}>"
end

private

def enum
Enumerator.new(-> { size }) do |yielder|
@resources.each do |resource|
yielder.yield(resource)
end

if next_collection
next_collection.each do |resource|
yielder.yield(resource)
end
end
end
end

def page_enum
Enumerator.new(-> { page_count }) do |yielder|
yielder.yield(@resources)

if next_collection
next_collection.each_page do |page|
yielder.yield(page)
end
end
end
end
end
end
8 changes: 8 additions & 0 deletions lib/lurch/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,13 @@ def initialize

@log_payloads = false
end

def pagination_record_count_key
@options[:pagination_record_count_key] || :record_count
end

def pagination_page_count_key
@options[:pagination_page_count_key] || :page_count
end
end
end
71 changes: 71 additions & 0 deletions lib/lurch/paginator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module Lurch
class Paginator
def initialize(store, document, inflector, config)
@store = store
@links = document["links"]
@meta = document["meta"]
@config = config
@inflector = inflector
end

def record_count
key = @inflector.encode_key(@config.pagination_record_count_key)
@meta[key]
end

def page_count
key = @inflector.encode_key(@config.pagination_page_count_key)
@meta[key]
end

def next_collection
next_link && @store.load_from_url(next_link)
end

def prev_collection
prev_link && @store.load_from_url(prev_link)
end

def first_collection
first_link && @store.load_from_url(first_link)
end

def last_collection
last_link && @store.load_from_url(last_link)
end

def next?
!!next_link
end

def prev?
!!prev_link
end

def first?
!!first_link
end

def last?
!!last_link
end

private

def next_link
@links["next"]
end

def prev_link
@links["prev"]
end

def first_link
@links["first"]
end

def last_link
@links["last"]
end
end
end
Loading

0 comments on commit 7f9c024

Please sign in to comment.