Alembic v2.1.0 Alembic.Document

JSON API refers to the top-level JSON structure as a document.

Summary

Types

t()

A JSON API Document

Functions

Tries to determine the common Alembic.Error.t status between all errors in the document

Converts a JSON object into a JSON API Document, t

Lookup table of included resources, so that Alembic.ResourceIdentifier.t can be converted to full Alembic.Resource.t

Merges the errors from two documents together

Since merge/2 adds the second errors to the beginning of a first document’s errors list, the final merged errors needs to be reversed to maintain the original order

Converts a document into one or more Ecto.Schema.t structs

Call to_ecto_schema/2 instead to automatically generate attributes_by_id_by_type

Extract paged pagination information

Transforms a t into the nested params format used by Ecto.Changeset.cast/4

Transforms a t into the nested params format used by Ecto.Changeset.cast/4 using the given resources_by_id_by_type

Transforms a t into the nested params format used by Ecto.Changeset.cast/4 using the given resources_by_id_by_type and converted_by_id_by_type

Types

t ::
  %Alembic.Document{data: nil, errors: [Alembic.Error.t], included: nil, jsonapi: term, links: Alembic.Links.t | nil, meta: Alembic.Meta.t | nil} |
  %Alembic.Document{data: nil, errors: nil, included: nil, jsonapi: term, links: Alembic.Links.t | nil, meta: Alembic.Meta.t} |
  %Alembic.Document{data: [Alembic.Resource.t] | Alembic.Resource.t, errors: nil, included: [Alembic.Resource.t] | nil, jsonapi: term, links: Alembic.Links.t | nil, meta: Alembic.Meta.t | nil}

A JSON API Document.

Data

When there are no errors, data are returned in the document and errors are not returned in the document.

FieldIncluded/Excluded/Optional
dataIncluded
errorsExcluded
includedOptional
linksOptional
metaOptional

Errors

When an error occurs, errors are returned in the document and data are not returned in the document.

FieldIncluded/Excluded/Optional
dataExcluded
errorsIncluded
includedExcluded
linksOptional
metaOptional

Meta

JSON API allows a meta only document, in which case data and errors are not returned in the document.

FieldIncluded/Excluded/Optional
dataExcluded
errorsExcluded
includedExcluded
linksOptional
metaIncluded

Functions

error_status_consensus(document)

Tries to determine the common Alembic.Error.t status between all errors in the document.

If it is not an errors document, nil is returned.

iex> Alembic.Document.error_status_consensus(
...>   %Alembic.Document{data: []}
...> )
nil

Single error

If there is one error, its status is returned. This could be nil as no field is required in a JSONAPI error.

iex> Alembic.Document.error_status_consensus(
...>   %Alembic.Document{
...>     errors: [
...>       %Alembic.Error{
...>         status: "404"
...>       }
...>     ]
...>   }
...> )
"404"
iex> Alembic.Document.error_status_consensus(
...>   %Alembic.Document{
...>     errors: [
...>       %Alembic.Error{}
...>     ]
...>   }
...> )
nil

Multiple errors

If there are multiple errors with the same status, then that is the consensus

iex> Alembic.Document.error_status_consensus(
...>   %Alembic.Document{
...>     errors: [
...>       %Alembic.Error{
...>         status: "404"
...>       },
...>       %Alembic.Error{
...>         status: "404"
...>       }
...>     ]
...>   }
...> )
"404"

If there are multiple errors, but some errors don’t have statuses, they are ignored

iex> Alembic.Document.error_status_consensus(
...>   %Alembic.Document{
...>     errors: [
...>       %Alembic.Error{},
...>       %Alembic.Error{
...>         status: "404"
...>       }
...>     ]
...>   }
...> )
"404"

If there are multiple errors, but they disagree within the same 100s block, then that is the consensus

iex> Alembic.Document.error_status_consensus(
...>   %Alembic.Document{
...>     errors: [
...>       %Alembic.Error{
...>         status: "404"
...>       },
...>       %Alembic.Error{
...>         status: "422"
...>       }
...>     ]
...>   }
...> )
"400"

If there are multiple errors, but they disagree without the same 100s block, then the greater 100s block is the consensus

iex> Alembic.Document.error_status_consensus(
...>   %Alembic.Document{
...>     errors: [
...>       %Alembic.Error{
...>         status: "422"
...>       },
...>       %Alembic.Error{
...>         status: "500"
...>       }
...>     ]
...>   }
...> )
"500"
from_json(json, error_template)

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

Data documents

Single

An empty single resource can represented as "data": null in encoded JSON, so it comes into from_json as data: nil

iex> Alembic.Document.from_json(
...>   %{ "data" => nil },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: nil
  }
}

A present single can be a resource

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "attributes" => %{
...>         "text" => "First Post!"
...>       },
...>       "id" => "1",
...>       "type" => "post"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: %Alembic.Resource{
      attributes: %{
        "text" => "First Post!"
      },
      id: "1",
      type: "post"
    }
  }
}

… or a present single can be just a resource identifier

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "id" => "1",
...>       "type" => "post"
...>     }
...>   },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: %Alembic.ResourceIdentifier{
      id: "1",
      type: "post"
    }
  }
}

You notice that whether a JSON object in "data" is treated as a Alembic.Resource.t or Alembic.ResourceIdentifier.t hinges on whether "attributes" or "relationships" is present as those members are only allowed for resources.

Collection

Resources

A collection can be a list of resources

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "attributes" => %{
...>           "text" => "First Post!"
...>         },
...>         "id" => "1",
...>         "relationships" => %{
...>           "comments" => %{
...>             "data" => [
...>               %{
...>                 "id" => "1",
...>                 "type" => "comment"
...>               }
...>             ]
...>           }
...>         },
...>         "type" => "post"
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: [
      %Alembic.Resource{
        attributes: %{
          "text" => "First Post!"
        },
        id: "1",
        relationships: %{
          "comments" => %Alembic.Relationship{
            data: [
              %Alembic.ResourceIdentifier{
                id: "1",
                type: "comment"
              }
            ]
          }
        },
        type: "post"
      }
    ]
  }
}

With "relationships", a resources collection can optionally have "included" for the attributes for the resource identifiers. If "included" is not given or the "id" and "type" for a resource identifier, then the resource identifier should just be considered a foreign key reference that needs to be fetched with another API query.

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "attributes" => %{
...>           "text" => "First Post!"
...>         },
...>         "id" => "1",
...>         "relationships" => %{
...>           "comments" => %{
...>             "data" => [
...>               %{
...>                 "id" => "1",
...>                 "type" => "comment"
...>               }
...>             ]
...>           }
...>         },
...>         "type" => "post"
...>       }
...>     ],
...>     "included" => [
...>       %{
...>         "attributes" => %{
...>           "text" => "First Comment!"
...>         },
...>         "id" => "1",
...>         "type" => "comment"
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: [
      %Alembic.Resource{
        attributes: %{
          "text" => "First Post!"
        },
        id: "1",
        relationships: %{
          "comments" => %Alembic.Relationship{
            data: [
              %Alembic.ResourceIdentifier{
                id: "1",
                type: "comment"
              }
            ]
          }
        },
        type: "post"
      }
    ],
    included: [
      %Alembic.Resource{
        attributes: %{
          "text" => "First Comment!"
        },
        id: "1",
        type: "comment"
      }
    ]
  }
}

Resource Identifiers

Or a list of resource identifiers

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "id" => "1",
...>         "type" => "post"
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: [
      %Alembic.ResourceIdentifier{
        id: "1",
        type: "post"
      }
    ]
  }
}

Empty

An empty collection can be signified with []. Because there is no type information, it’s not possible to tell whether it is an empty list of Alembic.Resource.t or Alembic.ResourceIdentifier.t.

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => []
...>   },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: []
  }
}

Errors documents

Errors from the sender must have an "errors" key set to a list of errors.

iex> Alembic.Document.from_json(
...>   %{
...>     "errors" => [
...>       %{
...>         "code" => "1",
...>         "detail" => "There was an error in data",
...>         "id" => "2",
...>         "links" => %{
...>           "about" => %{
...>             "href" => "/errors/2",
...>             "meta" => %{
...>               "extra" => "about meta"
...>             }
...>           }
...>         },
...>         "meta" => %{
...>           "extra" => "error meta"
...>         },
...>         "source" => %{
...>           "pointer" => "/data"
...>         },
...>         "status" => "422",
...>         "title" => "There was an error"
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        code: "1",
        detail: "There was an error in data",
        id: "2",
        links: %{
          "about" => %Alembic.Link{
            href: "/errors/2",
            meta: %{
              "extra" => "about meta"
            }
          }
        },
        meta: %{
          "extra" => "error meta"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "There was an error"
      }
    ]
  }
}

Error objects MUST be returned as an array keyed by "errors" in the top level of a JSON API document.

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

Meta documents

Returned documents can contain just "meta" with neither "data" nor "errors"

iex> Alembic.Document.from_json(
...>   %{
...>     "meta" => %{
...>       "copyright" => "2016"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    meta: %{
      "copyright" => "2016"
    }
  }
}

Incomplete documents

If neither "errors", "data", nor "meta" is present, then the document is invalid and an :error tuple will be returned. In the tuple with :error is an Alembic.Document.t that is a proper JSONAPI errors document that can be set back to the sender.

iex> Alembic.Document.from_json(
...>   %{},
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "At least one of the following children of `` must be present:\n" <>
                "data\n" <>
                "errors\n" <>
                "meta",
        meta: %{
          "children" => [
            "data",
            "errors",
            "meta"
          ]
        },
        source: %Alembic.Source{
          pointer: ""
        },
        status: "422",
        title: "Not enough children"
      }
    ],
  }
}
included_resource_by_id_by_type(document)

Specs

included_resource_by_id_by_type(t) :: Alembic.ToParams.resource_by_id_by_type

Lookup table of included resources, so that Alembic.ResourceIdentifier.t can be converted to full Alembic.Resource.t.

No included resources

With no included resources, an empty map is returned

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "type" => "post",
...>       "id" => "1"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
...> Alembic.Document.included_resource_by_id_by_type(document)
%{}

Included resources

With included resources, a nest map is built 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

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "type" => "articles",
...>         "id" => "1",
...>         "relationships" => %{
...>           "author" => %{
...>             "data" => %{
...>               "type" => "people",
...>               "id" => "9"
...>             }
...>           },
...>           "comments" => %{
...>             "data" => [
...>               %{
...>                 "type" => "comments",
...>                 "id" => "5"
...>               },
...>               %{
...>                 "type" => "comments",
...>                 "id" => "12"
...>               }
...>             ]
...>           }
...>         }
...>       }
...>     ],
...>     "included" => [
...>       %{
...>         "type" => "people",
...>         "id" => "9",
...>         "attributes" => %{
...>           "first-name" => "Dan",
...>           "last-name" => "Gebhardt",
...>           "twitter" => "dgeb"
...>         }
...>       },
...>       %{
...>         "type" => "comments",
...>         "id" => "5",
...>         "attributes" => %{
...>           "body" => "First!"
...>         },
...>         "relationships" => %{
...>           "author" => %{
...>             "data" => %{
...>               "type" => "people",
...>               "id" => "2"
...>             }
...>           }
...>         }
...>       },
...>       %{
...>         "type" => "comments",
...>         "id" => "12",
...>         "attributes" => %{
...>           "body" => "I like XML better"
...>         },
...>         "relationships" => %{
...>           "author" => %{
...>             "data" => %{
...>               "type" => "people",
...>               "id" => "9"
...>             }
...>           }
...>         }
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
...> Alembic.Document.included_resource_by_id_by_type(document)
%{
  "comments" => %{
    "12" => %Alembic.Resource{
      attributes: %{
        "body" => "I like XML better"
      },
      id: "12",
      relationships: %{
        "author" => %Alembic.Relationship{
          data: %Alembic.ResourceIdentifier{
            id: "9",
            type: "people"
          }
        }
      },
      type: "comments"
    },
    "5" => %Alembic.Resource{
      attributes: %{
        "body" => "First!"
      },
      id: "5",
      relationships: %{
        "author" => %Alembic.Relationship{
          data: %Alembic.ResourceIdentifier{
            id: "2",
            type: "people"
          }
        }
      },
      type: "comments"
    }
  },
  "people" => %{
    "9" => %Alembic.Resource{
      attributes: %{
        "first-name" => "Dan",
        "last-name" => "Gebhardt",
        "twitter" => "dgeb"
      },
      id: "9",
      type: "people"
    }
  }
}
merge(first, second)

Specs

merge(%Alembic.Document{data: term, errors: [Alembic.Error.t], included: term, jsonapi: term, links: term, meta: term}, %Alembic.Document{data: term, errors: [Alembic.Error.t], included: term, jsonapi: term, links: term, meta: term}) :: %Alembic.Document{data: term, errors: [Alembic.Error.t], included: term, jsonapi: term, links: term, meta: term}

Merges the errors from two documents together.

The errors from the second document are prepended to the errors of the first document so that the errors as a whole can be reversed with reverse/1

reverse(document)

Since merge/2 adds the second errors to the beginning of a first document’s errors list, the final merged errors needs to be reversed to maintain the original order.

iex> merged = %Alembic.Document{
...>   errors: [
...>     %Alembic.Error{
...>       detail: "The index `2` of `/data` is not a resource",
...>       source: %Alembic.Source{
...>         pointer: "/data/2"
...>       },
...>       title: "Element is not a resource"
...>     },
...>     %Alembic.Error{
...>       detail: "The index `1` of `/data` is not a resource",
...>       source: %Alembic.Source{
...>         pointer: "/data/1"
...>       },
...>       title: "Element is not a resource"
...>     }
...>   ]
...> }
iex> Alembic.Document.reverse(merged)
%Alembic.Document{
  errors: [
    %Alembic.Error{
      detail: "The index `1` of `/data` is not a resource",
      source: %Alembic.Source{
        pointer: "/data/1"
      },
      title: "Element is not a resource"
    },
    %Alembic.Error{
      detail: "The index `2` of `/data` is not a resource",
      source: %Alembic.Source{
        pointer: "/data/2"
      },
      title: "Element is not a resource"
    }
  ]
}
to_ecto_schema(document, ecto_schema_module_by_type)

Specs

to_ecto_schema(%Alembic.Document{data: term, errors: term, included: term, jsonapi: term, links: term, meta: term}, %{String.t => module}) ::
  {:ok, nil | struct | [] | [struct]} |
  {:error, t}

Converts a document into one or more Ecto.Schema.t structs.

Parameters

  • document - supplies resource attributes and associations (through included)
  • ecto_schema_module_by_type - Maps the Alembic.Resource.t type String.t to the Ecto.Schema module to use with Ecto.Changeset.cast/4.

Returns

  • {:ok, nil} - an empty single resource
  • {:ok, struct} - a single resource
  • {:ok, []} - an empty resource collection
  • {:ok, [struct]} - a non-empty resource collection
  • {:error, t} - the document have errors and so won’t be converted to struct(s).

Examples

No resource

No resource has to return nil for Ecto schema because the type is not present.

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => nil
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_ecto_schema(document, %{})
{:ok, nil}

Single resource

A single resource is converted to a single Ecto schema struct corresponding to the type.

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "attributes" => %{
...>         "name" => "Thing 1"
...>       },
...>       "id" => "1",
...>       "type" => "thing"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_ecto_schema(
...>   document,
...>   %{
...>     "thing" => Alembic.TestThing
...>   }
...> )
{
  :ok,
  %Alembic.TestThing{
    __meta__: %Ecto.Schema.Metadata{
      source: {nil, "things"},
      state: :built
    },
    id: 1,
    name: "Thing 1"
  }
}

Relationships

Relationships are merged into the struct for the resource

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "attributes" => %{
...>         "name" => "Thing 1"
...>       },
...>       "relationships" => %{
...>         "shirt" => %{
...>           "data" => %{
...>             "attributes" => %{
...>               "size" => "L"
...>             },
...>             "type" => "shirt"
...>           }
...>         }
...>       },
...>       "type" => "thing"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_ecto_schema(
...>   document,
...>   %{
...>     "shirt" => Alembic.TestShirt,
...>     "thing" => Alembic.TestThing
...>   }
...> )
{
  :ok,
  %Alembic.TestThing{
    __meta__: %Ecto.Schema.Metadata{
      source: {nil, "things"},
      state: :built
    },
    name: "Thing 1",
    shirt: %Alembic.TestShirt{
      __meta__: %Ecto.Schema.Metadata{
        source: {nil, "shirts"},
        state: :built
      },
      size: "L"
    }
  }
}

Multiple resources

Multiple resources are converted to a struct list where each element is a struct that combines the id and attributes

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "type" => "post",
...>         "id" => "1",
...>         "attributes" => %{
...>           "text" => "Welcome"
...>         }
...>       },
...>       %{
...>         "type" => "post",
...>         "id" => "2",
...>         "attributes" => %{
...>           "text" => "It's been awhile"
...>         }
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_ecto_schema(
...>   document,
...>   %{
...>     "post" => Alembic.TestPost
...>   }
...> )
{
  :ok,
  [
    %Alembic.TestPost{
      __meta__: %Ecto.Schema.Metadata{
        source: {nil, "posts"},
        state: :built
      },
      author: %Ecto.Association.NotLoaded{
        __cardinality__: :one,
        __field__: :author,
        __owner__: Alembic.TestPost
      },
      author_id: nil,
      id: 1,
      text: "Welcome"
    },
    %Alembic.TestPost{
      __meta__: %Ecto.Schema.Metadata{
        source: {nil, "posts"},
        state: :built
      },
      author: %Ecto.Association.NotLoaded{
        __cardinality__: :one,
        __field__: :author,
        __owner__: Alembic.TestPost
      },
      author_id: nil,
      id: 2,
      text: "It's been awhile"
    }
  ]
}

Relationships

Relationships are merged into the structs for the corresponding resource

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "type" => "post",
...>         "id" => "1",
...>         "attributes" => %{
...>           "text" => "Welcome"
...>         },
...>         "relationships" => %{
...>           "comments" => %{
...>             "data" => [
...>               %{
...>                 "type" => "comment",
...>                 "id" => "3"
...>               }
...>             ]
...>           }
...>         }
...>       },
...>       %{
...>         "type" => "post",
...>         "id" => "2",
...>         "attributes" => %{
...>           "text" => "It's been awhile"
...>         },
...>         "relationships" => %{
...>           "comments" => %{
...>             "data" => []
...>           }
...>         }
...>       }
...>     ],
...>     "included" => [
...>       %{
...>         "type" => "comment",
...>         "id" => "3",
...>         "attributes" => %{
...>           "text" => "First!"
...>         }
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_ecto_schema(
...>   document,
...>   %{
...>     "comment" => Alembic.TestComment,
...>     "post" => Alembic.TestPost
...>   }
...> )
{
  :ok,
  [
    %Alembic.TestPost{
      __meta__: %Ecto.Schema.Metadata{
        source: {nil, "posts"},
        state: :built
      },
      author: %Ecto.Association.NotLoaded{
        __cardinality__: :one,
        __field__: :author,
        __owner__: Alembic.TestPost
      },
      author_id: nil,
      comments: [
        %Alembic.TestComment{
          __meta__: %Ecto.Schema.Metadata{
            source: {nil, "comments"},
            state: :built
          },
          id: 3,
          post: %Ecto.Association.NotLoaded{
            __cardinality__: :one,
            __field__: :post,
            __owner__: Alembic.TestComment
          },
          post_id: nil,
          text: "First!"
        }
      ],
      id: 1,
      text: "Welcome"
    },
    %Alembic.TestPost{
      __meta__: %Ecto.Schema.Metadata{
        source: {nil, "posts"},
        state: :built
      },
      author: %Ecto.Association.NotLoaded{
        __cardinality__: :one,
        __field__: :author,
        __owner__: Alembic.TestPost
      },
      author_id: nil,
      comments: [],
      id: 2,
      text: "It's been awhile"
    }
  ]
}
to_ecto_schema(document, resource_by_id_by_type, ecto_schema_module_by_type)

Specs

to_ecto_schema(%Alembic.Document{data: [Alembic.Resource.t] | Alembic.Resource.t, errors: term, included: term, jsonapi: term, links: term, meta: term}, Alembic.ToParams.resource_by_id_by_type, Alembic.ToEctoSchema.ecto_schema_module_by_type) ::
  [struct] |
  struct

Call to_ecto_schema/2 instead to automatically generate attributes_by_id_by_type

to_pagination(document)

Specs

to_pagination(t) :: Alembic.Pagination.t | nil

Extract paged pagination information.

To support pagination, a document at minimum must have a "record_count" meta entry.

iex> Alembic.Document.to_pagination(
...>   %Alembic.Document{meta: %{ "record_count" => 10 }}
...> )
%Alembic.Pagination{ total_size: 10 }

Without it, the "record_count" meta entry, the pagination will be nil

iex> Alembic.Document.to_pagination(%Alembic.Document{})
nil

Single Page

When there is only one page, there will be a "first" and "last" link pointing to the same page, but no “next” or “prev” links. The total_size as given by the meta "record_count" will between 0 and the page size because the last is not necessarily full.

iex> Alembic.Document.to_pagination(
...>   %Alembic.Document{
...>     links: %{
...>       "first" => "https://example.com/api/v1/users?page%5Bnumber%5D=1&page%5Bsize%5D=10",
...>       "last" => "https://example.com/api/v1/users?page%5Bnumber%5D=1&page%5Bsize%5D=10"
...>     },
...>     meta: %{ "record_count" => 5 }
...>   }
...> )
%Alembic.Pagination{
  first: %Alembic.Pagination.Page{
    number: 1,
    size: 10
  },
  last: %Alembic.Pagination.Page{
    number: 1,
    size: 10
  },
  total_size: 5
}

Multiple Pages

When there are multiple pages, every page will have a "first" and "last" link pointing to the respective, different pages. The total_size as given by the meta "record_count" will be between (last.number - 1) * last.size and last.number * last.size because the last page is not necessarily full.

On the first page, the "next" link will be set, but not the "prev" link.

iex> Alembic.Document.to_pagination(
...>   %Alembic.Document{
...>     links: %{
...>       "first" => "https://example.com/api/v1/users?page%5Bnumber%5D=1&page%5Bsize%5D=10",
...>       "last" => "https://example.com/api/v1/users?page%5Bnumber%5D=3&page%5Bsize%5D=10",
...>       "next" => "https://example.com/api/v1/users?page%5Bnumber%5D=2&page%5Bsize%5D=10"
...>     },
...>     meta: %{ "record_count" => 25 }
...>   }
...> )
%Alembic.Pagination{
  first: %Alembic.Pagination.Page{
    number: 1,
    size: 10
  },
  last: %Alembic.Pagination.Page{
    number: 3,
    size: 10
  },
  next: %Alembic.Pagination.Page{
    number: 2,
    size: 10
  },
  total_size: 25
}

On any middle page, both the "next" and "prev" links will be set.

iex> Alembic.Document.to_pagination(
...>   %Alembic.Document{
...>     links: %{
...>       "first" => "https://example.com/api/v1/users?page%5Bnumber%5D=1&page%5Bsize%5D=10",
...>       "last" => "https://example.com/api/v1/users?page%5Bnumber%5D=3&page%5Bsize%5D=10",
...>       "next" => "https://example.com/api/v1/users?page%5Bnumber%5D=3&page%5Bsize%5D=10",
...>       "prev" => "https://example.com/api/v1/users?page%5Bnumber%5D=1&page%5Bsize%5D=10"
...>     },
...>     meta: %{ "record_count" => 25 }
...>   }
...> )
%Alembic.Pagination{
  first: %Alembic.Pagination.Page{
    number: 1,
    size: 10
  },
  last: %Alembic.Pagination.Page{
    number: 3,
    size: 10
  },
  next: %Alembic.Pagination.Page{
    number: 3,
    size: 10
  },
  previous: %Alembic.Pagination.Page{
    number: 1,
    size: 10
  },
  total_size: 25
}

On the last page, the "prev" link will be set, but not the "next" link.

iex> Alembic.Document.to_pagination(
...>   %Alembic.Document{
...>     links: %{
...>       "first" => "https://example.com/api/v1/users?page%5Bnumber%5D=1&page%5Bsize%5D=10",
...>       "last" => "https://example.com/api/v1/users?page%5Bnumber%5D=3&page%5Bsize%5D=10",
...>       "prev" => "https://example.com/api/v1/users?page%5Bnumber%5D=2&page%5Bsize%5D=10"
...>     },
...>     meta: %{ "record_count" => 25 }
...>   }
...> )
%Alembic.Pagination{
  first: %Alembic.Pagination.Page{
    number: 1,
    size: 10
  },
  last: %Alembic.Pagination.Page{
    number: 3,
    size: 10
  },
  previous: %Alembic.Pagination.Page{
    number: 2,
    size: 10
  },
  total_size: 25
}
to_params(document)

Specs

to_params(t) :: [map] | map

Transforms a t into the nested params format used by Ecto.Changeset.cast/4.

No resource

No resource is transformed to an empty map

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => nil
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_params(document)
%{}

Single resource

A single resource is converted to a params map that combines the id and attributes.

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "attributes" => %{
...>         "name" => "Thing 1"
...>       },
...>       "id" => "1",
...>       "type" => "thing"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_params(document)
%{
  "id" => "1",
  "name" => "Thing 1"
}

Relationships

Relationships are merged into the params for the resource

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "attributes" => %{
...>         "name" => "Thing 1"
...>       },
...>       "id" => "1",
...>       "relationships" => %{
...>         "shirt" => %{
...>           "data" => %{
...>             "attributes" => %{
...>               "size" => "L"
...>             },
...>             "type" => "shirt"
...>           }
...>         }
...>       },
...>       "type" => "thing"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_params(document)
%{
  "id" => "1",
  "name" => "Thing 1",
  "shirt" => %{
    "size" => "L"
  }
}

Multiple resources

Multiple resources are converted to a params list where each element is a map that combines the id and attributes

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "type" => "post",
...>         "id" => "1",
...>         "attributes" => %{
...>           "text" => "Welcome"
...>         }
...>       },
...>       %{
...>         "type" => "post",
...>         "id" => "2",
...>         "attributes" => %{
...>           "text" => "It's been awhile"
...>         }
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_params(document)
[
  %{
    "id" => "1",
    "text" => "Welcome"
  },
  %{
    "id" => "2",
    "text" => "It's been awhile"
  }
]

Relationships

Relationships are merged into the params for the corresponding resource

iex> {:ok, document} = Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "type" => "post",
...>         "id" => "1",
...>         "attributes" => %{
...>           "text" => "Welcome"
...>         },
...>         "relationships" => %{
...>           "comments" => %{
...>             "data" => [
...>               %{
...>                 "type" => "comment",
...>                 "id" => "1"
...>               }
...>             ]
...>           }
...>         }
...>       },
...>       %{
...>         "type" => "post",
...>         "id" => "2",
...>         "attributes" => %{
...>           "text" => "It's been awhile"
...>         },
...>         "relationships" => %{
...>           "comments" => %{
...>             "data" => []
...>           }
...>         }
...>       }
...>     ],
...>     "included" => [
...>       %{
...>         "type" => "comment",
...>         "id" => "1",
...>         "attributes" => %{
...>           "text" => "First!"
...>         }
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :fetch,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
iex> Alembic.Document.to_params(document)
[
  %{
    "id" => "1",
    "text" => "Welcome",
    "comments" => [
      %{
        "id" => "1",
        "text" => "First!"
      }
    ]
  },
  %{
    "id" => "2",
    "text" => "It's been awhile",
    "comments" => []
  }
]
to_params(document, resource_by_id_by_type)

Specs

to_params(%Alembic.Document{data: [Alembic.Resource.t] | Alembic.Resource.t | nil, errors: term, included: term, jsonapi: term, links: term, meta: term}, Alembic.ToParams.resource_by_id_by_type) :: Alembic.ToParams.params

Transforms a t into the nested params format used by Ecto.Changeset.cast/4 using the given resources_by_id_by_type.

See Alembic.Document.to_params/1

to_params(document, resource_by_id_by_type, converted_by_id_by_type)

Specs

to_params(%Alembic.Document{data: [Alembic.Resource.t] | Alembic.Resource.t | nil, errors: term, included: term, jsonapi: term, links: term, meta: term}, Alembic.ToParams.resource_by_id_by_type, Alembic.ToParams.converted_by_id_by_type) :: Alembic.ToParams.params

Transforms a t into the nested params format used by Ecto.Changeset.cast/4 using the given resources_by_id_by_type and converted_by_id_by_type.

See InterpreterServer.Api.Document.to_params/1