Resourceful.Type (Resourceful v0.1.6)
View SourceResourceful.Type
is a struct and set of functions for representing and
mapping internal data structures to data structures more appropriate for edge
clients (e.g. API clients). As a result, field names are always strings and
not atoms.
In addition to mapping data field names, it validates that client representations conform to various constraints set by the type. These include transversing field graphs, limiting which fields can be queried, and how deep down the graph queries can go.
The naming conventions and some of design philosophy is geared heavily toward APIs over HTTP and JSON:API specification. However, there is nothing JSON-API specific about types.
Fields
A "field" refers to an attribute or a relationship on a given type. These share a common namespace and in some respects can be treated interchangeably.
There is a distinction between "local" fields and "query" fields.
Local fields are those which are directly on the current type. For example, a type of album may have local attributes such as a title and release date and local relationships such as artist and songs.
Query fields are a combination of local fields and fields anywhere in the resource graph. So, in the above example, query fields would include something like an album's title and the related artist's name.
### Relationships and Registries
In order to use relationships, a type must be included in a
Resourceful.Registry
and, in general, types are meant to be used in
conjunction with a registry. In most functions dealing with relationships and
related types, a type's name
(just a string) is used rather than passing
a type struct. The struct itself will be looked up from the registry.
Queries
The term "query" is used to refer to filtering and sorting collections of resources. Since queries ultimately work on attributes, fields eligible to be queried must be attributes. You could sort songs by an album's title but you wouldn't reasonably sort them by an album resource.
Fields given for a query can be represented as a list of strings or as a dot
separated string. So, when looking at a song, the artist's name could be
accessed through "album.artist.name"
or ["album", "artist", "name"]
. As
with many things related to types, string input from API sources is going to
be the most common form of input.
"Root" Types
Resource graphs are put together from the perspective of a "root" type. Any type can be a root type. In the example of an API, if you were looking at an album, it would be the root with its songs and artist further down the graph.
Building Types
In addition to functions that actually do something with types, there are a
number of functions used for transforming types such as max_depth/2
. As
types are designed with registries in mind, types can be built at compile-time
using transformation functions in a manner that may be easier to read than
new/2
with options.
Ecto Schemas
There is some overlap with Ecto.Schema
. In fact, attribute types use the
same type system. While schemas can be used for edge data, primarily when
coupled with change sets, types are more specifically tailored to the task.
Types, combined with Resourceful.Collection
can be combined to construct a
queryable API with concerns that are specific to working with the edge.
The query format is specifically limited for this purpose.
Summary
Types
A field is an attribute or a relationship. They share the same namespace within a type.
Functions
Returns a list of the name of all attribute fields.
Sets a key in the cache
map. Because types generally intended to be static
at compile time, it can make sense to cache certain values and have functions
look for cached values in the cache map.
Fetches a local attribute or, if a registry is set, a graphed attribute.
Fetches a local field or, if a registry is set, a graphed field.
Same as fetch_field/2
but raises FieldError
if the field isn't present.
Fetches a field with related graph data using the resource's field graphs.
Same as fetch_graphed_field/2
but raises FieldError
if the graphed field
isn't present.
Fetches a local field by name.
Same as fetch_local_field/2
but raises FieldError
if the local field isn't
present.
Fetches another type by name from a type's registry.
Fetches a local relationship or, if a registry is set, a graphed relationship.
Fetches the field graph for a given type if the type exists and has a registry.
Checks if a type has a local field.
Sets the attribute to be used as the ID attribute for a given type. The ID
field has slightly special usage in that extensions will use it for both
identification and equality. There are also conveniences for working directly
with IDs such as get_id/2
.
Validates and returns the mapped names from a graph
Maps the ID value for a given resource. This is just shorthand for using
map_value/3
on whatever field is designated as the ID.
Maps a value for a given field name for a resource.
Takes mappable resource, a type, and a list of fields. Returns a list of
tuples with the field name and the mapped value. This is returned instead
of a map to preserve the order of the input list. If order is irrelevant, use
to_map/2
instead.
Sets max_depth
on a type.
Sets max_filters
on a type. This is the total number of filters allowed in
a single query.
Sets max_sorters
on a type. This is the total number of sorts allowed in a
single query.
Adds a value to the meta
map. Meta information is not used by types directly
in this module. It is intended to add more information that can be used by
extensions and other implementations. For example, JSON:API resources provide
linkage and describing that linkage is an appropriate use of the meta map.
Sets name
on a type. Name must be strings and cannot contain periods.
Atoms will be automatically converted to strings.
Creates a new Resourceful.Type
with valid attributes.
Puts a new field in the fields
map using the field's name as the
key. This will replace a field of the same name if present.
Sets the registry
module for a type. In general, this functional will be
called by a Resourceful.Registry
and not directly.
Returns a list of the name of all relationship fields.
Returns a name as a dot-separated string.
Like map_values/3
only returns a map with keys in the name of the attributes
with with values of the mapped values.
Validates a single filter on an attribute.
Returns a valid mapping name for a field. Any atom or string is valid and should map to the whatever the underlying resources will look like.
Validates that the max number of filters hasn't been exceeded.
Validates that the max number of sorters hasn't been exceeded.
Returns a valid string name for a type or field. Technically any string without a period is valid, but like most names, don't go nuts with URL characters, whitespace, etc.
Validates a single sorter on an attribute.
Returns an existing type with an empty cache
key.
Types
@type field() :: %Resourceful.Type.Attribute{ embedded_type: term(), filter?: term(), map_to: term(), name: term(), sort?: term(), type: term() } | %Resourceful.Type.Relationship{ embedded?: term(), graph?: term(), map_to: term(), name: term(), related_type: term(), type: term() }
A field is an attribute or a relationship. They share the same namespace within a type.
@type queryable() :: %Resourceful.Type.Attribute{ embedded_type: term(), filter?: term(), map_to: term(), name: term(), sort?: term(), type: term() } | %Resourceful.Type.GraphedField{ field: %Resourceful.Type.Attribute{ embedded_type: term(), filter?: term(), map_to: term(), name: term(), sort?: term(), type: term() }, map_to: term(), name: term(), parent: term(), query_alias: term() }
Functions
Returns a list of the name of all attribute fields.
@spec cache( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, atom(), any() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Sets a key in the cache
map. Because types generally intended to be static
at compile time, it can make sense to cache certain values and have functions
look for cached values in the cache map.
For instance, finalize/1
creates a MapSet
for related_types
which
related_types/1
will use instead of computed the MapSet
.
Caches are not meant to be memoized, rather set on a type once it is considered complete.
@spec fetch_attribute( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name() ) :: {:ok, %Resourceful.Type.Attribute{ embedded_type: term(), filter?: term(), map_to: term(), name: term(), sort?: term(), type: term() } | %Resourceful.Type.GraphedField{ field: %Resourceful.Type.Attribute{ embedded_type: term(), filter?: term(), map_to: term(), name: term(), sort?: term(), type: term() }, map_to: term(), name: term(), parent: term(), query_alias: term() }} | Resourceful.Error.t()
Fetches a local attribute or, if a registry is set, a graphed attribute.
@spec fetch_field( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name(), keyword() ) :: {:ok, field() | %Resourceful.Type.GraphedField{ field: term(), map_to: term(), name: term(), parent: term(), query_alias: term() }} | Resourceful.Error.t()
Fetches a local field or, if a registry is set, a graphed field.
@spec fetch_field!( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name() ) :: field() | %Resourceful.Type.GraphedField{ field: term(), map_to: term(), name: term(), parent: term(), query_alias: term() }
Same as fetch_field/2
but raises FieldError
if the field isn't present.
@spec fetch_graphed_field( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name(), keyword() ) :: {:ok, %Resourceful.Type.GraphedField{ field: term(), map_to: term(), name: term(), parent: term(), query_alias: term() }} | Resourceful.Error.t()
Fetches a field with related graph data using the resource's field graphs.
Unless you have a specific reason for fetching only graphed fields, use
fetch_field/3
instead.
@spec fetch_graphed_field!( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name() ) :: %Resourceful.Type.GraphedField{ field: term(), map_to: term(), name: term(), parent: term(), query_alias: term() }
Same as fetch_graphed_field/2
but raises FieldError
if the graphed field
isn't present.
Unless you have a specific reason for fetching only graphed fields, use
fetch_field!/3
instead.
@spec fetch_local_field( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, String.t(), keyword() ) :: {:ok, field()} | Resourceful.Error.t()
Fetches a local field by name.
Unless you have a specific reason for fetching local fields, use
fetch_field/3
instead.
Same as fetch_local_field/2
but raises FieldError
if the local field isn't
present.
Unless you have a specific reason for fetching local fields, use
fetch_field/3
instead.
@spec fetch_relationship( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name() ) :: {:ok, %Resourceful.Type.Relationship{ embedded?: term(), graph?: term(), map_to: term(), name: term(), related_type: term(), type: term() } | %Resourceful.Type.GraphedField{ field: %Resourceful.Type.Relationship{ embedded?: term(), graph?: term(), map_to: term(), name: term(), related_type: term(), type: term() }, map_to: term(), name: term(), parent: term(), query_alias: term() }} | Resourceful.Error.t()
Fetches a local relationship or, if a registry is set, a graphed relationship.
@spec field_graph(%Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }) :: field_graph()
Fetches the field graph for a given type if the type exists and has a registry.
@spec has_local_field?( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, String.t() ) :: boolean()
Checks if a type has a local field.
@spec id( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, String.t() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Sets the attribute to be used as the ID attribute for a given type. The ID
field has slightly special usage in that extensions will use it for both
identification and equality. There are also conveniences for working directly
with IDs such as get_id/2
.
A limitation of types is that currently composite ID fields are not supported.
@spec map_field( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name() ) :: {:ok, [atom() | String.t()]} | Resourceful.Error.t()
Validates and returns the mapped names from a graph
@spec map_id(any(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }) :: any()
Maps the ID value for a given resource. This is just shorthand for using
map_value/3
on whatever field is designated as the ID.
@spec map_value( map(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field_name() ) :: any()
Maps a value for a given field name for a resource.
@spec map_values( map(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, [field_name()] ) :: [{any(), any()}]
Takes mappable resource, a type, and a list of fields. Returns a list of
tuples with the field name and the mapped value. This is returned instead
of a map to preserve the order of the input list. If order is irrelevant, use
to_map/2
instead.
@spec max_depth( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, integer() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Sets max_depth
on a type.
max_depth
is specifically a reference to the depth of relationships that
will be transversed. This means the default max_depth
of 1
would expose
all immediate relationships and their attributes.
For example, a song type with a max_depth
of 1
would be able to graph
through album
and query against album.title
but would not be able to
access album.artist
or any of its attributes. Increasing the max_depth
to
2
would expose album.artist.name
.
@spec max_filters( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, integer() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Sets max_filters
on a type. This is the total number of filters allowed in
a single query.
@spec max_sorters( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, integer() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Sets max_sorters
on a type. This is the total number of sorts allowed in a
single query.
@spec meta( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, atom(), any() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Adds a value to the meta
map. Meta information is not used by types directly
in this module. It is intended to add more information that can be used by
extensions and other implementations. For example, JSON:API resources provide
linkage and describing that linkage is an appropriate use of the meta map.
Cached values should not be put in the meta map. Though both cache
and
meta
could essentially be used for the same thing, caches are expected to be
set specially when registering a type in Resourceful.Registry
because
without_cache/1
is called before finalizing a type.
@spec name( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, String.t() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Sets name
on a type. Name must be strings and cannot contain periods.
Atoms will be automatically converted to strings.
@spec new( String.t(), keyword() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Creates a new Resourceful.Type
with valid attributes.
See functions of the same name for more information on key functionality.
For fields, see Resourceful.Type.Attribute
and
Resourceful.Type.Relationship
.
@spec put_field( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, field() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Puts a new field in the fields
map using the field's name as the
key. This will replace a field of the same name if present.
@spec register( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, module() ) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Sets the registry
module for a type. In general, this functional will be
called by a Resourceful.Registry
and not directly.
Returns a list of the name of all relationship fields.
@spec string_name(field_name()) :: String.t()
Returns a name as a dot-separated string.
@spec to_map( any(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, list() ) :: map()
Like map_values/3
only returns a map with keys in the name of the attributes
with with values of the mapped values.
@spec validate_filter( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, any() ) :: {:ok, Resourceful.Collection.Filter.t()} | Resourceful.Error.t()
Validates a single filter on an attribute.
Returns a valid mapping name for a field. Any atom or string is valid and should map to the whatever the underlying resources will look like.
@spec validate_max_filters( list(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, map() ) :: list()
Validates that the max number of filters hasn't been exceeded.
@spec validate_max_sorters( list(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, map() ) :: list()
Validates that the max number of sorters hasn't been exceeded.
Returns a valid string name for a type or field. Technically any string without a period is valid, but like most names, don't go nuts with URL characters, whitespace, etc.
@spec validate_sorter( %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, any() ) :: {:ok, Resourceful.Collection.Sort.t()} | Resourceful.Error.t()
Validates a single sorter on an attribute.
@spec without_cache(%Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }) :: %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }
Returns an existing type with an empty cache
key.