├── ebin └── .gitignore ├── .gitignore ├── src ├── Makefile ├── couchdb.erl └── erlang_couchdb.erl ├── include └── erlang_couchdb.hrl ├── t ├── 001-load.t ├── 002-server.t ├── 003-database.t └── 004-documents.t ├── Makefile ├── support └── include.mk ├── README.markdown └── test └── erlang_couchdb_SUITE.erl /ebin/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | include ../support/include.mk 2 | 3 | all: $(EBIN_FILES) 4 | 5 | debug: 6 | $(MAKE) DEBUG=-DDEBUG 7 | 8 | clean: 9 | rm -rf $(EBIN_FILES) erl_crash.dump 10 | -------------------------------------------------------------------------------- /include/erlang_couchdb.hrl: -------------------------------------------------------------------------------- 1 | %% replace these defines with your data 2 | 3 | -define(DB_HOSTNAME, "localhost"). 4 | -define(DB_PORT, 5984). 5 | -define(DB_HOST, {?DB_HOSTNAME, ?DB_PORT}). 6 | -define(DB_DATABASE, "tests"). -------------------------------------------------------------------------------- /t/001-load.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin 4 | 5 | main(_) -> 6 | etap:plan(2), 7 | etap_can:loaded_ok(erlang_couchdb, "Module 'erlang_couchdb' loaded"), 8 | etap_can:can_ok(erlang_couchdb, server_info), 9 | etap:end_tests(), 10 | ok. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | mkdir -p ebin/ 3 | (cd src;$(MAKE)) 4 | 5 | compile: ${MODS:%=%.beam} 6 | 7 | ct: compile 8 | cp ./ebin/*.beam ./test 9 | mkdir -p ../../Erlang/erlandom/pshb/webhook/htdocs/erlang_couchdbtest 10 | /usr/local/lib/erlang/lib//common_test-1.4.5/priv/bin/run_test -dir . -logdir ../../Erlang/erlandom/pshb/webhook/htdocs/erlang_couchdbtest -cover ./config/couchdb.coverspec 11 | 12 | test: all 13 | prove t/*.t 14 | 15 | clean: 16 | (cd src;$(MAKE) clean) 17 | rm -rf erl_crash.dump *.beam *.hrl cover 18 | 19 | dist-src: clean 20 | tar zcvf erlang_couchdb-0.2.3.tgz Makefile src/ 21 | 22 | cover: all 23 | COVER=1 prove t/*.t 24 | erl -detached -noshell -eval 'etap_report:create()' -s init stop 25 | -------------------------------------------------------------------------------- /t/002-server.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin 4 | 5 | main(_) -> 6 | etap:plan(4), 7 | case (catch test()) of 8 | {'EXIT', Err} -> 9 | io:format("# ~p~n", [Err]), 10 | etap:bail(); 11 | _ -> 12 | etap:end_tests() 13 | end, 14 | ok. 15 | 16 | test() -> 17 | {ok, Data} = erlang_couchdb:server_info({"localhost", 5984}), 18 | etap:is(proplists:get_value(<<"couchdb">>, Data), <<"Welcome">>, "message ok"), 19 | etap:is(proplists:get_value(<<"version">>, Data), <<"0.9.0">>, "version ok"), 20 | etap_exception:dies_ok(fun() -> erlang_couchdb:server_info({"localhost", 5985}) end, "server_info/1 nok"), 21 | etap:fun_is(fun ({other, _}) -> true; (_) -> false end, erlang_couchdb:server_info({"example.com", 80}), "Triggering server 'other' response"), 22 | ok. 23 | -------------------------------------------------------------------------------- /support/include.mk: -------------------------------------------------------------------------------- 1 | ## -*- makefile -*- 2 | 3 | ERL := erl 4 | ERLC := $(ERL)c 5 | 6 | INCLUDE_DIRS := ../include $(wildcard ../deps/*/include) 7 | EBIN_DIRS := $(wildcard ../deps/*/ebin) 8 | ERLC_FLAGS := -W $(INCLUDE_DIRS:../%=-I ../%) $(EBIN_DIRS:%=-pa %) 9 | 10 | ifndef no_debug_info 11 | ERLC_FLAGS += +debug_info 12 | endif 13 | 14 | ifdef debug 15 | ERLC_FLAGS += -Ddebug 16 | endif 17 | 18 | EBIN_DIR := ../ebin 19 | DOC_DIR := ../doc 20 | EMULATOR := beam 21 | 22 | ERL_TEMPLATE := $(wildcard *.et) 23 | ERL_SOURCES := $(wildcard *.erl) 24 | ERL_HEADERS := $(wildcard *.hrl) $(wildcard ../include/*.hrl) 25 | ERL_OBJECTS := $(ERL_SOURCES:%.erl=$(EBIN_DIR)/%.beam) 26 | ERL_TEMPLATES := $(ERL_TEMPLATE:%.et=$(EBIN_DIR)/%.beam) 27 | ERL_OBJECTS_LOCAL := $(ERL_SOURCES:%.erl=./%.$(EMULATOR)) 28 | APP_FILES := $(wildcard *.app) 29 | EBIN_FILES = $(ERL_OBJECTS) $(APP_FILES:%.app=../ebin/%.app) $(ERL_TEMPLATES) 30 | MODULES = $(ERL_SOURCES:%.erl=%) 31 | 32 | ../ebin/%.app: %.app 33 | cp $< $@ 34 | 35 | $(EBIN_DIR)/%.$(EMULATOR): %.erl 36 | $(ERLC) $(ERLC_FLAGS) -o $(EBIN_DIR) $< 37 | 38 | $(EBIN_DIR)/%.$(EMULATOR): %.et 39 | $(ERL) -noshell -pa ../../elib/erltl/ebin/ -eval "erltl:compile(atom_to_list('$<'), [{outdir, \"../ebin\"}, report_errors, report_warnings, nowarn_unused_vars])." -s init stop 40 | 41 | ./%.$(EMULATOR): %.erl 42 | $(ERLC) $(ERLC_FLAGS) -o . $< 43 | 44 | $(DOC_DIR)/%.html: %.erl 45 | $(ERL) -noshell -run edoc file $< -run init stop 46 | mv *.html $(DOC_DIR) 47 | 48 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | erlang\_couchdb is a really simple CouchDB client. Simple means that it does as little as possible and doesn't get in the way. I developed this module because the existing modules seemed too big and did too much for my taste. This module provides several public functions to do things like manipulating databases, documents and views. 2 | 3 | The implemented functionality is really limited because I'm only really implementing the stuff that I'm using in I Play WoW. 4 | 5 | * Get server information 6 | * Create database 7 | * Get database information 8 | * Create document 9 | * Create document with specific ID 10 | * Update document 11 | * Get document 12 | * Create design document 13 | * Invoke a design document 14 | 15 | A quick demo: 16 | 17 | erlang_couchdb:create_database({"localhost", 5984}, "iplaywow"). 18 | erlang_couchdb:database_info({"localhost", 5984}, "iplaywow"). 19 | erlang_couchdb:server_info({"localhost", 5984}). 20 | erlang_couchdb:create_document({"localhost", 5984}, "iplaywow", [{<<"name">>, <<"Korale">>}, {<<"type">>, <<"character">>}]). 21 | erlang_couchdb:retrieve_document({"localhost", 5984}, "iplaywow", "0980..."). 22 | erlang_couchdb:update_document({"localhost", 5984}, "iplaywow", "0980...", [{<<"_rev">>, <<"3419...">>}, {<<"name">>, <<"Korale">>}, {<<"level">>, <<"70">>}, {<<"type">>}, <<"character">>}]). 23 | erlang_couchdb:delete_document({"localhost", 5984}, "iplaywow", "1fd0...", "1193..."). 24 | erlang_couchdb:create_view({"localhost", 5984}, "iplaywow", "characters", <<"javascript">>, [{<<"realm">>, <<"function(doc) { if (doc.type == 'character') emit(doc.realm_full, null) }">>}]). 25 | erlang_couchdb:invoke_view({"localhost", 5984}, "iplaywow", "characters", "realm", [{"key", "\"Medivh-US\""}]). 26 | 27 | Patches are welcome. For the time being this module should be considered alpha. Support is limited but feel free to contact me via email and submit patches. If you use this module please let me know. 28 | 29 | To retrieve object you can do: 30 | 31 | {json, Obj} = erlang_couchdb:invoke_view(...), 32 | erlang_couchdb:get_value(<<"rows">>, Obj), 33 | erlang_couchdb:get_value([<<"rows">>,<<"value">>], Obj). 34 | 35 | To create an object and set a number of attributes: 36 | 37 | erlang_couchdb:fold([erlang_couchdb:set_value(K, V) || {K,V} <- L], 38 | erlang_couchdb:empty()) 39 | 40 | # TODO 41 | 42 | * Document attachments 43 | 44 | Your contributions are welcome. 45 | -------------------------------------------------------------------------------- /t/003-database.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin 4 | 5 | main(_) -> 6 | etap:plan(14), 7 | pre_run(), 8 | test(), 9 | etap:end_tests(), 10 | ok. 11 | 12 | pre_run() -> 13 | {A1, A2, A3} = now(), 14 | random:seed(A1, A2, A3), 15 | ok. 16 | 17 | database() -> 18 | lists:flatten([ 19 | [[random:uniform(25) + 96] || _ <-lists:seq(1,5)], 20 | [[random:uniform(9) + 47] || _ <-lists:seq(1,3)] 21 | ]). 22 | 23 | test() -> 24 | Database = database(), 25 | 26 | (fun() -> 27 | etap:is(erlang_couchdb:create_database({"localhost", 5984}, Database), ok, "tmp database created"), 28 | {ok, DatabaseProps} = erlang_couchdb:database_info({"localhost", 5984}, Database), 29 | etap:is(proplists:get_value(<<"db_name">>, DatabaseProps), list_to_binary(Database), "name ok"), 30 | etap:is(proplists:get_value(<<"doc_count">>, DatabaseProps), 0, "document count ok"), 31 | etap:is(proplists:get_value(<<"doc_del_count">>, DatabaseProps), 0, "document delete count ok"), 32 | etap:is(proplists:get_value(<<"update_seq">>, DatabaseProps), 0, "update count ok"), 33 | etap:is(proplists:get_value(<<"purge_seq">>, DatabaseProps), 0, "purge count ok"), 34 | etap:is(proplists:get_value(<<"compact_running">>, DatabaseProps), false, "compaction status ok"), 35 | ok 36 | end)(), 37 | 38 | (fun() -> 39 | {ok, Databases} = erlang_couchdb:retrieve_all_dbs({"localhost", 5984}), 40 | etap:any(list_to_binary(Database), Databases, "tmp database listed"), 41 | ok 42 | end)(), 43 | 44 | (fun() -> 45 | etap:fun_is(fun ({error, _}) -> true; (_) -> false end, erlang_couchdb:retrieve_all_dbs({"example.com", 80}), "Triggering server 'other' response"), 46 | ok 47 | end)(), 48 | 49 | (fun() -> 50 | etap:fun_is(fun ({error, _}) -> true; (_) -> false end, erlang_couchdb:database_info({"example.com", 80}, "asdasdasd"), "Triggering server 'other' response"), 51 | ok 52 | end)(), 53 | 54 | (fun() -> 55 | Error = {ok,[{<<"error">>,<<"not_found">>}, {<<"reason">>,<<"Missing">>}]}, 56 | etap:is(erlang_couchdb:database_info({"localhost", 5984}, "hahahahano"), Error, "database_info/2 on non-existing db."), 57 | ok 58 | end)(), 59 | 60 | (fun() -> 61 | Error = {error,{json,{struct,[{<<"error">>,<<"file_exists">>}, {<<"reason">>, <<"The database could not be created, the file already exists.">>}]}}}, 62 | etap:is(erlang_couchdb:create_database({"localhost", 5984}, Database), Error, "tmp database created"), 63 | ok 64 | end)(), 65 | 66 | (fun() -> 67 | etap:is(erlang_couchdb:delete_database({"localhost", 5984}, Database), ok, "tmp database created"), 68 | ok 69 | end)(), 70 | 71 | (fun() -> 72 | Error = {error,{json,{struct,[{<<"error">>,<<"not_found">>}, {<<"reason">>,<<"Missing">>}]}}}, 73 | etap:is(erlang_couchdb:delete_database({"localhost", 5984}, Database), Error, "tmp database created"), 74 | ok 75 | end)(), 76 | 77 | ok. 78 | -------------------------------------------------------------------------------- /t/004-documents.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin 4 | 5 | main(_) -> 6 | etap:plan(26), 7 | pre_run(), 8 | test(), 9 | etap:end_tests(), 10 | ok. 11 | 12 | pre_run() -> 13 | {A1, A2, A3} = now(), 14 | random:seed(A1, A2, A3), 15 | ok. 16 | 17 | database() -> 18 | lists:flatten([ 19 | [[random:uniform(25) + 96] || _ <-lists:seq(1,5)], 20 | [[random:uniform(9) + 47] || _ <-lists:seq(1,3)] 21 | ]). 22 | 23 | test() -> 24 | Database = database(), 25 | 26 | (fun() -> 27 | etap:is(erlang_couchdb:create_database({"localhost", 5984}, Database), ok, "tmp database created"), 28 | {ok, DatabaseProps} = erlang_couchdb:database_info({"localhost", 5984}, Database), 29 | etap:is(proplists:get_value(<<"db_name">>, DatabaseProps), list_to_binary(Database), "name ok"), 30 | ok 31 | end)(), 32 | 33 | (fun() -> 34 | {ok, Databases} = erlang_couchdb:retrieve_all_dbs({"localhost", 5984}), 35 | etap:any(list_to_binary(Database), Databases, "tmp database listed"), 36 | ok 37 | end)(), 38 | 39 | %% Create a document 40 | (fun() -> 41 | etap:fun_is( 42 | fun ({json,{struct,[{<<"ok">>,true}, {<<"id">>,<<"FooDocument">>}, {<<"rev">>, FooRev}]}}) -> 43 | put(foo_rev, FooRev), 44 | true; 45 | (_) -> false 46 | end, 47 | erlang_couchdb:create_document({"localhost", 5984}, Database, "FooDocument", [{<<"foo">>, <<"bar">>}]), 48 | "Creating document" 49 | ), 50 | ok 51 | end)(), 52 | 53 | %% Fetch back that document 54 | (fun() -> 55 | etap:fun_is( 56 | fun ({json, {struct, Keys}}) -> 57 | etap:is(proplists:get_value(<<"_id">>, Keys), <<"FooDocument">>, "_id ok"), 58 | etap:is(proplists:get_value(<<"foo">>, Keys), <<"bar">>, "foo ok"), 59 | true; 60 | (_) -> false 61 | end, 62 | erlang_couchdb:retrieve_document({"localhost", 5984}, Database, "FooDocument"), 63 | "Fetching document" 64 | ), 65 | ok 66 | end)(), 67 | 68 | %% Create a document 69 | (fun() -> 70 | etap:fun_is( 71 | fun ({json,{struct,[{<<"ok">>,true}, {<<"id">>, DocID}, {<<"rev">>, DocRev}]}}) -> 72 | put(doc_id_1, DocID), 73 | put(doc_rev_1, DocRev), 74 | true; 75 | (_) -> 76 | false 77 | end, 78 | erlang_couchdb:create_document({"localhost", 5984}, Database, [{<<"bar">>, <<"baz">>}]), 79 | "Creating document" 80 | ), 81 | ok 82 | end)(), 83 | 84 | (fun() -> 85 | etap:fun_is( 86 | fun ({json, {struct, Keys}}) -> 87 | etap:is(proplists:get_value(<<"_id">>, Keys), get(doc_id_1), "_id ok"), 88 | etap:is(proplists:get_value(<<"bar">>, Keys), <<"baz">>, "bar ok"), 89 | true; 90 | (_) -> false 91 | end, 92 | erlang_couchdb:retrieve_document({"localhost", 5984}, Database, binary_to_list(get(doc_id_1))), 93 | "Fetching document" 94 | ), 95 | ok 96 | end)(), 97 | 98 | %% Create a document 99 | (fun() -> 100 | etap:fun_is( 101 | fun ({json,{struct,[{<<"ok">>,true}, {<<"id">>, DocID}, {<<"rev">>, _}]}}) -> 102 | true; 103 | (_) -> 104 | false 105 | end, 106 | erlang_couchdb:create_document({"localhost", 5984}, Database, {struct, [{<<"bar">>, <<"baz">>}]}), 107 | "Creating document" 108 | ), 109 | ok 110 | end)(), 111 | 112 | %% Create a document 113 | (fun() -> 114 | Documents = [ 115 | [{<<"username">>, <<"Nick">>}], 116 | [{<<"username">>, <<"Tom">>}], 117 | [{<<"username">>, <<"Jan">>}] 118 | ], 119 | etap:fun_is( 120 | fun ({json, [{struct,[{<<"id">>, ID1}, {<<"rev">>, _}]}, {struct,[{<<"id">>, ID2}, {<<"rev">>, _}]}, {struct,[{<<"id">>, ID3}, {<<"rev">>, _}]}]}) -> 121 | put(doc_id_2, ID1), 122 | put(doc_id_3, ID2), 123 | put(doc_id_4, ID3), 124 | true; 125 | (_) -> 126 | false 127 | end, 128 | erlang_couchdb:create_documents({"localhost", 5984}, Database, Documents), 129 | "Creating documents" 130 | ), 131 | ok 132 | end)(), 133 | 134 | %% Create a document 135 | (fun() -> 136 | etap:fun_is( 137 | fun ({json,{struct,[{<<"ok">>,true}, {<<"id">>, DocID}, {<<"rev">>, _}]}}) -> 138 | put(doc_id_5, DocID), 139 | true; 140 | (Other) -> 141 | io:format("Other ~p~n", [Other]), 142 | false 143 | end, 144 | erlang_couchdb:create_attachment( 145 | {"localhost", 5984}, 146 | Database, 147 | "DocWithAttachment", 148 | "t/001-load.t", 149 | "text/plain" 150 | ), 151 | "Creating document attachment" 152 | ), 153 | ok 154 | end)(), 155 | 156 | (fun() -> 157 | RevTuple = {ok, get(doc_id_1), get(doc_rev_1)}, 158 | etap:is( 159 | RevTuple, 160 | erlang_couchdb:document_revision({"localhost", 5984}, Database, binary_to_list(get(doc_id_1))), 161 | "Fetching rev" 162 | ), 163 | etap:is( 164 | RevTuple, 165 | erlang_couchdb:document_revision({"localhost", 5984}, Database, get(doc_id_1)), 166 | "Fetching rev" 167 | ), 168 | etap:is( 169 | {ok, undefined, undefined}, 170 | erlang_couchdb:document_revision({"localhost", 5984}, Database, "NotARealDoc"), 171 | "Fetching rev" 172 | ), 173 | etap:fun_is( 174 | fun ({error, _}) -> true; (Other) -> io:format("Other ~p~n", [Other]), false end, 175 | erlang_couchdb:document_revision({"google.com", 80}, Database, "NotARealDoc"), 176 | "Fetching rev" 177 | ), 178 | ok 179 | end)(), 180 | 181 | (fun() -> 182 | DocID = get(doc_id_1), 183 | etap:fun_is( 184 | fun ({json,{struct,[{<<"ok">>,true}, {<<"id">>, DocID}, {<<"rev">>, NewRev}]}}) -> 185 | put(doc_rev_1, NewRev), 186 | true; 187 | (_) -> false 188 | end, 189 | erlang_couchdb:update_document({"localhost", 5984}, Database, binary_to_list(get(doc_id_1)), [{<<"foo">>, <<"biz">>}, {<<"_rev">>, get(doc_rev_1)}]), 190 | "Creating document" 191 | ), 192 | ok 193 | end)(), 194 | 195 | (fun() -> 196 | DocID = get(doc_id_1), 197 | etap:fun_is( 198 | fun ({json, {struct, Keys}}) -> 199 | etap:is(proplists:get_value(<<"_id">>, Keys), DocID, "_id ok"), 200 | etap:is(proplists:get_value(<<"foo">>, Keys), <<"biz">>, "foo ok"), 201 | true; 202 | (_) -> false 203 | end, 204 | erlang_couchdb:retrieve_document({"localhost", 5984}, Database, binary_to_list(get(doc_id_1))), 205 | "Fetching document" 206 | ), 207 | ok 208 | end)(), 209 | 210 | (fun() -> 211 | DocID = get(doc_id_1), 212 | etap:fun_is( 213 | fun ({json,{struct,[{<<"ok">>,true}, {<<"id">>, DocID}, {<<"rev">>, NewRev}]}}) -> 214 | put(doc_rev_1, NewRev), 215 | true; 216 | (_) -> false 217 | end, 218 | erlang_couchdb:update_document({"localhost", 5984}, Database, binary_to_list(get(doc_id_1)), {struct, [{<<"foo">>, <<"buz">>}, {<<"_rev">>, get(doc_rev_1)}]}), 219 | "Creating document" 220 | ), 221 | ok 222 | end)(), 223 | 224 | (fun() -> 225 | etap:fun_is( 226 | fun ({json,{struct,[{<<"ok">>, true}, {<<"id">>, <<"FooDocument">>}, {<<"rev">>, _}]}}) -> true; 227 | (_) -> false 228 | end, 229 | erlang_couchdb:delete_document({"localhost", 5984}, Database, "FooDocument", binary_to_list(get(foo_rev))), 230 | "document deleted" 231 | ), 232 | ok 233 | end)(), 234 | 235 | (fun() -> 236 | etap:is(erlang_couchdb:delete_documents({"localhost", 5984}, Database, [get(doc_id_1), get(doc_id_2), get(doc_id_3)]), {json, []}, "documents deleted"), 237 | ok 238 | end)(), 239 | 240 | (fun() -> 241 | etap:is(erlang_couchdb:delete_database({"localhost", 5984}, Database), ok, "tmp database deleted"), 242 | ok 243 | end)(), 244 | 245 | ok. 246 | -------------------------------------------------------------------------------- /test/erlang_couchdb_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_couchdb_SUITE). 2 | -compile(export_all). 3 | 4 | -include("ct.hrl"). 5 | 6 | -define(CONNECTION, {"localhost", 5984}). 7 | -define(DBNAME, "t_erlang_couchdb_test"). 8 | 9 | %%-------------------------------------------------------------------- 10 | %% Test server callback functions 11 | %%-------------------------------------------------------------------- 12 | 13 | %%-------------------------------------------------------------------- 14 | %% Function: suite() -> DefaultData 15 | %% DefaultData: [tuple()] 16 | %% Description: Require variables and set default values for the suite 17 | %%-------------------------------------------------------------------- 18 | suite() -> [{timetrap,{minutes,1}}] 19 | . 20 | 21 | all() -> 22 | [serverinfo, all_databases, 23 | databaselifecycle, documentlifecycle, 24 | createview, 25 | viewaccess_nullmap, viewaccess_maponly, viewaccess_mapreduce, 26 | parseview 27 | ] 28 | . 29 | 30 | init_per_suite(Config) -> 31 | crypto:start(), 32 | inets:start(), 33 | case erlang_couchdb:database_info(?CONNECTION, ?DBNAME) of 34 | {ok, _Res} -> 35 | erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 36 | ; 37 | _ -> ok 38 | end, 39 | Config 40 | . 41 | 42 | end_per_suite(_Config) -> 43 | case erlang_couchdb:database_info(?CONNECTION, ?DBNAME) of 44 | {ok, _Res} -> 45 | erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 46 | ; 47 | _ -> ok 48 | end, 49 | inets:stop(), 50 | ok 51 | . 52 | 53 | serverinfo() -> 54 | [{userdata,[{doc,"Server connection, and information"}]}] 55 | . 56 | 57 | serverinfo(_Config) -> 58 | {ok, Res} = erlang_couchdb:server_info(?CONNECTION), 59 | ct:print(test_category, "server info ~p~n", [Res]) 60 | . 61 | 62 | all_databases() -> 63 | [{userdata,[{doc,"all databases information"}]}] 64 | . 65 | 66 | all_databases(_Config) -> 67 | {ok, Database} = erlang_couchdb:retrieve_all_dbs(?CONNECTION), 68 | ct:print(test_category, "all databases ~p~n", [Database]) 69 | . 70 | 71 | databaselifecycle(_Config) -> 72 | ok = erlang_couchdb:create_database(?CONNECTION, ?DBNAME), 73 | {ok, Res} = erlang_couchdb:database_info(?CONNECTION, ?DBNAME), 74 | ct:print(test_category, "database info for ~p~n~p~n", [?DBNAME, Res]), 75 | ok = erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 76 | . 77 | 78 | documentlifecycle() -> 79 | [{userdata,[{doc,"Document creation, retrieve, and deletion"}]}] 80 | . 81 | 82 | documentlifecycle(_Config) -> 83 | % setup 84 | ok = erlang_couchdb:create_database(?CONNECTION, ?DBNAME), 85 | 86 | {json,{struct,[_, {<<"id">>, Id},_]}} = erlang_couchdb:create_document(?CONNECTION, ?DBNAME, {struct, [{<<"foo">>, <<"bar">> } ]}), 87 | ct:print(test_category, "id: ~p", [Id]), 88 | Doc = erlang_couchdb:retrieve_document(?CONNECTION, ?DBNAME, binary_to_list((Id))), 89 | ct:print(test_category, "Document: ~p", [Doc]), 90 | 91 | % tear down 92 | ok = erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 93 | . 94 | 95 | 96 | createview() -> 97 | [{userdata,[{doc,"Documents creation, set view, and deletion"}]}] 98 | . 99 | 100 | createview(_Config) -> 101 | % setup 102 | ok = erlang_couchdb:create_database(?CONNECTION, ?DBNAME), 103 | {json,{struct,[_, {<<"id">>, Id},_]}} = erlang_couchdb:create_document(?CONNECTION, ?DBNAME, {struct, [{<<"type">>, <<"D">> } ]}), 104 | {json,{struct,[_, {<<"id">>, Id2},_]}} = erlang_couchdb:create_document(?CONNECTION, ?DBNAME, {struct, [{<<"type">>, <<"S">> } ]}), 105 | Doc = erlang_couchdb:retrieve_document(?CONNECTION, ?DBNAME, binary_to_list((Id))), 106 | ct:print(test_category, "Document: ~p", [Doc]), 107 | Doc2 = erlang_couchdb:retrieve_document(?CONNECTION, ?DBNAME, binary_to_list(Id2)), 108 | ct:print(test_category, "Document2: ~p", [Doc2]), 109 | View1 = "function(doc) { if(doc.type) { emit(doc.type)}}", 110 | Views = [{"all", View1}], 111 | Res = erlang_couchdb:create_view(?CONNECTION, ?DBNAME, "testview", <<"javasccript">>, Views, []), 112 | ct:print("view creation result: ~p~n",[Res]), 113 | 114 | % tear down 115 | ok = erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 116 | . 117 | 118 | viewaccess_nullmap() -> 119 | [{userdata,[{doc,"Documents creation, set null selection view, accessview and deletion"}]}] . 120 | 121 | viewaccess_nullmap(_Config) -> 122 | % setup 123 | ok = erlang_couchdb:create_database(?CONNECTION, ?DBNAME), 124 | 125 | do_viewaccess_maponly("function(doc) { emit(null, doc)}"), 126 | 127 | % assertion 128 | 129 | % tear down 130 | ok = erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 131 | . 132 | 133 | viewaccess_maponly() -> 134 | [{userdata,[{doc,"Documents creation, set view, accessview and deletion"}]}] . 135 | 136 | viewaccess_maponly(_Config) -> 137 | % setup 138 | ok = erlang_couchdb:create_database(?CONNECTION, ?DBNAME), 139 | 140 | ResView = do_viewaccess_maponly("function(doc) { if(doc.type) {emit(null, doc.type)}}"), 141 | ct:print("view access result: ~p~n",[ResView]), 142 | 143 | % assertion 144 | {json, 145 | {struct, 146 | [{<<"total_rows">>,2}, 147 | {<<"offset">>,0}, 148 | {<<"rows">>, 149 | [{struct, 150 | [{<<"id">>, 151 | _ID1}, 152 | {<<"key">>,null}, 153 | % {<<"value">>,<<"D">>}]}, 154 | {<<"value">>,_B1}]}, 155 | {struct, 156 | [{<<"id">>, 157 | _ID2}, 158 | {<<"key">>,null}, 159 | % {<<"value">>,<<"S">>}]}]}]}} 160 | {<<"value">>, _B2}]}]}]}} 161 | = ResView, 162 | 163 | % tear down 164 | ok = erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 165 | . 166 | 167 | do_viewaccess_maponly(Viewsource) -> 168 | do_viewaccess_maponly(Viewsource, fun(X) -> X end) 169 | . 170 | 171 | do_viewaccess_maponly(Viewsource, Cb) -> 172 | {json,{struct,[_, {<<"id">>, Id},_]}} = erlang_couchdb:create_document(?CONNECTION, ?DBNAME, {struct, [{<<"type">>, <<"D">> } ]}), 173 | {json,{struct,[_, {<<"id">>, Id2},_]}} = erlang_couchdb:create_document(?CONNECTION, ?DBNAME, {struct, [{<<"type">>, <<"S">> } ]}), 174 | Doc = erlang_couchdb:retrieve_document(?CONNECTION, ?DBNAME, binary_to_list((Id))), 175 | ct:print(test_category, "Document: ~p", [Doc]), 176 | Doc2 = erlang_couchdb:retrieve_document(?CONNECTION, ?DBNAME, binary_to_list(Id2)), 177 | ct:print(test_category, "Document2: ~p", [Doc2]), 178 | Views = [{"all", Viewsource}], 179 | Res = erlang_couchdb:create_view(?CONNECTION, ?DBNAME, "testview", "javascript", Views, []), 180 | ct:print("view creation result: ~p~n",[Res]), 181 | 182 | % view access 183 | Cb(erlang_couchdb:invoke_view(?CONNECTION, ?DBNAME, "testview", "all",[])) 184 | . 185 | 186 | parseview() -> 187 | [{userdata,[{doc,"Parse View request result"}]}] 188 | . 189 | 190 | parseview(_Config) -> 191 | % setup 192 | ok = erlang_couchdb:create_database(?CONNECTION, ?DBNAME), 193 | 194 | ct:print("parse_view~n",[]), 195 | ResView = do_viewaccess_maponly("function(doc) { if(doc.type) {emit(null, doc.type)}}", fun(X) -> erlang_couchdb:parse_view(X) end), 196 | ct:print("parse view access result: ~p~n",[ResView]), 197 | 198 | % assertion 199 | {2,0,_Array} = ResView, 200 | 201 | % tear down 202 | ok = erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 203 | . 204 | 205 | do_viewaccess_mapreduce(Mapsource, Reducesource) -> 206 | do_viewaccess_mapreduce(Mapsource, Reducesource, fun(X) -> X end) 207 | . 208 | 209 | do_viewaccess_mapreduce(Mapsource, Reducesource, Cb) -> 210 | {json,{struct,[_, {<<"id">>, Id},_]}} = erlang_couchdb:create_document(?CONNECTION, ?DBNAME, {struct, [{<<"type">>, <<"D">> },{<<"val">>, 1} ]}), 211 | {json,{struct,[_, {<<"id">>, Id2},_]}} = erlang_couchdb:create_document(?CONNECTION, ?DBNAME, {struct, [{<<"type">>, <<"S">> }, {<<"val">>, 2}]}), 212 | Doc = erlang_couchdb:retrieve_document(?CONNECTION, ?DBNAME, binary_to_list((Id))), 213 | ct:print(test_category, "Document A: ~p", [Doc]), 214 | Doc2 = erlang_couchdb:retrieve_document(?CONNECTION, ?DBNAME, binary_to_list(Id2)), 215 | ct:print(test_category, "Document B: ~p", [Doc2]), 216 | Views = [{"all", Mapsource, Reducesource}], 217 | Res = erlang_couchdb:create_view(?CONNECTION, ?DBNAME, "testview", "javascript", Views, []), 218 | ct:print("view creation result: ~p~n",[Res]), 219 | 220 | % view access 221 | Cb(erlang_couchdb:invoke_view(?CONNECTION, ?DBNAME, "testview", "all",[])) 222 | . 223 | 224 | viewaccess_mapreduce() -> 225 | [{userdata,[{doc,"Execute map reduce View request"}]}] 226 | . 227 | 228 | viewaccess_mapreduce(_Config) -> 229 | % setup 230 | ok = erlang_couchdb:create_database(?CONNECTION, ?DBNAME), 231 | 232 | ct:print("mapreduce~n",[]), 233 | Mapsource = "function(doc) { if(doc.type) {emit(doc.type, doc.val)}}", 234 | Reducesource = "function(keys, values) {return sum(values)}", 235 | 236 | ResView = do_viewaccess_mapreduce(Mapsource, Reducesource), 237 | ct:print("mapreduce view access result: ~p~n",[ResView]), 238 | 239 | % assertion 240 | ResView = {json, 241 | {struct, 242 | [{<<"rows">>, 243 | [{struct, 244 | [{<<"key">>,null},{<<"value">>,3}]}]}]}}, 245 | 246 | % tear down 247 | ok = erlang_couchdb:delete_database(?CONNECTION, ?DBNAME) 248 | . 249 | -------------------------------------------------------------------------------- /src/couchdb.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2009 Dmitrii 'Mamut' Dimandt 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | %% 24 | %% 25 | %% @author Dmitrii 'Mamut' Dimandt 26 | %% @copyright 2009 Dmitrii 'Mamut' Dimandt 27 | %% @version 0.1 28 | %% @doc A simple stupid wrapper for erlang_couchdb. Requires mochijson2 29 | %% 30 | %% This module was created for the purpose of further simplifying access 31 | %% to an already simple CouchDB interface. 32 | %% 33 | %% This code is available as Open Source Software under the MIT license. 34 | 35 | -module(couchdb). 36 | 37 | -include("./../include/erlang_couchdb.hrl"). 38 | 39 | -compile(export_all). 40 | 41 | 42 | create_database(Name) -> 43 | erlang_couchdb:create_database(?DB_HOST, Name). 44 | create_database(Server, Name) -> 45 | erlang_couchdb:create_database(Server, Name). 46 | 47 | database_info(Name) -> 48 | erlang_couchdb:database_info(?DB_HOST, Name). 49 | database_info(Server, Name) -> 50 | erlang_couchdb:database_info(Server, Name). 51 | 52 | server_info() -> 53 | erlang_couchdb:server_info(?DB_HOST). 54 | server_info({_Host, _Port} = Server) -> 55 | erlang_couchdb:server_info(Server). 56 | 57 | create_document({ID, Doc}) -> 58 | erlang_couchdb:create_document(?DB_HOST, ?DB_DATABASE, ID, Doc); 59 | create_document(Doc) -> 60 | erlang_couchdb:create_document(?DB_HOST, ?DB_DATABASE, Doc). 61 | 62 | create_document(Db, {ID, Doc}) -> 63 | erlang_couchdb:create_document(?DB_HOST, Db, ID, Doc); 64 | create_document(Db, Doc) -> 65 | erlang_couchdb:create_document(?DB_HOST, Db, Doc). 66 | 67 | create_document(Host, Db, {ID, Doc}) -> 68 | erlang_couchdb:create_document(Host, Db, ID, Doc); 69 | create_document(Server, Db, Doc) -> 70 | erlang_couchdb:create_document(Server, Db, Doc). 71 | 72 | retrieve_document(ID) -> 73 | retrieve_document(?DB_HOST, ?DB_DATABASE, ID). 74 | retrieve_document(Db, ID) -> 75 | retrieve_document(?DB_HOST, Db, ID). 76 | retrieve_document(Server, Db, ID) -> 77 | erlang_couchdb:retrieve_document(Server, Db, ID). 78 | 79 | %% @spec update_document(ID::string(), Doc::proplist()) -> ok | {error, Reason::any()} 80 | %% 81 | %% @doc Update only several fields in a document. Leave all other fields unmodified 82 | update_document(ID, Doc) -> 83 | {json, Document} = retrieve_document(ID), 84 | Rev = get_rev(Document), 85 | Doc2 = update_doc_fields(Document, Doc), 86 | replace_document(ID, Rev, Doc2). 87 | update_document(ID, Rev, Doc) -> 88 | {json, Document} = retrieve_document(ID), 89 | Doc2 = update_doc_fields(Document, Doc), 90 | replace_document(ID, Rev, Doc2). 91 | update_document(Db, ID, Rev, Doc) -> 92 | {json, Document} = retrieve_document(ID), 93 | Doc2 = update_doc_fields(Document, Doc), 94 | replace_document(?DB_HOST, Db, ID, Rev, Doc2). 95 | update_document(Server, Db, ID, Rev, Doc) -> 96 | {json, Document} = retrieve_document(ID), 97 | Doc2 = update_doc_fields(Document, Doc), 98 | replace_document(Server, Db, ID, Rev, Doc2). 99 | 100 | %% @spec replace_document(ID::string(), Doc::proplist()) -> ok | {error, Reason::any()} 101 | %% 102 | %% @doc Replace the doc by a new doc (default erlang_couchdb and couchdb behaviour for updates) 103 | replace_document(ID, Doc) -> 104 | {json, Document} = retrieve_document(ID), 105 | Rev = get_rev(Document), 106 | replace_document(ID, Rev, Doc). 107 | %erlang_couchdb:update_document(, [{<<"_rev">>, list_to_binary(Rev)} | Doc]). 108 | replace_document(ID, Rev, Doc) -> 109 | erlang_couchdb:update_document(?DB_HOST, ?DB_DATABASE, ID, [{<<"_rev">>, list_to_binary(Rev)} | Doc]). 110 | replace_document(Db, ID, Rev, Doc) -> 111 | erlang_couchdb:update_document(?DB_HOST, Db, ID, [{<<"_rev">>, list_to_binary(Rev)} | Doc]). 112 | replace_document(Server, Db, ID, Rev, Doc) -> 113 | erlang_couchdb:update_document(Server, Db, ID, [{<<"_rev">>, list_to_binary(Rev)} | Doc]). 114 | 115 | %% @spec replace_document(ID::string()) -> ok | {error, Reason::any()} 116 | %% 117 | %% @doc Delete a document 118 | delete_document(ID) -> 119 | {json, Document} = retrieve_document(ID), 120 | Rev = get_rev(Document), 121 | erlang_couchdb:delete_document(?DB_HOST, ?DB_DATABASE, ID, Rev). 122 | delete_document(ID, Rev) -> 123 | erlang_couchdb:delete_document(?DB_HOST, ?DB_DATABASE, ID, Rev). 124 | delete_document(Db, ID, Rev) -> 125 | erlang_couchdb:delete_document(?DB_HOST, Db, ID, Rev). 126 | delete_document(Server, Db, ID, Rev) -> 127 | erlang_couchdb:delete_document(Server, Db, ID, Rev). 128 | 129 | create_view(DocName, ViewList) when is_list(ViewList) -> 130 | erlang_couchdb:create_view(?DB_HOST, ?DB_DATABASE, DocName, <<"javascript">>, ViewList). 131 | 132 | create_view(DocName, ViewName, Data) -> 133 | erlang_couchdb:create_view(?DB_HOST, ?DB_DATABASE, DocName, <<"javascript">>, [{ViewName, Data}]). 134 | create_view(DocName, Type, ViewName, Data) -> 135 | erlang_couchdb:create_view(?DB_HOST, ?DB_DATABASE, DocName, Type, [{ViewName, Data}]). 136 | create_view(Db, DocName, Type, ViewName, Data) -> 137 | erlang_couchdb:create_view(?DB_HOST, Db, DocName, Type, [{ViewName, Data}]). 138 | create_view(Server, Db, DocName, Type, ViewName, Data) -> 139 | erlang_couchdb:create_view(Server, Db, DocName, Type, [{ViewName, Data}]). 140 | 141 | invoke_view(DocName, ViewName) -> 142 | erlang_couchdb:invoke_view(?DB_HOST, ?DB_DATABASE, DocName, ViewName, []). 143 | %% @spec invoke_view(DocName::string(), ViewName::string(), Keys::proplist()) -> result | {error, Reason::any()} 144 | %% 145 | %% @doc Invoke a CouchDB view 146 | invoke_view(DocName, ViewName, Keys) -> 147 | erlang_couchdb:invoke_view(?DB_HOST, ?DB_DATABASE, DocName, ViewName, Keys). 148 | invoke_view(Db, DocName, ViewName, Keys) -> 149 | erlang_couchdb:invoke_view(?DB_HOST, Db, DocName, ViewName, Keys). 150 | invoke_view(Server, Db, DocName, ViewName, Keys) -> 151 | erlang_couchdb:invoke_view(Server, Db, DocName, ViewName, Keys). 152 | 153 | 154 | %% @spec get_value(DocName::document(), Key::binary()) -> empty_string | {error, Reason::any()} 155 | %% 156 | %% @type document() = json_struct() 157 | %% 158 | %% @doc Invoke a CouchDB view 159 | get_value(Doc, Key) -> 160 | get_value(Doc, Key, ""). 161 | get_value({struct, L}, Key, DefaultValue) -> 162 | Values = proplists:get_value(<<"value">>, L, []), 163 | case Values of 164 | [] -> 165 | proplists:get_value(Key, L, DefaultValue); 166 | {struct, ValueList} -> 167 | proplists:get_value(Key, ValueList, DefaultValue) 168 | end; 169 | get_value({json, {struct, ValueList}}, Key, DefaultValue) -> 170 | proplists:get_value(Key, ValueList, DefaultValue); 171 | get_value(_, _Key, DefaultValue) -> 172 | DefaultValue. 173 | 174 | get_id(Doc) -> 175 | get_id(Doc, ""). 176 | get_id({struct, L}, DefaultValue) -> 177 | binary_to_list(proplists:get_value(<<"id">>, L, DefaultValue)); 178 | get_id({json, {struct, L}}, DefaultValue) -> 179 | binary_to_list(proplists:get_value(<<"id">>, L, DefaultValue)); 180 | get_id(_, DefaultValue) -> 181 | DefaultValue. 182 | 183 | get_rev(Doc) -> 184 | get_rev(Doc, ""). 185 | get_rev(Doc, DefaultValue) -> 186 | binary_to_list(get_value(Doc, <<"_rev">>, DefaultValue)). 187 | 188 | get_revs(ID) -> 189 | get_revs(?DB_HOST, ?DB_DATABASE, ID). 190 | get_revs(Db, ID) -> 191 | get_revs(?DB_HOST, Db, ID). 192 | get_revs(Server, Db, ID) -> 193 | {ServerName, Port} = Server, 194 | Type = "GET", 195 | URI = lists:concat(["/", Db, "/", ID, "?", "revs_info=true"]), 196 | Body = <<>>, 197 | Rows = erlang_couchdb:raw_request(Type, ServerName, Port, URI, Body), 198 | case Rows of 199 | {json, {struct, PropList}} -> 200 | Revs = proplists:get_value(<<"_revs_info">>, PropList, []), 201 | case Revs of 202 | [] -> 203 | []; 204 | RevList -> 205 | get_revision_list(RevList) 206 | end; 207 | _ -> 208 | [] 209 | end. 210 | 211 | get_revision_list(RevList) -> 212 | get_revision_list(RevList, []). 213 | 214 | get_revision_list([{struct, PropList}|T], Acc) -> 215 | Rev = binary_to_list(proplists:get_value(<<"rev">>, PropList, <<"">>)), 216 | Status = binary_to_list(proplists:get_value(<<"status">>, PropList, <<"">>)), 217 | get_revision_list( 218 | T, 219 | Acc ++ [{Rev, Status}] 220 | ); 221 | get_revision_list([], Acc) -> 222 | Acc. 223 | 224 | 225 | get_total({json, {struct, L}}) -> 226 | proplists:get_value(<<"total_rows">>, L, 0); 227 | get_total(_Any) -> 228 | 0. 229 | 230 | get_offset({json, {struct, L}}) -> 231 | proplists:get_value(<<"offset">>, L, 0); 232 | get_offset(_Any) -> 233 | 0. 234 | 235 | get_rows({json, {struct, L}}) -> 236 | proplists:get_value(<<"rows">>, L, []); 237 | get_rows(_Any) -> 238 | []. 239 | 240 | 241 | %% @private 242 | %% Update document fields wih new values. 243 | %% 244 | %% @see update_document 245 | 246 | 247 | update_doc_fields({struct, Doc}, NewFields) -> 248 | update_doc_fields(Doc, NewFields); 249 | 250 | update_doc_fields(OldDoc, []) -> 251 | OldDoc; 252 | update_doc_fields(OldDoc, [{Key, Value}|T]) -> 253 | case proplists:get_value(Key, OldDoc) of 254 | undefined -> 255 | update_doc_fields([{Key, Value}] ++ OldDoc, T); 256 | _ -> 257 | NewDoc = proplists:delete(Key, OldDoc), 258 | update_doc_fields([{Key, Value}] ++ NewDoc, T) 259 | end. 260 | -------------------------------------------------------------------------------- /src/erlang_couchdb.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2008 Nick Gerakines 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | %% 24 | %% Change Log: 25 | %% * v2009-01-23 ngerakines 26 | %% - Importing functionality from etnt_. GitHub merge didn't work. 27 | %% - Started adding etap tests. 28 | %% * v0.2.3 2008-10-26: ngerakines 29 | %% - Added ability to delete databases. 30 | %% - Added function to fetch Document by ID and return it's Document 31 | %% ID and revision. 32 | %% - Added experimental function to create design documents based on 33 | %% a .js file's contents. 34 | %% - Fixed bug in parse_view/1 when error is returned. 35 | %% * v0.2.2 2008-10-25: ngerakines 36 | %% - Applied a patch from Pablo Sortino that 37 | %% provides bulk document creation. 38 | %% - Created accessor function to create a new document with an ID. 39 | %% * v0.2.1 2008-10-05: ngerakines 40 | %% - Complete rewrite with reduced module size. 41 | %% - Moved away from the rfc4627 module and to mochijson2. 42 | %% - Moved away from urlencode dependancies from yaws to mochiweb. 43 | %% - Overall the module 'does less'. 44 | %% - Moved away from the gen_server model. 45 | %% 46 | %% @author Nick Gerakines 47 | %% @copyright 2008 Nick Gerakines 48 | %% @version 0.2.1 49 | %% @doc A simple CouchDB client. 50 | %% 51 | %% This module was created for the purpose of creating a very small and light 52 | %% CouchDB interface. It supports a limited number of API methods and features. 53 | %% 54 | %% This module was built for use in the I Play WoW Facebook Application and 55 | %% website. Support is limited and features will be added as needed by the 56 | %% developer/website. 57 | %% 58 | %% This code is available as Open Source Software under the MIT license. 59 | %% 60 | %% Updates at http://github.com/ngerakines/erlang_couchdb/ 61 | -module(erlang_couchdb). 62 | 63 | -author("Nick Gerakines "). 64 | -version("Version: 0.2.3"). 65 | 66 | -export([server_info/1]). 67 | -export([create_database/2, database_info/2, retrieve_all_dbs/1, delete_database/2]). 68 | -export([create_document/3, create_document/4, create_documents/3, create_attachment/5]). 69 | -export([retrieve_document/3, retrieve_document/4, document_revision/3]). 70 | -export([update_document/4, delete_document/4, delete_documents/3]). 71 | -export([create_view/5, create_view/6, invoke_view/5, invoke_multikey_view/6, load_view/4]). 72 | -export([raw_request/5, raw_request/6, fetch_ids/2, parse_view/1]). 73 | -export([get_value/2, set_value/2, set_value/3, fold/2, empty/0]). 74 | 75 | %% @private 76 | %% Instead of using ibrowse or http:request/4, this module uses 77 | %% gen_tcp:connect/3. Using ibrowse requires more dependancies and inets can 78 | %% create bottlenecks. 79 | raw_request(Type, Server, Port, URI, Body) -> 80 | {ok, Socket} = gen_tcp:connect(Server, Port, [binary, {active, false}, {packet, 0}]), 81 | Req = build_request(Type, URI, Body), 82 | gen_tcp:send(Socket, Req), 83 | {ok, Resp} = do_recv(Socket, []), 84 | gen_tcp:close(Socket), 85 | {ok,_, ResponseBody} = erlang:decode_packet(http, Resp, []), 86 | decode_json(parse_response(ResponseBody)). 87 | 88 | %% @private 89 | %% Added suport to change the ContentType 90 | raw_request(Type, Server, Port, URI, ContentType, Body) -> 91 | {ok, Socket} = gen_tcp:connect(Server, Port, [binary, {active, false}, {packet, 0}]), 92 | Req = build_request(Type, URI, ContentType, Body), 93 | gen_tcp:send(Socket, Req), 94 | {ok, Resp} = do_recv(Socket, []), 95 | gen_tcp:close(Socket), 96 | {ok,_, ResponseBody} = erlang:decode_packet(http, Resp, []), 97 | decode_json(parse_response(ResponseBody)). 98 | 99 | %% @private 100 | do_recv(Sock, Bs) -> 101 | case gen_tcp:recv(Sock, 0) of 102 | {ok, B} -> 103 | do_recv(Sock, [Bs | B]); 104 | {error, closed} -> 105 | {ok, erlang:iolist_to_binary(Bs)} 106 | end. 107 | 108 | %% @private 109 | %% For a given http response, disregard everything up to the first new line 110 | %% which should be the response body. 99.999% of the time we don't care 111 | %% about the response code or headers. Any sort of error will surface as 112 | %% a parse error in mochijson2:decode/1. 113 | parse_response(<<13,10,13,10,Data/binary>>) -> binary_to_list(Data); 114 | parse_response(<<_X:1/binary,Data/binary>>) -> parse_response(Data). 115 | 116 | %% @private 117 | %% Build the HTTP 1.0 request for the Type of request, the URI and 118 | %% optionally a body. If there is a body then find it's length and send that 119 | %% as well. The content-type is hard-coded because this client will never 120 | %% send anything other than json. 121 | build_request(Type, URI, []) -> 122 | list_to_binary(lists:concat([Type, " ", URI, " HTTP/1.0\r\nContent-Type: application/json\r\n\r\n"])); 123 | 124 | build_request(Type, URI, Body) -> 125 | erlang:iolist_to_binary([ 126 | lists:concat([Type, " ", URI, " HTTP/1.0\r\n" 127 | "Content-Length: ", erlang:iolist_size(Body), "\r\n" 128 | "Content-Type: application/json\r\n\r\n" 129 | ]), 130 | Body 131 | ]). 132 | 133 | %% @private 134 | %% Added suport to change the ContentType 135 | build_request(Type, URI, ContentType, Body) -> 136 | erlang:iolist_to_binary([ 137 | lists:concat([Type, " ", URI, " HTTP/1.0\r\n" 138 | "Content-Length: ", erlang:iolist_size(Body), "\r\n" 139 | "Content-Type: ", ContentType, "\r\n\r\n" 140 | ]), 141 | Body 142 | ]). 143 | 144 | %% @private 145 | %% The build_uri/0, /1, /2, /3 and view_uri/4 functions are used to create 146 | %% the URI's mapping to databases, documents and design documents. Some URIs 147 | %% also have query string parameters which are computed here as well. 148 | %% NOTE: Converting the property list representing query string parameters 149 | %% to the actual query string is done by mochiweb_util:urlencode/1. Make 150 | %% sure that module is in the Erlang lib path. 151 | build_uri() -> 152 | lists:concat(["/"]). 153 | 154 | %% @private 155 | build_uri(Database) -> 156 | lists:concat(["/", Database]). 157 | 158 | %% @private 159 | build_uri(Database, Request) -> 160 | lists:concat(["/", Database, "/", Request]). 161 | 162 | %% @private 163 | build_uri(Database, Request, Attributes) -> 164 | QueryString = build_querystring(Attributes), 165 | lists:concat(["/", Database, "/", Request, QueryString]). 166 | 167 | %% @private 168 | view_uri(Database, ViewName, ViewId, Args) -> 169 | lists:concat(["/", Database, "/_design/", ViewName, "/_view/", ViewId, build_querystring(Args)]). 170 | 171 | %% @private 172 | build_querystring([]) -> []; 173 | build_querystring(PropList) -> 174 | lists:concat(["?", mochiweb_util:urlencode(PropList)]). 175 | 176 | %% @private 177 | %% Attempt to decode a JSON body into Erlang structures. If parsing fails 178 | %% then simply return the raw data and let the user deal with it. This is 179 | %% the only place where a try/catch block is used to minimize this module's 180 | %% interaction with the user's environment. 181 | decode_json(Body) -> 182 | try mochijson2:decode(Body) of 183 | Response -> {json, Response} 184 | catch 185 | _:_ -> {raw, Body} 186 | end. 187 | 188 | %% @spec create_database(DBServer::server_address(), Database::string()) -> ok | {error, Reason::any()} 189 | %% 190 | %% @type server_address() = {Host::string(), ServerPort::integer()} 191 | %% 192 | %% @doc Create a new database. 193 | create_database({Server, ServerPort}, Database) when is_list(Server), is_integer(ServerPort) -> 194 | Url = build_uri(Database), 195 | case raw_request("PUT", Server, ServerPort, Url, []) of 196 | {json, {struct, [{<<"ok">>, true}]}} -> ok; 197 | Other -> {error, Other} 198 | end. 199 | 200 | %% @spec delete_database(DBServer::server_address(), Database::string()) -> ok | {error, Reason::any()} 201 | %% 202 | %% @doc Delete a database. 203 | delete_database({Server, ServerPort}, Database) when is_list(Server), is_integer(ServerPort) -> 204 | Url = build_uri(Database), 205 | case raw_request("DELETE", Server, ServerPort, Url, []) of 206 | {json, {struct, [{<<"ok">>, true}]}} -> ok; 207 | Other -> {error, Other} 208 | end. 209 | 210 | %% @spec database_info(DBServer::server_address(), Database::string()) -> {ok, Info::any()} | {error, Reason::any()} 211 | %% 212 | %% @doc Get info about a database. 213 | database_info({Server, ServerPort}, Database) when is_list(Server), is_integer(ServerPort) -> 214 | Url = build_uri(Database), 215 | case raw_request("GET", Server, ServerPort, Url, []) of 216 | {json, {struct, Info}} -> {ok, Info}; 217 | Other -> {error, Other} 218 | end. 219 | 220 | %% @spec server_info(DBServer::server_address()) -> {ok, Welcome::any()} | {other, Other::any()} 221 | %% 222 | %% @doc Get info about a server. 223 | server_info({Server, ServerPort}) when is_list(Server), is_integer(ServerPort) -> 224 | Url = build_uri(), 225 | case raw_request("GET", Server, ServerPort, Url, []) of 226 | {json, {struct, Welcome}} -> {ok, Welcome}; 227 | Other -> {other, Other} 228 | end. 229 | 230 | %% @spec retrieve_all_dbs(DBServer::server_address()) -> {ok, Database::any()} | {other, Other::any()} 231 | %% 232 | %% @doc Retieve all the databases 233 | retrieve_all_dbs({Server, ServerPort}) when is_list(Server), is_integer(ServerPort) -> 234 | case raw_request("GET", Server, ServerPort, "/_all_dbs",[]) of 235 | {json, Database} -> {ok, Database}; 236 | Other -> {error, Other} 237 | end. 238 | 239 | %% @spec create_attachment(DBServer::server_address(), Database::string(), DocumentID::string(), File::string(), ContentType::string()) -> {"ok": true, "id": "document", "rev": Rev::string()} 240 | %% 241 | %% @doc Create a new attachment document. 242 | create_attachment({Server, ServerPort}, Database, DocumentID, File, ContentType) -> 243 | {ok, Body} = file:read_file(File), 244 | Url = build_uri(Database, DocumentID ++ "/attachment"), 245 | erlang_couchdb:raw_request("PUT", Server, ServerPort, Url, ContentType, Body). 246 | 247 | %% @spec create_document(DBServer::server_address(), Database::string(), Attributes::any()) -> {json, Response::any()} | {raw, Other::any()} 248 | %% 249 | %% @doc Create a new document. This function will create a document with a 250 | %% list of attributes and leaves it up to the server to create an id for it. 251 | %% The attributes should be a list of binary key/value tuples. 252 | create_document({Server, ServerPort}, Database, Attributes) when is_list(Server), is_integer(ServerPort), is_list(Attributes) -> 253 | create_document({Server, ServerPort}, Database, {struct, Attributes}); 254 | create_document({Server, ServerPort}, Database, {struct, _} = Obj) when is_list(Server), is_integer(ServerPort) -> 255 | Url = build_uri(Database), 256 | JSON = list_to_binary(mochijson2:encode(Obj)), 257 | raw_request("POST", Server, ServerPort, Url, JSON). 258 | 259 | %% @spec create_document(DBServer::server_address(), Database::string(), DocumentID::string(), Attributes::any()) -> {json, Response::any()} | {raw, Other::any()} 260 | %% 261 | %% @doc Create a new document with a specific document ID. This is just an 262 | %% accessor function to update_document/4 when the intent is to create a 263 | %% new document. 264 | create_document({Server, ServerPort}, Database, DocumentID, Attributes) when is_list(Server), is_integer(ServerPort) -> 265 | update_document({Server, ServerPort}, Database, DocumentID, Attributes). 266 | 267 | %% @doc Create many documents in bulk. 268 | %% This function created and submitted by Pablo Sortino, applied on 2008-10-25. 269 | %% @todo Create a spec for this. 270 | create_documents({Server, ServerPort}, Database, Documents) when is_list(Server), is_integer(ServerPort) -> 271 | Url = build_uri(Database, "_bulk_docs"), 272 | BulkCreate = {struct, [ 273 | {<<"docs">>, [ 274 | {struct, Doc} || Doc <- Documents 275 | ]} 276 | ]}, 277 | JSON = list_to_binary(mochijson2:encode(BulkCreate)), 278 | raw_request("POST", Server, ServerPort, Url, JSON). 279 | 280 | %% @doc Return a tuple containing a document id and the document's latest 281 | %% revision. 282 | %% @todo Create a spec for this. 283 | document_revision({Server, ServerPort}, Database, DocID) when is_binary(DocID) -> 284 | document_revision({Server, ServerPort}, Database, binary_to_list(DocID)); 285 | document_revision({Server, ServerPort}, Database, DocID) when is_list(Server), is_integer(ServerPort) -> 286 | Url = build_uri(Database, DocID, []), 287 | case raw_request("GET", Server, ServerPort, Url, []) of 288 | {json, {struct, Props}} -> 289 | {ok, proplists:get_value(<<"_id">>, Props, undefined), proplists:get_value(<<"_rev">>, Props, undefined)}; 290 | Other -> {error, Other} 291 | end. 292 | 293 | %% @doc Fetches a document by it's id. 294 | %% @todo Create a spec for this. 295 | retrieve_document({Server, ServerPort}, Database, DocID) -> 296 | retrieve_document({Server, ServerPort}, Database, DocID, []). 297 | 298 | %% @doc Fetches a document by it's id and also some attributes. Attributes 299 | %% should be a list of non binary key/value pair tuples. 300 | %% @todo Create a spec for this. 301 | retrieve_document({Server, ServerPort}, Database, DocID, Attributes) when is_list(Server), is_integer(ServerPort) -> 302 | Url = build_uri(Database, DocID, Attributes), 303 | raw_request("GET", Server, ServerPort, Url, []). 304 | 305 | %% @doc Sets the attributes for a document with an idea. This function is a 306 | %% bit misleading because it can be used to update an existing document 307 | %% or create a new one with a specified id. If this function is used to 308 | %% update a document the attributes list must contain a '_rev' key/value 309 | %% pair tuple. 310 | %% @todo Create a spec for this. 311 | update_document({Server, ServerPort}, Database, DocID, {struct,_} = Obj) when is_list(Server), is_integer(ServerPort) -> 312 | Url = build_uri(Database, DocID), 313 | JSON = list_to_binary(mochijson2:encode(Obj)), 314 | raw_request("PUT", Server, ServerPort, Url, JSON); 315 | update_document({Server, ServerPort}, Database, DocID, Attributes) when is_list(Server), is_integer(ServerPort) -> 316 | Url = build_uri(Database, DocID), 317 | JSON = list_to_binary(mochijson2:encode({struct, Attributes})), 318 | raw_request("PUT", Server, ServerPort, Url, JSON). 319 | 320 | %% @doc Deletes a given document by id and revision. 321 | %% @todo Create a spec for this. 322 | delete_document({Server, ServerPort}, Database, DocID, Revision) when is_list(Server), is_integer(ServerPort) -> 323 | Url = build_uri(Database, DocID, [{"rev", Revision}]), 324 | raw_request("DELETE", Server, ServerPort, Url, []). 325 | 326 | %% @doc Delete a bunch of documents with a _bulk_docs request. 327 | delete_documents({Server, ServerPort}, Database, Documents) when is_list(Server), is_integer(ServerPort) -> 328 | Url = build_uri(Database, "_bulk_docs"), 329 | BulkDelete = {struct, [ 330 | {<<"docs">>, [ 331 | {struct, [{<<"_id">>, Id}, {<<"_rev">>, Rev}, {<<"_deleted">>, true}]} || {Id, Rev} <- Documents 332 | ]} 333 | ]}, 334 | JSON = list_to_binary(mochijson2:encode(BulkDelete)), 335 | raw_request("POST", Server, ServerPort, Url, JSON). 336 | 337 | %% @doc Creates a design document. See create_view/6 for more. 338 | %% @todo Create a spec for this. 339 | create_view({Server, ServerPort}, Database, ViewClass, Language, Views) -> 340 | create_view({Server, ServerPort}, Database, ViewClass, Language, Views, []). 341 | 342 | %% @doc Creates or updates a design document. The Views parameter should be 343 | %% a list of tuples representing the view's data. When updating an existing 344 | %% view please be sure to include the _rev field in the Attributes 345 | %% parameter. 346 | %% @todo Create a spec for this. 347 | create_view({Server, ServerPort}, Database, ViewClass, Language, Views, Attributes) when is_list(Server), is_integer(ServerPort), is_list(Language) -> 348 | create_view({Server, ServerPort}, Database, ViewClass, list_to_binary(Language), Views, Attributes) 349 | ; 350 | create_view({Server, ServerPort}, Database, ViewClass, Language, Views, Attributes) when is_list(Server), is_integer(ServerPort) -> 351 | Design = [ 352 | {<<"_id">>, list_to_binary("_design/" ++ ViewClass)}, 353 | {<<"language">>, Language}, 354 | {<<"views">>, {struct, [ 355 | begin 356 | case View of 357 | {Name, Map} -> 358 | {Name, {struct, [{<<"map">>, list_to_binary(Map)}]}}; 359 | {Name, Map, Reduce} -> 360 | {Name, {struct, [{<<"map">>, list_to_binary(Map)}, {<<"reduce">>, list_to_binary(Reduce)}]}} 361 | end 362 | end || View <- Views 363 | ]}} 364 | | Attributes], 365 | JSON = list_to_binary(mochijson2:encode({struct, Design})), 366 | Url = build_uri(Database, "_design/" ++ ViewClass), 367 | raw_request("PUT", Server, ServerPort, Url, JSON). 368 | 369 | %% @doc Executes a view with or without some attributes as modifiers. 370 | %% @todo Create a spec for this. 371 | invoke_view({Server, ServerPort}, Database, ViewClass, ViewId, Attributes) when is_list(Server), is_integer(ServerPort) -> 372 | Url = view_uri(Database, ViewClass, ViewId, Attributes), 373 | raw_request("GET", Server, ServerPort, Url, []). 374 | 375 | %% @todo Document this. 376 | %% @todo Create a spec for this. 377 | invoke_multikey_view({Server, ServerPort}, Database, ViewClass, ViewId, Keys, Attributes) when is_list(Server), is_integer(ServerPort) -> 378 | Url = view_uri(Database, ViewClass, ViewId, Attributes), 379 | JSON = list_to_binary(mochijson2:encode({struct, [{keys, Keys}]})), 380 | raw_request("POST", Server, ServerPort, Url, JSON). 381 | 382 | %% @doc Return a list of document ids for a given view. 383 | %% @todo Create a spec for this. 384 | parse_view({json, {struct, [{<<"error">>, _Code}, {_, _Reason}]}}) -> 385 | {0, 0, []}; 386 | parse_view({json, Structure}) -> 387 | {struct, Properties} = Structure, 388 | TotalRows = proplists:get_value(<<"total_rows">>, Properties, 0), 389 | Offset = proplists:get_value(<<"offset">>, Properties, 0), 390 | Data = proplists:get_value(<<"rows">>, Properties, []), 391 | Ids = [begin 392 | {struct, Bits} = Rec, 393 | Id = proplists:get_value(<<"id">>, Bits), 394 | case proplists:get_value(<<"value">>, Bits, []) of 395 | [] -> Id; 396 | {struct, RowValues} -> {Id, RowValues}; 397 | _ -> Id 398 | end 399 | end || Rec <- Data], 400 | {TotalRows, Offset, Ids}; 401 | parse_view(_Other) -> {0, 0, []}. 402 | 403 | %% @doc Fetch a number of UUIDs from a CouchDB server. 404 | %% @todo Create a spec for this. 405 | fetch_ids({Server, ServerPort}, Limit) -> 406 | Url = build_uri(lists:concat(["_uuids?count=", Limit])), 407 | raw_request("POST", Server, ServerPort, Url, []). 408 | 409 | %% @doc Create a design document based on a file's contents. 410 | %% Warning! This function is experimental. 411 | load_view({Server, ServerPort}, Database, ViewName, File) -> 412 | {ok, FH2} = file:open(File, [read, binary]), 413 | {ok, Data2} = file:read(FH2, 9999), 414 | erlang_couchdb:create_document( 415 | {Server, ServerPort}, Database, 416 | "_design/" ++ ViewName, 417 | [{<<"language">>, <<"javascript">>}, {<<"views">>, mochijson2:decode(Data2)}] 418 | ). 419 | 420 | %% @doc Get the specified (set of) attribute(s) 421 | %% @todo Create a spec for this. 422 | %% @private 423 | get_value(Path, Struct) when is_list(Path) -> 424 | get_val(Path, Struct); 425 | get_value(Key, Struct) when is_binary(Key) -> 426 | {struct, L} = Struct, 427 | proplists:get_value(Key, L). 428 | 429 | %% @private 430 | get_val([Key], Struct) -> 431 | get_value(Key, Struct); 432 | get_val([Key | T], Struct) -> 433 | case get_value(Key, Struct) of 434 | List when is_list(List) -> [get_val(T, X) || X <- List]; 435 | NewStruct when is_tuple(NewStruct) -> get_val(T, NewStruct) 436 | end. 437 | 438 | %% @private 439 | %% @doc Set the specified (set of) attribute(s) 440 | set_value(Path, Value, Struct) when is_list(Path) -> 441 | [H | T] = lists:reverse(Path), 442 | set_val(T, Struct, {struct, [{H, Value}]}); 443 | set_value(Key, Value, Struct) when is_binary(Key) -> 444 | extend(Struct, {struct, [{Key, Value}]}). 445 | 446 | %% @private 447 | set_val([], Struct, Result) -> 448 | extend(Struct, Result); 449 | set_val([Key | T], Struct, Result) -> 450 | set_val(T, Struct, {struct, [{Key, Result}]}). 451 | 452 | %% @private 453 | %% @doc To be used with the fold function 454 | set_value(Key, Value) when is_binary(Key) -> 455 | fun(Struct) -> set_value(Key, vals(Value), Struct) end. 456 | 457 | %% @private 458 | vals(B) when is_binary(B) -> B; 459 | vals(I) when is_integer(I) -> I; 460 | vals(L) when is_list(L) -> list_to_binary(L); 461 | vals(A) when is_atom(A) -> vals(atom_to_list(A)). 462 | 463 | %% @private 464 | %% @doc Apply a list of set-functions on an initial object. 465 | fold([H|T], Struct) -> fold(T, H(Struct)); 466 | fold([], Struct) -> Struct. 467 | 468 | %% @private 469 | %% @doc Return an empty object 470 | empty() -> {struct, []}. 471 | 472 | %% @private 473 | %% @doc Extend a json obj with one or more json obj (add new leaves and modify the existing ones). 474 | extend(S1, []) -> S1; 475 | extend(S1, [S|T]) -> 476 | NewS = extend(S1, S), 477 | extend(NewS, T); 478 | extend(S1, S2) -> 479 | {struct, L1} = S1, 480 | {struct, L2} = S2, 481 | ext(L1, L2, []). 482 | 483 | %% @private 484 | ext(L1, [], Result) -> 485 | {struct, lists:append(Result,L1)}; 486 | ext(L1, [{K, {struct, ChildL2}} | T], Result) -> 487 | case proplists:get_value(K, L1) of 488 | {struct, ChildL1} -> 489 | NewL1 = proplists:delete(K, L1), 490 | ext(NewL1, T, [{K, extend({struct, ChildL1}, {struct, ChildL2})} | Result]); 491 | _ -> 492 | NewL1 = proplists:delete(K, L1), 493 | ext(NewL1, T, [{K, {struct, ChildL2}} | Result]) 494 | end; 495 | ext(L1, [{K, V} | T], Result) -> 496 | NewL1 = proplists:delete(K, L1), 497 | ext(NewL1, T, [{K,V} | Result]). 498 | --------------------------------------------------------------------------------