Alembic v4.0.0 Alembic.Resource View Source

The individual JSON object of elements of the list of the data member of the JSON API document are resources as are the members of the included member.

Link to this section Summary

Types

The ID of a Resource.t. Usually the primary key or UUID for a resource in the server

t()

Resource objects” appear in a JSON API document to represent resources

The type of a Resource.t. Can be either singular or pluralized, althought the JSON API spec examples favor pluralized

Functions

Converts a JSON object into a JSON API Resource, t

Converts resource to params format used by Ecto.Changeset.cast/4. The id and attributes are combined into a single map for params

Unlike to_params/2, if type and id of convertable already exists in converted_by_id_by_type, then the params returned are only %{ "id" => id } without any further expansion, that is, a resource identifier, so that loops are prevented

Link to this section Types

The ID of a Resource.t. Usually the primary key or UUID for a resource in the server.

Link to this type t() View Source
t() :: %Alembic.Resource{
  attributes: Alembic.json_object() | nil,
  id: id() | nil,
  links: Alembic.Links.t() | nil,
  meta: Alembic.Meta.t() | nil,
  relationships: Alembic.Relationships.t() | nil,
  type: type()
}

Resource objects” appear in a JSON API document to represent resources.

A resource object MUST contain at least the following top-level members:

  • id
  • type

Exception: The id member is not required when the resource object originates at the client and represents a new resource to be created on the server. (%{action: :create, source: :client})

In addition, a resource object **MAY(( contain any of these top-level members:

  • attributes - an attributes object representing some of the resource’s data.
  • links - an Alembic.Link.links containing links related to the resource.
  • meta - contains non-standard meta-information about a resource that can not be represented as an attribute or relationship.
  • relationships - a relationships object describing relationships between the resource and other JSON API resources.

The type of a Resource.t. Can be either singular or pluralized, althought the JSON API spec examples favor pluralized.

Link to this section Functions

Link to this function from_json(json, error_template) View Source
from_json(Alembic.json_object(), %Alembic.Error{
  code: term(),
  detail: term(),
  id: term(),
  links: term(),
  meta: Alembic.Meta.t(),
  source: term(),
  status: term(),
  title: term()
}) :: {:ok, t()} | Alembic.FromJson.error()
from_json(
  nil | true | false | list() | float() | integer() | String.t(),
  Alembic.Error.t()
) :: Alembic.FromJson.error()

Converts a JSON object into a JSON API Resource, t.

Invalid

A non-resource will be matched, but return an error.

iex> Alembic.Resource.from_json(
...>   "1",
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data` type is not resource",
        meta: %{
          "type" => "resource"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Type is wrong"
      }
    ]
  }
}

Action

The Alembic.Error.t meta "action" key influences whether "id" is required: "id" is optional for "action" :create sent from "sender" :client; otherwise, it "id" is required.

Creating

Only "type" is required when creating a resource from a client because the "id" will be assigned by the server.

iex> Alembic.Resource.from_json(
...>   %{ "type" => "thing" },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{:ok, %Alembic.Resource{type: "thing"}}

Only "type" will be marked as missing when creating a resource from a client

iex> Alembic.Resource.from_json(
...>   %{},
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/type` is missing",
        meta: %{
          "child" => "type"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}

But, normally you’d include some "attributes" too

iex> Alembic.Resource.from_json(
...>   %{
...>     "attributes" => %{
...>       "name" => "Thing 1"
...>     },
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Resource{
    attributes: %{
      "name" => "Thing 1"
    },
    type: "thing"
  }
}

"attributes" are quite free-form, but must still be an Alembic.json_object

iex> Alembic.Resource.from_json(
...>   %{
...>     "attributes" => [
...>       "name"
...>     ],
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/attributes` type is not json object",
        meta: %{
          "type" => "json object"
        },
        source: %Alembic.Source{
          pointer: "/data/attributes"
        },
        status: "422",
        title: "Type is wrong"
      }
    ]
  }
}

Deleting

Only "id" and "type" is required when when deleting a resource from a client

iex> Alembic.Resource.from_json(
...>   %{ "id" => "1", "type" => "thing"},
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :delete,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{:ok, %Alembic.Resource{id: "1", type: "thing"}}

With "id", "type" will be marked as missing when deleting a resource from a client

iex> Alembic.Resource.from_json(
...>   %{ "id" => "1" },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :delete,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/type` is missing",
        meta: %{
          "child" => "type"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}

With "type", "id" will be marked as missing when deleting a resource from a client

iex> Alembic.Resource.from_json(
...>   %{ "type" => "thing" },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :delete,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/id` is missing",
        meta: %{
          "child" => "id"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}

Both "id" and "type" will be marked as missing when deleting a resource from a client

iex> Alembic.Resource.from_json(
...>   %{},
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :delete,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/id` is missing",
        meta: %{
          "child" => "id"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      },
      %Alembic.Error{
        detail: "`/data/type` is missing",
        meta: %{
          "child" => "type"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}

Updating

Only "id" and "type" is required when when updating a resource from a client

iex> Alembic.Resource.from_json(
...>   %{ "id" => "1", "type" => "thing"},
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :update,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{:ok, %Alembic.Resource{id: "1", type: "thing"}}

With "id", "type" will be marked as missing when upating a resource from a client

iex> Alembic.Resource.from_json(
...>   %{ "id" => "1" },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :update,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/type` is missing",
        meta: %{
          "child" => "type"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}

With "type", "id" will be marked as missing when updating a resource from a client

iex> Alembic.Resource.from_json(
...>   %{ "type" => "thing" },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :update,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/id` is missing",
        meta: %{"child" => "id"},
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}

Both "id" and "type" will be marked as missing when updating a resource from a client

iex> Alembic.Resource.from_json(
...>   %{},
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :update,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/id` is missing",
        meta: %{"child" => "id"},
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      },
      %Alembic.Error{
        detail: "`/data/type` is missing",
        meta: %{
          "child" => "type"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}

Optional members

"links", "meta" and "relationships" are optional, but if they are present, they MUST be valid or their errors will make the overall t invalid.

A valid "links" maps link names to either a JSON object with "href" and/or "meta" member or a String.t URL.

iex> Alembic.Resource.from_json(
...>   %{
...>     "links" => %{
...>        "string" => "http://example.com",
...>       "link_object" => %{
...>         "href" => "http://example.com",
...>         "meta" => %{
...>           "last_updated_on" => "2015-12-21"
...>         }
...>       }
...>     },
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Resource{
    links: %{
      "link_object" => %Alembic.Link{
        href: "http://example.com",
        meta: %{
          "last_updated_on" => "2015-12-21"
        }
      },
      "string" => "http://example.com"
    },
    type: "thing"
  }
}

Even though "links" is optional, if it has errors, those errors will make the entire resource invalid

iex> Alembic.Resource.from_json(
...>   %{
...>     "links" => ["http://example.com"],
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/links` type is not links object",
        meta: %{
          "type" => "links object"
        },
        source: %Alembic.Source{
          pointer: "/data/links"
        },
        status: "422",
        title: "Type is wrong"
      }
    ]
  }
}

"meta"

A valid "meta" is any JSON object

iex> Alembic.Resource.from_json(
...>   %{
...>     "meta" => %{"copyright" => "© 2015"},
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Resource{
    meta: %{
      "copyright" => "© 2015"
    },
    type: "thing"
  }
}

If "meta" isn’t a JSON object, then that error will make the whole resource invalid

iex> Alembic.Resource.from_json(
...>   %{
...>     "meta" => "© 2015",
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/meta` type is not meta object",
        meta: %{
          "type" => "meta object"
        },
        source: %Alembic.Source{
          pointer: "/data/meta"
        },
        status: "422",
        title: "Type is wrong"
      }
    ]
  }
}

Relationships

"relationships" allow linking to other resource in the documents "included" using resource identifiers

iex> Alembic.Resource.from_json(
...>   %{
...>     "relationships" => %{
...>       "shirt" => %{
...>         "data" => %{
...>           "id" => "1",
...>           "type" => "shirt"
...>         }
...>       }
...>     },
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Resource{
    relationships: %{
      "shirt" => %Alembic.Relationship{
        data: %Alembic.ResourceIdentifier{
          id: "1",
          type: "shirt"
        }
      }
    },
    type: "thing"
  }
}

If any relationship has an error, then it will make the entire resource invalid

iex> Alembic.Resource.from_json(
...>   %{
...>     "relationships" => %{
...>       "shirt" => %{
...>         "data" => %{}
...>       }
...>     },
...>     "type" => "thing"
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: "/data"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/data/relationships/shirt/data/type` is missing",
        meta: %{
          "child" => "type"
        },
        source: %Alembic.Source{
          pointer: "/data/relationships/shirt/data"
        },
        status: "422",
        title: "Child missing"
      },
      %Alembic.Error{
        detail: "`/data/relationships/shirt/data/id` is missing",
        meta: %{
          "child" => "id"
        },
        source: %Alembic.Source{
          pointer: "/data/relationships/shirt/data"
        },
        status: "422",
        title: "Child missing"
      }
    ]
  }
}
Link to this function to_ecto_schema(resource, resource_by_id_by_type, ecto_schema_module_by_type) View Source

Converts t to Ecto.Schema.t struct.

The id and attributes are combined into the struct.

iex> Alembic.Resource.to_ecto_schema(
...>   %Alembic.Resource{
...>     attributes: %{
...>       "text" => "First!"
...>     },
...>     id: "1",
...>     type: "post"
...>   },
...>   %{},
...>   %{
...>     "post" => Alembic.TestPost
...>   }
...> )
%Alembic.TestPost{
  id: 1,
  text: "First!"
}

id as nil will pass through to the struct.

iex> Alembic.Resource.to_ecto_schema(
...>   %Alembic.Resource{
...>     attributes: %{
...>       "text" => "First!"
...>     },
...>     type: "post"
...>   },
...>   %{},
...>   %{
...>     "post" => Alembic.TestPost
...>   }
...> )
%Alembic.TestPost{
  text: "First!"
}

Relationships

Relationships’s are merged into the resource’s struct using the relationship name (converted to an atom) as the key in the resource struct.

iex> Alembic.Resource.to_ecto_schema(
...>   %Alembic.Resource{
...>     attributes: %{"text" => "First!"},
...>     relationships: %{
...>       "author" => %Alembic.Relationship{
...>         data: %Alembic.ResourceIdentifier{id: 1, type: "author"}
...>       }
...>     },
...>     type: "post"
...>   },
...>   %{},
...>   %{
...>     "author" => Alembic.TestAuthor,
...>     "post" => Alembic.TestPost
...>   }
...> )
%Alembic.TestPost{
  author: %Alembic.TestAuthor{
    id: 1
  },
  author_id: 1,
  text: "First!"
}

If the relationships does not have have an id (as may be the case where the server does not include linkage data always), then the association will be %Ecto.Association.NotLoaded{} and its foriegn key will be nil.

iex> Alembic.Resource.to_ecto_schema(
...>   %Alembic.Resource{
...>     attributes: %{"text" => "First!"},
...>     relationships: %{
...>       "author" => %Alembic.Relationship{
...>         links:  %{
...>           "related" => "https://example.com/api/v1/posts/1/author"
...>         }
...>       }
...>     },
...>     type: "post"
...>   },
...>   %{},
...>   %{
...>     "author" => Alembic.TestAuthor,
...>     "post" => Alembic.TestPost
...>   }
...> )
%Alembic.TestPost{
  author_id: nil,
  text: "First!"
}

If a relationship does not exist on the Ecto.Schema struct as an association, then it is ignored, so that incomplete models can be used and the sending and receiving-side don’t have to remain in strict sync.

iex> Alembic.Resource.to_ecto_schema(
...>   %Alembic.Resource{
...>     attributes: %{"text" => "First!"},
...>     relationships: %{
...>       "author" => %Alembic.Relationship{
...>         data: %Alembic.ResourceIdentifier{id: 1, type: "author"}
...>       },
...>       "editor" => %Alembic.Relationship{
...>         links: %{
...>           "related" => "https://example.com/api/v1/posts/1/editor"
...>         }
...>       }
...>     },
...>     type: "post"
...>   },
...>   %{},
...>   %{
...>     "author" => Alembic.TestAuthor,
...>     "post" => Alembic.TestPost
...>   }
...> )
%Alembic.TestPost{
  author: %Alembic.TestAuthor{
    id: 1
  },
  author_id: 1,
  text: "First!"
}
Link to this function to_params(resource, resource_by_id_by_type) View Source

Converts resource to params format used by Ecto.Changeset.cast/4. The id and attributes are combined into a single map for params.

iex> Alembic.Resource.to_params(
...>   %Alembic.Resource{
...>     attributes: %{"text" => "First!"},
...>     id: "1",
...>     type: "post"
...>   },
...>   %{}
...> )
%{
  "id" => "1",
  "text" => "First!"
}

But, id won’t show up as “id” in params if it is nil

iex> Alembic.Resource.to_params(
...>   %Alembic.Resource{
...>     attributes: %{"text" => "First!"},
...>     type: "post"
...>   },
...>   %{}
...> )
%{
  "text" => "First!"
}

Relationships

Relationships’s params are merged into the resource’s params

iex> Alembic.Resource.to_params(
...>   %Alembic.Resource{
...>     attributes: %{"text" => "First!"},
...>     relationships: %{
...>       "author" => %Alembic.Relationship{
...>         data: %Alembic.ResourceIdentifier{id: 1, type: "author"}
...>       }
...>     },
...>     type: "post"
...>   },
...>   %{}
...> )
%{
  "text" => "First!",
  "author" => %{
    "id" => 1
  }
}
Link to this function to_params(resource, resource_by_id_by_type, converted_by_id_by_type) View Source

Unlike to_params/2, if type and id of convertable already exists in converted_by_id_by_type, then the params returned are only %{ "id" => id } without any further expansion, that is, a resource identifier, so that loops are prevented.

Parameters

  • convertable - an Alembic.Document.t hierarchy data structure
  • resources_by_id_by_type - A nest map with the outer layer keyed by the Alembic.Resource.type, then the next layer keyed by the Alembic.Resource.id with the values being the full Alembic.Resource.t from Alembic.Document.t included.
  • converted_by_id_by_type - Tracks which (type, id) have been converted already to prevent infinite recursion when expanding indirect relationships.

Returns

Success

  • {nil} if an empty singleton
  • %{} - if a non-empty singleton
  • [] - if an empty collection
  • [%{}] - if a non-empty collection

Errors

  • {:error, :already_converted} - if the type and id of convertable already exists in converted_by_id_by_type
  • {:error, :unset} - if the convertable data is not set

Callback implementation for Alembic.ToParams.to_params/3.