View Source Resourceful.Type (Resourceful v0.1.4)

Resourceful.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

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

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

"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

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

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.

Link to this section Summary

Types

A field is an attribute or a relationship. They share the same namespace within a type.

t()

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.

Same as fetch_field/2 but raises FieldError if the field isn't present.

Fetches a local field or, if a registry is set, a graphed field.

Same as fetch_graphed_field/2 but raises FieldError if the graphed field isn't present.

Fetches a field with related graph data using the resource's field graphs.

Same as fetch_local_field/2 but raises FieldError if the local field isn't present.

Fetches a local field by name.

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.

Link to this section 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 field_graph() :: %{
  required(String.t()) => %Resourceful.Type.GraphedField{
    field: term(),
    map_to: term(),
    name: term(),
    parent: term(),
    query_alias: term()
  }
}
@type field_name() :: String.t() | [String.t()]
@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()
    }
@type t() :: %Resourceful.Type{
  cache: term(),
  fields: term(),
  id: term(),
  max_depth: term(),
  max_filters: term(),
  max_sorters: term(),
  meta: term(),
  name: term(),
  registry: term()
}

Link to this section Functions

@spec attribute_names(t()) :: [String.t()]

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.

Link to this function

fetch_attribute(type, name)

View Source
@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.

Link to this function

fetch_field!(type, name)

View Source
@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.

Link to this function

fetch_field(type, name, opts \\ [])

View Source
@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.

Link to this function

fetch_graphed_field!(type, name)

View Source
@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.

Link to this function

fetch_graphed_field(type, name, opts \\ [])

View Source
@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.

Link to this function

fetch_local_field!(type, name, opts \\ [])

View Source

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.

Link to this function

fetch_local_field(type, name, opts \\ [])

View Source
@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.

Link to this function

fetch_relationship(type, name)

View Source
@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.

Link to this function

has_local_field?(type, name)

View Source
@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.

Link to this function

map_value(resource, type, name)

View Source
@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.

Link to this function

map_values(resource, type, fields \\ [])

View Source
@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.

Link to this function

max_depth(type, max_depth)

View Source
@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.

Link to this function

max_filters(type, max_filters)

View Source
@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.

Link to this function

max_sorters(type, max_sorters)

View Source
@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.

Link to this function

relationship_names(type)

View Source
@spec relationship_names(t()) :: [String.t()]

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.

Link to this function

to_map(resource, type, field_names \\ [])

View Source
@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.

Link to this function

validate_filter(type, filter)

View Source
@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.

Link to this function

validate_map_to!(map_to)

View Source
@spec validate_map_to!(atom() | String.t()) :: atom() | String.t()

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.

Link to this function

validate_max_filters(list, type, context \\ %{})

View Source
@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.

Link to this function

validate_max_sorters(list, type, context \\ %{})

View Source
@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.

@spec validate_name!(atom() | String.t()) :: String.t()

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.

Link to this function

validate_sorter(type, sorter)

View Source
@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.