Skip to content
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

Add pagination and collections. #17

Merged
merged 1 commit into from
Nov 15, 2016
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
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 @@ -24,7 +24,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 @@ -19,5 +19,13 @@ def inflection_mode
def types_mode
@options[:types_mode] || :pluralize
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