Skip to content

Commit

Permalink
doc: add put/2 & get/2
Browse files Browse the repository at this point in the history
  • Loading branch information
dch committed Apr 29, 2021
1 parent 68f15e2 commit 63a4ad1
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 74 deletions.
3 changes: 2 additions & 1 deletion lib/sofa.ex
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ defmodule Sofa do
) do
# each Tesla adapter handles "empty" options differently - some
# expect nil, others "", and some expect the key:value to be missing

case Tesla.request(sofa.client, url: path, method: method, query: query, body: body) do
{:ok, resp = %{body: %{"error" => _error, "reason" => _reason}}} ->
{:error,
Expand Down Expand Up @@ -286,7 +287,7 @@ defmodule Sofa do
def raw!(sofa = %Sofa{}, path \\ "", method \\ :get, query \\ [], body \\ %{}) do
case raw(sofa, path, method, query, body) do
{:ok, %Sofa{}, response = %Sofa.Response{}} -> response
{:error, reason} -> raise(Sofa.Error, "unhandled error in #{method} #{path}")
{:error, _reason} -> raise(Sofa.Error, "unhandled error in #{method} #{path}")
end
end
end
208 changes: 146 additions & 62 deletions lib/sofa/doc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule Sofa.Doc do
defstruct attachments: %{},
body: %{},
id: "",
rev: "",
rev: nil,
# type is used to allow Sofa to fake reading and writing Elixir
# Structs directly to/from CouchDB, by duck-typing an additional
# `type` key, which contains the usual `__MODULE__` struct name.
Expand Down Expand Up @@ -49,7 +49,6 @@ defmodule Sofa.Doc do
@doc """
Check if doc exists via `HEAD /:db/:doc and returns either:
- {:error, _reason} # an unhandled error occurred
- {:error, not_found} # doc doesn't exist
- {:ok, %Sofa.Doc{}} # doc exists and has metadata
"""
Expand Down Expand Up @@ -92,81 +91,166 @@ defmodule Sofa.Doc do
end
end

@doc """
Fetch doc and return either
- {:error, not_found} # doc doesn't exist
- {:ok, %Sofa.Doc{}} # doc exists and has metadata
"""
@spec get(Sofa.t(), String.t()) :: {:error, any()} | {:ok, Sofa.Doc.t()}
def get(sofa = %Sofa{database: db}, doc) when is_binary(doc) do
case Sofa.raw(sofa, db <> "/" <> doc, :get) do
{:ok, _sofa,
%Sofa.Response{
status: 404
}} ->
{:error, :not_found}

{:ok, _sofa,
%Sofa.Response{
status: 200,
body: body
}} ->
from_map(body)

{:error,
%Sofa.Response{
status: 401
}} ->
{:error, :unauthorized}

{:error,
%Sofa.Response{
status: 403
}} ->
{:error, :forbidden}

{:error,
%Sofa.Response{
status: 404
}} ->
{:error, :not_found}
end
end

@doc """
Optimistically write/update doc assuming rev matches
"""
@spec put(Sofa.t(), Sofa.Doc.t()) :: {:ok, Sofa.Doc.t()} | {:error, any()}
def put(sofa = %Sofa{database: db}, doc = %Sofa.Doc{id: id, rev: rev}) do
case Sofa.raw(sofa, db <> "/" <> id, :put, [], to_map(doc)) do
{:ok, _sofa,
%Sofa.Response{
status: 201,
body: %{"rev" => rev}
}} ->
{:ok, %Doc{doc | rev: rev}}

{:error,
%Sofa.Response{
status: 400
}} ->
{:error, :bad_request}

{:error,
%Sofa.Response{
status: 401
}} ->
{:error, :unauthorized}

{:error,
%Sofa.Response{
status: 403
}} ->
{:error, :forbidden}

{:error,
%Sofa.Response{
status: 409
}} ->
{:error, :conflict}
end
end

@doc """
Converts internal %Sofa.Doc{} format to CouchDB-native JSON-friendly map
## Examples
iex> %Sofa.Doc{id: "smol", rev: "1-cute", body: %{"yes" => true}} |> to_map()
%{ "_id" => "smol", "_rev" => "1-cute", "yes" => true}
"""
@spec to_map(%Sofa.Doc{}) :: map()
def to_map(doc = %Sofa.Doc{}) do
Map.from_struct(doc)
def to_map(
doc = %Sofa.Doc{
body: body,
id: id,
rev: rev,
type: type,
attachments: atts
}
)
when is_struct(doc, Sofa.Doc) do
# rebuild the couch-style map
m =
%{}
|> Map.put("_id", id)
|> Map.put("_rev", rev)
|> Map.put("_attachments", atts)
|> Map.put("type", type)

# skip all top level keys with value nil
m = :maps.filter(&Sofa.Doc.drop_nil_values/2, m)
# merge with precedence taking from Struct side
Map.merge(body, m)
end

@spec drop_nil_values(any, any) :: false | true
def drop_nil_values(_, v) do
case v do
nil -> false
_ -> true
end
end

@doc """
Converts CouchDB-native JSON-friendly map to internal %Sofa.Doc{} format
## Examples
iex> %{ "_id" => "smol", "_rev" => "1-cute", "yes" => true} |> from_map()
%Sofa.Doc{
attachments: nil,
body: %{"yes" => true},
id: "smol",
rev: "1-cute",
type: nil
}
"""
@spec from_map(map()) :: %Sofa.Doc{}
def from_map(m = %{id: id}) do
# remove all keys that are defined already in the struct
body = Map.drop(m, Map.from_struct(%Sofa.Doc{}) |> Map.keys())
@spec from_map(map()) :: Sofa.Doc.t()
def from_map(m = %{"_id" => id}) when not is_struct(m) do
# remove all keys that are defined already in the struct, and any
# key beginning with "_" as they are restricted within CouchDB
body =
Map.drop(m, [
"_rev",
"_id",
"_attachments",
:_rev,
:_id,
:_attachments
| Map.from_struct(%Sofa.Doc{}) |> Map.keys()
])

# grab the rest we need them
rev = Map.get(m, :rev, nil)
atts = Map.get(m, :attachments, nil)
type = Map.get(m, :type, nil)
rev = Map.get(m, "_rev", nil)
atts = Map.get(m, "_attachments", nil)
type = Map.get(m, "type", nil)
%Sofa.Doc{attachments: atts, body: body, id: id, rev: rev, type: type}
end

# this would be a Protocol for people to defimpl on their own structs
# @spec from_struct(map()) :: %Sofa.Doc{}
# def from_struct(m = %{id: id, __Struct__: type}) do
# end

# @doc """
# create an empty doc
# """

# @spec new() :: {%Sofa.Doc.t()}
# def new(), do: new(Sofa.Doc)

# @doc """
# create doc
# """
# @spec create(Sofa.t(), String.t()) :: {:error, any()} | {:ok, Sofa.t(), any()}
# def create(sofa = %Sofa{}, db) when is_binary(db) do
# case Sofa.raw(sofa, db, :put) do
# {:error, reason} ->
# {:error, reason}

# {:ok, _sofa, resp} ->
# {:ok, sofa,
# %Sofa.Response{
# body: resp.body,
# url: resp.url,
# query: resp.query,
# method: resp.method,
# headers: resp.headers,
# status: resp.status
# }}
# end
# end

# @doc """
# delete doc
# """
# @spec delete(Sofa.t(), String.t()) :: {:error, any()} | {:ok, Sofa.t(), any()}
# def delete(sofa = %Sofa{}, db) when is_binary(db) do
# case Sofa.raw(sofa, db, :delete) do
# {:error, reason} ->
# {:error, reason}

# {:ok, _sofa, resp} ->
# {:ok, sofa,
# %Sofa.Response{
# body: resp.body,
# url: resp.url,
# query: resp.query,
# method: resp.method,
# headers: resp.headers,
# status: resp.status
# }}
# end
# end
end
16 changes: 16 additions & 0 deletions test/fixtures/get_doc_200.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"_id": "everything",
"_rev": "1-7706b7ae496932824e67706d2e72aa3b",
"array": [
1,
"two",
{
"obj": 3
}
],
"int": 333,
"nested": {
"egg": "chicken"
},
"no": false
}
27 changes: 27 additions & 0 deletions test/fixtures/get_doc_with_attachment_stubs_200.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"_id": "attila",
"_rev": "4-322add00c33cab838bf9d7909f18d4f5",
"_attachments": {
"helmet.png": {
"content_type": "image/png",
"revpos": 4,
"digest": "md5-hCzUwAJpP8b6cq+SvEFfTQ==",
"length": 159877,
"stub": true
},
"the_hun.json": {
"content_type": "application/json",
"revpos": 3,
"digest": "md5-sGYAXXcvF3GvYkGw3x882A==",
"length": 18,
"stub": true
},
"the_hun.txt": {
"content_type": "text/plain",
"revpos": 2,
"digest": "md5-lSvsN0hwDhFKO2929yUNDg==",
"length": 8,
"stub": true
}
}
}
5 changes: 5 additions & 0 deletions test/fixtures/put_doc_201.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ok": true,
"_id": "new",
"rev": "1-leet"
}
4 changes: 4 additions & 0 deletions test/fixtures/put_doc_400.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"error": "doc_validation",
"reason": "Bad special document member: _invalid"
}
4 changes: 4 additions & 0 deletions test/fixtures/put_doc_401.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"error": "unauthorized",
"reason": "You are not authorized to access this db."
}
4 changes: 4 additions & 0 deletions test/fixtures/put_doc_403.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"error": "forbidden",
"reason": "doc.type must be user"
}
4 changes: 4 additions & 0 deletions test/fixtures/put_doc_409.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"error": "conflict",
"reason": "Document update conflict."
}
Loading

0 comments on commit 63a4ad1

Please sign in to comment.