Class: X::Resource

Inherits:
Object
  • Object
show all
Extended by:
AbstractClass, Attributes
Includes:
Identity, Marshalling, PublishedCount, Serialization
Defined in:
x-objects/lib/x/objects/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



323
# File 'x-objects/lib/x/objects/resource.rb', line 323

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



41
42
43
# File 'x-objects/lib/x/objects/resource.rb', line 41

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



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

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



156
# File 'x-objects/lib/x/objects/resource.rb', line 156

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



110
# File 'x-objects/lib/x/objects/resource.rb', line 110

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



221
222
223
224
225
# File 'x-objects/lib/x/objects/resource.rb', line 221

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

#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:



393
394
395
# File 'x-objects/lib/x/objects/resource.rb', line 393

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. A stub, a 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



351
352
353
# File 'x-objects/lib/x/objects/resource.rb', line 351

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



331
332
333
# File 'x-objects/lib/x/objects/resource.rb', line 331

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

#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



418
# File 'x-objects/lib/x/objects/resource.rb', line 418

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

#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



367
# File 'x-objects/lib/x/objects/resource.rb', line 367

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:



408
409
410
# File 'x-objects/lib/x/objects/resource.rb', line 408

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



377
# File 'x-objects/lib/x/objects/resource.rb', line 377

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