diff --git a/.gitignore b/.gitignore
index 43df91cb..4da82353 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,6 @@ mime.types
.rebar
*.plt
.idea
-*.iml
\ No newline at end of file
+*.im
+*.crashdump
+site/
diff --git a/LICENSE b/LICENSE
index 6f673fbd..7c2788cf 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-2009-2016 (c) Benoît Chesneau
+2009-2025 (c) Benoit Chesneau
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..3dafe413
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+DOCKER?=docker
+COMPOSE?=docker compose
+
+.PHONY: docker-build docker-up docker-down docker-logs docker-test docker-shell
+docs:
+ rebar3 ex_doc
+
+docker-build:
+ $(COMPOSE) build
+
+docker-up:
+ $(COMPOSE) up -d couchdb
+
+docker-down:
+ $(COMPOSE) down -v
+
+docker-logs:
+ $(COMPOSE) logs -f --tail=200
+
+docker-test:
+ # Start CouchDB in the background
+ $(COMPOSE) up -d --build couchdb ; \
+ # Run tests as a one-off container, attach only to test output
+ $(COMPOSE) run --rm test ; \
+ status=$$? ; \
+ $(COMPOSE) down -v ; \
+ exit $$status
+
+docker-shell:
+ $(COMPOSE) run --rm test bash
diff --git a/NEWS.md b/NEWS.md
index 966f8972..00a30467 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,6 +1,18 @@
couchbeam NEWS
--------------
+version 2.0.0 / 2025-09-03
+--------------------------
+
+- move from propos lists to maps for json
+- remove JSX & Jiffy Usage for erlang/0TP 28.0+ json module
+- View and changes streaming now use a simpler parser
+
+** BREAKING CHANGE **
+
+- move to maps, you will need to migrate your application to use them.
+no backward compatibility is provided
+
version 1.7.1 / 2025-07-24
---------------------------
@@ -152,7 +164,7 @@ version 1.1.1 / 2014-11-11
- update to [hackney 0.15.0](https://github.com/benoitc/hackney/releases ),
improving performances and concurrency
- fix `couchbeam:doc_exists/2`(#116)
-- fix `couchbeam:reply_att/1 (#114)
+- fix `couchbeam:reply_att/1` (#114)
version 1.1.0 / 2014-10-28
@@ -239,10 +251,9 @@ version 0.10.0 / 2013-12-21
revisions
- add support of the [multipart
API](http://docs.couchdb.org/en/latest/api/document/common.html#efficient-multiple-attachments-retrieving) when fetching a doc: This change make
- `couchbeam:open_doc/3` return a multipart response `{ok, {multipart,
-Stream}}` when using the setting `attachments=true` option. A new option
-{`accept. <<"multipart/mixed">>}" can also be used with the options
-`open_revs` or `revs` to fetch the response as a multipart.
+ `couchbeam:open_doc/3` returns a multipart response `{ok, {multipart, Stream}}`
+ when using the `attachments=true` option. A new option `{accept, <<"multipart/mixed">>}`
+ can also be used with the options `open_revs` or `revs` to fetch the response as a multipart.
- bump the [hackney](http://github.com/benoitc/hackney) version to
**0.9.1** .
@@ -254,7 +265,7 @@ attachments or a doc wit all its revisions.
version 0.9.3 / 2013-12-07
--------------------------
-- fix: `couchbeam:open_or_create_db/2'
+- fix: couchbeam:open_or_create_db/2
version 0.9.2 / 2013-12-07
--------------------------
@@ -272,17 +283,17 @@ version 0.9.0 / 2013-12-05
This is a major release pre-1.0. API is now frozen and won't change much
until the version 1.0.
-- replaced the use of `ibrowse` by `hackney` to handle HTTP connections
+- replaced the use of ibrowse by hackney to handle HTTP connections
- new [streaming
API](https://github.com/benoitc/couchbeam#stream-view-results) in view
- breaking change: remobe
- breaking change: remove deprecated view API. Everything is now managed in the
[couch_view](https://github.com/benoitc/couchbeam/blob/master/doc/couchbeam_view.md) module.
-- replace `couchbeam_changes:stream` and `couchbeam_changes:fetch`
- functions by `couchbeam_changes/follow` and `couchbeam_changes:follow_once`.
+- replace 'couchbeam_changes:stream' and 'couchbeam_changes:fetch'
+ functions by 'couchbeam_changes/follow' and 'couchbeam_changes:follow_once'.
- breaking change: new attachment API
- new: JSX a pure erlang JSON encoder/decoder is now the default. Jiffy
- can be set at the compilation by defining `WITH_JIFFY` in the Erlang
+ can be set at the compilation by defining 'WITH_JIFFY' in the Erlang
options.
- removed mochiweb dependency.
diff --git a/NOTICE b/NOTICE
index 5dcd4531..a56b2087 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,7 +1,7 @@
-Couchdbeam
-----------
+Couchbeam
+---------
-2009-2016 (c) Benoitît Chesneau
+2009-2025 (c) Benoit Chesneau
couchbeam is released under the MIT license. See the LICENSE file for the
complete license.
diff --git a/README.md b/README.md
index 8e645eb9..c3c5faef 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# Couchbeam - simple Apache CouchDB client library for Erlang applications #
-Copyright (c) 2009-2025 Benoît Chesneau.
+Copyright (c) 2009-2025 Benoit Chesneau.
__Version:__ 1.7.1
@@ -17,9 +17,7 @@ Couchbeam is a simple erlang library for [Barrel](https://barrel-db.org) or [Apa
- Stream changes feeds
- reduced memory usage
- fetch and send attachments in a streaming fashion
-- by default use the JSX module to encode/decode JSON
-- support [Jiffy](http://github.com/davisp/jiffy) a JSON encoder/decoder
-in C.
+- JSON encoding/decoding via Erlang/OTP stdlib `json` with maps
#### Useful modules are:
@@ -151,10 +149,10 @@ Make a new document:
```
erlang
-Doc = {[
-{<<"_id">>, <<"test">>},
-{<<"content">>, <<"some text">>}
-]}.
+Doc = #{
+ <<"_id">> => <<"test">>,
+ <<"content">> => <<"some text">>
+}.
```
And save it to the database:
@@ -201,18 +199,21 @@ Options = [include_docs],
{ok, AllDocs} = couchbeam_view:all(Db, Options).
```
-Ex of results:
+Example result (abridged):
```
erlang
-{ok,[{[{<<"id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"key">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"value">>,
- {[{<<"rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18bdb">>}]}},
- {<<"doc">>,
- {[{<<"_id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"_rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18"...>>}]}}]},
- ]}.
+{ok, [
+ #{
+ <<"id">> => <<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>,
+ <<"key">> => <<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>,
+ <<"value">> => #{<<"rev">> => <<"15-15c0b3c4efa74f9a80d28ac040f18bdb">>},
+ <<"doc">> => #{
+ <<"_id">> => <<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>,
+ <<"_rev">> => <<"15-15c0b3c4efa74f9a80d28ac040f18"...>>
+ }
+ }
+]}.
```
All functions to manipulate these results are in the `couchbeam_view` module.
@@ -238,21 +239,18 @@ Design doc are created like any documents:
```
erlang
-DesignDoc = {[
- {<<"_id">>, <<"_design/couchbeam">>},
- {<<"language">>,<<"javascript">>},
- {<<"views">>,
- {[{<<"test">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">>
- }]}
- },{<<"test2">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">>
- }]}
- }]}
+DesignDoc = #{
+ <<"_id">> => <<"_design/couchbeam">>,
+ <<"language">> => <<"javascript">>,
+ <<"views">> => #{
+ <<"test">> => #{
+ <<"map">> => <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">>
+ },
+ <<"test2">> => #{
+ <<"map">> => <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">>
}
- ]},
+ }
+},
{ok, DesignDoc1} = couchbeam:save_doc(Db, DesignDoc).
```
@@ -320,7 +318,7 @@ To fetch an attachment:
{ok Att1} = couchbeam:fetch_attachment(Db, DocId, AttName).
```
-You can use `couchbeam:stream_fetch_attachment/6` for the stream
+You can use `couchbeam:stream_attachment/1` for the stream
fetch.
To delete an attachment:
@@ -378,7 +376,7 @@ for more info.
You can authenticate to the database or Apache CouchDB or RCOUCH server by filling
options to the Option list in `couchbeam:server_connection/4` for the
server or in `couchbeam:create_db/3`, `couchbeam:open_db/3`,
-`couchbeam:wopen_or_create_db/3` functions.
+`couchbeam:open_or_create_db/3` functions.
To set basic_auth on a server:
@@ -399,26 +397,3 @@ in the `couchbeam:server_connection/2` documentation.
For issues, comments or feedback please [create an
issue](http://github.com/benoitc/couchbeam/issues).
-
-
-## Modules ##
-
-
-
-
diff --git a/doc/README.md b/doc/README.md
deleted file mode 100644
index d6d736af..00000000
--- a/doc/README.md
+++ /dev/null
@@ -1,424 +0,0 @@
-
-
-# Couchbeam - simple Apache CouchDB client library for Erlang applications #
-
-Copyright (c) 2009-2025 Benoît Chesneau.
-
-__Version:__ 1.5.0
-
-# couchbeam
-
-Couchbeam is a simple erlang library for [Barrel](https://barrel-db.org) or [Apache CouchDB](http://couchdb.apache.org). Couchbeam provides you a full featured and easy client to access and manage multiple nodes.
-
-#### Main features:
-
-- Complete support of the BarrelDB and Apache CouchDB API
-- Stream view results to your app
-- Stream changes feeds
-- reduced memory usage
-- fetch and send attachments in a streaming fashion
-- by default use the JSX module to encode/decode JSON
-- support [Jiffy](http://github.com/davisp/jiffy) a JSON encoder/decoder
-in C.
-
-#### Useful modules are:
-
-- [`couchbeam`](couchbeam.md): The `couchbeam` module is the main interface for interaction with this application. It includes functions for managing connections to Apache CouchDB or RCOUCH servers and databases and for performing document creations, updates, deletes, views...
-- [`couchbeam_doc`](couchbeam_doc.md) Module to manipulate Documents structures. You can set values,
-updates keys, ...
-- [`couchbeam_attachments`](couchbeam_attachments.md): Module to manipulate attachments. You can add, remove
-attachments in a Document structure (inline attachments).
-- [`couchbeam_view`](couchbeam_view.md): Module to manage view results.
-- [`couchbeam_changes`](couchbeam_changes.md): Module to manage changes feeds. Follow continuously
-the changes in a db or get all changes at once.
-
-The goal of Couchbeam is to ease the access to the Apache CouchDB and RCOUCH HTTP API in erlang.
-
-Read the [NEWS](https://raw.github.com/benoitc/couchbeam/master/NEWS) file
-to get last changelog.
-
-## Installation
-
-Download the sources from our [Github repository](http://github.com/benoitc/couchbeam)
-
-To build the application simply run 'make'. This should build .beam, .app
-files and documentation.
-
-To run tests run 'make test'.
-To generate doc, run 'make doc'.
-
-Or add it to your rebar config
-
-```
- erlang
-{deps, [
- ....
- {couchbeam, ".*", {git, "git://github.com/benoitc/couchbeam.git", {branch, "master"}}}
-]}.
-```
-
-Note to compile with jiffy you need to define in the erlang options the
-variable `WITH_JIFFY`.
-
-if you use rebar, add to your `rebar.config`:
-
-```
- erlang
-{erl_opts, [{d, 'WITH_JIFFY'}]}.
-```
-
-or use the `rebar` command with the `-D` options:
-
-```
- sh
-rebar compile -DWITH_JIFFY
-```
-
-## Basic Usage
-
-### Start couchbeam
-
-Couchbeam is an [OTP](http://www.erlang.org/doc/design_principles/users_guide.html)
-application. You have to start it first before using any of the
-functions. The couchbeam application will start the default socket pool
-for you.
-
-To start in the console run:
-
-```
- sh
-$ erl -pa ebin
-1> couchbeam:start().
-ok
-```
-
-It will start hackney and all of the application it depends on:
-
-```
- erlang
-application:start(crypto),
-application:start(asn1),
-application:start(public_key),
-application:start(ssl),
-application:start(hackney),
-application:start(couchbeam).
-```
-
-Or add couchbeam to the applications property of your .app in a release
-
-### Create a connection to the server
-
-To create a connection to a server machine:
-
-```
- erlang
-Url = "http://localhost:5984",
-Options = [],
-S = couchbeam:server_connection(Url, Options).
-```
-
-Test the connection with `couchbeam:server_info/1` :
-
-```
- erlang
-{ok, _Version} = couchbeam:server_info(S).
-```
-
-### Open or Create a database
-
-All document operations are done in databases. To open a database simply do:
-
-```
- erlang
-Options = [],
-{ok, Db} = couchbeam:open_db(Server, "testdb", Options).
-```
-
-To create a new one:
-
-```
- erlang
-Options = [],
-{ok, Db} = couchbeam:create_db(Server, "testdb", Options).
-```
-
-You can also use the shorcut `couchbeam:open_or_create_db/3`. that
-will create a database if it does not exist.
-
-### Make a new document
-
-Make a new document:
-
-```
- erlang
-Doc = {[
-{<<"_id">>, <<"test">>},
-{<<"content">>, <<"some text">>}
-]}.
-```
-
-And save it to the database:
-
-```
- erlang
-{ok, Doc1} = couchbeam:save_doc(Db, Doc).
-```
-
-The `couchbeam:save_doc/2` return a new document with updated
-revision and if you do not specify the _id, a unique document id.
-
-To change an document property use functions from `couchbeam_doc`.
-
-### Retrieve a document
-
-To retrieve a document do:
-
-```
- erlang
-{ok, Doc2} = couchbeam:open_doc(Db, "test").
-```
-
-If you want a specific revision:
-
-```
- erlang
-Rev = couchbeam_doc:get_rev(Doc1),
-Options = [{rev, Rev}],
-{ok, Doc3} = couchbeam:open_doc(Db, "test", Options).
-```
-
-Here we get the revision from the document we previously stored. Any
-options from the Apache CouchDB and RCOUCH API can be used.
-
-### Get all documents
-
-To get all documents you have first to create an object
-that will keep all informations.
-
-```
- erlang
-Options = [include_docs],
-{ok, AllDocs} = couchbeam_view:all(Db, Options).
-```
-
-Ex of results:
-
-```
- erlang
-{ok,[{[{<<"id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"key">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"value">>,
- {[{<<"rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18bdb">>}]}},
- {<<"doc">>,
- {[{<<"_id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"_rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18"...>>}]}}]},
- ]}.
-```
-
-All functions to manipulate these results are in the `couchbeam_view` module.
-
-### Couch DB views
-
-Views are workin like all_docs. You have to create a View object before
-doing anything.
-
-```
- erlang
-Options = [],
-DesignName = "designname",
-ViewName = "viewname",
-{ok, ViewResults} = couchbeam_view:fetch(Db, {DesignName, ViewName}, Options).
-```
-
-Like the `all_docs` function, use the functions
-from `couchbeam_view` module to manipulate results. You can pass
-any querying options from the [view API](http://docs.rcouch.org/en/latest/api/ddoc/views.html).
-
-Design doc are created like any documents:
-
-```
- erlang
-DesignDoc = {[
- {<<"_id">>, <<"_design/couchbeam">>},
- {<<"language">>,<<"javascript">>},
- {<<"views">>,
- {[{<<"test">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">>
- }]}
- },{<<"test2">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">>
- }]}
- }]}
- }
- ]},
-{ok, DesignDoc1} = couchbeam:save_doc(Db, DesignDoc).
-```
-
-You can also use [couchapp](http://github.com/couchapp/couchapp) to manage them
-more easily.
-
-### Stream View results
-
-While you can get results using `couchbeam_views:fetch/2`, you can also retrieve
-all rows in a streaming fashion:
-
-```
- erlang
-ViewFun = fun(Ref, F) ->
- receive
- {Ref, done} ->
- io:format("done", []),
- done;
- {Ref, {row, Row}} ->
- io:format("got ~p~n", [Row]),
- F(Ref, F);
- {error, Ref, Error} ->
- io:format("error: ~p~n", [Error])
- end
-end,
-
-{ok, StreamRef} = couchbeam_view:stream(Db, 'all_docs'),
-ViewFun(StreamRef, ViewFun),
-{ok, StreamRef2} = couchbeam_view:stream(Db, 'all_docs', [include_docs]),
-ViewFun(StreamRef2, ViewFun).
-```
-
-You can of course do the same with a view:
-
-```
- erlang
-DesignNam = "designname",
-ViewName = "viewname",
-{ok, StreamRef3} = couchbeam_view:stream(Db, {DesignNam, ViewName}, [include_docs]),
-ViewFun(StreamRef3, ViewFun).
-```
-
-### Put, Fetch and Delete documents attachments
-
-You can add attachments to any documents. Attachments could be anything.
-
-To send an attachment:
-
-```
- erlang
-DocID = "test",
-AttName = "test.txt",
-Att = "some content I want to attach",
-Options = []
-{ok, _Result} = couchbeam:put_attachment(Db, DocId, AttName, Att, Options).
-```
-
-All attachments are streamed to servers. `Att` could be also be an iolist
-or functions, see `couchbeam:put_attachment/5` for more information.
-
-To fetch an attachment:
-
-```
- erlang
-{ok Att1} = couchbeam:fetch_attachment(Db, DocId, AttName).
-```
-
-You can use `couchbeam:stream_fetch_attachment/6` for the stream
-fetch.
-
-To delete an attachment:
-
-```
- erlang
-{ok, Doc4} = couchbeam:open_doc(Db, DocID),
-ok = couchbeam:delete_attachment(Db, Doc4, AttName).
-```
-
-### Changes
-
-Apache CouchDB and RCOUCH provide a means to get a list of changes made to documents in
-the database. With couchbeam you can get changes using `couchbeam_changes:follow_once/2`.
-This function returns all changes immediately. But you can also retrieve
-all changes rows using longpolling :
-
-```
- erlang
-Options = [],
-{ok, LastSeq, Rows} = couchbeam_changes:follow_once(Db, Options).
-```
-
-Options can be any Changes query parameters. See the [change API](http://docs.rcouch.org/en/latest/api/database/changes.html) for more informations.
-
-You can also get [continuous](http://docs.rcouch.org/en/latest/api/database/changes.html#continuous):
-
-```
- erlang
-ChangesFun = fun(StreamRef, F) ->
- receive
- {StreamRef, {done, LastSeq}} ->
- io:format("stopped, last seq is ~p~n", [LastSeq]),
- ok;
- {StreamRef, {change, Change}} ->
- io:format("change row ~p ~n", [Change]),
- F(StreamRef, F);
- {StreamRef, Error}->
- io:format("error ? ~p ~n,", [Error])
- end
-end,
-Options = [continuous, heartbeat],
-{ok, StreamRef} = couchbeam_changes:follow(Db, Options),
-ChangesFun(StreamRef, ChangesFun).
-```
-
-> **Note**: a `gen_changes` behaviour exists in couchbeam that you can
-use to create your own specific gen_server receiving changes. Have a
-look in the
-[example](https://github.com/benoitc/couchbeam/blob/master/examples/test_gen_changes.erl)
-for more info.
-
-### Authentication/ Connections options
-
-You can authenticate to the database or Apache CouchDB or RCOUCH server by filling
-options to the Option list in `couchbeam:server_connection/4` for the
-server or in `couchbeam:create_db/3`, `couchbeam:open_db/3`,
-`couchbeam:wopen_or_create_db/3` functions.
-
-To set basic_auth on a server:
-
-```
- erlang
-UserName = "guest",
-Password = "test",
-Url = "http://localhost:5984",
-Options = [{basic_auth, {UserName, Password}}],
-S1 = couchbeam:server_connection(Url, Options).
-```
-
-Couchbeam support SSL, OAuth, Basic Authentication, and Proxy. You can
-also set a cookie. For more informations about the options have a look
-in the `couchbeam:server_connection/2` documentation.
-
-## Contribute
-
-For issues, comments or feedback please [create an
-issue](http://github.com/benoitc/couchbeam/issues).
-
-
-## Modules ##
-
-
-
-
diff --git a/doc/couchbeam.md b/doc/couchbeam.md
deleted file mode 100644
index 964a35bc..00000000
--- a/doc/couchbeam.md
+++ /dev/null
@@ -1,783 +0,0 @@
-
-
-# Module couchbeam #
-* [Data Types](#types)
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Data Types ##
-
-
-
-
-### doc_stream() ###
-
-
-__abstract datatype__: `doc_stream()`
-
-
-
-
-### mp_attachments() ###
-
-
-
-mp_attachments() = {Name::binary(), Bin::binary()} | {Name::binary(), Bin::binary(), Encoding::binary()} | {Name::binary(), Bin::binary(), Type::binary(), Encoding::binary()} | {Name::binary(), {file, Path::string()}} | {Name::binary(), {file, Path::string()}, Encoding::binary()} | {Name::binary(), Fun::function(), Length::integer()} | {Name::binary(), Fun::function(), Length::integer(), Encoding::binary()} | {Name::binary(), Fun::function(), Length::integer(), Type::binary(), Encoding::binary()} | {Name::binary(), {Fun::function(), Acc::any()}, Length::integer()} | {Name::binary(), {Fun::function(), Acc::any()}, Length::integer(), Encoding::binary()} | {Name::binary(), {Fun::function(), Acc::any()}, Length::integer(), Type::binary(), Encoding::binary()}
-
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### all_dbs/1 ###
-
-
-all_dbs(Server::server()) -> {ok, iolist()}
-
-
-
-get list of databases on a CouchDB node
-
-
-
-### all_dbs/2 ###
-
-
-all_dbs(Server::server(), Options::view_options()) -> {ok, iolist()}
-
-
-
-get list of databases on a CouchDB node with optional filter
-
-
-
-### compact/1 ###
-
-
-compact(Db::db()) -> ok | {error, term()}
-
-
-
-Compaction compresses the database file by removing unused
-sections created during updates.
-See [`http://wiki.apache.org/couchdb/Compaction`](http://wiki.apache.org/couchdb/Compaction) for more informations
-
-
-
-### compact/2 ###
-
-
-compact(Db::db(), ViewName::string()) -> ok | {error, term()}
-
-
-
-Like compact/1 but this compacts the view index from the
-current version of the design document.
-See [`http://wiki.apache.org/couchdb/Compaction#View_compaction`](http://wiki.apache.org/couchdb/Compaction#View_compaction) for more informations
-
-
-
-### copy_doc/2 ###
-
-`copy_doc(Db, Doc) -> any()`
-
-duplicate a document using the doc API
-
-
-
-### copy_doc/3 ###
-
-`copy_doc(Db, Doc, Dest) -> any()`
-
-copy a doc to a destination. If the destination exist it will
-use the last revision, in other case a new doc is created with the
-the current doc revision.
-
-
-
-### create_db/2 ###
-
-`create_db(Server, DbName) -> any()`
-
-Equivalent to [`create_db(Server, DbName, [], [])`](#create_db-4).
-
-Create a database and a client for connectiong to it.
-
-
-
-### create_db/3 ###
-
-`create_db(Server, DbName, Options) -> any()`
-
-Equivalent to [`create_db(Server, DbName, Options, [])`](#create_db-4).
-
-Create a database and a client for connectiong to it.
-
-
-
-### create_db/4 ###
-
-
-create_db(Server::server(), DbName::string(), Options::optionList(), Params::list()) -> {ok, db() | {error, Error}}
-
-
-
-Create a database and a client for connectiong to it.
-
-Connections are made to:
-
-```
- http://Host:PortPrefix/DbName
-```
-
-If ssl is set https is used. See server_connections for options.
-Params is a list of optionnal query argument you want to pass to the
-db. Useful for bigcouch for example.
-
-
-
-### db_exists/2 ###
-
-
-db_exists(Server::server(), DbName::string()) -> boolean()
-
-
-
-test if db with dbname exists on the CouchDB node
-
-
-
-### db_info/1 ###
-
-
-db_info(Db::db()) -> {ok, iolist() | {error, Error}}
-
-
-
-get database info
-
-
-
-### delete_attachment/3 ###
-
-`delete_attachment(Db, Doc, Name) -> any()`
-
-Equivalent to [`delete_attachment(Db, Doc, Name, [])`](#delete_attachment-4).
-
-delete a document attachment
-
-
-
-### delete_attachment/4 ###
-
-`delete_attachment(Db, DocOrDocId, Name, Options) -> any()`
-
-delete a document attachment
-
-
-
-### delete_db/1 ###
-
-`delete_db(Db) -> any()`
-
-Equivalent to [`delete_db(Server, DbName)`](#delete_db-2).
-
-delete database
-
-
-
-### delete_db/2 ###
-
-
-delete_db(Server::server(), DbName) -> {ok, iolist() | {error, Error}}
-
-
-
-delete database
-
-
-
-### delete_doc/2 ###
-
-`delete_doc(Db, Doc) -> any()`
-
-Equivalent to [`delete_doc(Db, Doc, [])`](#delete_doc-3).
-
-delete a document
-
-
-
-### delete_doc/3 ###
-
-
-delete_doc(Db, Doc, Options) -> {ok, Result} | {error, Error}
-
-
-
-delete a document
-if you want to make sure the doc it emptied on delete, use the option
-{empty_on_delete, true} or pass a doc with just _id and _rev
-members.
-
-
-
-### delete_docs/2 ###
-
-`delete_docs(Db, Docs) -> any()`
-
-Equivalent to [`delete_docs(Db, Docs, [])`](#delete_docs-3).
-
-delete a list of documents
-
-
-
-### delete_docs/3 ###
-
-
-delete_docs(Db::db(), Docs::list(), Options::list()) -> {ok, Result} | {error, Error}
-
-
-
-delete a list of documents
-if you want to make sure the doc it emptied on delete, use the option
-{empty_on_delete, true} or pass a doc with just _id and _rev
-members.
-
-
-
-### design_info/2 ###
-
-`design_info(Db, DesignName) -> any()`
-
-
-
-### doc_exists/2 ###
-
-
-doc_exists(Db::db(), DocId::string()) -> boolean()
-
-
-
-test if doc with uuid exists in the given db
-
-
-
-### end_doc_stream/1 ###
-
-
-end_doc_stream(X1::doc_stream()) -> ok
-
-
-
-stop to receive the multipart response of the doc api and close
-the connection.
-
-
-
-### ensure_full_commit/1 ###
-
-`ensure_full_commit(Db) -> any()`
-
-Equivalent to [`ensure_full_commit(Db, [])`](#ensure_full_commit-2).
-
-commit all docs in memory
-
-
-
-### ensure_full_commit/2 ###
-
-
-ensure_full_commit(Db::db(), Options::list()) -> {ok, InstancestartTime::binary()} | {error, term()}
-
-
-
-commit all docs in memory
-
-
-
-### fetch_attachment/3 ###
-
-`fetch_attachment(Db, DocId, Name) -> any()`
-
-Equivalent to [`fetch_attachment(Db, DocId, Name, [])`](#fetch_attachment-4).
-
-fetch a document attachment
-
-
-
-### fetch_attachment/4 ###
-
-
-fetch_attachment(Db::db(), DocId::string(), Name::string(), Options0::list()) -> {ok, binary()} | {ok, atom()} | {error, term()}
-
-
-
-fetch a document attachment
-Options are
-
-* `stream`: to start streaming an attachment. the function return
-`{ok, Ref}` where is a ref to the attachment
-
-* Other options that can be sent using the REST API
-
-
-
-
-
-### get_missing_revs/2 ###
-
-
-get_missing_revs(Db::#db{}, IdRevs::[{binary(), [binary()]}]) -> {ok, [{DocId::binary(), [MissingRev::binary()], [PossibleAncestor::binary()]}]} | {error, term()}
-
-
-
-get missing revisions
-
-
-
-### get_uuid/1 ###
-
-
-get_uuid(Server::server()) -> lists()
-
-
-
-Get one uuid from the server
-
-
-
-### get_uuids/2 ###
-
-
-get_uuids(Server::server(), Count::integer()) -> lists()
-
-
-
-Get a list of uuids from the server
-
-
-
-### lookup_doc_rev/2 ###
-
-`lookup_doc_rev(Db, DocId) -> any()`
-
-get the last revision of the document
-
-
-
-### lookup_doc_rev/3 ###
-
-`lookup_doc_rev(Db, DocId, Params) -> any()`
-
-
-
-### open_db/2 ###
-
-`open_db(Server, DbName) -> any()`
-
-Equivalent to [`open_db(Server, DbName, [])`](#open_db-3).
-
-Create a client for connection to a database
-
-
-
-### open_db/3 ###
-
-
-open_db(Server::server(), DbName::string(), Options::optionList()) -> {ok, db()}
-
-
-
-Create a client for connection to a database
-
-
-
-### open_doc/2 ###
-
-`open_doc(Db, DocId) -> any()`
-
-Equivalent to [`open_doc(Db, DocId, [])`](#open_doc-3).
-
-open a document
-
-
-
-### open_doc/3 ###
-
-
-open_doc(Db::db(), DocId::string(), Params::list()) -> {ok, Doc} | {error, Error}
-
-
-
-open a document
-Params is a list of query argument. Have a look in CouchDb API
-
-
-
-### open_or_create_db/2 ###
-
-`open_or_create_db(Server, DbName) -> any()`
-
-Equivalent to [`open_or_create_db(Server, DbName, [], [])`](#open_or_create_db-4).
-
-Create a client for connecting to a database and create the
-database if needed.
-
-
-
-### open_or_create_db/3 ###
-
-`open_or_create_db(Server, DbName, Options) -> any()`
-
-Equivalent to [`open_or_create_db(Server, DbName, Options, [])`](#open_or_create_db-4).
-
-Create a client for connecting to a database and create the
-database if needed.
-
-
-
-### open_or_create_db/4 ###
-
-
-open_or_create_db(Server::server(), DbName0::string(), Options::list(), Params::list()) -> {ok, db() | {error, Error}}
-
-
-
-Create a client for connecting to a database and create the
-database if needed.
-
-
-
-### put_attachment/4 ###
-
-`put_attachment(Db, DocId, Name, Body) -> any()`
-
-Equivalent to [`put_attachment(Db, DocId, Name, Body, [])`](#put_attachment-5).
-
-put an attachment
-
-
-
-### put_attachment/5 ###
-
-
-put_attachment(Db::db(), DocId::string(), Name::string(), Body::body(), Option::optionList()) -> {ok, iolist()}
-
-
-
-
-put an attachment
-
-
-
-### replicate/2 ###
-
-
-replicate(Server::server(), RepObj::{list()}) -> {ok, Result} | {error, Error}
-
-
-
-Handle replication. Pass an object containting all informations
-It allows to pass for example an authentication info
-
-```
- RepObj = {[
- {<<"source">>, <<"sourcedb">>},
- {<<"target">>, <<"targetdb">>},
- {<<"create_target">>, true}
- ]}
- replicate(Server, RepObj).
-```
-
-
-
-
-### replicate/3 ###
-
-
-replicate(Server::server(), Source::string(), Target::target()) -> {ok, Result} | {error, Error}
-
-
-
-Handle replication.
-
-
-
-### replicate/4 ###
-
-`replicate(Server, Source, Target, Options) -> any()`
-
-handle Replication. Allows to pass options with source and
-target. Options is a Json object.
-ex:
-
-```
- Options = [{<<"create_target">>, true}]}
- couchbeam:replicate(S, "testdb", "testdb2", Options).
-```
-
-
-
-### save_doc/2 ###
-
-`save_doc(Db, Doc) -> any()`
-
-Equivalent to [`save_doc(Db, Doc, [])`](#save_doc-3).
-
-save a document
-
-
-
-### save_doc/3 ###
-
-
-save_doc(Db::db(), Doc, Options::list()) -> {ok, Doc1} | {error, Error}
-
-
-
-save a *document
-A document is a Json object like this one:
-
-```
- {[
- {<<"_id">>, <<"myid">>},
- {<<"title">>, <<"test">>}
- ]}
-```
-
-Options are arguments passed to the request. This function return a
-new document with last revision and a docid. If _id isn't specified in
-document it will be created. Id is created by extracting an uuid from
-the couchdb node.
-
-
-
-### save_doc/4 ###
-
-
-save_doc(Db::db(), Doc::doc(), Atts::mp_attachments(), Options::list()) -> {ok, doc()} | {error, term()}
-
-
-
-save a *document with all its attacjments
-A document is a Json object like this one:
-
-```
- {[
- {<<"_id">>, <<"myid">>},
- {<<"title">>, <<"test">>}
- ]}
-```
-
-Options are arguments passed to the request. This function return a
-new document with last revision and a docid. If _id isn't specified in
-document it will be created. Id is created by extracting an uuid from
-the couchdb node.
-
-If the attachments is not empty, the doc will be sent as multipart.
-Attachments are passed as a list of the following tuples:
-
-- `{Name :: binary(), Bin :: binary()}`
-- `{Name :: binary(), Bin :: binary(), Encoding :: binary()}`
-- `{ Name :: binary(), Bin :: binary(), Type :: binary(), Encoding :: binary()}`
-- `{ Name :: binary(), {file, Path :: string()}}`
-- `{ Name :: binary(), {file, Path :: string()}, Encoding :: binary()}`
-- `{ Name :: binary(), Fun :: fun(), Length :: integer()}`
-- `{ Name :: binary(), Fun :: fun(), Length :: integer(), Encoding :: binary()}`
-- `{Name :: binary(), Fun :: fun(), Length :: integer(), Type :: binary(), Encoding :: binary()}`
-- `{ Name :: binary(), {Fun :: fun(), Acc :: any()}, Length :: integer()}`
-- `{ Name :: binary(), {Fun :: fun(), Acc :: any()}, Length :: integer(), Encoding :: binary()}`
-- `{ Name :: binary(), {Fun :: fun(), Acc :: any()}, Length :: integer(), Type :: binary(), Encoding :: binary()}.`
-
-where `Type` is the content-type of the attachments (detected in other
-case) and `Encoding` the encoding of the attachments:
-`<<"identity">>` if normal or `<<"gzip">>` if the attachments is
-gzipped.
-
-
-
-### save_docs/2 ###
-
-`save_docs(Db, Docs) -> any()`
-
-Equivalent to [`save_docs(Db, Docs, [])`](#save_docs-3).
-
-save a list of documents
-
-
-
-### save_docs/3 ###
-
-
-save_docs(Db::db(), Docs::list(), Options::list()) -> {ok, Result} | {error, Error}
-
-
-
-save a list of documents
-
-
-
-### send_attachment/2 ###
-
-`send_attachment(Ref, Msg) -> any()`
-
-send an attachment chunk
-Msg could be Data, eof to stop sending.
-
-
-
-### server_connection/0 ###
-
-`server_connection() -> any()`
-
-Equivalent to [`server_connection("127.0.0.1", 5984, "", [], false)`](#server_connection-5).
-
-Create a server for connectiong to a CouchDB node
-
-
-
-### server_connection/1 ###
-
-`server_connection(URL) -> any()`
-
-
-
-### server_connection/2 ###
-
-`server_connection(URL, Options) -> any()`
-
-Equivalent to [`server_connection(Host, Port, "", [])`](#server_connection-4).
-
-Create a server for connectiong to a CouchDB node
-
-
-
-### server_connection/4 ###
-
-
-server_connection(Host::string(), Port::non_neg_integer(), Prefix::string(), OptionsList::list()) -> Server::server()
-
-
-
-Create a server for connectiong to a CouchDB node
-
-Connections are made to:
-
-```
- http://Host:PortPrefix
-```
-
-If ssl is set https is used.
-
-For a description of SSL Options, look in the [ssl](http://www.erlang.org/doc/apps/ssl/index.html) manpage.
-
-
-
-### server_info/1 ###
-
-
-server_info(Server::server()) -> {ok, iolist()}
-
-
-
-Get Information from the server
-
-
-
-### stream_attachment/1 ###
-
-
-stream_attachment(Ref::atom()) -> {ok, binary()} | done | {error, term()}
-
-
-
-fetch an attachment chunk.
-Use this function when you pass the `stream` option to the
-`couchbeam:fetch_attachment/4` function.
-This function return the following response:
-
-
-
-done
-
-
-
-
-You got all the attachment
-
-
-
-
-{ok, binary()}
-
-
-
-
-Part of the attachment
-
-
-
-
-{error, term()}
-
-
-
-
-n error occurred
-
-
-
-
-
-
-### stream_doc/1 ###
-
-
-stream_doc(X1::doc_stream()) -> {doc, doc()} | {att, Name::binary(), doc_stream()} | {att_body, Name::binary(), Chunk::binary(), doc_stream()} | {att_eof, Name::binary(), doc_stream()} | eof | {error, term()}
-
-
-
-stream the multipart response of the doc API. Use this function
-when you get `{ok, {multipart, State}}` from the function
-`couchbeam:open_doc/3`.
-
-
-
-### view_cleanup/1 ###
-
-`view_cleanup(Db) -> any()`
-
diff --git a/doc/couchbeam_app.md b/doc/couchbeam_app.md
deleted file mode 100644
index 40b0c2f3..00000000
--- a/doc/couchbeam_app.md
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-# Module couchbeam_app #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-__Behaviours:__ [`application`](application.md).
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### start/2 ###
-
-`start(Type, StartArgs) -> any()`
-
-
-
-### stop/1 ###
-
-`stop(State) -> any()`
-
diff --git a/doc/couchbeam_attachments.md b/doc/couchbeam_attachments.md
deleted file mode 100644
index ea3fb7cc..00000000
--- a/doc/couchbeam_attachments.md
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-# Module couchbeam_attachments #
-* [Description](#description)
-* [Function Index](#index)
-* [Function Details](#functions)
-
-This module contains utilities to manage attachments.
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### add_inline/3 ###
-
-
-add_inline(Doc::json_obj(), Content::attachment_content(), AName::string()) -> json_obj()
-
-
-
-add attachment to a doc and encode it. Give possibility to send attachments inline.
-
-
-
-### add_inline/4 ###
-
-
-add_inline(Doc::json_obj(), Content::attachment_content(), AName::string(), ContentType::string()) -> json_obj()
-
-
-
-add attachment to a doc and encode it with ContentType fixed.
-
-
-
-### add_stub/3 ###
-
-`add_stub(Doc, Name, ContentType) -> any()`
-
-
-
-### delete_inline/2 ###
-
-
-delete_inline(Doc::json_obj(), AName::string()) -> json_obj()
-
-
-
-delete an attachment record in doc. This is different from delete_attachment
-change is only applied in Doc object. Save_doc should be save to save changes.
-
diff --git a/doc/couchbeam_changes.md b/doc/couchbeam_changes.md
deleted file mode 100644
index 32c93f58..00000000
--- a/doc/couchbeam_changes.md
+++ /dev/null
@@ -1,206 +0,0 @@
-
-
-# Module couchbeam_changes #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### cancel_stream/1 ###
-
-`cancel_stream(Ref) -> any()`
-
-
-
-### follow/1 ###
-
-
-follow(Db::db()) -> {ok, StreamRef::atom()} | {error, term()}
-
-
-
-
-
-### follow/2 ###
-
-
-follow(Db::db(), Options::changes_options()) -> {ok, StreamRef::atom()} | {error, term()}
-
-
-
-Stream changes to a pid
-
-Db : a db record
-
-Client : pid or callback where to send changes events where events are
-The pid receive these events:
-
-
-
-{change, StartRef, {done, Lastseq::integer()}
-
-
-
-
-Connection terminated or you got all changes
-
-
-
-
-{change, StartRef, Row :: ejson_object()}
-
-
-
-
-Line of change
-
-
-
-
-{error, LastSeq::integer(), Msg::term()}
-
-
-
-
-Got an error, connection is closed when an error
-happend.
-
-
-
-LastSeq is the last sequence of changes.
-
-While the callbac could be like:
-
-```
-
- fun({done, LastSeq}) ->
- ok;
- fun({done, LastSeq}) ->
- ok;
- fun({done, LastSeq}) ->
- ok.
-```
-
-
-```
->Options :: changes_stream_options() [continuous
- | longpoll
- | normal
- | include_docs
- | {since, integer() | now}
- | {timeout, integer()}
- | heartbeat | {heartbeat, integer()}
- | {filter, string()} | {filter, string(), list({string(), string() | integer()})}
- | {view, string()},
- | {docids, list))},
- | {stream_to, pid()},
- | {async, once | normal}]
-```
-
-* `continuous | longpoll | normal`: set the type of changes
-feed to get
-
-* `include_doc`: if you want to include the doc in the line of
-change
-
-* `{timeout, Timeout::integer()}`: timeout
-
-* `heartbeat | {heartbeat, Heartbeat::integer()}`: set couchdb
-to send a heartbeat to maintain connection open
-
-* `{filter, FilterName} | {filter, FilterName, Args::list({key,
-value})}`: set the filter to use with optional arguments
-
-* `{view, ViewName}`: use a view function as filter. Note
-that it requires to set filter special value `"_view"`
-to enable this feature.
-
-* >`{stream_to, Pid}`: the pid where the changes will be sent,
-by default the current pid. Used for continuous and longpoll
-connections
-
-
-Return {ok, StartRef, ChangesPid} or {error, Error}. Ref can be
-used to disctint all changes from this pid. ChangesPid is the pid of
-the changes loop process. Can be used to monitor it or kill it
-when needed.
-
-
-
-### follow_once/1 ###
-
-
-follow_once(Db::db()) -> {ok, LastSeq::integer(), Changes::list()} | {error, term()}
-
-
-
-
-
-### follow_once/2 ###
-
-
-follow_once(Db::db(), Options::changes_options()) -> {ok, LastSeq::integer(), Changes::list()} | {error, term()}
-
-
-
-fetch all changes at once using a normal or longpoll
-connections.
-
-Db : a db record
-
-```
-Options :: changes_options() [
- | longpoll
- | normal
- | include_docs
- | {since, integer() | now}
- | {timeout, integer()}
- | heartbeat | {heartbeat, integer()}
- | {filter, string()}
- | {filter, string(), list({string(), string() | integer()})}
- | {docids, list()))},
- | {stream_to, pid()}
- ]
-```
-
-* `longpoll | normal`: set the type of changes
-feed to get
-
-* `include_docs`: if you want to include the doc in the line of
-change
-
-* `{timeout, Timeout::integer()}`: timeout
-
-* `heartbeat | {heartbeat, Heartbeat::integer()}`: set couchdb
-to send a heartbeat to maintain connection open
-
-* `{filter, FilterName} | {filter, FilterName, Args::list({key,
-value})`: set the filter to use with optional arguments
-
-* `{view, ViewName}`: use a view function as filter. Note
-that it requires to set filter special value `"_view"`
-to enable this feature.
-
-
-Result: `{ok, LastSeq::integer(), Rows::list()}` or
-`{error, LastSeq, Error}`. LastSeq is the last sequence of changes.
-
-
-
-### stream_next/1 ###
-
-`stream_next(Ref) -> any()`
-
diff --git a/doc/couchbeam_changes_stream.md b/doc/couchbeam_changes_stream.md
deleted file mode 100644
index 545ec862..00000000
--- a/doc/couchbeam_changes_stream.md
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-# Module couchbeam_changes_stream #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### collect_object/2 ###
-
-`collect_object(X1, X2) -> any()`
-
-
-
-### handle_event/2 ###
-
-`handle_event(Event, St) -> any()`
-
-
-
-### init/1 ###
-
-`init(X1) -> any()`
-
-
-
-### init_stream/5 ###
-
-`init_stream(Parent, Owner, StreamRef, Db, Options) -> any()`
-
-
-
-### maybe_continue/1 ###
-
-`maybe_continue(State) -> any()`
-
-
-
-### maybe_continue_decoding/1 ###
-
-`maybe_continue_decoding(State) -> any()`
-
-
-
-### start_link/4 ###
-
-`start_link(Owner, StreamRef, Db, Options) -> any()`
-
-
-
-### system_code_change/4 ###
-
-`system_code_change(Misc, X2, X3, X4) -> any()`
-
-
-
-### system_continue/3 ###
-
-`system_continue(X1, X2, X3) -> any()`
-
-
-
-### system_terminate/4 ###
-
-
-system_terminate(Reason::any(), X2::term(), X3::term(), State::term()) -> no_return()
-
-
-
-
-
-### wait_reconnect/1 ###
-
-`wait_reconnect(State) -> any()`
-
-
-
-### wait_results/2 ###
-
-`wait_results(X1, St) -> any()`
-
-
-
-### wait_results1/2 ###
-
-`wait_results1(X1, X2) -> any()`
-
diff --git a/doc/couchbeam_changes_sup.md b/doc/couchbeam_changes_sup.md
deleted file mode 100644
index dd397ac4..00000000
--- a/doc/couchbeam_changes_sup.md
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-# Module couchbeam_changes_sup #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-__Behaviours:__ [`supervisor`](supervisor.md).
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### init/1 ###
-
-`init(X1) -> any()`
-
-
-
-### start_link/0 ###
-
-
-start_link() -> {ok, pid()}
-
-
-
diff --git a/doc/couchbeam_deps.md b/doc/couchbeam_deps.md
deleted file mode 100644
index a1707379..00000000
--- a/doc/couchbeam_deps.md
+++ /dev/null
@@ -1,117 +0,0 @@
-
-
-# Module couchbeam_deps #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-
-## Function Index ##
-
-
-| deps_on_path/0 | List of project dependencies on the path. |
| ensure/0 | Ensure that the ebin and include paths for dependencies of
-this application are on the code path. |
| ensure/1 | Ensure that all ebin and include paths for dependencies
-of the application for Module are on the code path. |
| get_base_dir/0 | Return the application directory for this application. |
| get_base_dir/1 | Return the application directory for Module. |
| local_path/1 | Return an application-relative directory for this application. |
| local_path/2 | Return an application-relative directory from Module's application. |
| new_siblings/1 | Find new siblings paths relative to Module that aren't already on the
-code path. |
-
-
-
-
-## Function Details ##
-
-
-
-### deps_on_path/0 ###
-
-
-
-deps_on_path() -> [ProjNameAndVers]
-
-
-
-List of project dependencies on the path.
-
-
-### ensure/0 ###
-
-
-
-ensure() -> ok
-
-
-
-Ensure that the ebin and include paths for dependencies of
-this application are on the code path. Equivalent to
-ensure(?Module).
-
-
-### ensure/1 ###
-
-
-
-ensure(Module) -> ok
-
-
-
-Ensure that all ebin and include paths for dependencies
-of the application for Module are on the code path.
-
-
-### get_base_dir/0 ###
-
-
-
-get_base_dir() -> string()
-
-
-
-Return the application directory for this application. Equivalent to
-get_base_dir(?MODULE).
-
-
-### get_base_dir/1 ###
-
-
-
-get_base_dir(Module) -> string()
-
-
-
-Return the application directory for Module. It assumes Module is in
-a standard OTP layout application in the ebin or src directory.
-
-
-### local_path/1 ###
-
-
-
-local_path(Components) -> string()
-
-
-
-Return an application-relative directory for this application.
-Equivalent to local_path(Components, ?MODULE).
-
-
-### local_path/2 ###
-
-
-
-local_path(Components::[string()], Module) -> string()
-
-
-
-Return an application-relative directory from Module's application.
-
-
-### new_siblings/1 ###
-
-
-
-new_siblings(Module) -> [Dir]
-
-
-
-Find new siblings paths relative to Module that aren't already on the
-code path.
diff --git a/doc/couchbeam_doc.md b/doc/couchbeam_doc.md
deleted file mode 100644
index df26b826..00000000
--- a/doc/couchbeam_doc.md
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-# Module couchbeam_doc #
-* [Data Types](#types)
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Data Types ##
-
-
-
-
-### key_val() ###
-
-
-
-key_val() = lis() | binary()
-
-
-
-
-
-### property() ###
-
-
-
-property() = json_obj() | tuple()
-
-
-
-
-## Function Index ##
-
-
-| delete_value/2 | Deletes all entries associated with Key in json object. |
| extend/2 | extend a jsonobject by a property, list of property or another jsonobject. |
| extend/3 | extend a jsonobject by key, value. |
| get_id/1 | get document id. |
| get_idrev/1 | get a tuple containing docucment id and revision. |
| get_rev/1 | get document revision. |
| get_value/2 | Returns the value of a simple key/value property in json object
-Equivalent to get_value(Key, JsonObj, undefined). |
| get_value/3 | Returns the value of a simple key/value property in json object
-function from erlang_couchdb. |
| is_saved/1 | If document have been saved (revision is defined) return true,
-else, return false. |
| set_value/3 | set a value for a key in jsonobj. |
| take_value/2 | Returns the value of a simple key/value property in json object and deletes
-it form json object
-Equivalent to take_value(Key, JsonObj, undefined). |
| take_value/3 | Returns the value of a simple key/value property in json object and deletes
-it from json object. |
-
-
-
-
-## Function Details ##
-
-
-
-### delete_value/2 ###
-
-
-delete_value(Key::key_val(), JsonObj::json_obj()) -> json_obj()
-
-
-
-Deletes all entries associated with Key in json object.
-
-
-
-### extend/2 ###
-
-
-extend(Prop::property(), JsonObj::json_obj()) -> json_obj()
-
-
-
-extend a jsonobject by a property, list of property or another jsonobject
-
-
-
-### extend/3 ###
-
-
-extend(Key::binary(), Value::json_term(), JsonObj::json_obj()) -> json_obj()
-
-
-
-extend a jsonobject by key, value
-
-
-
-### get_id/1 ###
-
-
-get_id(Doc::json_obj()) -> binary()
-
-
-
-get document id.
-
-
-
-### get_idrev/1 ###
-
-
-get_idrev(Doc::json_obj()) -> {DocId, DocRev}
-
-
-
-get a tuple containing docucment id and revision.
-
-
-
-### get_rev/1 ###
-
-
-get_rev(Doc::json_obj()) -> binary()
-
-
-
-get document revision.
-
-
-
-### get_value/2 ###
-
-
-get_value(Key::key_val(), JsonObj::json_obj()) -> term()
-
-
-
-Returns the value of a simple key/value property in json object
-Equivalent to get_value(Key, JsonObj, undefined).
-
-
-
-### get_value/3 ###
-
-
-get_value(Key::lis() | binary(), JsonObj::json_obj(), Default::term()) -> term()
-
-
-
-Returns the value of a simple key/value property in json object
-function from erlang_couchdb
-
-
-
-### is_saved/1 ###
-
-
-is_saved(Doc::json_obj()) -> boolean()
-
-
-
-If document have been saved (revision is defined) return true,
-else, return false.
-
-
-
-### set_value/3 ###
-
-
-set_value(Key::key_val(), Value::term(), JsonObj::json_obj()) -> term()
-
-
-
-set a value for a key in jsonobj. If key exists it will be updated.
-
-
-
-### take_value/2 ###
-
-
-take_value(Key::key_val(), JsonObj::json_obj()) -> {term(), json_obj()}
-
-
-
-Returns the value of a simple key/value property in json object and deletes
-it form json object
-Equivalent to take_value(Key, JsonObj, undefined).
-
-
-
-### take_value/3 ###
-
-
-take_value(Key::key_val() | binary(), JsonObj::json_obj(), Default::term()) -> {term(), json_obj()}
-
-
-
-Returns the value of a simple key/value property in json object and deletes
-it from json object
-
diff --git a/doc/couchbeam_ejson.md b/doc/couchbeam_ejson.md
deleted file mode 100644
index 80862c4c..00000000
--- a/doc/couchbeam_ejson.md
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-# Module couchbeam_ejson #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### decode/1 ###
-
-
-decode(D::binary()) -> ejson()
-
-
-
-decode a binary to an EJSON term. Throw an exception if there is
-any error.
-
-
-
-### encode/1 ###
-
-
-encode(D::ejson()) -> binary()
-
-
-
-encode an erlang term to JSON. Throw an exception if there is
-any error.
-
-
-
-### post_decode/1 ###
-
-`post_decode(Rest) -> any()`
-
diff --git a/doc/couchbeam_httpc.md b/doc/couchbeam_httpc.md
deleted file mode 100644
index b79ead95..00000000
--- a/doc/couchbeam_httpc.md
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-# Module couchbeam_httpc #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### db_request/5 ###
-
-`db_request(Method, Url, Headers, Body, Options) -> any()`
-
-
-
-### db_request/6 ###
-
-`db_request(Method, Url, Headers, Body, Options, Expect) -> any()`
-
-
-
-### db_resp/2 ###
-
-`db_resp(Resp, Expect) -> any()`
-
-
-
-### db_url/1 ###
-
-`db_url(Db) -> any()`
-
-
-
-### doc_url/2 ###
-
-`doc_url(Db, DocId) -> any()`
-
-
-
-### json_body/1 ###
-
-`json_body(Ref) -> any()`
-
-
-
-### make_headers/4 ###
-
-`make_headers(Method, Url, Headers, Options) -> any()`
-
-
-
-### maybe_oauth_header/4 ###
-
-`maybe_oauth_header(Method, Url, Headers, Options) -> any()`
-
-
-
-### request/5 ###
-
-`request(Method, Url, Headers, Body, Options) -> any()`
-
-
-
-### server_url/1 ###
-
-
-server_url(Server::{Host, Port}) -> iolist()
-
-
-
-Asemble the server URL for the given client
-
diff --git a/doc/couchbeam_sup.md b/doc/couchbeam_sup.md
deleted file mode 100644
index 0435f6d1..00000000
--- a/doc/couchbeam_sup.md
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-# Module couchbeam_sup #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-__Behaviours:__ [`supervisor`](supervisor.md).
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### init/1 ###
-
-`init(X1) -> any()`
-
-
-
-### start_link/0 ###
-
-`start_link() -> any()`
-
diff --git a/doc/couchbeam_util.md b/doc/couchbeam_util.md
deleted file mode 100644
index 6810db46..00000000
--- a/doc/couchbeam_util.md
+++ /dev/null
@@ -1,203 +0,0 @@
-
-
-# Module couchbeam_util #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### binary_env/2 ###
-
-`binary_env(Key, Default) -> any()`
-
-
-
-### dbname/1 ###
-
-`dbname(DbName) -> any()`
-
-
-
-### deprecated/3 ###
-
-`deprecated(Old, New, When) -> any()`
-
-
-
-### encode_att_name/1 ###
-
-`encode_att_name(Name) -> any()`
-
-
-
-### encode_docid/1 ###
-
-`encode_docid(DocId) -> any()`
-
-
-
-### encode_docid1/1 ###
-
-`encode_docid1(DocId) -> any()`
-
-
-
-### encode_docid_noop/1 ###
-
-`encode_docid_noop(DocId) -> any()`
-
-
-
-### encode_query/1 ###
-
-`encode_query(QSL) -> any()`
-
-Encode needed value of Query proplists in json
-
-
-
-### encode_query_value/2 ###
-
-`encode_query_value(K, V) -> any()`
-
-Encode value in JSON if needed depending on the key
-
-
-
-### force_param/3 ###
-
-`force_param(Key, Value, Options) -> any()`
-
-replace a value in a proplist
-
-
-
-### get_app_env/2 ###
-
-`get_app_env(Env, Default) -> any()`
-
-
-
-### get_value/2 ###
-
-
-get_value(Key::term(), Prop::[term()]) -> term()
-
-
-
-emulate proplists:get_value/2,3 but use faster lists:keyfind/3
-
-
-
-### get_value/3 ###
-
-
-get_value(Key::term(), Prop::[term()], Default::term()) -> term()
-
-
-
-
-
-### oauth_header/3 ###
-
-`oauth_header(Url, Action, OauthProps) -> any()`
-
-
-
-### parse_options/1 ###
-
-`parse_options(Options) -> any()`
-
-make view options a list
-
-
-
-### parse_options/2 ###
-
-`parse_options(Rest, Acc) -> any()`
-
-
-
-### propmerge/3 ###
-
-`propmerge(F, L1, L2) -> any()`
-
-merge 2 proplists. All the Key - Value pairs from both proplists
-are included in the new proplists. If a key occurs in both dictionaries
-then Fun is called with the key and both values to return a new
-value. This a wreapper around dict:merge
-
-
-
-### propmerge1/2 ###
-
-`propmerge1(L1, L2) -> any()`
-
-Update a proplist with values of the second. In case the same
-key is in 2 proplists, the value from the first are kept.
-
-
-
-### proxy_header/3 ###
-
-`proxy_header(UserName, Roles, Secret) -> any()`
-
-
-
-### proxy_token/2 ###
-
-`proxy_token(Secret, UserName) -> any()`
-
-
-
-### shutdown_sync/1 ###
-
-`shutdown_sync(Pid) -> any()`
-
-
-
-### start_app_deps/1 ###
-
-
-start_app_deps(App::atom()) -> ok
-
-
-
-Start depedent applications of App.
-
-
-
-### to_atom/1 ###
-
-`to_atom(V) -> any()`
-
-
-
-### to_binary/1 ###
-
-`to_binary(V) -> any()`
-
-
-
-### to_integer/1 ###
-
-`to_integer(V) -> any()`
-
-
-
-### to_list/1 ###
-
-`to_list(V) -> any()`
-
diff --git a/doc/couchbeam_uuids.md b/doc/couchbeam_uuids.md
deleted file mode 100644
index 43c1e4e5..00000000
--- a/doc/couchbeam_uuids.md
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
-# Module couchbeam_uuids #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-__Behaviours:__ [`gen_server`](gen_server.md).
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### code_change/3 ###
-
-`code_change(OldVsn, State, Extra) -> any()`
-
-
-
-### get_uuids/2 ###
-
-
-get_uuids(Server::server(), Count::integer()) -> lists()
-
-
-
-Get a list of uuids from the server
-
-
-
-### handle_call/3 ###
-
-`handle_call(X1, From, State) -> any()`
-
-
-
-### handle_cast/2 ###
-
-`handle_cast(Msg, State) -> any()`
-
-
-
-### handle_info/2 ###
-
-`handle_info(Info, State) -> any()`
-
-
-
-### random/0 ###
-
-
-random() -> binary()
-
-
-
-return a random uuid
-
-
-
-### start_link/0 ###
-
-
-start_link() -> {ok, pid()}
-
-
-
-Starts the couchbeam process linked to the calling process. Usually
-invoked by the supervisor couchbeam_sup
-
-
-
-### terminate/2 ###
-
-`terminate(Reason, State) -> any()`
-
-
-
-### utc_random/0 ###
-
-
-utc_random() -> binary()
-
-
-
-return a random uuid based on time
-
diff --git a/doc/couchbeam_view.md b/doc/couchbeam_view.md
deleted file mode 100644
index d8ce7394..00000000
--- a/doc/couchbeam_view.md
+++ /dev/null
@@ -1,402 +0,0 @@
-
-
-# Module couchbeam_view #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Function Index ##
-
-
-| all/1 | fetch all docs. |
| all/2 | fetch all docs. |
| cancel_stream/1 | |
| count/1 | Equivalent to count(Db, all_docs, []). |
| count/2 | Equivalent to count(Db, ViewName, []). |
| count/3 | count number of doc in a view (or all docs). |
| fetch/1 | Equivalent to fetch(Db, all_docs, []). |
| fetch/2 | Equivalent to fetch(Db, ViewName, []). |
| fetch/3 | Collect view results. |
| first/1 | Equivalent to first(Db, all_docs, []). |
| first/2 | Equivalent to first(Db, ViewName, []). |
| first/3 | get first result of a view. |
| fold/4 | Equivalent to fold(Function, Acc, Db, ViewName, []). |
| fold/5 | call Function(Row, AccIn) on succesive row, starting with
-AccIn == Acc. |
| foreach/3 | Equivalent to foreach(Function, Db, ViewName, []). |
| foreach/4 | call Function(Row) on succesive row. |
| parse_view_options/1 | parse view options. |
| stream/2 | Equivalent to stream(Db, ViewName, Client, []). |
| stream/3 | stream view results to a pid. |
| stream_next/1 | |
-
-
-
-
-## Function Details ##
-
-
-
-### all/1 ###
-
-
-all(Db::db()) -> {ok, Rows::[ejson_object()]} | {error, term()}
-
-
-
-Equivalent to [`fetch(Db, all_docs, [])`](#fetch-3).
-
-fetch all docs
-
-
-
-### all/2 ###
-
-
-all(Db::db(), Options::view_options()) -> {ok, Rows::[ejson_object()]} | {error, term()}
-
-
-
-Equivalent to [`fetch(Db, all_docs, Options)`](#fetch-3).
-
-fetch all docs
-
-
-
-### cancel_stream/1 ###
-
-`cancel_stream(Ref) -> any()`
-
-
-
-### count/1 ###
-
-
-count(Db::db()) -> integer() | {error, term()}
-
-
-
-Equivalent to [`count(Db, all_docs, [])`](#count-3).
-
-
-
-### count/2 ###
-
-
-count(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> integer() | {error, term()}
-
-
-
-Equivalent to [`count(Db, ViewName, [])`](#count-3).
-
-
-
-### count/3 ###
-
-
-count(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> integer() | {error, term()}
-
-
-
-count number of doc in a view (or all docs)
-
-
-
-### fetch/1 ###
-
-
-fetch(Db::db()) -> {ok, Rows::[ejson_object()]} | {error, term()}
-
-
-
-Equivalent to [`fetch(Db, all_docs, [])`](#fetch-3).
-
-
-
-### fetch/2 ###
-
-
-fetch(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> {ok, Rows::[ejson_object()]} | {error, term()}
-
-
-
-Equivalent to [`fetch(Db, ViewName, [])`](#fetch-3).
-
-
-
-### fetch/3 ###
-
-
-fetch(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> {ok, Rows::[ejson_object()]} | {error, term()}
-
-
-
-Collect view results
-
-Db: a db record
-
-ViewName: `'all_docs'` to get all docs or `{DesignName,
-ViewName}`
-
-
-```
-Options :: view_options() [{key, binary()}
- | {start_docid, binary()} | {startkey_docid, binary()}
- | {end_docid, binary()} | {endkey_docid, binary()}
- | {start_key, binary()} | {end_key, binary()}
- | {limit, integer()}
- | {stale, stale()}
- | descending
- | {skip, integer()}
- | group | {group_level, integer()}
- | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
- | {keys, list(binary())}
-```
-
-
-See [`couchbeam_view:stream/4`](couchbeam_view.md#stream-4) for more information about
-options.
-
-Return: {ok, Rows} or {error, Error}
-
-
-
-### first/1 ###
-
-
-first(Db::db()) -> {ok, Row::ejson_object()} | {error, term()}
-
-
-
-Equivalent to [`first(Db, all_docs, [])`](#first-3).
-
-
-
-### first/2 ###
-
-
-first(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> {ok, Row::ejson_object()} | {error, term()}
-
-
-
-Equivalent to [`first(Db, ViewName, [])`](#first-3).
-
-
-
-### first/3 ###
-
-
-first(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> {ok, Rows::ejson_object()} | {error, term()}
-
-
-
-get first result of a view
-
-Db: a db record
-
-ViewName: 'all_docs' to get all docs or {DesignName,
-ViewName}
-
-
-```
-Options :: view_options() [{key, binary()}
- | {start_docid, binary()} | {startkey_docid, binary()}
- | {end_docid, binary()} | {endkey_docid, binary()}
- | {start_key, binary()} | {end_key, binary()}
- | {limit, integer()}
- | {stale, stale()}
- | descending
- | {skip, integer()}
- | group | {group_level, integer()}
- | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
- | {keys, list(binary())}
-```
-
-
-See [`couchbeam_view:stream/4`](couchbeam_view.md#stream-4) for more information about
-options.
-
-Return: {ok, Row} or {error, Error}
-
-
-
-### fold/4 ###
-
-
-fold(Function::function(), Acc::any(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> [term()] | {error, term()}
-
-
-
-Equivalent to [`fold(Function, Acc, Db, ViewName, [])`](#fold-5).
-
-
-
-### fold/5 ###
-
-
-fold(Function::function(), Acc::any(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> [term()] | {error, term()}
-
-
-
-call Function(Row, AccIn) on succesive row, starting with
-AccIn == Acc. Function/2 must return a new list accumultator or the
-atom _done_ to stop fetching results. Acc0 is returned if the
-list is empty. For example:
-
-```
- couchbeam_view:fold(fun(Row, Acc) -> [Row|Acc] end, [], Db, 'all_docs').
-```
-
-
-
-### foreach/3 ###
-
-
-foreach(Function::function(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> [term()] | {error, term()}
-
-
-
-Equivalent to [`foreach(Function, Db, ViewName, [])`](#foreach-4).
-
-
-
-### foreach/4 ###
-
-
-foreach(Function::function(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> [term()] | {error, term()}
-
-
-
-call Function(Row) on succesive row. Example:
-
-```
- couchbeam_view:foreach(fun(Row) -> io:format("got row ~p~n", [Row]) end, Db, 'all_docs').
-```
-
-
-
-### parse_view_options/1 ###
-
-
-parse_view_options(Options::list()) -> view_query_args()
-
-
-
-parse view options
-
-
-
-### stream/2 ###
-
-
-stream(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> {ok, StartRef::term(), ViewPid::pid()} | {error, term()}
-
-
-
-Equivalent to [`stream(Db, ViewName, Client, [])`](#stream-4).
-
-
-
-### stream/3 ###
-
-
-stream(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> {ok, StartRef::term()} | {error, term()}
-
-
-
-stream view results to a pid
-
-Db: a db record
-
-ViewName: 'all_docs' to get all docs or {DesignName,
-ViewName}
-
-Client: pid where to send view events where events are:
-
-
-
-{row, StartRef, done}
-
-
-
-
-All view results have been fetched
-
-
-
-
-{row, StartRef, Row :: ejson_object()}
-
-
-
-
-A row in the view
-
-
-
-
-{error, StartRef, Error}
-
-
-
-
-Got an error, connection is closed when an error
-happend.
-
-
-
-```
-Options :: view_options() [{key, binary()}
- | {start_docid, binary()} | {startkey_docid, binary()}
- | {end_docid, binary()} | {endkey_docid, binary()}
- | {start_key, binary()} | {end_key, binary()}
- | {limit, integer()}
- | {stale, stale()}
- | descending
- | {skip, integer()}
- | group | {group_level, integer()}
- | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
- | {keys, list(binary())}
- | {stream_to, Pid}: the pid where the changes will be sent,
- by default the current pid. Used for continuous and longpoll
- connections
-```
-
-* `{key, Key}`: key value
-
-* `{start_docid, DocId}` | `{startkey_docid, DocId}`: document id to start with (to allow pagination
-for duplicate start keys
-
-* `{end_docid, DocId}` | `{endkey_docid, DocId}`: last document id to include in the result (to
-allow pagination for duplicate endkeys)
-
-* `{start_key, Key}`: start result from key value
-
-* `{end_key, Key}`: end result from key value
-
-* `{limit, Limit}`: Limit the number of documents in the result
-
-* `{stale, Stale}`: If stale=ok is set, CouchDB will not refresh the view
-even if it is stale, the benefit is a an improved query latency. If
-stale=update_after is set, CouchDB will update the view after the stale
-result is returned. If stale=false is set, CouchDB will update the view before
-the query. The default value of this parameter is update_after.
-
-* `descending`: reverse the result
-
-* `{skip, N}`: skip n number of documents
-
-* `group`: the reduce function reduces to a single result
-row.
-
-* `{group_level, Level}`: the reduce function reduces to a set
-of distinct keys.
-
-* `{reduce, boolean()}`: whether to use the reduce function of the view. It defaults to
-true, if a reduce function is defined and to false otherwise.
-
-* `include_docs`: automatically fetch and include the document
-which emitted each view entry
-
-* `{inclusive_end, boolean()}`: Controls whether the endkey is included in
-the result. It defaults to true.
-
-* `conflicts`: include conflicts
-
-* `{keys, [Keys]}`: to pass multiple keys to the view query
-
-
-Return `{ok, StartRef, ViewPid}` or `{error,
-Error}`. Ref can be
-used to disctint all changes from this pid. ViewPid is the pid of
-the view loop process. Can be used to monitor it or kill it
-when needed.
-
-
-
-### stream_next/1 ###
-
-`stream_next(Ref) -> any()`
-
diff --git a/doc/couchbeam_view_stream.md b/doc/couchbeam_view_stream.md
deleted file mode 100644
index 13614756..00000000
--- a/doc/couchbeam_view_stream.md
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-# Module couchbeam_view_stream #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### collect_object/2 ###
-
-`collect_object(X1, X2) -> any()`
-
-
-
-### handle_event/2 ###
-
-`handle_event(Event, St) -> any()`
-
-
-
-### init/1 ###
-
-`init(X1) -> any()`
-
-
-
-### init_stream/5 ###
-
-`init_stream(Parent, Owner, StreamRef, Req, StreamOptions) -> any()`
-
-
-
-### maybe_continue/1 ###
-
-`maybe_continue(State) -> any()`
-
-
-
-### maybe_continue_decoding/1 ###
-
-`maybe_continue_decoding(Viewst) -> any()`
-
-
-
-### start_link/4 ###
-
-`start_link(Owner, StreamRef, X3, StreamOptions) -> any()`
-
-
-
-### system_code_change/4 ###
-
-`system_code_change(Misc, X2, X3, X4) -> any()`
-
-
-
-### system_continue/3 ###
-
-`system_continue(X1, X2, X3) -> any()`
-
-
-
-### system_terminate/4 ###
-
-
-system_terminate(Reason::any(), X2::term(), X3::term(), State::term()) -> no_return()
-
-
-
-
-
-### wait_rows/2 ###
-
-`wait_rows(X1, St) -> any()`
-
-
-
-### wait_rows1/2 ###
-
-`wait_rows1(X1, X2) -> any()`
-
-
-
-### wait_val/2 ###
-
-`wait_val(X1, X2) -> any()`
-
diff --git a/doc/couchbeam_view_sup.md b/doc/couchbeam_view_sup.md
deleted file mode 100644
index 30f6f7fe..00000000
--- a/doc/couchbeam_view_sup.md
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-# Module couchbeam_view_sup #
-* [Function Index](#index)
-* [Function Details](#functions)
-
-__Behaviours:__ [`supervisor`](supervisor.md).
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### init/1 ###
-
-`init(X1) -> any()`
-
-
-
-### start_link/0 ###
-
-
-start_link() -> {ok, pid()}
-
-
-
diff --git a/doc/gen_changes.md b/doc/gen_changes.md
deleted file mode 100644
index b7e1576d..00000000
--- a/doc/gen_changes.md
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
-# Module gen_changes #
-* [Description](#description)
-* [Function Index](#index)
-* [Function Details](#functions)
-
-gen_changes CouchDB continuous changes consumer behavior
-This behaviour allows you to create easily a server that consume
-Couchdb continuous changes.
-
-__This module defines the `gen_changes` behaviour.__
Required callback functions: `init/1`, `handle_change/2`, `handle_call/3`, `handle_cast/2`, `handle_info/2`, `terminate/2`.
-
-
-
-## Function Index ##
-
-
-
-
-
-
-
-## Function Details ##
-
-
-
-### behaviour_info/1 ###
-
-`behaviour_info(X1) -> any()`
-
-
-
-### call/2 ###
-
-`call(Name, Request) -> any()`
-
-
-
-### call/3 ###
-
-`call(Name, Request, Timeout) -> any()`
-
-
-
-### cast/2 ###
-
-`cast(Dest, Request) -> any()`
-
-
-
-### code_change/3 ###
-
-`code_change(OldVersion, State, Extra) -> any()`
-
-
-
-### get_seq/1 ###
-
-`get_seq(Pid) -> any()`
-
-
-
-### handle_call/3 ###
-
-`handle_call(Request, From, State) -> any()`
-
-
-
-### handle_cast/2 ###
-
-`handle_cast(Msg, State) -> any()`
-
-
-
-### handle_info/2 ###
-
-`handle_info(Info, State) -> any()`
-
-
-
-### init/1 ###
-
-`init(X1) -> any()`
-
-
-
-### start_link/4 ###
-
-
-start_link(Module, Db::db(), Options::changesoptions(), InitArgs::list()) -> term()
-
-
-
-
-create a gen_changes process as part of a supervision tree.
-The function should be called, directly or indirectly, by the supervisor.
-
-
-
-### stop/1 ###
-
-`stop(Pid) -> any()`
-
-
-
-### terminate/2 ###
-
-`terminate(Reason, Gen_changes_state) -> any()`
-
diff --git a/doc/overview.edoc b/doc/overview.edoc
deleted file mode 100644
index f86101df..00000000
--- a/doc/overview.edoc
+++ /dev/null
@@ -1,390 +0,0 @@
-%% -*- erlang -*-
-%%
-%% This file is part of couchbeam released under the MIT license.
-%% See the NOTICE for more information.
-
-
-
-@copyright 2009-2025 Benoît Chesneau.
-@version 1.5.4
-@title Couchbeam - simple Apache CouchDB client library for Erlang applications
-
-@doc
-
-# couchbeam
-
-Couchbeam is a simple erlang library for [Barrel](https://barrel-db.org) or [Apache CouchDB](http://couchdb.apache.org). Couchbeam provides you a full featured and easy client to access and manage multiple nodes.
-
-#### Main features:
-
-- Complete support of the BarrelDB and Apache CouchDB API
-- Stream view results to your app
-- Stream changes feeds
-- reduced memory usage
-- fetch and send attachments in a streaming fashion
-- by default use the JSX module to encode/decode JSON
-- support [Jiffy](http://github.com/davisp/jiffy) a JSON encoder/decoder
- in C.
-
-
-#### Useful modules are:
-
-- {@link couchbeam}: The `couchbeam' module is the main interface for interaction with this application. It includes functions for managing connections to Apache CouchDB or RCOUCH servers and databases and for performing document creations, updates, deletes, views...
-- {@link couchbeam_doc} Module to manipulate Documents structures. You can set values,
-updates keys, ...
-- {@link couchbeam_attachments}: Module to manipulate attachments. You can add, remove
-attachments in a Document structure (inline attachments).
-- {@link couchbeam_view}: Module to manage view results.
-- {@link couchbeam_changes}: Module to manage changes feeds. Follow continuously
-the changes in a db or get all changes at once.
-
-
-The goal of Couchbeam is to ease the access to the Apache CouchDB and RCOUCH HTTP API in erlang.
-
-Read the [NEWS](https://raw.github.com/benoitc/couchbeam/master/NEWS) file
-to get last changelog.
-
-## Installation
-
-Download the sources from our [Github repository](http://github.com/benoitc/couchbeam)
-
-To build the application simply run 'make'. This should build .beam, .app
-files and documentation.
-
-To run tests run 'make test'.
-To generate doc, run 'make doc'.
-
-
-Or add it to your rebar config
-
-```erlang
-{deps, [
- ....
- {couchbeam, ".*", {git, "git://github.com/benoitc/couchbeam.git", {branch, "master"}}}
-]}.
-'''
-
-Note to compile with jiffy you need to define in the erlang options the
-variable `WITH_JIFFY'.
-
-if you use rebar, add to your `rebar.config':
-
-```erlang
-{erl_opts, [{d, 'WITH_JIFFY'}]}.
-'''
-
-or use the `rebar' command with the `-D' options:
-
-```sh
-rebar compile -DWITH_JIFFY
-'''
-
-## Basic Usage
-
-### Start couchbeam
-
-Couchbeam is an [OTP](http://www.erlang.org/doc/design_principles/users_guide.html)
-application. You have to start it first before using any of the
-functions. The couchbeam application will start the default socket pool
-for you.
-
-To start in the console run:
-
-
-```sh
-$ erl -pa ebin
-1> couchbeam:start().
-ok
-'''
-
-
-It will start hackney and all of the application it depends on:
-
-```erlang
-application:start(crypto),
-application:start(asn1),
-application:start(public_key),
-application:start(ssl),
-application:start(hackney),
-application:start(couchbeam).
-'''
-
-Or add couchbeam to the applications property of your .app in a release
-
-### Create a connection to the server
-
-To create a connection to a server machine:
-
-```erlang
-Url = "http://localhost:5984",
-Options = [],
-S = couchbeam:server_connection(Url, Options).
-'''
-
-Test the connection with `couchbeam:server_info/1' :
-
-```erlang
-{ok, _Version} = couchbeam:server_info(S).
-'''
-
-### Open or Create a database
-
-All document operations are done in databases. To open a database simply do:
-
-```erlang
-Options = [],
-{ok, Db} = couchbeam:open_db(Server, "testdb", Options).
-'''
-
-To create a new one:
-
-```erlang
-Options = [],
-{ok, Db} = couchbeam:create_db(Server, "testdb", Options).
-'''
-
-You can also use the shorcut `couchbeam:open_or_create_db/3'. that
-will create a database if it does not exist.
-
-### Make a new document
-
-Make a new document:
-
-```erlang
-Doc = {[
-{<<"_id">>, <<"test">>},
-{<<"content">>, <<"some text">>}
-]}.
-'''
-
-And save it to the database:
-
-```erlang
-{ok, Doc1} = couchbeam:save_doc(Db, Doc).
-'''
-
-The `couchbeam:save_doc/2' return a new document with updated
-revision and if you do not specify the _id, a unique document id.
-
-To change an document property use functions from `couchbeam_doc'.
-
-### Retrieve a document
-
-To retrieve a document do:
-
-```erlang
-{ok, Doc2} = couchbeam:open_doc(Db, "test").
-'''
-
-If you want a specific revision:
-
-```erlang
-Rev = couchbeam_doc:get_rev(Doc1),
-Options = [{rev, Rev}],
-{ok, Doc3} = couchbeam:open_doc(Db, "test", Options).
-'''
-
-Here we get the revision from the document we previously stored. Any
-options from the Apache CouchDB and RCOUCH API can be used.
-
-### Get all documents
-
-To get all documents you have first to create an object
-that will keep all informations.
-
-```erlang
-Options = [include_docs],
-{ok, AllDocs} = couchbeam_view:all(Db, Options).
-'''
-
-
-Ex of results:
-
-```erlang
-{ok,[{[{<<"id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"key">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"value">>,
- {[{<<"rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18bdb">>}]}},
- {<<"doc">>,
- {[{<<"_id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>},
- {<<"_rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18"...>>}]}}]},
- ]}.
-'''
-
-All functions to manipulate these results are in the `couchbeam_view' module.
-
-### Couch DB views
-
-Views are workin like all_docs. You have to create a View object before
-doing anything.
-
-```erlang
-Options = [],
-DesignName = "designname",
-ViewName = "viewname",
-{ok, ViewResults} = couchbeam_view:fetch(Db, {DesignName, ViewName}, Options).
-'''
-
-Like the `all_docs' function, use the functions
-from `couchbeam_view' module to manipulate results. You can pass
-any querying options from the [view API](http://docs.rcouch.org/en/latest/api/ddoc/views.html).
-
-Design doc are created like any documents:
-
-```erlang
-DesignDoc = {[
- {<<"_id">>, <<"_design/couchbeam">>},
- {<<"language">>,<<"javascript">>},
- {<<"views">>,
- {[{<<"test">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">>
- }]}
- },{<<"test2">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">>
- }]}
- }]}
- }
- ]},
-{ok, DesignDoc1} = couchbeam:save_doc(Db, DesignDoc).
-'''
-
-You can also use couchapp to manage them
-more easily.
-
-### Stream View results
-
-While you can get results using `couchbeam_views:fetch/2', you can also retrieve
-all rows in a streaming fashion:
-
-```erlang
-ViewFun = fun(Ref, F) ->
- receive
- {Ref, done} ->
- io:format("done", []),
- done;
- {Ref, {row, Row}} ->
- io:format("got ~p~n", [Row]),
- F(Ref, F);
- {error, Ref, Error} ->
- io:format("error: ~p~n", [Error])
- end
-end,
-
-{ok, StreamRef} = couchbeam_view:stream(Db, 'all_docs'),
-ViewFun(StreamRef, ViewFun),
-{ok, StreamRef2} = couchbeam_view:stream(Db, 'all_docs', [include_docs]),
-ViewFun(StreamRef2, ViewFun).
-'''
-
-You can of course do the same with a view:
-
-```erlang
-DesignNam = "designname",
-ViewName = "viewname",
-{ok, StreamRef3} = couchbeam_view:stream(Db, {DesignNam, ViewName}, [include_docs]),
-ViewFun(StreamRef3, ViewFun).
-'''
-
-### Put, Fetch and Delete documents attachments
-
-You can add attachments to any documents. Attachments could be anything.
-
-To send an attachment:
-
-```erlang
-DocID = "test",
-AttName = "test.txt",
-Att = "some content I want to attach",
-Options = []
-{ok, _Result} = couchbeam:put_attachment(Db, DocId, AttName, Att, Options).
-'''
-
-All attachments are streamed to servers. `Att' could be also be an iolist
-or functions, see `couchbeam:put_attachment/5' for more information.
-
-To fetch an attachment:
-
-```erlang
-{ok Att1} = couchbeam:fetch_attachment(Db, DocId, AttName).
-'''
-
-You can use `couchbeam:stream_fetch_attachment/6' for the stream
-fetch.
-
-To delete an attachment:
-
-```erlang
-{ok, Doc4} = couchbeam:open_doc(Db, DocID),
-ok = couchbeam:delete_attachment(Db, Doc4, AttName).
-'''
-
-### Changes
-
-Apache CouchDB and RCOUCH provide a means to get a list of changes made to documents in
-the database. With couchbeam you can get changes using `couchbeam_changes:follow_once/2'.
-This function returns all changes immediately. But you can also retrieve
-all changes rows using longpolling :
-
-```erlang
-Options = [],
-{ok, LastSeq, Rows} = couchbeam_changes:follow_once(Db, Options).
-'''
-
-Options can be any Changes query parameters. See the [change API](http://docs.rcouch.org/en/latest/api/database/changes.html) for more informations.
-
-You can also get [continuous](http://docs.rcouch.org/en/latest/api/database/changes.html#continuous):
-
-```erlang
-ChangesFun = fun(StreamRef, F) ->
- receive
- {StreamRef, {done, LastSeq}} ->
- io:format("stopped, last seq is ~p~n", [LastSeq]),
- ok;
- {StreamRef, {change, Change}} ->
- io:format("change row ~p ~n", [Change]),
- F(StreamRef, F);
- {StreamRef, Error}->
- io:format("error ? ~p ~n,", [Error])
- end
-end,
-Options = [continuous, heartbeat],
-{ok, StreamRef} = couchbeam_changes:follow(Db, Options),
-ChangesFun(StreamRef, ChangesFun).
-'''
-
-> **Note**: a `gen_changes' behaviour exists in couchbeam that you can
-use to create your own specific gen_server receiving changes. Have a
-look in the
-[example](https://github.com/benoitc/couchbeam/blob/master/examples/test_gen_changes.erl)
-for more info.
-
-### Authentication/ Connections options
-
-You can authenticate to the database or Apache CouchDB or RCOUCH server by filling
-options to the Option list in `couchbeam:server_connection/4' for the
-server or in `couchbeam:create_db/3', `couchbeam:open_db/3',
-`couchbeam:wopen_or_create_db/3' functions.
-
-To set basic_auth on a server:
-
-```erlang
-UserName = "guest",
-Password = "test",
-Url = "http://localhost:5984",
-Options = [{basic_auth, {UserName, Password}}],
-S1 = couchbeam:server_connection(Url, Options).
-'''
-
-Couchbeam support SSL, OAuth, Basic Authentication, and Proxy. You can
-also set a cookie. For more informations about the options have a look
-in the `couchbeam:server_connection/2' documentation.
-
-
-## Contribute
-
-For issues, comments or feedback please [create an
-issue](http://github.com/benoitc/couchbeam/issues).
-
-@end
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..a3e4db91
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,24 @@
+services:
+ couchdb:
+ image: couchdb:3.3
+ environment:
+ - COUCHDB_USER=admin
+ - COUCHDB_PASSWORD=change_me
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://admin:change_me@localhost:5984/"]
+ interval: 2s
+ timeout: 2s
+ retries: 30
+ test:
+ image: erlang:28
+ working_dir: /workspace
+ volumes:
+ - .:/workspace
+ environment:
+ - COUCHDB_URL=http://couchdb:5984
+ - COUCHDB_ADMIN=admin
+ - COUCHDB_PASSWORD=change_me
+ depends_on:
+ couchdb:
+ condition: service_healthy
+ command: ["bash", "-lc", "chmod +x ./support/wait-for-couch.sh && ./support/wait-for-couch.sh && rebar3 eunit"]
diff --git a/include/couchbeam.hrl b/include/couchbeam.hrl
index 9cbed182..b1f189d0 100644
--- a/include/couchbeam.hrl
+++ b/include/couchbeam.hrl
@@ -1,5 +1,5 @@
-%% @author Benoît Chesneau
-%% @copyright 2009 Benoît Chesneau.
+%% @author Benoit Chesneau
+%% @copyright 2009-2025 Benoit Chesneau.
%%
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,10 +31,11 @@
-type design_name() :: binary() | string().
-type view_name() :: binary() | string().
+% JSON types now use maps instead of proplists
-type ejson() :: ejson_object() | ejson_array().
-type ejson_array() :: [ejson_term()].
--type ejson_object() :: {[{ejson_key(), ejson_term()}]}.
+-type ejson_object() :: map().
-type ejson_key() :: binary() | atom().
diff --git a/rebar.config b/rebar.config
index f23f3edc..6a1d062c 100644
--- a/rebar.config
+++ b/rebar.config
@@ -4,22 +4,37 @@
{erl_opts, [debug_info,
{platform_define, "^(2[3-9])", 'USE_CRYPTO_MAC'}]}.
+%% ExDoc plugin for generating HTML docs
+{plugins, [rebar3_ex_doc, rebar3_hex]}.
+
+%% ExDoc settings
+{ex_doc, [
+ {source_url, "https://github.com/benoitc/couchbeam"},
+ {main, "readme"},
+ {output, "doc"},
+ {extras, [
+ {"README.md", #{title => "Readme"}},
+ {"NEWS.md", #{title => "Changelog"}},
+ {"NOTICE", #{title => "Notice"}},
+ {"LICENSE", #{title => "License"}}
+ ]}
+]}.
+
+%% Hex.pm configuration
+%% - Use ExDoc for docs when running `rebar3 hex docs`
+{hex, [
+ {doc, #{provider => ex_doc}}
+]}.
+
{deps, [
- {jsx, "3.1.0"},
{hackney, "1.25.0"}
]}.
-{profiles, [{docs, [{deps, [{edown,"0.9.1"}]},
- {edoc_opts, [{doclet, edown_doclet},
- {packages, false},
- {subpackages, true},
- {top_level_readme,
- {"./README.md", "http://github.com/benoitc/couchbeam"}}
- ]}]},
+{profiles, [
{test, [
- {cover_enabled, true},
- {eunit_opts, [verbose]},
- {deps, [{oauth, "2.1.0"}]}
- ]}
+ {cover_enabled, true},
+ {eunit_opts, [verbose]},
+ {deps, [{oauth, "2.1.0"}]}
+ ]}
]}.
diff --git a/rebar.lock b/rebar.lock
index c680c08b..b17ce661 100644
--- a/rebar.lock
+++ b/rebar.lock
@@ -2,7 +2,6 @@
[{<<"certifi">>,{pkg,<<"certifi">>,<<"2.15.0">>},1},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.25.0">>},0},
{<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},1},
- {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.4.0">>},1},
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.1">>},1},
@@ -13,7 +12,6 @@
{<<"certifi">>, <<"0E6E882FCDAAA0A5A9F2B3DB55B1394DBA07E8D6D9BCAD08318FB604C6839712">>},
{<<"hackney">>, <<"390E9B83F31E5B325B9F43B76E1A785CBDB69B5B6CD4E079AA67835DED046867">>},
{<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>},
- {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>},
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
{<<"mimerl">>, <<"3882A5CA67FBBE7117BA8947F27643557ADEC38FA2307490C4C4207624CB213B">>},
{<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>},
@@ -23,7 +21,6 @@
{<<"certifi">>, <<"B147ED22CE71D72EAFDAD94F055165C1C182F61A2FF49DF28BCC71D1D5B94A60">>},
{<<"hackney">>, <<"7209BFD75FD1F42467211FF8F59EA74D6F2A9E81CBCEE95A56711EE79FD6B1D4">>},
{<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>},
- {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>},
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
{<<"mimerl">>, <<"13AF15F9F68C65884ECCA3A3891D50A7B57D82152792F3E19D88650AA126B144">>},
{<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>},
diff --git a/src/couchbeam.app.src b/src/couchbeam.app.src
index db0e822f..48fd8df7 100644
--- a/src/couchbeam.app.src
+++ b/src/couchbeam.app.src
@@ -6,7 +6,7 @@
{application, couchbeam,
[{description, "Erlang CouchDB client"},
- {vsn, "1.7.1"},
+ {vsn, "2.0.0"},
{modules, []},
{registered, [
couchbeam_sup
diff --git a/src/couchbeam.erl b/src/couchbeam.erl
index beaa420c..c048df4e 100644
--- a/src/couchbeam.erl
+++ b/src/couchbeam.erl
@@ -66,7 +66,7 @@
%% --------------------------------------------------------------------
%% @doc Create a server for connectiong to a CouchDB node
-%% @equiv server_connection("127.0.0.1", 5984, "", [], false)
+%% @equiv server_connection(URL, Options)
server_connection() ->
URL = couchbeam_util:binary_env("COUCHDB_URL", "http://127.0.0.1:5984"),
ADMIN = couchbeam_util:binary_env("COUCHDB_ADMIN", "admin"),
@@ -152,7 +152,6 @@ server_connection(Host, Port, Prefix, Options) ->
server_connection(Url, Options).
%% @doc Get Information from the server
-%% @spec server_info(server()) -> {ok, iolist()}
server_info(#server{url=Url, options=Opts}) ->
case hackney:get(Url, [], <<>>, Opts) of
{ok, 200, _, Ref} ->
@@ -167,49 +166,41 @@ server_info(#server{url=Url, options=Opts}) ->
end.
%% @doc Get one uuid from the server
-%% @spec get_uuid(server()) -> lists()
get_uuid(Server) ->
couchbeam_uuids:get_uuids(Server, 1).
%% @doc Get a list of uuids from the server
-%% @spec get_uuids(server(), integer()) -> lists()
get_uuids(Server, Count) ->
couchbeam_uuids:get_uuids(Server, Count).
-%% @doc Handle replication. Pass an object containting all informations
-%% It allows to pass for example an authentication info
+%% @doc Handle replication. Pass a map containing all information.
+%% It allows passing authentication info, etc.
%% ```
-%% RepObj = {[
-%% {<<"source">>, <<"sourcedb">>},
-%% {<<"target">>, <<"targetdb">>},
-%% {<<"create_target">>, true}
-%% ]}
+%% RepObj = #{
+%% <<"source">> => <<"sourcedb">>,
+%% <<"target">> => <<"targetdb">>,
+%% <<"create_target">> => true
+%% }.
%% replicate(Server, RepObj).
%% '''
%%
-%% @spec replicate(Server::server(), RepObj::{list()})
%% -> {ok, Result}|{error, Error}
-replicate(#server{}=Server, RepObj) ->
+replicate(#server{}=Server, RepObj) when is_map(RepObj) ->
case open_db(Server, <<"_replicator">>) of
- {ok, ReplicatorDb} ->
- case save_doc(ReplicatorDb, RepObj) of
- {ok, Doc} ->
-
- {ok, Doc};
- Error ->
- Error
- end
- end.
+ {ok, ReplicatorDb} -> save_doc(ReplicatorDb, RepObj);
+ Error -> Error
+ end;
+replicate(Server, Props) when is_list(Props) ->
+ replicate(Server, maps:from_list(Props)).
%% @doc Handle replication.
-%% @spec replicate(Server::server(), Source::string(), Target::target())
%% -> {ok, Result}|{error, Error}
replicate(Server, Source, Target) ->
replicate(Server, Source, Target, []).
%% @doc handle Replication. Allows to pass options with source and
-%% target. Source and target can be either simple URI strings or
+%% target. Source and target can be either simple URI strings or
%% complex document structures with authentication. Options is a Json object.
%% ex:
%% ```
@@ -219,9 +210,13 @@ replicate(Server, Source, Target) ->
%%
%% %% Complex replication with authentication
%% Source = "http://user:pass@remote.com:5984/db",
-%% Target = {[{<<"url">>, <<"http://localhost:5984/target_db">>},
-%% {<<"auth">>, {[{<<"basic">>, {[{<<"username">>, <<"user">>},
-%% {<<"password">>, <<"pass">>}]}}]}}]},
+%% Target = #{
+%% <<"url">> => <<"http://localhost:5984/target_db">>,
+%% <<"auth">> => #{
+%% <<"basic">> => #{<<"username">> => <<"user">>,
+%% <<"password">> => <<"pass">>}
+%% }
+%% },
%% couchbeam:replicate(S, Source, Target, [{<<"continuous">>, true}]).
%% '''
replicate(Server, Source, Target, {Props}) ->
@@ -233,14 +228,12 @@ replicate(Server, Source, Target, Options) ->
{<<"source">>, SourceProp},
{<<"target">>, TargetProp} | Options
],
- replicate(Server, {RepProp}).
+ replicate(Server, RepProp).
%% @doc get list of databases on a CouchDB node
-%% @spec all_dbs(server()) -> {ok, iolist()}
all_dbs(#server{}=Server) -> all_dbs(Server, []).
%% @doc get list of databases on a CouchDB node with optional filter
-%% @spec all_dbs(server(), view_options()) -> {ok, iolist()}
all_dbs(#server{url=ServerUrl, options=Opts}, Options) ->
Args = couchbeam_view:parse_view_options(Options),
Url = hackney_url:make_url(ServerUrl, <<"_all_dbs">>, Args#view_query_args.options),
@@ -281,7 +274,6 @@ view_cleanup(#db{server=Server, name=DbName, options=Opts}) ->
end.
%% @doc test if db with dbname exists on the CouchDB node
-%% @spec db_exists(server(), string()) -> boolean()
db_exists(#server{url=ServerUrl, options=Opts}, DbName) ->
Url = hackney_url:make_url(ServerUrl, couchbeam_util:dbname(DbName), []),
case couchbeam_httpc:db_request(head, Url, [], <<>>, Opts, [200]) of
@@ -311,7 +303,6 @@ create_db(Server, DbName, Options) ->
%% Params is a list of optionnal query argument you want to pass to the
%% db. Useful for bigcouch for example.
%%
-%% @spec create_db(Server::server(), DbName::string(),
%% Options::optionList(), Params::list()) -> {ok, db()|{error, Error}}
create_db(#server{url=ServerUrl, options=Opts}=Server, DbName0, Options,
Params) ->
@@ -335,7 +326,6 @@ open_db(Server, DbName) ->
open_db(Server, DbName, []).
%% @doc Create a client for connection to a database
-%% @spec open_db(Server::server(), DbName::string(), Options::optionList())
%% -> {ok, db()}
open_db(#server{options=Opts}=Server, DbName, Options) ->
Options1 = couchbeam_util:propmerge1(Options, Opts),
@@ -356,7 +346,6 @@ open_or_create_db(Server, DbName, Options) ->
%% @doc Create a client for connecting to a database and create the
%% database if needed.
-%% @spec open_or_create_db(server(), string(), list(), list()) -> {ok, db()|{error, Error}}
open_or_create_db(#server{url=ServerUrl, options=Opts}=Server, DbName0,
Options, Params) ->
@@ -380,7 +369,6 @@ delete_db(#db{server=Server, name=DbName}) ->
delete_db(Server, DbName).
%% @doc delete database
-%% @spec delete_db(server(), DbName) -> {ok, iolist()|{error, Error}}
delete_db(#server{url=ServerUrl, options=Opts}, DbName) ->
Url = hackney_url:make_url(ServerUrl, couchbeam_util:dbname(DbName), []),
Resp = couchbeam_httpc:request(delete, Url, [], <<>>, Opts),
@@ -392,7 +380,6 @@ delete_db(#server{url=ServerUrl, options=Opts}, DbName) ->
end.
%% @doc get database info
-%% @spec db_info(db()) -> {ok, iolist()|{error, Error}}
db_info(#db{server=Server, name=DbName, options=Opts}) ->
Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), couchbeam_util:dbname(DbName), []),
case couchbeam_httpc:db_request(get, Url, [], <<>>, Opts, [200]) of
@@ -406,7 +393,6 @@ db_info(#db{server=Server, name=DbName, options=Opts}) ->
end.
%% @doc test if doc with uuid exists in the given db
-%% @spec doc_exists(db(), string()) -> boolean()
doc_exists(#db{server=Server, options=Opts}=Db, DocId) ->
DocId1 = couchbeam_util:encode_docid(DocId),
Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), couchbeam_httpc:doc_url(Db, DocId1), []),
@@ -422,7 +408,6 @@ open_doc(Db, DocId) ->
%% @doc open a document
%% Params is a list of query argument. Have a look in CouchDb API
-%% @spec open_doc(Db::db(), DocId::string(), Params::list())
%% -> {ok, Doc}|{error, Error}
open_doc(#db{server=Server, options=Opts}=Db, DocId, Params) ->
DocId1 = couchbeam_util:encode_docid(DocId),
@@ -490,17 +475,16 @@ save_doc(Db, Doc) ->
%% @doc save a *document
%% A document is a Json object like this one:
%%
-%% ```{[
-%% {<<"_id">>, <<"myid">>},
-%% {<<"title">>, <<"test">>}
-%% ]}'''
+%% ```#{
+%% <<"_id">> => <<"myid">>,
+%% <<"title">> => <<"test">>
+%% }'''
%%
%% Options are arguments passed to the request. This function return a
%% new document with last revision and a docid. If _id isn't specified in
%% document it will be created. Id is created by extracting an uuid from
%% the couchdb node.
%%
-%% @spec save_doc(Db::db(), Doc, Options::list()) -> {ok, Doc1}|{error, Error}
save_doc(Db, Doc, Options) ->
save_doc(Db, Doc, [], Options).
@@ -508,10 +492,10 @@ save_doc(Db, Doc, Options) ->
%% @doc save a *document with all its attacjments
%% A document is a Json object like this one:
%%
-%% ```{[
-%% {<<"_id">>, <<"myid">>},
-%% {<<"title">>, <<"test">>}
-%% ]}'''
+%% ```#{
+%% <<"_id">> => <<"myid">>,
+%% <<"title">> => <<"test">>
+%% }'''
%%
%% Options are arguments passed to the request. This function return a
%% new document with last revision and a docid. If _id isn't specified in
@@ -539,8 +523,8 @@ save_doc(Db, Doc, Options) ->
%% gzipped.
-spec save_doc(Db::db(), doc(), mp_attachments(), Options :: [{binary(), binary() | true}] | binary()) ->
{ok, doc()} | {error, term()}.
-save_doc(#db{server=Server, options=Opts}=Db, {Props}=Doc, Atts, Options) ->
- DocId = case couchbeam_util:get_value(<<"_id">>, Props) of
+save_doc(#db{server=Server, options=Opts}=Db, MapDoc, Atts, Options) when is_map(MapDoc) ->
+ DocId = case maps:get(<<"_id">>, MapDoc, undefined) of
undefined ->
[Id] = get_uuid(Server),
Id;
@@ -551,16 +535,15 @@ save_doc(#db{server=Server, options=Opts}=Db, {Props}=Doc, Atts, Options) ->
Options),
case Atts of
[] ->
- JsonDoc = couchbeam_ejson:encode(Doc),
+ JsonDoc = couchbeam_ejson:encode(MapDoc),
Headers = [{<<"Content-Type">>, <<"application/json">>}],
case couchbeam_httpc:db_request(put, Url, Headers, JsonDoc, Opts,
[200, 201, 202]) of
{ok, _, _, Ref} ->
- {JsonProp} = couchbeam_httpc:json_body(Ref),
- NewRev = couchbeam_util:get_value(<<"rev">>, JsonProp),
- NewDocId = couchbeam_util:get_value(<<"id">>, JsonProp),
- Doc1 = couchbeam_doc:set_value(<<"_rev">>, NewRev,
- couchbeam_doc:set_value(<<"_id">>, NewDocId, Doc)),
+ JsonProp = couchbeam_httpc:json_body(Ref),
+ NewRev = maps:get(<<"rev">>, JsonProp),
+ NewDocId = maps:get(<<"id">>, JsonProp),
+ Doc1 = MapDoc#{<<"_id">> => NewDocId, <<"_rev">> => NewRev},
{ok, Doc1};
Error ->
Error
@@ -572,7 +555,7 @@ save_doc(#db{server=Server, options=Opts}=Db, {Props}=Doc, Atts, Options) ->
%% so we have to calculate the content-length. It also means
%% that we need to know the size of each attachments. (Which
%% should be expected).
- {CLen, JsonDoc, Doc2} = couchbeam_httpc:len_doc_to_mp_stream(Atts, Boundary, Doc),
+ {CLen, JsonDoc, Doc2} = couchbeam_httpc:len_doc_to_mp_stream(Atts, Boundary, MapDoc),
CType = <<"multipart/related; boundary=\"",
Boundary/binary, "\"" >>,
@@ -597,7 +580,6 @@ delete_doc(Db, Doc) ->
%% if you want to make sure the doc it emptied on delete, use the option
%% {empty_on_delete, true} or pass a doc with just _id and _rev
%% members.
-%% @spec delete_doc(Db, Doc, Options) -> {ok,Result}|{error,Error}
delete_doc(Db, Doc, Options) ->
delete_docs(Db, [Doc], Options).
@@ -610,21 +592,20 @@ delete_docs(Db, Docs) ->
%% if you want to make sure the doc it emptied on delete, use the option
%% {empty_on_delete, true} or pass a doc with just _id and _rev
%% members.
-%% @spec delete_docs(Db::db(), Docs::list(),Options::list()) -> {ok, Result}|{error, Error}
delete_docs(Db, Docs, Options) ->
Empty = couchbeam_util:get_value("empty_on_delete", Options, false),
{FinalDocs, FinalOptions} = case Empty of
true ->
- Docs1 = lists:map(fun(Doc)->
- {[{<<"_id">>, couchbeam_doc:get_id(Doc)},
- {<<"_rev">>, couchbeam_doc:get_rev(Doc)},
- {<<"_deleted">>, true}]}
+ Docs1 = lists:map(fun(Doc0)->
+ #{ <<"_id">> => couchbeam_doc:get_id(Doc0),
+ <<"_rev">> => couchbeam_doc:get_rev(Doc0),
+ <<"_deleted">> => true}
end, Docs),
{Docs1, proplists:delete("all_or_nothing", Options)};
_ ->
- Docs1 = lists:map(fun({DocProps})->
- {[{<<"_deleted">>, true}|DocProps]}
+ Docs1 = lists:map(fun(DocMap)->
+ maps:put(<<"_deleted">>, true, DocMap)
end, Docs),
{Docs1, Options}
end,
@@ -636,7 +617,6 @@ save_docs(Db, Docs) ->
save_docs(Db, Docs, []).
%% @doc save a list of documents
-%% @spec save_docs(Db::db(), Docs::list(),Options::list()) -> {ok, Result}|{error, Error}
save_docs(#db{server=Server, options=Opts}=Db, Docs, Options) ->
Docs1 = [maybe_docid(Server, Doc) || Doc <- Docs],
Options1 = couchbeam_util:parse_options(Options),
@@ -648,7 +628,8 @@ save_docs(#db{server=Server, options=Opts}=Db, Docs, Options) ->
{K, V} || {K, V} <- Options1,
K =/= "all_or_nothing" andalso K =/= "new_edits"
],
- Body = couchbeam_ejson:encode({[{<<"docs">>, Docs1}|DocOptions]}),
+ BodyMap = maps:from_list(DocOptions),
+ Body = couchbeam_ejson:encode(maps:put(<<"docs">>, Docs1, BodyMap)),
Url = hackney_url:make_url(couchbeam_httpc:server_url(Server),
[couchbeam_httpc:db_url(Db), <<"_bulk_docs">>],
Options2),
@@ -677,17 +658,17 @@ copy_doc(Db, Doc, Dest) when is_binary(Dest) ->
{Dest, <<>>}
end,
do_copy(Db, Doc, Destination);
-copy_doc(Db, Doc, {Props}) ->
- DocId = proplists:get_value(<<"_id">>, Props),
- Rev = proplists:get_value(<<"_rev">>, Props, <<>>),
+copy_doc(Db, Doc, Props) when is_map(Props) ->
+ DocId = maps:get(<<"_id">>, Props),
+ Rev = maps:get(<<"_rev">>, Props, <<>>),
do_copy(Db, Doc, {DocId, Rev}).
-do_copy(Db, {Props}, Destination) ->
- case proplists:get_value(<<"_id">>, Props) of
+do_copy(Db, Props, Destination) when is_map(Props) ->
+ case maps:get(<<"_id">>, Props, undefined) of
undefined ->
{error, invalid_source};
DocId ->
- DocRev = proplists:get_value(<<"_rev">>, Props, nil),
+ DocRev = maps:get(<<"_rev">>, Props, nil),
do_copy(Db, {DocId, DocRev}, Destination)
end;
do_copy(Db, DocId, Destination) when is_binary(DocId) ->
@@ -715,9 +696,9 @@ do_copy(#db{server=Server, options=Opts}=Db, {DocId, DocRev},
case couchbeam_httpc:db_request(copy, Url, Headers1, <<>>,
Opts, [201]) of
{ok, _, _, Ref} ->
- {JsonProp} = couchbeam_httpc:json_body(Ref),
- NewRev = couchbeam_util:get_value(<<"rev">>, JsonProp),
- NewDocId = couchbeam_util:get_value(<<"id">>, JsonProp),
+ JsonProp = couchbeam_httpc:json_body(Ref),
+ NewRev = maps:get(<<"rev">>, JsonProp),
+ NewDocId = maps:get(<<"id">>, JsonProp),
{ok, NewDocId, NewRev};
Error ->
Error
@@ -823,7 +804,6 @@ put_attachment(Db, DocId, Name, Body)->
put_attachment(Db, DocId, Name, Body, []).
%% @doc put an attachment
-%% @spec put_attachment(Db::db(), DocId::string(), Name::string(),
%% Body::body(), Option::optionList()) -> {ok, iolist()}
%% optionList() = [option()]
%% option() = {rev, string()} |
@@ -865,8 +845,8 @@ put_attachment(#db{server=Server, options=Opts}=Db, DocId, Name, Body,
[201, 202]) of
{ok, _, _, Ref} ->
JsonBody = couchbeam_httpc:json_body(Ref),
- {[{<<"ok">>, true}|R]} = JsonBody,
- {ok, {R}};
+ #{<<"ok">> := true} = JsonBody,
+ {ok, maps:remove(<<"ok">>, JsonBody)};
{ok, Ref} ->
{ok, Ref};
Error ->
@@ -894,14 +874,13 @@ delete_attachment(Db, Doc, Name) ->
delete_attachment(Db, Doc, Name, []).
%% @doc delete a document attachment
-%% @spec(db(), string()|list(), string(), list() -> {ok, Result} | {error, Error}
delete_attachment(#db{server=Server, options=Opts}=Db, DocOrDocId, Name,
Options) ->
Options1 = couchbeam_util:parse_options(Options),
{Rev, DocId} = case DocOrDocId of
- {Props} ->
- Rev1 = couchbeam_util:get_value(<<"_rev">>, Props),
- DocId1 = couchbeam_util:get_value(<<"_id">>, Props),
+ Props when is_map(Props) ->
+ Rev1 = maps:get(<<"_rev">>, Props),
+ DocId1 = maps:get(<<"_id">>, Props),
{Rev1, DocId1};
DocId1 ->
Rev1 = couchbeam_util:get_value("rev", Options1),
@@ -925,8 +904,9 @@ delete_attachment(#db{server=Server, options=Opts}=Db, DocOrDocId, Name,
case couchbeam_httpc:db_request(delete, Url, [], <<>>, Opts,
[200]) of
{ok, _, _, Ref} ->
- {[{<<"ok">>,true}|R]} = couchbeam_httpc:json_body(Ref),
- {ok, {R}};
+ Map = couchbeam_httpc:json_body(Ref),
+ #{<<"ok">> := true} = Map,
+ {ok, maps:remove(<<"ok">>, Map)};
Error ->
Error
end
@@ -948,8 +928,8 @@ ensure_full_commit(#db{server=Server, options=Opts}=Db, Options) ->
Headers = [{<<"Content-Type">>, <<"application/json">>}],
case couchbeam_httpc:db_request(post, Url, Headers, <<>>, Opts, [201]) of
{ok, _, _, Ref} ->
- {Props} = couchbeam_httpc:json_body(Ref),
- {ok, proplists:get_value(<<"instance_start_time">>, Props)};
+ Map = couchbeam_httpc:json_body(Ref),
+ {ok, maps:get(<<"instance_start_time">>, Map)};
Error ->
Error
end.
@@ -957,7 +937,6 @@ ensure_full_commit(#db{server=Server, options=Opts}=Db, Options) ->
%% @doc Compaction compresses the database file by removing unused
%% sections created during updates.
%% See [http://wiki.apache.org/couchdb/Compaction] for more informations
-%% @spec compact(Db::db()) -> ok|{error, term()}
compact(#db{server=Server, options=Opts}=Db) ->
Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), [couchbeam_httpc:db_url(Db),
<<"_compact">>],
@@ -973,7 +952,6 @@ compact(#db{server=Server, options=Opts}=Db) ->
%% @doc Like compact/1 but this compacts the view index from the
%% current version of the design document.
%% See [http://wiki.apache.org/couchdb/Compaction#View_compaction] for more informations
-%% @spec compact(Db::db(), ViewName::string()) -> ok|{error, term()}
compact(#db{server=Server, options=Opts}=Db, DesignName) ->
Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), [couchbeam_httpc:db_url(Db),
<<"_compact">>,
@@ -994,7 +972,7 @@ compact(#db{server=Server, options=Opts}=Db, DesignName) ->
PossibleAncestor :: binary()]}]}
| {error, term()}.
get_missing_revs(#db{server=Server, options=Opts}=Db, IdRevs) ->
- Json = couchbeam_ejson:encode({IdRevs}),
+ Json = couchbeam_ejson:encode(maps:from_list(IdRevs)),
Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), [couchbeam_httpc:db_url(Db),
<<"_revs_diff">>],
[]),
@@ -1003,16 +981,16 @@ get_missing_revs(#db{server=Server, options=Opts}=Db, IdRevs) ->
case couchbeam_httpc:db_request(post, Url, Headers, Json, Opts,
[200]) of
{ok, _, _, Ref} ->
- {Props} = couchbeam_httpc:json_body(Ref),
- Res = lists:map(fun({Id, {Result}}) ->
- MissingRevs = proplists:get_value(
- <<"missing">>, Result
- ),
- PossibleAncestors = proplists:get_value(
- <<"possible_ancestors">>, Result, []
- ),
- {Id, MissingRevs, PossibleAncestors}
- end, Props),
+ Props = couchbeam_httpc:json_body(Ref),
+ Res = [
+ begin
+ Result = maps:get(Id, Props),
+ MissingRevs = maps:get(<<"missing">>, Result, []),
+ PossibleAncestors = maps:get(<<"possible_ancestors">>, Result, []),
+ {Id, MissingRevs, PossibleAncestors}
+ end
+ || {Id, _} <- maps:to_list(Props)
+ ],
{ok, Res};
Error ->
Error
@@ -1027,7 +1005,7 @@ find(#db{server=Server, options=Opts}=Db, Selector, Params) ->
Headers = [{<<"content-type">>, <<"application/json">>}
,{<<"accept">>, <<"application/json">>}
],
- BodyJson = {[{selector, Selector} | Params]},
+ BodyJson = maps:put(<<"selector">>, Selector, maps:from_list(Params)),
case couchbeam_httpc:db_request(post, Url, Headers, couchbeam_ejson:encode(BodyJson), Opts, [200, 201]) of
{ok, _, _, Ref} ->
{ok, couchbeam_httpc:json_body(Ref)};
@@ -1040,19 +1018,24 @@ find(#db{server=Server, options=Opts}=Db, Selector, Params) ->
%% --------------------------------------------------------------------
%% add missing docid to a list of documents if needed
-maybe_docid(Server, {DocProps}) ->
- case couchbeam_util:get_value(<<"_id">>, DocProps) of
+maybe_docid(Server, Doc) when is_map(Doc) ->
+ case maps:get(<<"_id">>, Doc, undefined) of
undefined ->
[DocId] = get_uuid(Server),
- {[{<<"_id">>, DocId}|DocProps]};
+ maps:put(<<"_id">>, DocId, Doc);
_DocId ->
- {DocProps}
- end.
+ Doc
+ end;
+maybe_docid(_Server, {DocProps}) ->
+ %% legacy path not supported in maps-only mode
+ maps:from_list(DocProps).
%% format replication endpoint for source or target
%% supports Db record, simple URI strings and complex document structures
+format_replication_endpoint(Props) when is_map(Props) ->
+ Props;
format_replication_endpoint({Props}) when is_list(Props) ->
- {Props};
+ maps:from_list(Props);
format_replication_endpoint(<<"http://", _/binary>> = Endpoint)->
couchbeam_util:to_binary(Endpoint);
format_replication_endpoint(<<"https://", _/binary>> = Endpoint)->
@@ -1067,6 +1050,8 @@ format_replication_endpoint(#db{server=#server{url=BaseUrl, options=Options}, na
hackney_url:make_url(BaseUrl, [DbName], [])
end.
+%% maps-only; helpers removed
+
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -1089,21 +1074,25 @@ wait_for_replication(_Db, DocId, 0) ->
io:format("Timeout waiting for document ~p to replicate~n", [DocId]),
throw({timeout, waiting_for_replication});
wait_for_replication(Db, DocId, Retries) ->
- case couchbeam:open_doc(Db, DocId) of
+ case catch couchbeam:open_doc(Db, DocId) of
{ok, _} ->
io:format("Document ~p found in target db~n", [DocId]),
ok;
{error, not_found} ->
io:format("Document ~p not found, retries left: ~p~n", [DocId, Retries-1]),
timer:sleep(200),
+ wait_for_replication(Db, DocId, Retries - 1);
+ _Other ->
+ io:format("Transient error waiting for document ~p, retries left: ~p~n", [DocId, Retries-1]),
+ timer:sleep(300),
wait_for_replication(Db, DocId, Retries - 1)
end.
basic_test() ->
start_couchbeam_tests(),
Server = couchbeam:server_connection(),
- {ok, {Data}} = couchbeam:server_info(Server),
- ?assertEqual(<<"Welcome">>, proplists:get_value(<<"couchdb">>, Data)),
+ {ok, Data} = couchbeam:server_info(Server),
+ ?assertEqual(<<"Welcome">>, maps:get(<<"couchdb">>, Data)),
ok.
db_test() ->
@@ -1136,20 +1125,20 @@ basic_doc_test() ->
start_couchbeam_tests(),
Server = couchbeam:server_connection(),
{ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"),
- {ok, Doc} = couchbeam:save_doc(Db, {[{<<"test">>, <<"blah">>}]}),
- ?assertMatch({_}, Doc),
- {ok, {Props}} = couchbeam:save_doc(Db, {[{<<"_id">>,<<"test">>}, {<<"test">>,<<"blah">>}]}),
- ?assertEqual(<<"test">>, proplists:get_value(<<"_id">>, Props)),
- ?assertEqual({error, conflict}, couchbeam:save_doc(Db, {[{<<"_id">>,<<"test">>}, {<<"test">>,<<"blah">>}]})),
+ {ok, Doc} = couchbeam:save_doc(Db, #{<<"test">> => <<"blah">>}),
+ ?assert(is_map(Doc) =:= true),
+ {ok, Props} = couchbeam:save_doc(Db, #{<<"_id">> => <<"test">>, <<"test">> => <<"blah">>}),
+ ?assertEqual(<<"test">>, maps:get(<<"_id">>, Props)),
+ ?assertEqual({error, conflict}, couchbeam:save_doc(Db, #{<<"_id">> => <<"test">>, <<"test">> => <<"blah">>})),
Rev = couchbeam:lookup_doc_rev(Db, "test"),
- {ok, {Doc1}} = couchbeam:open_doc(Db, <<"test">>),
- ?assertEqual(Rev, proplists:get_value(<<"_rev">>, Doc1)),
- ?assertEqual(<<"blah">>, proplists:get_value(<<"test">>, Doc1)),
+ {ok, Doc1} = couchbeam:open_doc(Db, <<"test">>),
+ ?assertEqual(Rev, maps:get(<<"_rev">>, Doc1)),
+ ?assertEqual(<<"blah">>, maps:get(<<"test">>, Doc1)),
- _ = couchbeam:save_doc(Db, {[{<<"_id">>,<<"test2">>}, {<<"test">>,<<"blah">>}]}),
+ _ = couchbeam:save_doc(Db, #{<<"_id">> => <<"test2">>, <<"test">> => <<"blah">>}),
{ok, Doc2} = couchbeam:open_doc(Db, "test2"),
- ?assertMatch({_}, Doc2),
+ ?assert(is_map(Doc2) =:= true),
?assertEqual(true, couchbeam_doc:is_saved(Doc2)),
?assertEqual(<<"test2">>, couchbeam_doc:get_id(Doc2)),
?assertMatch(true, couchbeam:doc_exists(Db, "test2")),
@@ -1157,7 +1146,7 @@ basic_doc_test() ->
?assertEqual({error, not_found}, couchbeam:open_doc(Db, "test2")),
?assertMatch(false, couchbeam:doc_exists(Db, "test2")),
- Doc3 = {[{<<"_id">>, <<"~!@#$%^&*()_+-=[]{}|;':,./<> ?">>}]},
+ Doc3 = #{<<"_id">> => <<"~!@#$%^&*()_+-=[]{}|;':,./<> ?">>},
{ok, _Doc4} = couchbeam:save_doc(Db, Doc3),
{ok, Doc5} = couchbeam:open_doc(Db, <<"~!@#$%^&*()_+-=[]{}|;':,./<> ?">>),
?assertEqual( <<"~!@#$%^&*()_+-=[]{}|;':,./<> ?">>, couchbeam_doc:get_id(Doc5)),
@@ -1168,11 +1157,11 @@ bulk_doc_test() ->
Server = couchbeam:server_connection(),
{ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"),
- Doc1 = {[{<<"_id">>, <<"a">>}]},
- Doc2 = {[{<<"_id">>, <<"b">>}]},
- {ok, [{Props1}, {Props2}]} = couchbeam:save_docs(Db, [Doc1, Doc2]),
- ?assertEqual(<<"a">>, proplists:get_value(<<"id">>, Props1)),
- ?assertEqual(<<"b">>, proplists:get_value(<<"id">>, Props2)),
+ Doc1 = #{<<"_id">> => <<"a">>},
+ Doc2 = #{<<"_id">> => <<"b">>},
+ {ok, [Props1, Props2]} = couchbeam:save_docs(Db, [Doc1, Doc2]),
+ ?assertEqual(<<"a">>, maps:get(<<"id">>, Props1)),
+ ?assertEqual(<<"b">>, maps:get(<<"id">>, Props2)),
?assertMatch(true, couchbeam:doc_exists(Db, "a")),
?assertMatch(true, couchbeam:doc_exists(Db, "b")),
@@ -1180,6 +1169,7 @@ bulk_doc_test() ->
{ok, Doc3} = couchbeam:open_doc(Db, <<"a">>),
{ok, Doc4} = couchbeam:open_doc(Db, <<"b">>),
couchbeam:delete_docs(Db, [Doc3, Doc4]),
+ couchbeam:ensure_full_commit(Db),
?assertEqual({error, not_found}, couchbeam:open_doc(Db, <<"a">>)),
ok.
@@ -1188,12 +1178,12 @@ copy_doc_test() ->
Server = couchbeam:server_connection(),
{ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"),
- {ok, Doc} = couchbeam:save_doc(Db, {[{<<"test">>, 1}]}),
+ {ok, Doc} = couchbeam:save_doc(Db, #{<<"test">> => 1}),
{ok, CopyId, _Rev} = couchbeam:copy_doc(Db, Doc),
{ok, Doc2} = couchbeam:open_doc(Db, CopyId),
?assertEqual(1, couchbeam_doc:get_value(<<"test">>, Doc2)),
- {ok, _Doc3} = couchbeam:save_doc(Db, {[{<<"_id">>, <<"test_copy">>}]}),
+ {ok, _Doc3} = couchbeam:save_doc(Db, #{<<"_id">> => <<"test_copy">>}),
{ok, CopyId1, _} = couchbeam:copy_doc(Db, Doc, <<"test_copy">>),
?assertEqual(<<"test_copy">>, CopyId1),
{ok, Doc4} = couchbeam:open_doc(Db, CopyId1),
@@ -1205,17 +1195,17 @@ attachments_test() ->
start_couchbeam_tests(),
Server = couchbeam:server_connection(),
{ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"),
- Doc = {[{<<"_id">>, <<"test">>}]},
+ Doc = #{<<"_id">> => <<"test">>},
{ok, Doc1} = couchbeam:save_doc(Db, Doc),
RevDoc1 = couchbeam_doc:get_rev(Doc1),
- {ok, {Res}} = couchbeam:put_attachment(Db,"test", "test", "test", [{rev, RevDoc1}]),
- RevDoc11 = proplists:get_value(<<"rev">>, Res),
+ {ok, Res} = couchbeam:put_attachment(Db,"test", "test", "test", [{rev, RevDoc1}]),
+ RevDoc11 = maps:get(<<"rev">>, Res),
?assertNot(RevDoc1 =:= RevDoc11),
{ok, Attachment} = couchbeam:fetch_attachment(Db, "test", "test"),
?assertEqual( <<"test">>, Attachment),
{ok, Doc2} = couchbeam:open_doc(Db, "test"),
- ?assertMatch({ok, {_}}, couchbeam:delete_attachment(Db, Doc2, "test")),
- Doc3 = {[{<<"_id">>, <<"test2">>}]},
+ ?assertMatch({ok, _}, couchbeam:delete_attachment(Db, Doc2, "test")),
+ Doc3 = #{<<"_id">> => <<"test2">>},
Doc4 = couchbeam_attachments:add_inline(Doc3, "test", "test.txt"),
Doc5 = couchbeam_attachments:add_inline(Doc4, "test2", "test2.txt"),
{ok, _} = couchbeam:save_doc(Db, Doc5),
@@ -1229,7 +1219,7 @@ attachments_test() ->
?assertEqual({error, not_found}, couchbeam:fetch_attachment(Db, "test2", "test2.txt")),
{ok, Attachment4} = couchbeam:fetch_attachment(Db, "test2", "test.txt"),
?assertEqual( <<"test">>, Attachment4),
- {ok, Doc8} = couchbeam:save_doc(Db, {[]}),
+ {ok, Doc8} = couchbeam:save_doc(Db, #{}),
TestFileName = data_path("1M"),
{ok, FileInfo} = file:read_file_info(TestFileName),
@@ -1289,7 +1279,7 @@ multipart_test() ->
MpDocId1 = couchbeam_doc:get_id(MpDoc1),
?assertEqual(<<"test">>, MpDocId1),
?assertEqual(<<"test">>, proplists:get_value(<<"test">>, Collected1)),
- {ok, Doc} = couchbeam:save_doc(Db, {[{<<"_id">>, <<"test2">>}]},
+ {ok, Doc} = couchbeam:save_doc(Db, #{<<"_id">> => <<"test2">>},
[{<<"test.txt">>, <<"test">>}], []),
?assertEqual(<<"test2">>, couchbeam_doc:get_id(Doc)),
{ok, MpAttachment1} = couchbeam:fetch_attachment(Db, <<"test2">>, <<"test.txt">>),
@@ -1299,7 +1289,7 @@ multipart_test() ->
{ok, FileInfo} = file:read_file_info(TestFileName),
FileSize = FileInfo#file_info.size,
- {ok, Doc1} = couchbeam:save_doc(Db, {[{<<"_id">>, <<"test5">>}]},
+ {ok, Doc1} = couchbeam:save_doc(Db, #{<<"_id">> => <<"test5">>},
[{<<"1M">>, {file, TestFileName}}], []),
?assert(couchbeam_doc:is_saved(Doc1)),
@@ -1336,10 +1326,13 @@ replicate_test() ->
{ok, Db2} = couchbeam:create_db(Server, <<"couchbeam_testdb2">>),
DocId11 = <<"test">>,
- {ok, Doc11} = couchbeam:save_doc(Db, {[{<<"_id">>, DocId11}]}),
+ {ok, Doc11} = couchbeam:save_doc(Db, #{<<"_id">> => DocId11}),
DocRev11 = couchbeam_doc:get_rev(Doc11),
io:format("Created document with ID: ~p, Rev: ~p~n", [DocId11, DocRev11]),
+ %% Ensure the source doc is committed before starting replication
+ {ok, _} = couchbeam:ensure_full_commit(Db),
+
Result = couchbeam:replicate(Server, Db, Db2, [{<<"continuous">>, true}]),
?assertMatch({ok, _}, Result),
@@ -1355,7 +1348,7 @@ replicate_test() ->
%% Wait for replication to complete by polling for the document
io:format("Waiting for document ~p to appear in target db~n", [DocId11]),
- wait_for_replication(Db2, DocId11, 30),
+ wait_for_replication(Db2, DocId11, 100),
{ok, Doc11_2} = couchbeam:open_doc(Db2, DocId11),
DocRev11_2 = couchbeam_doc:get_rev(Doc11_2),
diff --git a/src/couchbeam_attachments.erl b/src/couchbeam_attachments.erl
index 0d1fac1a..f342246b 100644
--- a/src/couchbeam_attachments.erl
+++ b/src/couchbeam_attachments.erl
@@ -13,93 +13,49 @@
-export([add_inline/3, add_inline/4,
add_stub/3,
delete_inline/2]).
-
-%% @spec add_inline(Doc::json_obj(),Content::attachment_content(),
-%% AName::string()) -> json_obj()
+
+-spec add_inline(doc(), iodata(), string() | binary()) -> doc().
%% @doc add attachment to a doc and encode it. Give possibility to send attachments inline.
add_inline(Doc, Content, AName) ->
AName1 = hackney_bstr:to_binary(AName),
ContentType = mimerl:filename(AName1),
add_inline(Doc, Content, AName1, ContentType).
-%% @spec add_inline(Doc::json_obj(), Content::attachment_content(),
-%% AName::string(), ContentType::string()) -> json_obj()
+-spec add_inline(doc(), iodata(), string() | binary(), string() | binary()) -> doc().
%% @doc add attachment to a doc and encode it with ContentType fixed.
add_inline(Doc, Content, AName, ContentType) ->
- {Props} = Doc,
Data = base64:encode(Content),
- Attachment = {AName, {[{<<"content_type">>, ContentType},
- {<<"data">>, Data}]}},
-
- Attachments1 = case proplists:get_value(<<"_attachments">>, Props) of
- undefined ->
- [Attachment];
- {Attachments} ->
- case set_attachment(Attachments, [], Attachment) of
- notfound ->
- [Attachment|Attachments];
- A ->
- A
- end
- end,
- couchbeam_doc:set_value(<<"_attachments">>, {Attachments1}, Doc).
+ NewAtt = #{<<"content_type">> => ContentType,
+ <<"data">> => Data},
+ Atts0 = maps:get(<<"_attachments">>, Doc, #{}),
+ Atts1 = maps:put(AName, NewAtt, Atts0),
+ couchbeam_doc:set_value(<<"_attachments">>, Atts1, Doc).
-add_stub({Props} = Doc, Name, ContentType) ->
- Att = {couchbeam_util:to_binary(Name), {[
- {<<"content_type">>, couchbeam_util:to_binary(ContentType)}
- ]}},
+-spec add_stub(doc(), string() | binary(), string() | binary()) -> doc().
+add_stub(Doc, Name, ContentType) ->
+ AttName = couchbeam_util:to_binary(Name),
+ Att = #{<<"content_type">> => couchbeam_util:to_binary(ContentType)},
+ Atts0 = maps:get(<<"_attachments">>, Doc, #{}),
+ Atts1 = maps:put(AttName, Att, Atts0),
+ couchbeam_doc:set_value(<<"_attachments">>, Atts1, Doc).
- Attachments1 = case proplists:get_value(<<"_attachments">>, Props) of
- undefined ->
- [Att];
- {Attachments} ->
- case set_attachment(Attachments, [], Att) of
- notfound ->
- [Att|Attachments];
- A ->
- A
- end
- end,
- couchbeam_doc:set_value(<<"_attachments">>, {Attachments1}, Doc).
-
-%% @spec delete_inline(Doc::json_obj(), AName::string()) -> json_obj()
+-spec delete_inline(doc(), string() | binary()) -> doc().
%% @doc delete an attachment record in doc. This is different from delete_attachment
%% change is only applied in Doc object. Save_doc should be save to save changes.
delete_inline(Doc, AName) when is_list(AName) ->
delete_inline(Doc, list_to_binary(AName));
delete_inline(Doc, AName) when is_binary(AName) ->
- {Props} = Doc,
- case proplists:get_value(<<"_attachments">>, Props) of
- undefined ->
- Doc;
- {Attachments} ->
- case proplists:get_value(AName, Attachments) of
- undefined ->
- Doc;
- _ ->
- Attachments1 = proplists:delete(AName, Attachments),
- couchbeam_doc:set_value(<<"_attachments">>, {Attachments1}, Doc)
- end
- end.
-
-% @private
-set_attachment(Attachments, NewAttachments, Attachment) ->
- set_attachment(Attachments, NewAttachments, Attachment, false).
-set_attachment([], Attachments, _Attachment, Found) ->
- case Found of
- true ->
- Attachments;
- false ->
- notfound
- end;
-set_attachment([{Name, V}|T], Attachments, Attachment, Found) ->
- {AName, _} = Attachment,
- {Attachment1, Found1} = if
- Name =:= AName, Found =:= false ->
- {Attachment, true};
- true ->
- {{Name, V}, Found}
- end,
- set_attachment(T, [Attachment1|Attachments], Attachment, Found1).
+ case maps:get(<<"_attachments">>, Doc, undefined) of
+ undefined -> Doc;
+ Atts when is_map(Atts) ->
+ case maps:is_key(AName, Atts) of
+ false -> Doc;
+ true ->
+ Atts1 = maps:remove(AName, Atts),
+ couchbeam_doc:set_value(<<"_attachments">>, Atts1, Doc)
+ end
+ end.
+
+% no-op legacy placeholder removed
diff --git a/src/couchbeam_changes.erl b/src/couchbeam_changes.erl
index d1198790..9fec260f 100644
--- a/src/couchbeam_changes.erl
+++ b/src/couchbeam_changes.erl
@@ -175,7 +175,7 @@ stream_next(Ref) ->
Pid ! {Ref, stream_next}
end).
-%% @private
+%% internal
collect_changes(Ref) ->
collect_changes(Ref, []).
@@ -209,6 +209,42 @@ with_changes_stream(Ref, Fun) ->
end
end.
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+changes_once_normal_test() ->
+ {ok, _} = application:ensure_all_started(couchbeam),
+ Server = couchbeam:server_connection(),
+ {ok, Db} = couchbeam:create_db(Server, <<"couchbeam_changes_test">>),
+ Docs = [#{<<"_id">> => <<"c1">>}, #{<<"_id">> => <<"c2">>}],
+ couchbeam:save_docs(Db, Docs),
+ couchbeam:ensure_full_commit(Db),
+ {ok, _Seq, Changes} = follow_once(Db, [normal, {since, 0}]),
+ Ids = [maps:get(<<"id">>, C) || C <- Changes],
+ ?assert(lists:member(<<"c1">>, Ids)),
+ ?assert(lists:member(<<"c2">>, Ids)),
+ ok.
+
+changes_follow_longpoll_test() ->
+ {ok, _} = application:ensure_all_started(couchbeam),
+ Server = couchbeam:server_connection(),
+ {ok, Db} = couchbeam:create_db(Server, <<"couchbeam_changes_test2">>),
+ {ok, Ref} = follow(Db, [longpoll, {since, now}]),
+ %% trigger a change
+ {ok, _} = couchbeam:save_doc(Db, #{<<"_id">> => <<"lc1">>}),
+ %% expect one change back
+ receive
+ {Ref, {change, Change}} ->
+ ?assertMatch(#{}, Change),
+ %% we can cancel after receiving
+ couchbeam_changes:cancel_stream(Ref),
+ ok
+ after 5000 ->
+ ?assert(false)
+ end.
+
+-endif.
+
changes_request(#db{server=Server, options=ConnOptions}=Db, Options) ->
%% if we are filtering the changes using docids, send a POST request
@@ -234,7 +270,7 @@ changes_request(#db{server=Server, options=ConnOptions}=Db, Options) ->
couchbeam_httpc:db_request(get, Url, [], <<>>, ConnOptions,
[200, 202]);
_ ->
- Body = couchbeam_ejson:encode({[{<<"doc_ids">>, DocIds}]}),
+ Body = couchbeam_ejson:encode(#{<<"doc_ids">> => DocIds}),
Headers = [{<<"Content-Type">>, <<"application/json">>}],
couchbeam_httpc:db_request(post, Url, Headers, Body, ConnOptions,
[200, 202])
@@ -242,9 +278,9 @@ changes_request(#db{server=Server, options=ConnOptions}=Db, Options) ->
case Resp of
{ok, _, _, Ref} ->
- {Props} = couchbeam_httpc:json_body(Ref),
- LastSeq = couchbeam_util:get_value(<<"last_seq">>, Props),
- Changes = couchbeam_util:get_value(<<"results">>, Props),
+ Props = couchbeam_httpc:json_body(Ref),
+ LastSeq = maps:get(<<"last_seq">>, Props),
+ Changes = maps:get(<<"results">>, Props),
{ok, LastSeq, Changes};
Error ->
Error
diff --git a/src/couchbeam_changes_stream.erl b/src/couchbeam_changes_stream.erl
index 7220caee..6fdfa50f 100644
--- a/src/couchbeam_changes_stream.erl
+++ b/src/couchbeam_changes_stream.erl
@@ -15,12 +15,7 @@
system_terminate/4,
system_code_change/4]).
--export([init/1,
- handle_event/2,
- wait_results/2,
- wait_results1/2,
- collect_object/2,
- maybe_continue_decoding/1]).
+%% no incremental JSON decoder exports; using buffered/line-based decode
-include("couchbeam.hrl").
@@ -32,7 +27,7 @@
db,
options,
client_ref=nil,
- decoder,
+ buffer,
feed_type=continuous,
reconnect_after=1000,
async=normal}).
@@ -131,14 +126,14 @@ do_init_stream(#state{mref=MRef,
[] ->
couchbeam_httpc:request(get, Url, [], <<>>, ConnOpts1);
DocIds ->
- Body = couchbeam_ejson:encode({[{<<"doc_ids">>, DocIds}]}),
+ Body = couchbeam_ejson:encode(#{<<"doc_ids">> => DocIds}),
Headers = [{<<"Content-Type">>, <<"application/json">>}],
couchbeam_httpc:request(post, Url, Headers, Body, ConnOpts1)
end,
case {FeedType, proplists:get_value(since, Options, 0)} of
{continuous, now} ->
- {ok, State#state{decoder = nil, client_ref=ClientRef}};
+ {ok, State#state{client_ref=ClientRef}};
_ ->
receive
{'DOWN', MRef, _, _, _} ->
@@ -146,13 +141,7 @@ do_init_stream(#state{mref=MRef,
exit(normal);
{hackney_response, ClientRef, {status, 200, _}} ->
State1 = State#state{client_ref=ClientRef},
- DecoderFun = case FeedType of
- longpoll ->
- jsx:decoder(?MODULE, [State1], [stream]);
- _ ->
- nil
- end,
- {ok, State1#state{decoder=DecoderFun}};
+ {ok, State1};
{hackney_response, ClientRef, {error, Reason}} ->
exit(Reason)
after ?TIMEOUT ->
@@ -176,7 +165,7 @@ loop(#state{owner=Owner,
{hackney_response, ClientRef, {status, 200, _V}} ->
loop(State);
{hackney_response, ClientRef, done} ->
- maybe_reconnect(State);
+ handle_done(State);
{hackney_response, ClientRef, <<"\n">>} ->
maybe_continue(State);
{hackney_response, ClientRef, Data} when is_binary(Data) ->
@@ -249,62 +238,53 @@ wait_reconnect(#state{parent=Parent,
seq(Props,#state{owner=Owner,ref=Ref}) ->
- Seq = couchbeam_util:get_value(<<"seq">>, Props),
+ Seq = maps:get(<<"seq">>, Props, undefined),
put(last_seq, Seq),
- Owner ! {Ref, {change, {Props}}}.
-
-decode(Data) ->
- jsx:decode(Data,[return_tail,stream]).
-
-decodefun(nil) ->
- fun(Data) -> decode(Data) end;
-decodefun(Fun) ->
- Fun.
-
-decode_with_tail(Data, Fun, State) ->
- case (decodefun(Fun))(Data) of
- {with_tail,Props,Rest} ->
- seq(Props,State),
- decode_with_tail(Rest,decodefun(nil),State);
- Other -> Other
- end.
-
-decode_data(Data, #state{feed_type=continuous,
- decoder=DecodeFun}=State) ->
-
- {incomplete, DecodeFun2} =
+ Owner ! {Ref, {change, Props}}.
+
+decode_data(Data, #state{feed_type=continuous, buffer=undefined}=State) ->
+ decode_data(Data, State#state{buffer = <<>>});
+decode_data(Data, #state{feed_type=continuous, buffer=Buf}=State) ->
+ NewBuf = <>,
+ %% split on newlines to get complete JSON objects per change
+ Lines = binary:split(NewBuf, <<"\n">>, [global]),
+ %% if last line is empty, keep empty remainder; else keep last as remainder
+ {Complete, Remainder} =
+ case lists:last(Lines) of
+ <<>> -> {lists:sublist(Lines, 1, length(Lines)-1), <<>>};
+ Last -> {lists:sublist(Lines, 1, length(Lines)-1), Last}
+ end,
+ lists:foreach(fun(Line) ->
+ case Line of
+ <<>> -> ok;
+ Bin ->
+ try
+ Props = couchbeam_ejson:decode(Bin),
+ seq(Props, State)
+ catch _:_ -> ok
+ end
+ end
+ end, Complete),
+ maybe_continue(State#state{buffer = Remainder});
+decode_data(Data, #state{buffer=undefined}=State) ->
+ decode_data(Data, State#state{buffer = <<>>});
+decode_data(Data, #state{buffer=Buf}=State) ->
+ %% longpoll or normal: accumulate buffer
+ maybe_continue(State#state{buffer = <>}).
+
+handle_done(#state{feed_type=continuous}=State) ->
+ %% for continuous feeds, just reconnect/finish
+ maybe_reconnect(State);
+handle_done(#state{client_ref=ClientRef, buffer=Buf}=State) ->
+ %% stop and decode longpoll body
+ catch hackney:stop_async(ClientRef),
+ catch hackney:skip_body(ClientRef),
try
- decode_with_tail(Data,DecodeFun,State)
- catch error:badarg ->
- maybe_close(State),
- exit(badarg)
- end,
-
- try DecodeFun2(end_stream) of
- Props ->
- seq(Props,State),
- maybe_continue(State#state{decoder=nil})
- catch error:badarg -> maybe_continue(State#state{decoder=DecodeFun2})
- end;
-decode_data(Data, #state{client_ref=ClientRef,
- decoder=DecodeFun}=State) ->
- try
- {incomplete, DecodeFun2} = DecodeFun(Data),
- try DecodeFun2(end_stream) of done ->
- %% stop the request
- catch hackney:stop_async(ClientRef),
- %% skip the rest of the body so the socket is
- %% replaced in the pool
- catch hackney:skip_body(ClientRef),
- %% maybe reconnect
- maybe_reconnect(State)
- catch error:badarg ->
- maybe_continue(State#state{decoder=DecodeFun2})
- end
- catch error:badarg ->
- maybe_close(State),
- exit(badarg)
- end.
+ Map = couchbeam_ejson:decode(case Buf of undefined -> <<>>; _ -> Buf end),
+ Results = maps:get(<<"results">>, Map, []),
+ lists:foreach(fun(Props) -> seq(Props, State) end, Results),
+ maybe_reconnect(State)
+ catch _:_ -> maybe_reconnect(State) end.
maybe_continue(#state{parent=Parent,
owner=Owner,
@@ -386,158 +366,9 @@ system_code_change(Misc, _, _, _) ->
{ok, Misc}.
-%%% json decoder %%%
-
-init([State]) ->
- {wait_results, 0, [[]], State}.
-
-
-handle_event(end_json, _) ->
- done;
-handle_event(Event, {Fun, _, _, _}=St) ->
- ?MODULE:Fun(Event, St).
-
-
-
-wait_results(start_object, St) ->
- St;
-wait_results(end_object, St) ->
- St;
-wait_results({key, <<"results">>}, {_, _, _, St}) ->
- {wait_results1, 0, [[]], St};
-wait_results(_, {_, _, _, St}) ->
- {wait_results, 0, [[]], St}.
-
-
-
-wait_results1(start_array, {_, _, _, St}) ->
- {wait_results1, 0, [[]], St};
-wait_results1(start_object, {_, _, Terms, St}) ->
- {collect_object, 0, [[]|Terms], St};
-wait_results1(end_array, {_, _, _, St}) ->
- {wait_results, 0, [[]], St}.
-
-
-collect_object(start_object, {_, NestCount, Terms, St}) ->
- {collect_object, NestCount + 1, [[]|Terms], St};
-
-collect_object(end_object, {_, NestCount, [[], {key, Key}, Last|Terms],
- St}) ->
- {collect_object, NestCount - 1, [[{Key, {[{}]}}] ++ Last] ++ Terms,
- St};
-
-collect_object(end_object, {_, NestCount, [Object, {key, Key},
- Last|Terms], St}) ->
- {collect_object, NestCount - 1,
- [[{Key, {lists:reverse(Object)}}] ++ Last] ++ Terms, St};
-
-collect_object(end_object, {_, 0, [[], Last|Terms], St}) ->
- [[Change]] = [[{[{}]}] ++ Last] ++ Terms,
- send_change(Change, St);
-
-collect_object(end_object, {_, NestCount, [[], Last|Terms], St}) ->
- {collect_object, NestCount - 1, [[{[{}]}] ++ Last] ++ Terms, St};
-
-collect_object(end_object, {_, 0, [Object, Last|Terms], St}) ->
- [[Change]] = [[{lists:reverse(Object)}] ++ Last] ++ Terms,
- send_change(Change, St);
-
-
-collect_object(end_object, {_, NestCount, [Object, Last|Terms], St}) ->
- Acc = [[{lists:reverse(Object)}] ++ Last] ++ Terms,
- {collect_object, NestCount - 1, Acc, St};
-
-
-collect_object(start_array, {_, NestCount, Terms, St}) ->
- {collect_object, NestCount, [[]|Terms], St};
-collect_object(end_array, {_, NestCount, [List, {key, Key}, Last|Terms],
- St}) ->
- {collect_object, NestCount,
- [[{Key, lists:reverse(List)}] ++ Last] ++ Terms, St};
-collect_object(end_array, {_, NestCount, [List, Last|Terms], St}) ->
- {collect_object, NestCount, [[lists:reverse(List)] ++ Last] ++ Terms,
- St};
-
-collect_object({key, Key}, {_, NestCount, Terms, St}) ->
- {collect_object, NestCount, [{key, Key}] ++ Terms,
- St};
-
-collect_object({_, Event}, {_, NestCount, [{key, Key}, Last|Terms], St}) ->
- {collect_object, NestCount, [[{Key, Event}] ++ Last] ++ Terms, St};
-collect_object({_, Event}, {_, NestCount, [Last|Terms], St}) ->
- {collect_object, NestCount, [[Event] ++ Last] ++ Terms, St}.
-
-send_change({Props}=Change, #state{owner=Owner, ref=Ref}=St) ->
- Seq = couchbeam_util:get_value(<<"seq">>, Props),
- put(last_seq, Seq),
- Owner ! {Ref, {change, Change}},
- maybe_continue_decoding(St).
-
-
-%% eventually wait for the next call from the parent
-maybe_continue_decoding(#state{parent=Parent,
- owner=Owner,
- ref=Ref,
- mref=MRef,
- client_ref=ClientRef,
- async=once}=St) ->
- receive
- {'DOWN', MRef, _, _, _} ->
- %% parent exited there is no need to continue
- exit(normal);
- {Ref, stream_next} ->
- {wait_results1, 0, [[]], St};
- {Ref, cancel} ->
- hackney:close(ClientRef),
- %% unregister the stream
- ets:delete(couchbeam_changes_streams, Ref),
- %% tell the parent we exited
- Owner ! {Ref, ok},
- %% and exit
- exit(normal);
- {system, From, Request} ->
- sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
- {maybe_continue_decoding, St});
- Else ->
- error_logger:error_msg("Unexpected message: ~w~n", [Else]),
- %% unregister the stream
- ets:delete(couchbeam_changes_streams, Ref),
- %% report the error
- report_error(Else, Ref, Owner),
- exit(Else)
- after 5000 ->
- erlang:hibernate(?MODULE, maybe_continue_decoding, [St])
- end;
-
-maybe_continue_decoding(#state{parent=Parent,
- owner=Owner,
- ref=Ref,
- mref=MRef,
- client_ref=ClientRef}=St) ->
- receive
- {'DOWN', MRef, _, _, _} ->
- %% parent exited there is no need to continue
- exit(normal);
- {Ref, cancel} ->
- hackney:close(ClientRef),
- Owner ! {Ref, ok},
- exit(normal);
- {Ref, pause} ->
- erlang:hibernate(?MODULE, maybe_continue_decoding, [St]);
- {Ref, resume} ->
- {wait_results1, 0, [[]], St};
- {system, From, Request} ->
- sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
- {maybe_continue_decoding, St});
- Else ->
- error_logger:error_msg("Unexpected message: ~w~n", [Else]),
- report_error(Else, Ref, Owner),
- exit(Else)
- after 0 ->
- {wait_results1, 0, [[]], St}
- end.
+%%% removed legacy jsx-driven decoder code
-%% @private
+%% internal
%% parse options to get feed type when it's not passed in a tuple
%% to support the old api.
@@ -572,11 +403,27 @@ maybe_close(#state{client_ref=nil}) ->
maybe_close(#state{client_ref=Ref}) ->
hackney:close(Ref).
-%post_decode([{}]) ->
-% {[]};
-%post_decode([{_Key, _Value} | _Rest] = PropList) ->
-% {[ {Key, post_decode(Value)} || {Key, Value} <- PropList ]};
-%post_decode(List) when is_list(List) ->
-% [ post_decode(Term) || Term <- List];
-%post_decode(Term) ->
-% Term.
+%% legacy jsx-driven post_decode removed; maps are used natively
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+stream_changes_longpoll_test() ->
+ {ok, _} = application:ensure_all_started(couchbeam),
+ Server = couchbeam:server_connection(),
+ {ok, Db} = couchbeam:create_db(Server, <<"couchbeam_changes_stream_test">>),
+ %% start longpoll stream
+ {ok, Ref} = couchbeam_changes:follow(Db, [longpoll, {since, now}]),
+ %% create a change
+ {ok, _} = couchbeam:save_doc(Db, #{<<"_id">> => <<"sc1">>}),
+ %% receive change and validate map structure
+ receive
+ {Ref, {change, Change}} ->
+ ?assertMatch(#{}, Change),
+ ?assertEqual(<<"sc1">>, maps:get(<<"id">>, Change)),
+ ok
+ after 5000 ->
+ ?assert(false)
+ end.
+
+-endif.
diff --git a/src/couchbeam_doc.erl b/src/couchbeam_doc.erl
index 50f3138c..a1d6e49b 100644
--- a/src/couchbeam_doc.erl
+++ b/src/couchbeam_doc.erl
@@ -12,24 +12,24 @@
delete_value/2, extend/2, extend/3]).
-export([get_id/1, get_rev/1, get_idrev/1, is_saved/1]).
-%% @spec get_id(Doc::json_obj()) -> binary()
+-spec get_id(doc()) -> binary().
%% @doc get document id.
get_id(Doc) ->
get_value(<<"_id">>, Doc).
-%% @spec get_rev(Doc::json_obj()) -> binary()
+-spec get_rev(doc()) -> binary().
%% @doc get document revision.
get_rev(Doc) ->
get_value(<<"_rev">>, Doc).
-%% @spec get_idrev(Doc::json_obj()) -> {DocId, DocRev}
+-spec get_idrev(doc()) -> {binary(), binary()}.
%% @doc get a tuple containing docucment id and revision.
get_idrev(Doc) ->
DocId = get_value(<<"_id">>, Doc),
DocRev = get_value(<<"_rev">>, Doc),
{DocId, DocRev}.
-%% @spec is_saved(Doc::json_obj()) -> boolean()
+-spec is_saved(doc()) -> boolean().
%% @doc If document have been saved (revision is defined) return true,
%% else, return false.
is_saved(Doc) ->
@@ -38,36 +38,30 @@ is_saved(Doc) ->
_ -> true
end.
-%% @spec set_value(Key::key_val(), Value::term(), JsonObj::json_obj()) -> term()
+-spec set_value(binary() | list(), term(), doc()) -> doc().
%% @doc set a value for a key in jsonobj. If key exists it will be updated.
set_value(Key, Value, JsonObj) when is_list(Key)->
set_value(list_to_binary(Key), Value, JsonObj);
-set_value(Key, Value, JsonObj) when is_binary(Key) ->
- {Props} = JsonObj,
- case proplists:is_defined(Key, Props) of
- true -> set_value1(Props, Key, Value, []);
- false-> {lists:reverse([{Key, Value}|lists:reverse(Props)])}
- end.
+set_value(Key, Value, JsonObj) when is_binary(Key), is_map(JsonObj) ->
+ maps:put(Key, Value, JsonObj).
-%% @spec get_value(Key::key_val(), JsonObj::json_obj()) -> term()
-%% @type key_val() = lis() | binary()
+-spec get_value(binary() | list(), doc()) -> term().
%% @doc Returns the value of a simple key/value property in json object
%% Equivalent to get_value(Key, JsonObj, undefined).
get_value(Key, JsonObj) ->
get_value(Key, JsonObj, undefined).
-%% @spec get_value(Key::lis() | binary(), JsonObj::json_obj(), Default::term()) -> term()
+-spec get_value(binary() | list(), doc(), term()) -> term().
%% @doc Returns the value of a simple key/value property in json object
%% function from erlang_couchdb
get_value(Key, JsonObj, Default) when is_list(Key) ->
get_value(list_to_binary(Key), JsonObj, Default);
-get_value(Key, JsonObj, Default) when is_binary(Key) ->
- {Props} = JsonObj,
- couchbeam_util:get_value(Key, Props, Default).
+get_value(Key, JsonObj, Default) when is_binary(Key), is_map(JsonObj) ->
+ maps:get(Key, JsonObj, Default).
-%% @spec take_value(Key::key_val(), JsonObj::json_obj()) -> {term(), json_obj()}
+-spec take_value(binary() | list(), doc()) -> {term(), doc()}.
%% @doc Returns the value of a simple key/value property in json object and deletes
%% it form json object
%% Equivalent to take_value(Key, JsonObj, undefined).
@@ -75,37 +69,32 @@ take_value(Key, JsonObj) ->
take_value(Key, JsonObj, undefined).
-%% @spec take_value(Key::key_val() | binary(), JsonObj::json_obj(),
-%% Default::term()) -> {term(), json_obj()}
+-spec take_value(binary() | list(), doc(), term()) -> {term(), doc()}.
%% @doc Returns the value of a simple key/value property in json object and deletes
%% it from json object
take_value(Key, JsonObj, Default) when is_list(Key) ->
- get_value(list_to_binary(Key), JsonObj, Default);
-take_value(Key, JsonObj, Default) when is_binary(Key) ->
- {Props} = JsonObj,
- case lists:keytake(Key, 1, Props) of
- {value, {Key, Value}, Rest} ->
- {Value, {Rest}};
+ take_value(list_to_binary(Key), JsonObj, Default);
+take_value(Key, JsonObj, Default) when is_binary(Key), is_map(JsonObj) ->
+ case maps:is_key(Key, JsonObj) of
+ true ->
+ {maps:get(Key, JsonObj), maps:remove(Key, JsonObj)};
false ->
{Default, JsonObj}
end.
-%% @spec delete_value(Key::key_val(), JsonObj::json_obj()) -> json_obj()
+-spec delete_value(binary() | list(), doc()) -> doc().
%% @doc Deletes all entries associated with Key in json object.
delete_value(Key, JsonObj) when is_list(Key) ->
delete_value(list_to_binary(Key), JsonObj);
-delete_value(Key, JsonObj) when is_binary(Key) ->
- {Props} = JsonObj,
- Props1 = proplists:delete(Key, Props),
- {Props1}.
+delete_value(Key, JsonObj) when is_binary(Key), is_map(JsonObj) ->
+ maps:remove(Key, JsonObj).
-%% @spec extend(Key::binary(), Value::json_term(), JsonObj::json_obj()) -> json_obj()
+-spec extend(binary(), ejson_term(), doc()) -> doc().
%% @doc extend a jsonobject by key, value
extend(Key, Value, JsonObj) ->
extend({Key, Value}, JsonObj).
-%% @spec extend(Prop::property(), JsonObj::json_obj()) -> json_obj()
-%% @type property() = json_obj() | tuple()
+-spec extend(term(), doc()) -> doc().
%% @doc extend a jsonobject by a property, list of property or another jsonobject
extend([], JsonObj) ->
JsonObj;
@@ -117,17 +106,7 @@ extend([Prop|R], JsonObj)->
extend({Key, Value}, JsonObj) ->
set_value(Key, Value, JsonObj).
-%% @private
-set_value1([], _Key, _Value, Acc) ->
- {lists:reverse(Acc)};
-set_value1([{K, V}|T], Key, Value, Acc) ->
- Acc1 = if
- K =:= Key ->
- [{Key, Value}|Acc];
- true ->
- [{K, V}|Acc]
- end,
- set_value1(T, Key, Value, Acc1).
+%% maps only in new API
-ifdef(TEST).
@@ -135,43 +114,43 @@ set_value1([{K, V}|T], Key, Value, Acc) ->
-include_lib("eunit/include/eunit.hrl").
get_value_test() ->
- Doc = {[{<<"a">>, 1}]},
+ Doc = #{<<"a">> => 1},
?assertEqual(1, couchbeam_doc:get_value(<<"a">>, Doc)),
- ?assertEqual(1, couchbeam_doc:get_value("a", Doc)),
- ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc)),
- ?assertEqual(nil, couchbeam_doc:get_value("b", Doc, nil)),
+ ?assertEqual(1, couchbeam_doc:get_value(<<"a">>, Doc)),
+ ?assertEqual(undefined, couchbeam_doc:get_value(<<"b">>, Doc)),
+ ?assertEqual(nil, couchbeam_doc:get_value(<<"b">>, Doc, nil)),
ok.
set_value_test() ->
- Doc = {[{<<"a">>, 1}]},
- ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc)),
- Doc1 = couchbeam_doc:set_value("b", 1, Doc),
- ?assertEqual(1, couchbeam_doc:get_value("b", Doc1)),
- Doc2 = couchbeam_doc:set_value("b", 0, Doc1),
- ?assertEqual(0, couchbeam_doc:get_value("b", Doc2)),
+ Doc = #{<<"a">> => 1},
+ ?assertEqual(undefined, couchbeam_doc:get_value(<<"b">>, Doc)),
+ Doc1 = couchbeam_doc:set_value(<<"b">>, 1, Doc),
+ ?assertEqual(1, couchbeam_doc:get_value(<<"b">>, Doc1)),
+ Doc2 = couchbeam_doc:set_value(<<"b">>, 0, Doc1),
+ ?assertEqual(0, couchbeam_doc:get_value(<<"b">>, Doc2)),
ok.
delete_value_test() ->
- Doc = {[{<<"a">>, 1}, {<<"b">>, 1}]},
+ Doc = #{<<"a">> => 1, <<"b">> => 1},
Doc1 = couchbeam_doc:delete_value("b", Doc),
- ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc1)),
+ ?assertEqual(undefined, couchbeam_doc:get_value(<<"b">>, Doc1)),
ok.
extend_test() ->
- Doc = {[{<<"a">>, 1}]},
- ?assertEqual(1, couchbeam_doc:get_value("a", Doc)),
- ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc)),
- ?assertEqual(undefined, couchbeam_doc:get_value("c", Doc)),
+ Doc = #{<<"a">> => 1},
+ ?assertEqual(1, couchbeam_doc:get_value(<<"a">>, Doc)),
+ ?assertEqual(undefined, couchbeam_doc:get_value(<<"b">>, Doc)),
+ ?assertEqual(undefined, couchbeam_doc:get_value(<<"c">>, Doc)),
Doc1 = couchbeam_doc:extend([{<<"b">>, 1}, {<<"c">>, 1}], Doc),
- ?assertEqual(1, couchbeam_doc:get_value("b", Doc1)),
- ?assertEqual(1, couchbeam_doc:get_value("c", Doc1)),
+ ?assertEqual(1, couchbeam_doc:get_value(<<"b">>, Doc1)),
+ ?assertEqual(1, couchbeam_doc:get_value(<<"c">>, Doc1)),
Doc2 = couchbeam_doc:extend([{<<"b">>, 3}, {<<"d">>, 1}], Doc1),
- ?assertEqual(3, couchbeam_doc:get_value("b", Doc2)),
- ?assertEqual(1, couchbeam_doc:get_value("d", Doc2)),
+ ?assertEqual(3, couchbeam_doc:get_value(<<"b">>, Doc2)),
+ ?assertEqual(1, couchbeam_doc:get_value(<<"d">>, Doc2)),
ok.
id_rev_test() ->
- Doc = {[{<<"a">>, 1}]},
+ Doc = #{<<"a">> => 1},
?assertEqual(undefined, couchbeam_doc:get_id(Doc)),
?assertEqual(undefined, couchbeam_doc:get_rev(Doc)),
?assertEqual({undefined, undefined}, couchbeam_doc:get_idrev(Doc)),
@@ -182,7 +161,7 @@ id_rev_test() ->
ok.
is_saved_test() ->
- Doc = {[{<<"a">>, 1}]},
+ Doc = #{<<"a">> => 1},
?assertEqual(false, couchbeam_doc:is_saved(Doc)),
Doc1 = couchbeam_doc:set_value(<<"_rev">>, <<"x">>, Doc),
?assertEqual(true, couchbeam_doc:is_saved(Doc1)),
@@ -190,11 +169,9 @@ is_saved_test() ->
take_value_test() ->
- Doc = {[{<<"a">>, 1}, {<<"b">>, 2}]},
+ Doc = #{<<"a">> => 1, <<"b">> => 2},
?assertEqual({undefined, Doc}, couchbeam_doc:take_value(<<"c">>, Doc)),
- ?assertEqual({1, {[{<<"b">>, 2}]}}, couchbeam_doc:take_value(<<"a">>, Doc)),
+ ?assertEqual({1, #{<<"b">> => 2}}, couchbeam_doc:take_value(<<"a">>, Doc)),
ok.
-endif.
-
-
diff --git a/src/couchbeam_ejson.erl b/src/couchbeam_ejson.erl
index 49474b7e..a384e970 100644
--- a/src/couchbeam_ejson.erl
+++ b/src/couchbeam_ejson.erl
@@ -12,14 +12,8 @@
-include("couchbeam.hrl").
--ifndef('WITH_JIFFY').
--define(JSON_ENCODE(D), jsx:encode(pre_encode(D))).
--define(JSON_DECODE(D), post_decode(jsx:decode(D, [{return_maps, false}]))).
-
--else.
--define(JSON_ENCODE(D), jiffy:encode(D, [uescape])).
--define(JSON_DECODE(D), jiffy:decode(D)).
--endif.
+%% JSON handling uses Erlang/OTP stdlib json with maps.
+%% JSON objects are represented as maps; arrays as lists.
-spec encode(ejson()) -> binary().
@@ -27,49 +21,22 @@
%% @doc encode an erlang term to JSON. Throw an exception if there is
%% any error.
encode(D) ->
- ?JSON_ENCODE(D).
+ %% json:encode returns iodata(); convert to binary
+ iolist_to_binary(json:encode(D)).
-spec decode(binary()) -> ejson().
%% @doc decode a binary to an EJSON term. Throw an exception if there is
%% any error.
-decode(D) ->
+decode(D) when is_binary(D) ->
try
- ?JSON_DECODE(D)
+ json:decode(D)
catch
- throw:Error ->
- throw({invalid_json, Error});
- error:badarg ->
- throw({invalid_json, badarg})
- end.
-
-pre_encode({[]}) ->
- [{}];
-pre_encode({PropList}) ->
- pre_encode(PropList);
-pre_encode([{_, _}|_] = PropList) ->
- [ {Key, pre_encode(Value)} || {Key, Value} <- PropList ];
-pre_encode(List) when is_list(List) ->
- [ pre_encode(Term) || Term <- List ];
-pre_encode(true) ->
- true;
-pre_encode(false) ->
- false;
-pre_encode(null) ->
- null;
-pre_encode(Atom) when is_atom(Atom) ->
- erlang:atom_to_binary(Atom, utf8);
-pre_encode(Term) when is_integer(Term); is_float(Term); is_binary(Term) ->
- Term.
-
-post_decode({[{}]}) ->
- {[]};
-post_decode([{}]) ->
- {[]};
-post_decode([{_Key, _Value} | _Rest] = PropList) ->
- {[ {Key, post_decode(Value)} || {Key, Value} <- PropList ]};
-post_decode(List) when is_list(List) ->
- [ post_decode(Term) || Term <- List];
-post_decode({Term}) ->
- post_decode(Term);
+ error:Reason ->
+ throw({invalid_json, Reason})
+ end;
+decode(D) ->
+ decode(iolist_to_binary(D)).
+%% post_decode was previously used to convert jsx proplists / old tuple-wrapped object forms
+%% to ejson. Since objects are now maps, it is identity.
post_decode(Term) ->
Term.
diff --git a/src/couchbeam_httpc.erl b/src/couchbeam_httpc.erl
index 31ed6168..5345540a 100644
--- a/src/couchbeam_httpc.erl
+++ b/src/couchbeam_httpc.erl
@@ -121,14 +121,16 @@ db_resp_body(Ref) ->
<<>>
end.
+-spec server_url(server()) -> binary().
%% @doc Asemble the server URL for the given client
-%% @spec server_url({Host, Port}) -> iolist()
server_url(#server{url=Url}) ->
Url.
+-spec db_url(db()) -> binary().
db_url(#db{name=DbName}) ->
DbName.
+-spec doc_url(db(), binary()) -> binary().
doc_url(Db, DocId) ->
iolist_to_binary([db_url(Db), <<"/">>, DocId]).
@@ -148,8 +150,8 @@ reply_att({ok, 409, _, Ref}) ->
{error, conflict};
reply_att({ok, Status, _, Ref}) when Status =:= 200 orelse Status =:= 201 ->
case couchbeam_httpc:json_body(Ref) of
- {[{<<"ok">>, true}|R]} ->
- {ok, {R}};
+ #{<<"ok">> := true} = Map ->
+ {ok, maps:remove(<<"ok">>, Map)};
{error, _} = Error ->
Error
end;
@@ -173,18 +175,18 @@ wait_mp_doc(Ref, Buffer) ->
wait_mp_doc(Ref, Buffer);
end_of_part ->
%% decode the doc
- {Props} = Doc = couchbeam_ejson:decode(Buffer),
- case couchbeam_util:get_value(<<"_attachments">>, Props, {[]}) of
- {[]} ->
+ Doc = couchbeam_ejson:decode(Buffer),
+ case maps:get(<<"_attachments">>, Doc, #{}) of
+ Atts when map_size(Atts) =:= 0 ->
%% not attachments wait for the eof or the next doc
NState = {Ref, fun() -> wait_mp_doc(Ref, <<>>) end},
{doc, Doc, NState};
- {Atts} ->
+ Atts ->
%% start to receive the attachments
%% we get the list of attnames for the versions of
%% couchdb that don't provide the att name in the
%% header.
- AttNames = [AttName || {AttName, _} <- Atts],
+ AttNames = maps:keys(Atts),
NState = {Ref, fun() ->
wait_mp_att(Ref, {nil, AttNames})
end},
@@ -254,7 +256,7 @@ content_disposition(Data) ->
end).
%% @hidden
-len_doc_to_mp_stream(Atts, Boundary, {Props}) ->
+len_doc_to_mp_stream(Atts, Boundary, Props) ->
{AttsSize, Stubs} = lists:foldl(fun(Att, {AccSize, AccAtts}) ->
{AttLen, Name, Type, Encoding, _Msg} = att_info(Att),
AccSize1 = AccSize +
@@ -275,33 +277,26 @@ len_doc_to_mp_stream(Atts, Boundary, {Props}) ->
byte_size(Encoding) +
byte_size(<<"\r\nContent-Encoding: ">>)
end,
- AccAtts1 = [{Name, {[{<<"content_type">>, Type},
- {<<"length">>, AttLen},
- {<<"follows">>, true},
- {<<"encoding">>, Encoding}]}}
- | AccAtts],
+ AccAtts1 = maps:put(Name,
+ #{<<"content_type">> => Type,
+ <<"length">> => AttLen,
+ <<"follows">> => true,
+ <<"encoding">> => Encoding},
+ AccAtts),
{AccSize1, AccAtts1}
- end, {0, []}, Atts),
+ end, {0, #{}}, Atts),
- Doc1 = case couchbeam_util:get_value(<<"_attachments">>, Props) of
+ Doc1 = case maps:get(<<"_attachments">>, Props, undefined) of
undefined ->
- {Props ++ [{<<"_attachments">>, {Stubs}}]};
- {OldAtts} ->
+ maps:put(<<"_attachments">>, Stubs, Props);
+ OldAtts when is_map(OldAtts) ->
%% remove updated attachments from the old list of
%% attachments
- OldAtts1 = lists:foldl(fun({Name, AttProps}, Acc) ->
- case couchbeam_util:get_value(Name, Stubs) of
- undefined ->
- [{Name, AttProps} | Acc];
- _ ->
- Acc
- end
- end, [], OldAtts),
- %% update the list of the attachnebts with the attachments
- %% that will be sent in the multipart
- FinalAtts = lists:reverse(OldAtts1) ++ Stubs,
- {lists:keyreplace(<<"_attachments">>, 1, Props,
- {<<"_attachments">>, {FinalAtts}})}
+ Keep = maps:filter(
+ fun(Name, _V) -> not maps:is_key(Name, Stubs) end,
+ OldAtts),
+ FinalAtts = maps:merge(Keep, Stubs),
+ maps:put(<<"_attachments">>, FinalAtts, Props)
end,
%% eencode the doc
@@ -382,9 +377,9 @@ mp_doc_reply(Ref, Doc) ->
Resp = hackney:start_response(Ref),
case couchbeam_httpc:db_resp(Resp, [200, 201]) of
{ok, _, _, Ref} ->
- {JsonProp} = couchbeam_httpc:json_body(Ref),
- NewRev = couchbeam_util:get_value(<<"rev">>, JsonProp),
- NewDocId = couchbeam_util:get_value(<<"id">>, JsonProp),
+ JsonProp = couchbeam_httpc:json_body(Ref),
+ NewRev = maps:get(<<"rev">>, JsonProp),
+ NewDocId = maps:get(<<"id">>, JsonProp),
%% set the new doc ID
Doc1 = couchbeam_doc:set_value(<<"_id">>, NewDocId, Doc),
%% set the new rev
diff --git a/src/couchbeam_util.erl b/src/couchbeam_util.erl
index 8ef07e54..e7e86d2a 100644
--- a/src/couchbeam_util.erl
+++ b/src/couchbeam_util.erl
@@ -62,7 +62,7 @@ encode_docid1(DocId) ->
encode_docid_noop(DocId) ->
DocId.
-%% @doc Encode needed value of Query proplists in json
+%% @doc Encode needed query parameter values for JSON
encode_query([]) ->
[];
encode_query(QSL) when is_list(QSL) ->
@@ -118,19 +118,19 @@ oauth_header(Url, Action, OauthProps) ->
{<<"Authorization">>, list_to_binary(Realm)}.
-%% @doc merge 2 proplists. All the Key - Value pairs from both proplists
-%% are included in the new proplists. If a key occurs in both dictionaries
+%% @doc Merge two property lists (lists of {Key,Value}). All the Key - Value
+%% pairs from both lists are included in the new list. If a key occurs in both dictionaries
%% then Fun is called with the key and both values to return a new
%% value. This a wreapper around dict:merge
propmerge(F, L1, L2) ->
dict:to_list(dict:merge(F, dict:from_list(L1), dict:from_list(L2))).
-%% @doc Update a proplist with values of the second. In case the same
-%% key is in 2 proplists, the value from the first are kept.
+%% @doc Update a property list with values of the second. In case the same
+%% key is in both lists, the value from the first is kept.
propmerge1(L1, L2) ->
propmerge(fun(_, V1, _) -> V1 end, L1, L2).
-%% @doc replace a value in a proplist
+%% @doc Replace a value in a property list
force_param(Key, Value, Options) ->
case couchbeam_util:get_value(Key, Options) of
undefined ->
@@ -139,7 +139,7 @@ force_param(Key, Value, Options) ->
lists:keystore(Key, 1, Options, {Key, Value})
end.
-%% @doc emulate proplists:get_value/2,3 but use faster lists:keyfind/3
+%% @doc Emulate proplists:get_value/2,3 for property lists but use faster lists:keyfind/3
-spec get_value(Key :: term(), Prop :: [term()]) -> term().
get_value(Key, Prop) ->
get_value(Key, Prop, undefined).
@@ -243,14 +243,14 @@ shutdown_sync(Pid) ->
erlang:demonitor(MRef, [flush])
end.
-%% @spec start_app_deps(App :: atom()) -> ok
+-spec start_app_deps(atom()) -> ok.
%% @doc Start depedent applications of App.
start_app_deps(App) ->
{ok, DepApps} = application:get_key(App, applications),
[ensure_started(A) || A <- DepApps],
ok.
-%% @spec ensure_started(Application :: atom()) -> ok
+-spec ensure_started(atom()) -> ok.
%% @doc Start the named application if not already started.
ensure_started(App) ->
case application:start(App) of
diff --git a/src/couchbeam_uuids.erl b/src/couchbeam_uuids.erl
index 91f74cd8..8264a4f6 100644
--- a/src/couchbeam_uuids.erl
+++ b/src/couchbeam_uuids.erl
@@ -34,8 +34,8 @@ random() ->
utc_random() ->
utc_suffix(hackney_bstr:to_hex(crypto:strong_rand_bytes(9))).
+-spec get_uuids(server(), integer()) -> list().
%% @doc Get a list of uuids from the server
-%% @spec get_uuids(server(), integer()) -> lists()
get_uuids(Server, Count) ->
gen_server:call(?MODULE, {get_uuids, Server, Count}, infinity).
@@ -43,16 +43,16 @@ get_uuids(Server, Count) ->
%% Function: start_link/0
%% Description: Starts the server
%%--------------------------------------------------------------------
+-spec start_link() -> {ok, pid()} | {error, term()}.
%% @doc Starts the couchbeam process linked to the calling process. Usually
%% invoked by the supervisor couchbeam_sup
-%% @spec start_link() -> {ok, pid()}
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%%---------------------------------------------------------------------------
%% gen_server callbacks
%%---------------------------------------------------------------------------
-%% @private
+%% internal
init(_) ->
process_flag(trap_exit, true),
@@ -123,7 +123,7 @@ get_new_uuids(#server{url=ServerUrl, options=Opts}=Server, Backoff, Acc) ->
case couchbeam_httpc:request(get, Url, [], <<>>, Opts) of
{ok, 200, _, Ref} ->
{ok, Body} = hackney:body(Ref),
- {[{<<"uuids">>, Uuids}]} = couchbeam_ejson:decode(Body),
+ #{<<"uuids">> := Uuids} = couchbeam_ejson:decode(Body),
ServerUuids = #server_uuids{server_url=ServerUrl,
uuids=(Acc ++ Uuids)},
ets:insert(couchbeam_uuids, ServerUuids),
diff --git a/src/couchbeam_view.erl b/src/couchbeam_view.erl
index 90b33635..19333cca 100644
--- a/src/couchbeam_view.erl
+++ b/src/couchbeam_view.erl
@@ -75,7 +75,7 @@ fetch(Db, ViewName) ->
%% | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
%% | {keys, list(binary())}
%% | async_query
-%% See {@link couchbeam_view:stream/4} for more information about
+%%
See {@link couchbeam_view:stream/3} for more information about
%% options.
%% Return: {ok, Rows} or {error, Error}
fetch(Db, ViewName, Options) ->
@@ -100,8 +100,8 @@ fetch_sync_fun(Db) ->
fun(Args, Url) ->
case view_request(Db, Url, Args) of
{ok, _, _, Ref} ->
- {Props} = couchbeam_httpc:json_body(Ref),
- {ok, couchbeam_util:get_value(<<"rows">>, Props)};
+ Props = couchbeam_httpc:json_body(Ref),
+ {ok, maps:get(<<"rows">>, Props)};
Error ->
Error
end
@@ -119,7 +119,7 @@ show(Db, ShowName) ->
show(Db, ShowName, DocId) ->
show(Db, ShowName, DocId, []).
--type show_option() :: {'query_string', binary()}. % "foo=bar&baz=biz"
+-type show_option() :: {'query_string', binary()}. % example: "foo=bar&baz=biz"
-type show_options() :: [show_option()].
-spec show(db(), {binary(), binary()}, 'null' | binary(), show_options()) ->
@@ -163,7 +163,7 @@ show_doc_id(<>) -> [<<"/">>, couchbeam_util:encode_docid(DocId)].
-spec stream(Db::db(), ViewName::'all_docs' | {DesignName::design_name(),
ViewName::view_name()}) -> {ok, StartRef::term(),
ViewPid::pid()} | {error, term()}.
-%% @equiv stream(Db, ViewName, Client, [])
+%% @equiv stream(Db, ViewName, [])
stream(Db, ViewName) ->
stream(Db, ViewName, []).
@@ -171,63 +171,32 @@ stream(Db, ViewName) ->
ViewName::view_name()}, Options::view_options())
-> {ok, StartRef::term()} | {error, term()}.
%% @doc stream view results to a pid
-%% Db: a db record
-%% ViewName: 'all_docs' to get all docs or {DesignName,
-%% ViewName}
-%% Client: pid where to send view events where events are:
-%%
-%% - {row, StartRef, done}
-%% - All view results have been fetched
-%% - {row, StartRef, Row :: ejson_object()}
-%% - A row in the view
-%% - {error, StartRef, Error}
-%% happend.
-%%
-%% Options :: view_options() [{key, binary()}
-%% | descending
-%% | {skip, integer()}
-%% | group | {group_level, integer()}
-%% | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
-%% | {keys, list(binary())}
-%% | `{stream_to, Pid}': the pid where the changes will be sent,
-%% by default the current pid. Used for continuous and longpoll
-%% connections
-%%
-%%
-%% {key, Key}: key value
-%% {start_docid, DocId} | {startkey_docid, DocId}: document id to start with (to allow pagination
-%% for duplicate start keys
-%% {end_docid, DocId} | {endkey_docid, DocId}: last document id to include in the result (to
-%% allow pagination for duplicate endkeys)
-%% {start_key, Key}: start result from key value
-%% {end_key, Key}: end result from key value
-%% {limit, Limit}: Limit the number of documents in the result
-%% {stale, Stale}: If stale=ok is set, CouchDB will not refresh the view
-%% even if it is stale, the benefit is a an improved query latency. If
-%% stale=update_after is set, CouchDB will update the view after the stale
-%% result is returned. If stale=false is set, CouchDB will update the view before
-%% the query. The default value of this parameter is update_after.
-%% descending: reverse the result
-%% {skip, N}: skip n number of documents
-%% group: the reduce function reduces to a single result
-%% row.
-%% {group_level, Level}: the reduce function reduces to a set
-%% of distinct keys.
-%% {reduce, boolean()}: whether to use the reduce function of the view. It defaults to
-%% true, if a reduce function is defined and to false otherwise.
-%% include_docs: automatically fetch and include the document
-%% which emitted each view entry
-%% {inclusive_end, boolean()}: Controls whether the endkey is included in
-%% the result. It defaults to true.
-%% conflicts: include conflicts
-%% {keys, [Keys]}: to pass multiple keys to the view query
-%%
+%% Db: a db record
+%% ViewName: 'all_docs' to get all docs or {DesignName, ViewName}
+%% Client receives messages:
+%% - {row, StartRef, done} All rows have been fetched
+%% - {row, StartRef, Row :: ejson_object()} A row in the view
+%% - {error, StartRef, Error} An error occurred; stream closed
+%% Options include (see couchbeam_view:parse_view_options/1):
+%% - {key, Key}
+%% - {start_docid, DocId} | {startkey_docid, DocId}
+%% - {end_docid, DocId} | {endkey_docid, DocId}
+%% - {start_key, Key} | {end_key, Key}
+%% - {limit, N}
+%% - {stale, ok | update_after | false}
+%% - descending | {skip, N}
+%% - group | {group_level, integer()}
+%% - reduce | {reduce, boolean()}
+%% - include_docs | conflicts | {inclusive_end, boolean()}
+%% - {keys, [Key]}
+%% - {stream_to, Pid}
+
+
+
%%
-%% Return {ok, StartRef, ViewPid} or {error,
- %Error}. Ref can be
-%% used to disctint all changes from this pid. ViewPid is the pid of
-%% the view loop process. Can be used to monitor it or kill it
-%% when needed.
+%% Return: {ok, StartRef, ViewPid} or {error, Reason}.
+%% Ref can be used to distinguish streams from this pid.
+%% ViewPid is the pid of the view loop process.
stream(Db, ViewName, Options) ->
{To, Options1} = case proplists:get_value(stream_to, Options) of
undefined ->
@@ -297,8 +266,8 @@ count(Db, ViewName, Options)->
make_view(Db, ViewName, Options1, fun(Args, Url) ->
case view_request(Db, Url, Args) of
{ok, _, _, Ref} ->
- {Props} = couchbeam_httpc:json_body(Ref),
- couchbeam_util:get_value(<<"total_rows">>, Props);
+ Props = couchbeam_httpc:json_body(Ref),
+ maps:get(<<"total_rows">>, Props);
Error ->
Error
end
@@ -334,7 +303,7 @@ first(Db, ViewName) ->
%% | group | {group_level, integer()}
%% | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
%% | {keys, list(binary())}
-%% See {@link couchbeam_view:stream/4} for more information about
+%%
See {@link couchbeam_view:stream/3} for more information about
%% options.
%% Return: {ok, Row} or {error, Error}
first(Db, ViewName, Options) ->
@@ -346,8 +315,8 @@ first(Db, ViewName, Options) ->
make_view(Db, ViewName, Options1, fun(Args, Url) ->
case view_request(Db, Url, Args) of
{ok, _, _, Ref} ->
- {Props} = couchbeam_httpc:json_body(Ref),
- case couchbeam_util:get_value(<<"rows">>, Props) of
+ Props = couchbeam_httpc:json_body(Ref),
+ case maps:get(<<"rows">>, Props) of
[] ->
{ok, nil};
[Row] ->
@@ -511,7 +480,7 @@ parse_view_options([{Key, Value}|Rest], #view_query_args{options=Opts}=Args)
parse_view_options([_|Rest], Args) ->
parse_view_options(Rest, Args).
-%% @private
+%% internal
make_view(#db{server=Server}=Db, ViewName, Options, Fun) ->
Args = parse_view_options(Options),
@@ -583,9 +552,7 @@ view_request(#db{options=Opts}, Url, Args) ->
couchbeam_httpc:db_request(get, Url, [], <<>>,
Opts, [200]);
post ->
- Body = couchbeam_ejson:encode(
- {[{<<"keys">>, Args#view_query_args.keys}]}
- ),
+ Body = couchbeam_ejson:encode(#{<<"keys">> => Args#view_query_args.keys}),
Hdrs = [{<<"Content-Type">>, <<"application/json">>}],
couchbeam_httpc:db_request(post, Url, Hdrs, Body,
@@ -629,25 +596,17 @@ basic_test() ->
{ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"),
- DesignDoc = {[
- {<<"_id">>, <<"_design/couchbeam">>},
- {<<"language">>,<<"javascript">>},
- {<<"views">>,
- {[{<<"test">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">>
- }]}
- },{<<"test2">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">>
- }]}
- }]}
- }
- ]},
-
- Doc = {[
- {<<"type">>, <<"test">>}
- ]},
+ Views = #{
+ <<"test">> => #{<<"map">> => <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">>},
+ <<"test2">> => #{<<"map">> => <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">>}
+ },
+ DesignDoc = #{
+ <<"_id">> => <<"_design/couchbeam">>,
+ <<"language">> => <<"javascript">>,
+ <<"views">> => Views
+ },
+
+ Doc = #{<<"type">> => <<"test">>},
couchbeam:save_docs(Db, [DesignDoc, Doc, Doc]),
couchbeam:ensure_full_commit(Db),
@@ -661,15 +620,15 @@ basic_test() ->
Count = couchbeam_view:count(Db, {"couchbeam", "test"}),
?assertEqual(2, Count),
- {ok, {FirstRow}} = couchbeam_view:first(Db, {"couchbeam", "test"}, [include_docs]),
- {Doc1} = proplists:get_value(<<"doc">>, FirstRow),
- ?assertEqual(<<"test">>, proplists:get_value(<<"type">>, Doc1)),
+ {ok, FirstRow} = couchbeam_view:first(Db, {"couchbeam", "test"}, [include_docs]),
+ Doc1 = maps:get(<<"doc">>, FirstRow),
+ ?assertEqual(<<"test">>, maps:get(<<"type">>, Doc1)),
Docs = [
- {[{<<"_id">>, <<"test1">>}, {<<"type">>, <<"test">>}, {<<"value">>, 1}]},
- {[{<<"_id">>, <<"test2">>}, {<<"type">>, <<"test">>}, {<<"value">>, 2}]},
- {[{<<"_id">>, <<"test3">>}, {<<"type">>, <<"test">>}, {<<"value">>, 3}]},
- {[{<<"_id">>, <<"test4">>}, {<<"type">>, <<"test">>}, {<<"value">>, 4}]}
+ #{<<"_id">> => <<"test1">>, <<"type">> => <<"test">>, <<"value">> => 1},
+ #{<<"_id">> => <<"test2">>, <<"type">> => <<"test">>, <<"value">> => 2},
+ #{<<"_id">> => <<"test3">>, <<"type">> => <<"test">>, <<"value">> => 3},
+ #{<<"_id">> => <<"test4">>, <<"type">> => <<"test">>, <<"value">> => 4}
],
couchbeam:save_docs(Db, Docs),
@@ -685,6 +644,31 @@ basic_test() ->
Rst5 = couchbeam_view:fold(AccFun, [], Db, {"couchbeam", "test"}, [{start_key, <<"test">>}, {end_key,<<"test3">>}]),
?assertEqual(3, length(Rst5)).
+stream_basic_test() ->
+ start_couchbeam_tests(),
+ Server = couchbeam:server_connection(),
+ {ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"),
+ Docs = [
+ #{<<"_id">> => <<"s1">>},
+ #{<<"_id">> => <<"s2">>},
+ #{<<"_id">> => <<"s3">>}
+ ],
+ couchbeam:save_docs(Db, Docs),
+ couchbeam:ensure_full_commit(Db),
+
+ {ok, Ref} = stream(Db, 'all_docs', []),
+ Collected = collect_stream(Ref, []),
+ %% there may be design docs; ensure at least our 3 are present
+ Ids = [maps:get(<<"id">>, Row) || Row <- Collected],
+ ?assert(lists:all(fun(E) -> lists:member(E, Ids) end, [<<"s1">>,<<"s2">>,<<"s3">>])).
+
+collect_stream(Ref, Acc) ->
+ receive
+ {Ref, {row, Row}} -> collect_stream(Ref, [Row|Acc]);
+ {Ref, done} -> lists:reverse(Acc)
+ after 5000 -> Acc
+ end.
+
view_notfound_test() ->
start_couchbeam_tests(),
Server = couchbeam:server_connection(),
diff --git a/src/couchbeam_view_stream.erl b/src/couchbeam_view_stream.erl
index c853398e..314d0895 100644
--- a/src/couchbeam_view_stream.erl
+++ b/src/couchbeam_view_stream.erl
@@ -14,13 +14,7 @@
system_code_change/4]).
--export([init/1,
- handle_event/2,
- wait_rows/2,
- wait_rows1/2,
- wait_val/2,
- collect_object/2,
- maybe_continue_decoding/1]).
+%% no incremental JSON decoder exports; using buffered decode
-include("couchbeam.hrl").
@@ -31,15 +25,10 @@
ref,
mref,
client_ref=nil,
- decoder,
+ parser,
async=normal}).
--record(viewst, {parent,
- owner,
- ref,
- mref,
- client_ref,
- async=false}).
+%% viewst no longer used
-define(TIMEOUT, 10000).
@@ -95,8 +84,8 @@ do_init_stream({#db{options=Opts}, Url, Args}, #state{mref=MRef}=State) ->
get ->
couchbeam_httpc:request(get, Url, [], <<>>, FinalOpts);
post ->
- Body = couchbeam_ejson:encode({[{<<"keys">>,
- Args#view_query_args.keys}]}),
+ Body = couchbeam_ejson:encode(#{<<"keys">> =>
+ Args#view_query_args.keys}),
Headers = [{<<"Content-Type">>, <<"application/json">>}],
couchbeam_httpc:request(post, Url, Headers, Body, FinalOpts)
end,
@@ -112,16 +101,12 @@ do_init_stream({#db{options=Opts}, Url, Args}, #state{mref=MRef}=State) ->
%% parent exited there is no need to continue
exit(normal);
{hackney_response, Ref, {status, 200, _}} ->
- #state{parent=Parent,
- owner=Owner,
- ref=StreamRef,
- async=Async} = State,
-
- DecoderFun = jsx:decoder(?MODULE, [Parent, Owner,
- StreamRef, MRef, Ref,
- Async], [stream]),
- {ok, State#state{client_ref=Ref,
- decoder=DecoderFun}};
+ #state{parent=_Parent,
+ owner=_Owner,
+ ref=_StreamRef,
+ async=_Async} = State,
+ Parser = json_stream_parse:init(),
+ {ok, State#state{client_ref=Ref, parser=Parser}};
{hackney_response, Ref, {status, 404, _}} ->
{error, not_found};
@@ -151,10 +136,7 @@ loop(#state{owner=Owner,
{hackney_response, ClientRef, {headers, _Headers}} ->
loop(State);
{hackney_response, ClientRef, done} ->
- %% unregister the stream
- ets:delete(couchbeam_view_streams, StreamRef),
- %% tell to the owner that we are done and exit,
- Owner ! {StreamRef, done};
+ handle_done(State);
{hackney_response, ClientRef, Data} when is_binary(Data) ->
decode_data(Data, State);
{hackney_response, ClientRef, Error} ->
@@ -164,29 +146,10 @@ loop(#state{owner=Owner,
exit(Error)
end.
-decode_data(Data, #state{owner=Owner,
- ref=StreamRef,
- client_ref=ClientRef,
- decoder=DecodeFun}=State) ->
- try
- {incomplete, DecodeFun2} = DecodeFun(Data),
- try DecodeFun2(end_stream) of done ->
- %% stop the request
- {ok, _} = hackney:stop_async(ClientRef),
- %% skip the rest of the body so the socket is
- %% replaced in the pool
- catch hackney:skip_body(ClientRef),
- %% unregister the stream
- ets:delete(couchbeam_view_streams, StreamRef),
- %% tell to the owner that we are done and exit,
- Owner ! {StreamRef, done}
- catch error:badarg ->
- maybe_continue(State#state{decoder=DecodeFun2})
- end
- catch error:badarg ->
- maybe_close(State),
- exit(badarg)
- end.
+decode_data(Data, #state{parser=Parser, owner=Owner, ref=Ref}=State) ->
+ {Rows, Parser1} = json_stream_parse:feed(Data, Parser),
+ lists:foreach(fun(Row) -> Owner ! {Ref, {row, Row}} end, Rows),
+ maybe_continue(State#state{parser=Parser1}).
maybe_continue(#state{parent=Parent, owner=Owner, ref=Ref, mref=MRef,
async=once}=State) ->
@@ -275,158 +238,20 @@ system_code_change(Misc, _, _, _) ->
%%% json decoder %%%
-init([Parent, Owner, StreamRef, MRef, ClientRef, Async]) ->
- InitialState = #viewst{parent=Parent,
- owner=Owner,
- ref=StreamRef,
- mref=MRef,
- client_ref=ClientRef,
- async=Async},
- {wait_rows, 0, [[]], InitialState}.
-
-handle_event(end_json, _) ->
- done;
-handle_event(Event, {Fun, _, _, _}=St) ->
- ?MODULE:Fun(Event, St).
-
-
-
-wait_rows(start_object, St) ->
- St;
-wait_rows(end_object, St) ->
- St;
-wait_rows({key, <<"rows">>}, {_, _, _, ViewSt}) ->
- {wait_rows1, 0, [[]], ViewSt};
-wait_rows({key, <<"total_rows">>}, {_, _, _, ViewSt}) ->
- {wait_val, 0, [[]], ViewSt};
-wait_rows({key, <<"offset">>}, {_, _, _, ViewSt}) ->
- {wait_val, 0, [[]], ViewSt}.
-
-wait_val({_, _}, {_, _, _, ViewSt}) ->
- {wait_rows, 0, [[]], ViewSt}.
-
-wait_rows1(start_array, {_, _, _, ViewSt}) ->
- {wait_rows1, 0, [[]], ViewSt};
-wait_rows1(start_object, {_, _, Terms, ViewSt}) ->
- {collect_object, 0, [[]|Terms], ViewSt};
-wait_rows1(end_array, {_, _, _, ViewSt}) ->
- {wait_rows, 0, [[]], ViewSt}.
-
-
-collect_object(start_object, {_, NestCount, Terms, ViewSt}) ->
- {collect_object, NestCount + 1, [[]|Terms], ViewSt};
-
-collect_object(end_object, {_, NestCount, [[], {key, Key}, Last|Terms],
- ViewSt}) ->
- {collect_object, NestCount - 1, [[{Key, {[{}]}}] ++ Last] ++ Terms,
- ViewSt};
-
-collect_object(end_object, {_, NestCount, [Object, {key, Key},
- Last|Terms], ViewSt}) ->
- {collect_object, NestCount - 1,
- [[{Key, {lists:reverse(Object)}}] ++ Last] ++ Terms, ViewSt};
-
-collect_object(end_object, {_, 0, [[], Last|Terms], ViewSt}) ->
- [[Row]] = [[{[{}]}] ++ Last] ++ Terms,
- send_row(Row, ViewSt);
-
-collect_object(end_object, {_, NestCount, [[], Last|Terms], ViewSt}) ->
- {collect_object, NestCount - 1, [[{[{}]}] ++ Last] ++ Terms, ViewSt};
-
-collect_object(end_object, {_, 0, [Object, Last|Terms], ViewSt}) ->
- [[Row]] = [[{lists:reverse(Object)}] ++ Last] ++ Terms,
- send_row(Row, ViewSt);
-
-
-collect_object(end_object, {_, NestCount, [Object, Last|Terms], ViewSt}) ->
- Acc = [[{lists:reverse(Object)}] ++ Last] ++ Terms,
- {collect_object, NestCount - 1, Acc, ViewSt};
-
-
-collect_object(start_array, {_, NestCount, Terms, ViewSt}) ->
- {collect_object, NestCount, [[]|Terms], ViewSt};
-collect_object(end_array, {_, NestCount, [List, {key, Key}, Last|Terms],
- ViewSt}) ->
- {collect_object, NestCount,
- [[{Key, lists:reverse(List)}] ++ Last] ++ Terms, ViewSt};
-collect_object(end_array, {_, NestCount, [List, Last|Terms], ViewSt}) ->
- {collect_object, NestCount, [[lists:reverse(List)] ++ Last] ++ Terms,
- ViewSt};
-
-collect_object({key, Key}, {_, NestCount, Terms, ViewSt}) ->
- {collect_object, NestCount, [{key, Key}] ++ Terms,
- ViewSt};
-
-collect_object({_, Event}, {_, NestCount, [{key, Key}, Last|Terms], ViewSt}) ->
- {collect_object, NestCount, [[{Key, Event}] ++ Last] ++ Terms, ViewSt};
-collect_object({_, Event}, {_, NestCount, [Last|Terms], ViewSt}) ->
- {collect_object, NestCount, [[Event] ++ Last] ++ Terms, ViewSt}.
-
-send_row(Row, #viewst{owner=Owner, ref=Ref}=ViewSt) ->
- Owner ! {Ref, {row, couchbeam_ejson:post_decode(Row)}},
- maybe_continue_decoding(ViewSt).
-
-%% eventually wait for the next call from the parent
-maybe_continue_decoding(#viewst{parent=Parent,
- owner=Owner,
- ref=Ref,
- mref=MRef,
- client_ref=ClientRef,
- async=once}=ViewSt) ->
- receive
- {'DOWN', MRef, _, _, _} ->
- %% parent exited there is no need to continue
- exit(normal);
- {Ref, stream_next} ->
- {wait_rows1, 0, [[]], ViewSt};
- {Ref, cancel} ->
- hackney:close(ClientRef),
- %% unregister the stream
- ets:delete(couchbeam_view_streams, Ref),
- %% tell the parent we exited
- Owner ! {Ref, ok},
- %% and exit
- exit(normal);
- {system, From, Request} ->
- sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
- {maybe_continue_decoding, ViewSt});
- Else ->
- error_logger:error_msg("Unexpected message: ~w~n", [Else]),
- %% unregister the stream
- ets:delete(couchbeam_view_streams, Ref),
- %% report the error
- report_error(Else, Ref, Owner),
- exit(Else)
- after 5000 ->
- erlang:hibernate(?MODULE, maybe_continue_decoding, [ViewSt])
- end;
-
-maybe_continue_decoding(#viewst{parent=Parent,
- owner=Owner,
- ref=Ref,
- mref=MRef,
- client_ref=ClientRef}=ViewSt) ->
- receive
- {'DOWN', MRef, _, _, _} ->
- %% parent exited there is no need to continue
- exit(normal);
- {Ref, cancel} ->
- hackney:close(ClientRef),
- Owner ! {Ref, ok},
- exit(normal);
- {Ref, pause} ->
- erlang:hibernate(?MODULE, maybe_continue_decoding, [ViewSt]);
- {Ref, resume} ->
- {wait_rows1, 0, [[]], ViewSt};
- {system, From, Request} ->
- sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
- {maybe_continue_decoding, ViewSt});
- Else ->
- error_logger:error_msg("Unexpected message: ~w~n", [Else]),
- report_error(Else, Ref, Owner),
- exit(Else)
- after 0 ->
- {wait_rows1, 0, [[]], ViewSt}
+handle_done(#state{owner=Owner, ref=StreamRef, client_ref=ClientRef, parser=Parser}) ->
+ %% stop the request and cleanup
+ catch hackney:stop_async(ClientRef),
+ catch hackney:skip_body(ClientRef),
+ %% decode and send rows
+ try
+ {Rows, _} = json_stream_parse:finish(Parser),
+ lists:foreach(fun(Row) -> Owner ! {StreamRef, {row, Row}} end, Rows),
+ %% unregister the stream
+ ets:delete(couchbeam_view_streams, StreamRef),
+ Owner ! {StreamRef, done}
+ catch _:Reason ->
+ ets:delete(couchbeam_view_streams, StreamRef),
+ report_error(Reason, StreamRef, Owner)
end.
report_error({error, _What}=Error, Ref, Pid) ->
diff --git a/src/gen_changes.erl b/src/gen_changes.erl
index 1b3eab0a..72f0af03 100644
--- a/src/gen_changes.erl
+++ b/src/gen_changes.erl
@@ -50,7 +50,6 @@ cast(Dest, Request) ->
%% @doc create a gen_changes process as part of a supervision tree.
%% The function should be called, directly or indirectly, by the supervisor.
-%% @spec start_link(Module, Db::db(), Options::changesoptions(),
%% InitArgs::list()) -> term()
%% changesoptions() = [changeoption()]
%% changeoption() = {include_docs, string()} |
diff --git a/src/json_stream_parse.erl b/src/json_stream_parse.erl
new file mode 100644
index 00000000..665a2d4e
--- /dev/null
+++ b/src/json_stream_parse.erl
@@ -0,0 +1,137 @@
+%%% -*- erlang -*-
+%% Simple streaming JSON row parser for CouchDB view responses.
+%% Parses the response and emits each row (as a map) incrementally.
+
+-module(json_stream_parse).
+
+-export([init/0, feed/2, finish/1]).
+%% tests
+-ifdef(TEST).
+-export([parse_rows/1]).
+-endif.
+
+-record(st, {
+ phase = find_rows :: find_rows | in_rows | done,
+ buf = <<>> :: binary(),
+ i = 0 :: non_neg_integer(),
+ in_string = false :: boolean(),
+ escape = false :: boolean(),
+ arr_depth = 0 :: non_neg_integer(),
+ obj_depth = 0 :: non_neg_integer(),
+ obj_start = -1 :: integer()
+}).
+
+init() -> #st{}.
+
+%% feed(Data, State) -> {Rows, NewState}
+feed(Data, #st{buf=Buf0}=S0) when is_binary(Data) ->
+ S = S0#st{buf = <>},
+ parse(S, []).
+
+finish(S=#st{phase=done}) -> {[], S};
+finish(S) ->
+ %% Try parse any remaining complete rows
+ {Rows, S1} = parse(S, []),
+ {Rows, S1}.
+
+parse(S=#st{phase=find_rows, buf=Buf, i=I}, Acc) ->
+ case find_rows_start(Buf, I) of
+ not_found -> {lists:reverse(Acc), S#st{i=byte_size(Buf)}};
+ {_, ArrIdx} ->
+ S1 = S#st{phase=in_rows, i=ArrIdx+1, arr_depth=1},
+ parse(S1, Acc)
+ end;
+parse(S=#st{phase=in_rows, buf=Buf, i=I,
+ in_string=Str, escape=Esc,
+ arr_depth=AD, obj_depth=OD, obj_start=OS}, Acc) when I < byte_size(Buf) ->
+ <<_:I/binary, C, _/binary>> = Buf,
+ case Str of
+ true ->
+ case {Esc, C} of
+ {true, _} -> parse(S#st{i=I+1, escape=false}, Acc);
+ {_, $\\} -> parse(S#st{i=I+1, escape=true}, Acc);
+ {_, $\"} -> parse(S#st{i=I+1, in_string=false}, Acc);
+ _ -> parse(S#st{i=I+1}, Acc)
+ end;
+ false ->
+ case C of
+ $\" -> parse(S#st{i=I+1, in_string=true}, Acc);
+ ${ when OD =:= 0, OS < 0 ->
+ parse(S#st{i=I+1, obj_depth=1, obj_start=I}, Acc);
+ ${ -> parse(S#st{i=I+1, obj_depth=OD+1}, Acc);
+ $} when OD > 0 ->
+ case OD-1 of
+ 0 ->
+ ObjBin = binary:part(Buf, OS, I-OS+1),
+ Row = couchbeam_ejson:decode(ObjBin),
+ S1 = S#st{i=I+1, obj_depth=0, obj_start=-1},
+ parse(S1, [Row|Acc]);
+ N -> parse(S#st{i=I+1, obj_depth=N}, Acc)
+ end;
+ $[ -> parse(S#st{i=I+1, arr_depth=AD+1}, Acc);
+ $] ->
+ case AD-1 of
+ 0 ->
+ %% rows array ended; we can stop
+ %% Drop consumed buffer to current index
+ {lists:reverse(Acc), S#st{phase=done, i=I+1}};
+ N -> parse(S#st{i=I+1, arr_depth=N}, Acc)
+ end;
+ _ -> parse(S#st{i=I+1}, Acc)
+ end
+ end;
+parse(S=#st{phase=in_rows}, Acc) -> {lists:reverse(Acc), S};
+parse(S, Acc) -> {lists:reverse(Acc), S}.
+
+%% Find the start of the rows array: look for the token "rows" then the following '['
+find_rows_start(Buf, StartI) ->
+ case binary:match(Buf, <<"\"rows\"">>, [{scope, {StartI, byte_size(Buf)-StartI}}]) of
+ nomatch -> not_found;
+ {Pos, _Len} ->
+ %% find ':' then '[' after it
+ case find_next(Buf, Pos+5, $:) of
+ not_found -> not_found;
+ ColonI ->
+ case find_next(Buf, ColonI+1, $[) of
+ not_found -> not_found;
+ ArrI -> {Pos, ArrI}
+ end
+ end
+ end.
+
+find_next(Buf, I, Char) when I < byte_size(Buf) ->
+ <<_:I/binary, C, _/binary>> = Buf,
+ if C =:= Char -> I;
+ true -> find_next(Buf, I+1, Char)
+ end;
+find_next(_Buf, _I, _Char) -> not_found.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+parse_rows(Json) ->
+ P0 = init(),
+ {R1, P1} = feed(Json, P0),
+ {R2, _} = finish(P1),
+ R1 ++ R2.
+
+basic_chunked_parse_test() ->
+ %% Simulate a view JSON response split in chunks
+ Row1 = #{<<"id">> => <<"a">>, <<"key">> => <<"a">>, <<"value">> => 1},
+ Row2 = #{<<"id">> => <<"b">>, <<"key">> => <<"b">>, <<"value">> => 2},
+ BodyMap = #{<<"total_rows">> => 2, <<"offset">> => 0, <<"rows">> => [Row1, Row2]},
+ Bin = couchbeam_ejson:encode(BodyMap),
+ <> = Bin,
+ P = init(),
+ {Rows0, P1} = feed(C1, P),
+ ?assertEqual([], Rows0),
+ {Rows1, P2} = feed(C2, P1),
+ %% depending on split, may or may not have a row; just ensure maps when present
+ lists:foreach(fun(X) -> ?assert(is_map(X)) end, Rows1),
+ {Rows2, _} = feed(C3, P2),
+ AllRows = Rows1 ++ Rows2,
+ ?assertEqual(2, length(AllRows)),
+ lists:foreach(fun(X) -> ?assert(is_map(X)) end, AllRows),
+ ok.
+
+-endif.
diff --git a/support/test-docker.sh b/support/test-docker.sh
new file mode 100644
index 00000000..1276bfe1
--- /dev/null
+++ b/support/test-docker.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+echo "[docker-test] Starting CouchDB and running eunit..."
+docker compose up -d --build couchdb
+docker compose run --rm test
+rc=$?
+echo "[docker-test] Bringing down stack..."
+docker compose down -v
+exit $rc
diff --git a/support/wait-for-couch.sh b/support/wait-for-couch.sh
new file mode 100755
index 00000000..75fe02fd
--- /dev/null
+++ b/support/wait-for-couch.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+URL=${COUCHDB_URL:-http://localhost:5984}
+USER=${COUCHDB_ADMIN:-admin}
+PASS=${COUCHDB_PASSWORD:-change_me}
+
+echo "Waiting for CouchDB at $URL ..."
+for i in {1..60}; do
+ if curl -fsS "$URL/" >/dev/null; then
+ echo "CouchDB up."
+ break
+ fi
+ sleep 1
+done
+
+# verify auth works (if admin party disabled)
+curl -fsS -u "$USER:$PASS" "$URL/_all_dbs" >/dev/null || true
+
+# ensure system databases exist (idempotent)
+curl -fs -u "$USER:$PASS" -X PUT "$URL/_users" >/dev/null 2>/dev/null || true
+curl -fs -u "$USER:$PASS" -X PUT "$URL/_replicator" >/dev/null 2>/dev/null || true
+curl -fs -u "$USER:$PASS" -X PUT "$URL/_global_changes" >/dev/null 2>/dev/null || true
+echo "Ready."