Class: X::Resource

Inherits:
Object
  • Object
show all
Extended by:
AbstractClass, Attributes
Includes:
Identity, Marshalling, PublishedCount, Serialization, X::Resources::Identity, X::Resources::Marshalling, X::Resources::Serialization
Defined in:
x-resources/lib/x/resources/resource.rb

Overview

Base class for immutable API resources with identity, references, and hydration

A reader of an object the API nests in a resource, or of a list of them, such as the entities, urls, public_metrics, edit_controls, attachments, and withheld of a post, the variants of media, the options of a poll, or the subscription and affiliation of a user, returns it as the API sends it: a frozen Hash keyed by String, or an Array of them. Each returns that throughout 1.x, and raises InvalidAttribute for a response that holds anything else in its place. A reader that returns an object, as the matching_rules of a post and the topics of a space do, is only ever added under a new name, never in place of one of these.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs, client: nil, hydrated: false) ⇒ Resource

Initialize a new immutable resource

Examples:

Create a user from attributes

X::User.new({"id" => "7505382", "username" => "sferik"}, client: client)

Parameters:

  • attrs (Hash) —

    the attributes, which must include the identifier

  • client (Object, nil) (defaults to: nil) —

    the client used to fetch references

  • hydrated (Boolean) (defaults to: false) —

    whether the resource holds every requested field

Raises:

  • (ArgumentError) —

    if the attributes are not a Hash, do not include the identifier, or hold an identifier that is not one



330
# File 'x-resources/lib/x/resources/resource.rb', line 330

def initialize(attrs, client: nil, hydrated: false) = setup(Utils.attributes!(attrs), client:, hydrated:)

Instance Attribute Details

#attrs ⇒ Hash{String => Object} (readonly)

The frozen attributes returned by the API

Examples:

Get the raw attributes

user.attrs # => {"id" => "7505382", "name" => "Erik Berlin", "username" => "sferik"}

Returns:

  • (Hash{String => Object}) —

    the attributes



48
49
50
# File 'x-resources/lib/x/resources/resource.rb', line 48

def attrs
  @attrs
end

#client ⇒ Object? (readonly)

The client used to fetch this resource and its references

Examples:

Get the client

user.client

Returns:

  • (Object, nil) —

    the client



55
56
57
# File 'x-resources/lib/x/resources/resource.rb', line 55

def client
  @client
end

Class Method Details

.default_params ⇒ Hash{String => Array<String>}

The default query parameters requesting every field and expansion

They are built from the FIELDS and EXPANSIONS of the classes they name, which a minor release may add to; see #hydrated?.

Examples:

Get the default parameters

X::User.default_params

Returns:

  • (Hash{String => Array<String>}) —

    the default query parameters



163
# File 'x-resources/lib/x/resources/resource.rb', line 163

def default_params = {}

.from_id(id, client: nil) ⇒ Resource

Build a resource from an identifier, or from a resource, without a request

Examples:

Page through the followers of a user without looking the user up

X::User.from_id(7505382, client: client).followers

Parameters:

  • id (String, Integer, Resource) —

    the identifier, or a resource of this class, whose identifier is taken

  • client (Object, nil) (defaults to: nil) —

    the client used to fetch the resource and its references

Returns:

  • (Resource) —

    a stub that hydrates to the full resource

Raises:

  • (ArgumentError) —

    if the identifier is not a number, for a resource whose identifiers are numbers, or the resource is of another class



117
# File 'x-resources/lib/x/resources/resource.rb', line 117

def from_id(id, client: nil) = from_id_in_batch(id, client:)

.from_response(body, client:, hydrated: false) ⇒ Resource, ...

Build the resource or resources a response holds

A response whose data is an object builds one resource, and one whose data is an array builds a Page of one for each element, which holds the meta of the response, such as its next_token, and the problems it reported. A list the API finds empty holds no data, only a meta, which may still name the token of a page after it, so a response with a meta and no data builds an empty Page. A lookup of several resources by their identifiers, such as users?ids=, that finds none of them holds no data and no meta, only a problem for each, which names the ids, media_keys, or usernames parameter, so it builds an empty Page of those problems, as a lookup that finds some of them builds a Page of those it found. A client calls this when a resource class is the object_class of a request. A later version of x-core may pass it keywords of its own, which are ignored, as X::Client asks of what it calls from_response on.

A response holds only the fields its request asked for, so what this builds is not hydrated unless told otherwise, and hydrate fetches the full resource.

Examples:

Build a user from a response

X::User.from_response({"data" => {"id" => "7505382"}}, client: client)

Build users from a client request, and read the token of the next page

client.get("users/7505382/blocking", object_class: X::User).next_token

Parameters:

  • body (Hash, nil) —

    the parsed response body

  • client (Object) —

    the client used to make the request

  • hydrated (Boolean) (defaults to: false) —

    whether the response holds every field the object layer requests

Returns:

  • (Resource, Page, nil) —

    the resource, or the page of resources, or nil if the response has no data, no meta, and no problem of a lookup of several

Raises:

  • (InvalidAttribute) —

    if the response holds a resource without an identifier, or with one that is not one



228
229
230
231
232
# File 'x-resources/lib/x/resources/resource.rb', line 228

def from_response(body, client:, hydrated: false, **)
  return collection_from_response(body, client:, hydrated:) if Page.__send__(:list?, body.to_h)

  resource_from_response(body, client:, hydrated:)
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql? Originally defined in module X::Resources::Identity

Compare resources by class and identifier

Examples:

Compare users fetched in different requests

post.author == client.find_user("sferik")

Parameters:

  • other (Object) —

    the object to compare with

Returns:

  • (Boolean) —

    true if the other object is the same kind of resource with the same identifier

#as_json ⇒ Hash{String => Object} Originally defined in module X::Resources::Serialization

The attributes, as a JSON encoder and ActiveSupport read them

Examples:

Serialize a resource

user.as_json # => {"id" => "7505382", "username" => "sferik"}

Serialize a problem

problem.as_json # => {"title" => "Not Found Error"}

Returns:

  • (Hash{String => Object}) —

    the attributes

#deconstruct_keys(keys) ⇒ Hash{Symbol => Object} Originally defined in module X::Resources::Identity

Deconstruct the resource into its attributes, so it matches a hash pattern

Every attribute the resource declares is read as its own method reads it, so a pattern sees the identifier as a number, a timestamp as a Time, and a metric by the name it is read by. A pattern can ask for an attribute by another name it is read by, such as retweet_count for repost_count, and a pattern that asks for every attribute, with a double splat, gets each once, by the name the resource declares.

Examples:

Match a post by its author

puts "by sferik" if post in {author_id: 7505382}

Match a post by a name from before posts were posts

puts "widely reposted" if post in {retweet_count: 100..}

Parameters:

  • keys (Array<Symbol>, nil) —

    the keys the pattern asks for, or nil for every attribute

Returns:

  • (Hash{Symbol => Object}) —

    the attributes

#encode_with(coder) ⇒ void Originally defined in module X::Resources::Marshalling

This method returns an undefined value.

Write the state Marshal writes as YAML, which leaves out the client

YAML reads no marshal_dump, and would write every instance variable, the client and its credentials among them, so a resource says how it is written: each part of the state Marshal writes, under its name.

Examples:

Write a user as YAML, as a queue writes the arguments of a job

YAML.dump(user)

Parameters:

  • coder (Psych::Coder) —

    the coder YAML writes the resource with

#hash ⇒ Integer Originally defined in module X::Resources::Identity

Hash resources by class and identifier

Examples:

Use resources as hash keys

{user => 1}[client.find_user("sferik")]

Returns:

  • (Integer) —

    the hash code

#hydrate ⇒ Resource?

Fetch the full resource, memoizing the result

Each resource that is not hydrated costs a request of its own, so hydrate many resources, such as the authors of the posts of a page, with the hydrate_all of their class, which looks them up a hundred at a time, rather than call hydrate on each.

Examples:

Fetch the full user a stub names

X::User.from_id(7_505_382, client: client).hydrate.description

Fetch the full authors of many posts in batches, rather than a request for each

X::User.hydrate_all(posts.map(&:author), client: client).map(&:description)

Returns:

  • (Resource, nil) —

    the full resource or nil if it no longer exists

Raises:



402
403
404
# File 'x-resources/lib/x/resources/resource.rb', line 402

def hydrate
  @memo.fetch { hydrated? ? self : fetch }
end

#hydrated? ⇒ Boolean

Check whether the resource holds every field the object layer requests

A resource is hydrated when it was the subject of a response to a request that asked for every default field and expansion, whether or not it asked for more, or a reference a response included that expands nothing, such as media, a poll, a place, or a topic, when the request asked for every default field of it. A stub, any other reference a response included, and a resource looked up with parameters that leave out some of those defaults are not, so hydrate fetches the full resource.

The defaults are the FIELDS and EXPANSIONS of each class, which a minor release may add to as the API adds fields and expansions, so that a lookup with the defaults asks for them too. A resource looked up with a list of its own, even one that named every field of the release it was written for, then leaves out what was added, so it is no longer hydrated, and hydrate costs a lookup of the full resource that the same code did not pay before. To ask for more than the defaults, add to what default_params gives rather than list every value.

Examples:

Check whether a referenced user is hydrated

post.author.hydrated? # => false

Returns:

  • (Boolean) —

    true if the resource holds every field the object layer requests



360
361
362
# File 'x-resources/lib/x/resources/resource.rb', line 360

def hydrated?
  @hydrated
end

#id ⇒ Integer, String

The identifier

Examples:

Get the identifier

user.id # => 7505382

Returns:

  • (Integer, String) —

    the identifier, an Integer unless the resource's identifiers are not numbers



338
339
340
# File 'x-resources/lib/x/resources/resource.rb', line 338

def id
  Attributes::CONVERTERS.fetch(self.class.__send__(:id_type)).call(attrs.fetch(self.class.__send__(:id_key)))
end

#init_with(coder) ⇒ void Originally defined in module X::Resources::Marshalling

This method returns an undefined value.

Restore a resource YAML read, as Marshal restores one

It has no client, and so makes no request.

Examples:

Read a user written as YAML

YAML.unsafe_load(YAML.dump(user)).username # => "sferik"

Parameters:

  • coder (Psych::Coder) —

    the coder YAML read the resource with

Raises:

#inspect ⇒ String

Summarize the resource for the console

Examples:

Inspect a user

user.inspect # => #<X::User id="7505382" username="sferik">

Returns:

  • (String) —

    the class name and attributes



427
# File 'x-resources/lib/x/resources/resource.rb', line 427

def inspect = "#<#{self.class} #{attrs.map { |key, value| "#{key}=#{value.inspect}" }.join(" ")}>"

#marshal_dump ⇒ Array Originally defined in module X::Resources::Marshalling

The state Marshal writes, which leaves out the client

The client is left out, since it holds credentials and a connection. What is written is plain data, led by the number of its format, so that a resource written by one release of 1.x is read by a later one: its attributes, whether it is hydrated, and, of the response it came from, the included objects it refers to, and those they refer to in turn, the problems about any of them, and the query, so that the references it resolves, and the problems it and they report, are what they were, while the rest of the response is left out, however many other resources it held.

Examples:

Cache a user

Rails.cache.write("user", user)

Returns:

  • (Array) —

    the number of the format, then the state of the resource

#marshal_load(state) ⇒ void Originally defined in module X::Resources::Marshalling

This method returns an undefined value.

Restore a resource Marshal read, which has no client and so makes no request

What Marshal reads is a client-less resource that answers its readers, resolves the references its response included, and reports its problems, as the resource that was written did, and raises from a hydrate, refresh, or collection that would make a request. It is hydrated if it was, and the query of its request asks for every field this release requests, so that one written before a minor release added to the fields is not.

Examples:

Read a cached user

Marshal.load(Marshal.dump(user)).username # => "sferik"

Parameters:

  • state (Array) —

    the state Marshal wrote

Raises:

#problems ⇒ Array<Problem>

The problems the API reported about this resource in the response it came from

A problem is about the resource when the identifier it names, as its resource_id or its value, is the identifier of the resource or of one the resource refers to directly, such as the author of a post, or the pinned post of a user, so each post of a page reports that its own author no longer exists, and none reports it of another. A problem that names no identifier could be about any resource of the response, so every one of them reports it. The page of a cursor reports every problem of its response, as a finder yields them.

Examples:

Check whether a user's pinned post still exists

client.current_user!.problems.select(&:not_found?)

Returns:

  • (Array<Problem>) —

    the problems, such as expansions whose resources no longer exist



376
# File 'x-resources/lib/x/resources/resource.rb', line 376

def problems = includes.problems_about([id, *self.class.__send__(:referenced_ids, attrs)])

#refresh ⇒ Resource?

Fetch the full resource again, replacing the memoized result

A stub that hydrates together with the others of its page looks itself up on its own, rather than read what the lookup of the page found.

Examples:

Refresh a user's follower count

user.refresh.followers_count

Returns:

  • (Resource, nil) —

    the fresh resource or nil if it no longer exists

Raises:



417
418
419
# File 'x-resources/lib/x/resources/resource.rb', line 417

def refresh
  @memo.store(look_up)
end

#stub? ⇒ Boolean

Check whether the resource holds nothing but its identifier

A reference the response did not expand is a stub, and so is a resource built with from_id.

Examples:

Check whether the author of a post was included in the response

post.author.stub? # => false

Returns:

  • (Boolean) —

    true if the resource holds only its identifier



386
# File 'x-resources/lib/x/resources/resource.rb', line 386

def stub? = attrs.keys.eql?([self.class.__send__(:id_key)])

#to_h {|key, value| ... } ⇒ Hash Originally defined in module X::Resources::Serialization

The attributes, or with a block the Hash of the pairs it returns for them

With a block it is the to_h of the attributes, as a Hash builds it, rather than the attributes, the block ignored.

Examples:

Convert a resource to a hash

user.to_h # => {"id" => "7505382", "username" => "sferik"}

Key the attributes by Symbol

user.to_h { |key, value| [key.to_sym, value] } # => {id: "7505382", username: "sferik"}

Delete the rule a post matched

streaming_client.delete_rules(post.matching_rules.first.to_h)

Yield Parameters:

  • key (String) —

    the name of each attribute

  • value (Object) —

    its value

Yield Returns:

  • (Array(Object, Object)) —

    the key and value of the attribute in the Hash

Returns:

  • (Hash) —

    the attributes, frozen, or the pairs the block returns

#to_json(state = nil) ⇒ String Originally defined in module X::Resources::Serialization

The attributes as JSON

Examples:

Serialize a resource

user.to_json # => "{\"id\":\"7505382\",\"username\":\"sferik\"}"

Cache a resource as JSON

Rails.cache.write("user", user.to_json)

Parameters:

  • state (JSON::State, nil) (defaults to: nil) —

    the state a JSON encoder passes, which the attributes are given

Returns:

  • (String) —

    the attributes as a JSON object