You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@couchdb.apache.org by rn...@apache.org on 2015/06/25 11:38:54 UTC
[2/6] couchdb-couch-epi git commit: Initial version
Initial version
Add `ignore_providers` option
Rename `hash(FilePath)` into `hashof_file(FilePath)`
Use monitor to unsubscribe when subscriber die
Rename couch_epi:all into couch_epi:dump
Remove modules from dispatch on termination
Add all/any convinence funs to couch_epi
Document `ignore_providers` option
Add childspec helper to _data_source and _functions
Add license to test suite
Project: http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/commit/32ba6e79
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/tree/32ba6e79
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/diff/32ba6e79
Branch: refs/heads/master
Commit: 32ba6e793460f9a4962cd5885fd14361554c52ed
Parents: cf2e463
Author: ILYA Khlopotov <ii...@ca.ibm.com>
Authored: Wed Jun 10 16:02:59 2015 -0700
Committer: ILYA Khlopotov <ii...@ca.ibm.com>
Committed: Wed Jun 24 15:13:41 2015 -0700
----------------------------------------------------------------------
LICENSE | 203 +++++++++++++++
README.md | 113 ++++++++
rebar.config | 3 +
src/couch_epi.app.src | 24 ++
src/couch_epi.erl | 158 ++++++++++++
src/couch_epi_app.erl | 23 ++
src/couch_epi_codegen.erl | 72 ++++++
src/couch_epi_data_gen.erl | 283 ++++++++++++++++++++
src/couch_epi_data_source.erl | 191 ++++++++++++++
src/couch_epi_functions.erl | 155 +++++++++++
src/couch_epi_functions_gen.erl | 347 +++++++++++++++++++++++++
src/couch_epi_server.erl | 144 +++++++++++
src/couch_epi_sup.erl | 38 +++
src/couch_epi_util.erl | 24 ++
test/couch_epi_data_source_tests.erl | 90 +++++++
test/couch_epi_functions_tests.erl | 126 +++++++++
test/couch_epi_tests.erl | 413 ++++++++++++++++++++++++++++++
test/fixtures/app_data1.cfg | 4 +
test/fixtures/app_data2.cfg | 8 +
19 files changed, 2419 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/LICENSE
----------------------------------------------------------------------
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..94ad231
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,203 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..008adf6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,113 @@
+# What it is
+
+`couch_epi` is extensible plugin interface (EPI) for couchdb.
+
+## Requirements
+
+ 1. Automatically discoverable
+ 2. Minimize apps that need to be started for tests
+ 3. Support release upgrades
+
+## Glossary
+
+ * service - an abstract functionality defined by unique name and API
+ * provider - a self-contained implementation of `Service`'s API
+ * subscriber - an application or a process which uses functionality provided by `Provider`
+ * epi_key - is a routing key it has to be in on of the following forms
+ - `{service_id :: atom(), key :: term()}` - for `couch_epi_data_source`
+ - `service_id :: atom()` - for `couch_epi_functions`
+ * handle - is opaque data structure returned from `couch_epi:get_handle(EpiKey)`
+
+## Support release upgrade
+
+We monitor the source of config information and have an ability to notify the subscriber.
+The source is either a file for a `couch_epi_data_source` or module for `couch_epi_functions`.
+
+If the subscriber wants to receive notifications when the config has been updated it can use:
+
+ couch_epi:subscribe(App, Key, Module, Func, ExtraArgs)
+
+The function would be called with following arguments
+
+ Fun(App :: app(), Key :: key(),
+ OldData :: notification(), NewData :: notification(),
+ ExtraArgs :: term()
+
+The `notification()` is either `{data, term()}` or `{modules, [module()]}`
+
+
+
+## data_source example
+
+Any application that wants to register some configuration data for a service
+could add an entry in its supervision tree with something like:
+
+ {
+ appname_stats,
+ {couch_epi_data_source, start_link, [
+ appname,
+ {epi_key, {couch_stats, definitions}},
+ {priv_file, "couch_stats.cfg"},
+ [{interval, 100}]
+ ]},
+ permanent,
+ 5000,
+ worker,
+ dynamic
+ }
+
+Note we also support `{file, FilePath}` instead of `{priv_file, File}`
+
+When service provider wants to learn about all the installed config data for it to use
+it would then just do something like:
+
+
+ couch_epi:get(Handle, Service, Key)
+
+There are also additional functions to get the same data in various formats
+
+- `couch_epi:all(Handle)` - returns config data for all services for a given handle
+- `couch_epi:get(Handle, Subscriber)` - returns config data for a given subscriber
+- `couch_epi:get_value(Handle, Subscriber, Key)` - returns config data for a given subscriber and key
+- `couch_epi:by_key(Handle, Key)` - returns config data for a given key
+- `couch_epi:by_key(Handle)` - returns config data grouped by key
+- `couch_epi:by_source(Handle)` - returns config data grouped by source (subscriber)
+- `couch_epi:keys(Handle)` - returns list of configured keys
+- `couch_epi:subscribers(Handle)` - return list of known subscribers
+
+# Function dispatch example
+
+Any application that wants to register some functions for a service
+could add an entry in its supervision tree with something like:
+
+ {
+ appname_stats,
+ {couch_epi_functions, start_link, [
+ appname,
+ {epi_key, my_service},
+ {modules, [my_module]},
+ [{interval, 100}]
+ ]},
+ permanent,
+ 5000,
+ worker,
+ dynamic
+ }
+
+Adding the entry would generate a dispatch methods for any exported function of modules passed.
+
+
+When app wants to dispatch the call to all service providers it calls
+
+ couch_epi:apply(Handle, ServiceId, Function, Args, Opts)
+
+There are multiple ways of doing the apply which is controlled by Opts
+
+ - ignore_errors - the call is wrapped into try/catch
+ - concurrent - spawn a new process for every service provider
+ - pipe - use output of one service provider as an input for the next one
+ - ignore_providers - do not fail if there are no providers for the service are available
+
+Notes:
+
+ - `concurrent` is incompatible with `pipe`
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/rebar.config
----------------------------------------------------------------------
diff --git a/rebar.config b/rebar.config
new file mode 100644
index 0000000..82db830
--- /dev/null
+++ b/rebar.config
@@ -0,0 +1,3 @@
+{cover_enabled, true}.
+
+{cover_print_enabled, true}.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi.app.src
----------------------------------------------------------------------
diff --git a/src/couch_epi.app.src b/src/couch_epi.app.src
new file mode 100644
index 0000000..b4a433f
--- /dev/null
+++ b/src/couch_epi.app.src
@@ -0,0 +1,24 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+{application, couch_epi,
+ [
+ {description, "extensible plugin interface"},
+ {vsn, "1"},
+ {registered, [couch_epi_sup, couch_epi_server]},
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {mod, { couch_epi_app, []}},
+ {env, []}
+ ]}.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi.erl b/src/couch_epi.erl
new file mode 100644
index 0000000..8787c88
--- /dev/null
+++ b/src/couch_epi.erl
@@ -0,0 +1,158 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi).
+
+%% subscribtion management
+-export([subscribe/5, unsubscribe/1, get_handle/1]).
+
+%% queries and introspection
+-export([
+ dump/1, get/2, get_value/3,
+ by_key/1, by_key/2, by_source/1, by_source/2,
+ keys/1, subscribers/1]).
+
+%% apply
+-export([apply/5]).
+-export([any/5, all/5]).
+
+-export_type([service_id/0, app/0, key/0, handle/0, notify_cb/0]).
+
+-type app() :: atom().
+-type key() :: term().
+-type service_id() :: atom().
+
+-type properties() :: [{key(), term()}].
+
+-type notification() :: {data, term()} | {modules, [module()]}.
+-type notify_cb() :: fun(
+ (App :: app(), Key :: key(), Data :: notification(), Extra :: term()) -> ok).
+
+-type subscription() :: term().
+
+-opaque handle() :: module().
+
+-type apply_opt()
+ :: ignore_errors
+ | ignore_providers
+ | concurrent
+ | pipe.
+
+-type apply_opts() :: [apply_opt()].
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+-spec dump(Handle :: handle()) ->
+ [Config :: properties()].
+
+dump(Handle) ->
+ couch_epi_data_gen:get(Handle).
+
+-spec get(Handle :: handle(), Key :: key()) ->
+ [Config :: properties()].
+
+get(Handle, Key) ->
+ couch_epi_data_gen:get(Handle, Key).
+
+-spec get_value(Handle :: handle(), Subscriber :: app(), Key :: key()) ->
+ properties().
+
+get_value(Handle, Subscriber, Key) ->
+ couch_epi_data_gen:get(Handle, Subscriber, Key).
+
+
+-spec by_key(Handle :: handle()) ->
+ [{Key :: key(), [{Source :: app(), properties()}]}].
+
+by_key(Handle) ->
+ couch_epi_data_gen:by_key(Handle).
+
+
+-spec by_key(Handle :: handle(), Key :: key()) ->
+ [{Source :: app(), properties()}].
+
+by_key(Handle, Key) ->
+ couch_epi_data_gen:by_key(Handle, Key).
+
+
+-spec by_source(Handle :: handle()) ->
+ [{Source :: app(), [{Key :: key(), properties()}]}].
+
+by_source(Handle) ->
+ couch_epi_data_gen:by_source(Handle).
+
+
+-spec by_source(Handle :: handle(), Subscriber :: app()) ->
+ [{Key :: key(), properties()}].
+
+by_source(Handle, Subscriber) ->
+ couch_epi_data_gen:by_source(Handle, Subscriber).
+
+
+-spec keys(Handle :: handle()) ->
+ [Key :: key()].
+
+keys(Handle) ->
+ couch_epi_data_gen:keys(Handle).
+
+
+-spec subscribers(Handle :: handle()) ->
+ [Subscriber :: app()].
+
+subscribers(Handle) ->
+ couch_epi_data_gen:subscribers(Handle).
+
+
+%% Passed MFA should implement notify_cb() type
+-spec subscribe(App :: app(), Key :: key(),
+ Module :: module(), Function :: atom(), Args :: [term()]) ->
+ {ok, SubscriptionId :: subscription()}.
+
+subscribe(App, Key, M, F, A) ->
+ couch_epi_server:subscribe(App, Key, {M, F, A}).
+
+
+-spec unsubscribe(SubscriptionId :: subscription()) -> ok.
+
+unsubscribe(SubscriptionId) ->
+ couch_epi_server:unsubscribe(SubscriptionId).
+
+%% The success typing is (atom() | tuple(),_,_,[any()],_) -> [any()]
+-spec apply(Handle :: handle(), ServiceId :: atom(), Function :: atom(),
+ Args :: [term()], Opts :: apply_opts()) -> ok.
+
+apply(Handle, ServiceId, Function, Args, Opts) ->
+ couch_epi_functions_gen:apply(Handle, ServiceId, Function, Args, Opts).
+
+-spec get_handle({ServiceId :: service_id(), Key :: key()}) -> handle();
+ (ServiceId :: service_id()) -> handle().
+
+get_handle({_ServiceId, _Key} = EPIKey) ->
+ couch_epi_data_gen:get_handle(EPIKey);
+get_handle(ServiceId) when is_atom(ServiceId) ->
+ couch_epi_functions_gen:get_handle(ServiceId).
+
+-spec any(Handle :: handle(), ServiceId :: atom(), Function :: atom(),
+ Args :: [term()], Opts :: apply_opts()) -> boolean().
+
+any(Handle, ServiceId, Function, Args, Opts) ->
+ Replies = apply(Handle, ServiceId, Function, Args, Opts),
+ [] /= [Reply || Reply <- Replies, Reply == true].
+
+-spec all(Handle :: handle(), ServiceId :: atom(), Function :: atom(),
+ Args :: [term()], Opts :: apply_opts()) -> boolean().
+
+all(Handle, ServiceId, Function, Args, Opts) ->
+ Replies = apply(Handle, ServiceId, Function, Args, Opts),
+ [] == [Reply || Reply <- Replies, Reply == false].
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_app.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_app.erl b/src/couch_epi_app.erl
new file mode 100644
index 0000000..0dd42c2
--- /dev/null
+++ b/src/couch_epi_app.erl
@@ -0,0 +1,23 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_app).
+
+-behaviour(application).
+
+-export([start/2, stop/1]).
+
+start(_Type, _Args) ->
+ couch_epi_sup:start_link().
+
+stop(_State) ->
+ ok.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_codegen.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_codegen.erl b/src/couch_epi_codegen.erl
new file mode 100644
index 0000000..caedb88
--- /dev/null
+++ b/src/couch_epi_codegen.erl
@@ -0,0 +1,72 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_codegen).
+
+-export([generate/2, scan/1, parse/1, function/1, format_term/1]).
+
+generate(ModuleName, Forms) when is_atom(ModuleName) ->
+ generate(atom_to_list(ModuleName), Forms);
+generate(ModuleName, Forms0) ->
+ Forms = scan("-module(" ++ ModuleName ++ ").") ++ Forms0,
+ ASTForms = parse(Forms),
+ {ok, Mod, Bin} = compile:forms(ASTForms, [verbose, report_errors]),
+ {module, Mod} = code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin),
+ ok.
+
+scan(String) ->
+ Exprs = [E || E <- re:split(String, "\\.\n", [{return, list}, trim])],
+ FormsTokens = lists:foldl(fun(Expr, Acc) ->
+ case erl_scan:string(Expr) of
+ {ok, [], _} ->
+ Acc;
+ {ok, Tokens, _} ->
+ [{Expr, fixup_terminator(Tokens)} | Acc]
+ end
+ end, [], Exprs),
+ lists:reverse(FormsTokens).
+
+parse(FormsTokens) ->
+ ASTForms = lists:foldl(fun(Tokens, Forms) ->
+ {ok, AST} = parse_form(Tokens),
+ [AST | Forms]
+ end, [], FormsTokens),
+ lists:reverse(ASTForms).
+
+format_term(Data) ->
+ lists:flatten(io_lib:format("~w", [Data])).
+
+parse_form(Tokens) ->
+ {Expr, Forms} = split_expression(Tokens),
+ case erl_parse:parse_form(Forms) of
+ {ok, AST} -> {ok, AST};
+ {error,{_,_, Reason}} ->
+ %%Expr = [E || {E, _Form} <- Tokens],
+ {error, Expr, Reason}
+ end.
+
+split_expression({Expr, Forms}) -> {Expr, Forms};
+split_expression(Tokens) ->
+ {Exprs, Forms} = lists:unzip(Tokens),
+ {string:join(Exprs, "\n"), lists:append(Forms)}.
+
+function(Clauses) ->
+ [lists:flatten(Clauses)].
+
+fixup_terminator(Tokens) ->
+ case lists:last(Tokens) of
+ {dot, _} -> Tokens;
+ {';', _} -> Tokens;
+ Token ->
+ {line, Line} = erl_scan:token_info(Token, line),
+ Tokens ++ [{dot, Line}]
+ end.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_data_gen.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_data_gen.erl b/src/couch_epi_data_gen.erl
new file mode 100644
index 0000000..73cf901
--- /dev/null
+++ b/src/couch_epi_data_gen.erl
@@ -0,0 +1,283 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_data_gen).
+
+%% @doc
+%% We generate and compile module with name constructed as:
+%% "couch_epi_data_" + Service + "_" + Key
+%% To get an idea about he code of the generated module see preamble()
+
+-export([get_handle/1]).
+-export([set/3, get/1, get/2, get/3]).
+-export([by_key/1, by_key/2]).
+-export([by_source/1, by_source/2]).
+-export([keys/1, subscribers/1]).
+
+set(Handle, Source, Data) ->
+ case is_updated(Handle, Source, Data) of
+ false ->
+ ok;
+ true ->
+ save(Handle, Source, Data)
+ end.
+
+get(Handle) ->
+ Handle:all().
+
+get(Handle, Key) ->
+ Handle:all(Key).
+
+get(Handle, Source, Key) ->
+ Handle:get(Source, Key).
+
+by_key(Handle) ->
+ Handle:by_key().
+
+by_key(Handle, Key) ->
+ Handle:by_key(Key).
+
+by_source(Handle) ->
+ Handle:by_source().
+
+by_source(Handle, Source) ->
+ Handle:by_source(Source).
+
+keys(Handle) ->
+ Handle:keys().
+
+subscribers(Handle) ->
+ Handle:subscribers().
+
+get_handle({Service, Key}) ->
+ module_name({atom_to_list(Service), atom_to_list(Key)}).
+
+%% ------------------------------------------------------------------
+%% Codegeneration routines
+%% ------------------------------------------------------------------
+
+preamble() ->
+ "
+ -export([by_key/0, by_key/1]).
+ -export([by_source/0, by_source/1]).
+ -export([all/0, all/1, get/2]).
+ -export([version/0, version/1]).
+ -export([keys/0, subscribers/0]).
+ -compile({no_auto_import,[get/0, get/1]}).
+ all() ->
+ lists:foldl(fun({Key, Defs}, Acc) ->
+ [D || {_Subscriber, D} <- Defs ] ++ Acc
+ end, [], by_key()).
+
+ all(Key) ->
+ lists:foldl(fun({Subscriber, Data}, Acc) ->
+ [Data | Acc]
+ end, [], by_key(Key)).
+
+ by_key() ->
+ [{Key, by_key(Key)} || Key <- keys()].
+
+ by_key(Key) ->
+ lists:foldl(
+ fun(Source, Acc) -> append_if_defined(Source, get(Source, Key), Acc)
+ end, [], subscribers()).
+
+
+ by_source() ->
+ [{Source, by_source(Source)} || Source <- subscribers()].
+
+ by_source(Source) ->
+ lists:foldl(
+ fun(Key, Acc) -> append_if_defined(Key, get(Source, Key), Acc)
+ end, [], keys()).
+
+ version() ->
+ [{Subscriber, version(Subscriber)} || Subscriber <- subscribers()].
+
+ %% Helper functions
+ append_if_defined(Type, undefined, Acc) -> Acc;
+ append_if_defined(Type, Value, Acc) -> [{Type, Value} | Acc].
+ "
+ %% In addition to preamble we also generate following methods
+ %% get(Source1, Key1) -> Data;
+ %% get(Source, Key) -> undefined.
+
+ %% version(Source1) -> "HASH";
+ %% version(Source) -> {error, {unknown, Source}}.
+
+ %% keys() -> [].
+ %% subscribers() -> [].
+ .
+
+generate(Handle, Defs) ->
+ GetFunForms = couch_epi_codegen:function(getters(Defs)),
+ VersionFunForms = couch_epi_codegen:function(version_method(Defs)),
+ KeysForms = keys_method(Defs),
+ SubscribersForms = subscribers_method(Defs),
+
+ Forms = couch_epi_codegen:scan(preamble())
+ ++ GetFunForms ++ VersionFunForms
+ ++ KeysForms ++ SubscribersForms,
+
+ couch_epi_codegen:generate(Handle, Forms).
+
+keys_method(Defs) ->
+ Keys = couch_epi_codegen:format_term(defined_keys(Defs)),
+ couch_epi_codegen:scan("keys() -> " ++ Keys ++ ".").
+
+subscribers_method(Defs) ->
+ Subscribers = couch_epi_codegen:format_term(defined_subscribers(Defs)),
+ couch_epi_codegen:scan("subscribers() -> " ++ Subscribers ++ ".").
+
+getters(Defs) ->
+ DefaultClause = "get(_S, _K) -> undefined.",
+ fold_defs(Defs, [couch_epi_codegen:scan(DefaultClause)],
+ fun({Source, Key, Data}, Acc) ->
+ getter(Source, Key, Data) ++ Acc
+ end).
+
+version_method(Defs) ->
+ DefaultClause = "version(S) -> {error, {unknown, S}}.",
+ lists:foldl(fun({Source, Data}, Clauses) ->
+ version(Source, Data) ++ Clauses
+ end, [couch_epi_codegen:scan(DefaultClause)], Defs).
+
+getter(Source, Key, Data) ->
+ D = couch_epi_codegen:format_term(Data),
+ Src = atom_to_list(Source),
+ K = couch_epi_codegen:format_term(Key),
+ couch_epi_codegen:scan(
+ "get(" ++ Src ++ ", " ++ K ++ ") ->" ++ D ++ ";").
+
+version(Source, Data) ->
+ Src = atom_to_list(Source),
+ VSN = couch_epi_util:hash(Data),
+ couch_epi_codegen:scan("version(" ++ Src ++ ") ->" ++ VSN ++ ";").
+
+%% ------------------------------------------------------------------
+%% Helper functions
+%% ------------------------------------------------------------------
+
+module_name({Service, Key}) when is_list(Service) andalso is_list(Key) ->
+ list_to_atom(string:join([atom_to_list(?MODULE), Service, Key], "_")).
+
+is_updated(Handle, Source, Data) ->
+ Sig = couch_epi_util:hash(Data),
+ try Handle:version(Source) of
+ {error, {unknown, Source}} -> true;
+ {error, Reason} -> throw(Reason);
+ Sig -> false;
+ _ -> true
+ catch
+ error:undef -> true;
+ Class:Reason ->
+ throw({Class, {Source, Reason}})
+ end.
+
+save(Handle, Source, Data) ->
+ CurrentData = get_current_data(Handle),
+ NewDefs = lists:keystore(Source, 1, CurrentData, {Source, Data}),
+ generate(Handle, NewDefs).
+
+get_current_data(Handle) ->
+ try Handle:by_source()
+ catch error:undef -> []
+ end.
+
+
+defined_keys(Defs) ->
+ Keys = fold_defs(Defs, [], fun({_Source, Key, _Data}, Acc) ->
+ [Key | Acc]
+ end),
+ lists:usort(Keys).
+
+defined_subscribers(Defs) ->
+ [Source || {Source, _} <- Defs].
+
+fold_defs(Defs, Acc, Fun) ->
+ lists:foldl(fun({Source, SourceData}, Clauses) ->
+ lists:foldl(fun({Key, Data}, InAcc) ->
+ Fun({Source, Key, Data}, InAcc)
+ end, [], SourceData) ++ Clauses
+ end, Acc, Defs).
+
+%% ------------------------------------------------------------------
+%% Tests
+%% ------------------------------------------------------------------
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+basic_test() ->
+ Module = foo_bar_baz_bugz,
+ Data1 = [some_nice_data],
+ Data2 = "other data",
+ Data3 = {"even more data"},
+ Defs1 = [{foo, Data1}],
+ Defs2 = lists:usort([{foo, Data2}, {bar, Data3}]),
+
+ set(Module, app1, Defs1),
+ set(Module, app2, Defs2),
+
+ ?assertEqual([bar, foo], lists:usort(Module:keys())),
+ ?assertEqual([app1, app2], lists:usort(Module:subscribers())),
+
+ ?assertEqual(Data1, Module:get(app1, foo)),
+ ?assertEqual(Data2, Module:get(app2, foo)),
+ ?assertEqual(Data3, Module:get(app2, bar)),
+
+ ?assertEqual(undefined, Module:get(bad, key)),
+ ?assertEqual(undefined, Module:get(source, bad)),
+
+ ?assertEqual("3KZ4EG4WBF4J683W8GSDDPYR3", Module:version(app1)),
+ ?assertEqual("4EFUU47W9XDNMV9RMZSSJQU3Y", Module:version(app2)),
+
+ ?assertEqual({error,{unknown,bad}}, Module:version(bad)),
+
+ ?assertEqual(
+ [{app1,"3KZ4EG4WBF4J683W8GSDDPYR3"},
+ {app2,"4EFUU47W9XDNMV9RMZSSJQU3Y"}], lists:usort(Module:version())),
+
+ ?assertEqual(
+ [{app1,[some_nice_data]},{app2,"other data"}],
+ lists:usort(Module:by_key(foo))),
+
+ ?assertEqual([], lists:usort(Module:by_key(bad))),
+
+ ?assertEqual(
+ [
+ {bar, [{app2, {"even more data"}}]},
+ {foo, [{app2, "other data"}, {app1, [some_nice_data]}]}
+ ],
+ lists:usort(Module:by_key())),
+
+
+ ?assertEqual(Defs1, lists:usort(Module:by_source(app1))),
+ ?assertEqual(Defs2, lists:usort(Module:by_source(app2))),
+
+ ?assertEqual([], lists:usort(Module:by_source(bad))),
+
+ ?assertEqual(
+ [
+ {app1, [{foo, [some_nice_data]}]},
+ {app2, [{foo, "other data"}, {bar, {"even more data"}}]}
+ ],
+ lists:usort(Module:by_source())),
+
+ ?assertEqual(
+ lists:usort([Data1, Data2, Data3]), lists:usort(Module:all())),
+ ?assertEqual(lists:usort([Data1, Data2]), lists:usort(Module:all(foo))),
+ ?assertEqual([], lists:usort(Module:all(bad))),
+
+ ok.
+
+-endif.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_data_source.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_data_source.erl b/src/couch_epi_data_source.erl
new file mode 100644
index 0000000..68b4aab
--- /dev/null
+++ b/src/couch_epi_data_source.erl
@@ -0,0 +1,191 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_data_source).
+
+-behaviour(gen_server).
+-define(MONITOR_INTERVAL, 5000).
+
+%% ------------------------------------------------------------------
+%% API Function Exports
+%% ------------------------------------------------------------------
+
+-export([childspec/5]).
+-export([start_link/4, reload/1]).
+-export([wait/1, stop/1]).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Exports
+%% ------------------------------------------------------------------
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {
+ subscriber, locator, key, hash, handle,
+ initialized = false, pending = []}).
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+childspec(Id, App, EpiKey, Locator, Options) ->
+ {
+ Id,
+ {?MODULE, start_link, [
+ App,
+ EpiKey,
+ Locator,
+ Options
+ ]},
+ permanent,
+ 5000,
+ worker,
+ [?MODULE]
+ }.
+
+start_link(SubscriberApp, {epi_key, Key}, Src, Options) ->
+ {ok, Locator} = locate(SubscriberApp, Src),
+ gen_server:start_link(?MODULE, [SubscriberApp, Locator, Key, Options], []).
+
+reload(Server) ->
+ gen_server:call(Server, reload).
+
+wait(Server) ->
+ gen_server:call(Server, wait).
+
+stop(Server) ->
+ catch gen_server:call(Server, stop).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Definitions
+%% ------------------------------------------------------------------
+
+init([Subscriber, Locator, Key, Options]) ->
+ gen_server:cast(self(), init),
+ Interval = proplists:get_value(interval, Options, ?MONITOR_INTERVAL),
+ {ok, _Timer} = timer:send_interval(Interval, self(), tick),
+ {ok, #state{
+ subscriber = Subscriber,
+ locator = Locator,
+ key = Key,
+ handle = couch_epi_data_gen:get_handle(Key)}}.
+
+handle_call(wait, _From, #state{initialized = true} = State) ->
+ {reply, ok, State};
+handle_call(wait, From, #state{pending = Pending} = State) ->
+ {noreply, State#state{pending = [From | Pending]}};
+handle_call(reload, _From, State) ->
+ {Res, NewState} = reload_if_updated(State),
+ {reply, Res, NewState};
+handle_call(stop, _From, State) ->
+ {stop, normal, State};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+handle_cast(init, #state{pending = Pending} = State) ->
+ {_, NewState} = reload_if_updated(State),
+ [gen_server:reply(Client, ok) || Client <- Pending],
+ {noreply, NewState#state{initialized = true, pending = []}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(tick, State0) ->
+ {_Res, State1} = reload_if_updated(State0),
+ {noreply, State1};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+locate(App, {priv_file, FileName}) ->
+ case priv_path(App, FileName) of
+ {ok, FilePath} ->
+ ok = ensure_exists(FilePath),
+ {ok, {file, FilePath}};
+ Else ->
+ Else
+ end;
+locate(_App, {file, FilePath}) ->
+ ok = ensure_exists(FilePath),
+ {ok, {file, FilePath}}.
+
+priv_path(AppName, FileName) ->
+ case code:priv_dir(AppName) of
+ {error, _Error} = Error ->
+ Error;
+ Dir ->
+ {ok, filename:join(Dir, FileName)}
+ end.
+
+ensure_exists(FilePath) ->
+ case filelib:is_regular(FilePath) of
+ true ->
+ ok;
+ false ->
+ {error, {notfound, FilePath}}
+ end.
+
+reload_if_updated(#state{hash = OldHash, locator = Locator} = State) ->
+ case read(Locator) of
+ {ok, OldHash, _Data} ->
+ {ok, State};
+ {ok, Hash, Data} ->
+ safe_set(Hash, Data, State);
+ Else ->
+ {Else, State}
+ end.
+
+safe_set(Hash, Data, #state{} = State) ->
+ #state{
+ handle = Handle,
+ subscriber = Subscriber,
+ key = Key} = State,
+
+ try
+ OldData = current(Handle, Subscriber),
+ ok = couch_epi_data_gen:set(Handle, Subscriber, Data),
+ couch_epi_server:notify(Subscriber, Key, {data, OldData}, {data, Data}),
+ {ok, State#state{hash = Hash}}
+ catch Class:Reason ->
+ {{Class, Reason}, State}
+ end.
+
+read({file, FilePath}) ->
+ case file:consult(FilePath) of
+ {ok, Data} ->
+ {ok, hash_of_file(FilePath), Data};
+ {error, Reason} ->
+ {error, {FilePath, Reason}}
+ end.
+
+hash_of_file(FilePath) ->
+ {ok, Data} = file:read_file(FilePath),
+ crypto:hash(md5, Data).
+
+current(Handle, Subscriber) ->
+ try
+ case couch_epi_data_gen:by_source(Handle, Subscriber) of
+ undefined -> [];
+ Data -> Data
+ end
+ catch error:undef ->
+ []
+ end.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_functions.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_functions.erl b/src/couch_epi_functions.erl
new file mode 100644
index 0000000..f86ca61
--- /dev/null
+++ b/src/couch_epi_functions.erl
@@ -0,0 +1,155 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_functions).
+
+-behaviour(gen_server).
+-define(MONITOR_INTERVAL, 5000).
+
+%% ------------------------------------------------------------------
+%% API Function Exports
+%% ------------------------------------------------------------------
+
+-export([childspec/4]).
+-export([start_link/4, reload/1]).
+-export([wait/1, stop/1]).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Exports
+%% ------------------------------------------------------------------
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {
+ provider, service_id, modules, hash, handle,
+ initialized = false, pending = []}).
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+childspec(Id, App, EpiKey, Module) ->
+ {
+ Id,
+ {?MODULE, start_link, [
+ App,
+ EpiKey,
+ Module,
+ []
+ ]},
+ permanent,
+ 5000,
+ worker,
+ [Module]
+ }.
+
+start_link(ProviderApp, {epi_key, ServiceId}, {modules, Modules}, Options) ->
+ gen_server:start_link(
+ ?MODULE, [ProviderApp, ServiceId, Modules, Options], []).
+
+reload(Server) ->
+ gen_server:call(Server, reload).
+
+wait(Server) ->
+ gen_server:call(Server, wait).
+
+stop(Server) ->
+ catch gen_server:call(Server, stop).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Definitions
+%% ------------------------------------------------------------------
+
+init([Provider, ServiceId, Modules, Options]) ->
+ gen_server:cast(self(), init),
+ Interval = proplists:get_value(interval, Options, ?MONITOR_INTERVAL),
+ {ok, _Timer} = timer:send_interval(Interval, self(), tick),
+ {ok, #state{
+ provider = Provider,
+ modules = Modules,
+ service_id = ServiceId,
+ handle = couch_epi_functions_gen:get_handle(ServiceId)}}.
+
+handle_call(wait, _From, #state{initialized = true} = State) ->
+ {reply, ok, State};
+handle_call(wait, From, #state{pending = Pending} = State) ->
+ {noreply, State#state{pending = [From | Pending]}};
+handle_call(reload, _From, State) ->
+ {Res, NewState} = reload_if_updated(State),
+ {reply, Res, NewState};
+handle_call(stop, _From, State) ->
+ {stop, normal, State};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+handle_cast(init, #state{pending = Pending} = State) ->
+ {_, NewState} = reload_if_updated(State),
+ [gen_server:reply(Client, ok) || Client <- Pending],
+ {noreply, NewState#state{initialized = true, pending = []}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(tick, State) ->
+ {_Res, NewState} = reload_if_updated(State),
+ {noreply, NewState};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ safe_remove(State),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+reload_if_updated(#state{hash = OldHash, modules = Modules} = State) ->
+ case couch_epi_functions_gen:hash(Modules) of
+ OldHash ->
+ {ok, State};
+ Hash ->
+ safe_add(Hash, State)
+ end.
+
+safe_add(Hash, #state{modules = OldModules} = State) ->
+ #state{
+ handle = Handle,
+ provider = Provider,
+ modules = Modules,
+ service_id = ServiceId} = State,
+ try
+ ok = couch_epi_functions_gen:add(Handle, Provider, Modules),
+ couch_epi_server:notify(
+ Provider, ServiceId, {modules, OldModules}, {modules, Modules}),
+ {ok, State#state{hash = Hash}}
+ catch Class:Reason ->
+ {{Class, Reason}, State}
+ end.
+
+safe_remove(#state{} = State) ->
+ #state{
+ handle = Handle,
+ provider = Provider,
+ modules = Modules,
+ service_id = ServiceId} = State,
+ try
+ ok = couch_epi_functions_gen:remove(Handle, Provider, Modules),
+ couch_epi_server:notify(
+ Provider, ServiceId, {modules, Modules}, {modules, []}),
+ {ok, State#state{modules = []}}
+ catch Class:Reason ->
+ {{Class, Reason}, State}
+ end.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_functions_gen.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_functions_gen.erl b/src/couch_epi_functions_gen.erl
new file mode 100644
index 0000000..b62fbb5
--- /dev/null
+++ b/src/couch_epi_functions_gen.erl
@@ -0,0 +1,347 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_functions_gen).
+
+-export([add/3, remove/3, get_handle/1, hash/1, apply/4, apply/5]).
+
+-ifdef(TEST).
+
+-export([foo/2, bar/0]).
+
+-endif.
+
+-record(opts, {
+ ignore_errors = false,
+ ignore_providers = false,
+ pipe = false,
+ concurrent = false
+}).
+
+add(Handle, Source, Modules) ->
+ case is_updated(Handle, Source, Modules) of
+ false ->
+ ok;
+ true ->
+ save(Handle, Source, Modules)
+ end.
+
+remove(Handle, Source, Modules) ->
+ CurrentDefs = get_current_definitions(Handle),
+ {SourceDefs, Defs} = remove_from_definitions(CurrentDefs, Source),
+
+ NewSourceDefs = lists:filter(fun({M, _}) ->
+ not lists:member(M, Modules)
+ end, SourceDefs),
+
+ generate(Handle, Defs ++ NewSourceDefs).
+
+get_handle(ServiceId) ->
+ module_name(atom_to_list(ServiceId)).
+
+apply(ServiceId, Function, Args, Opts) when is_atom(ServiceId) ->
+ apply(get_handle(ServiceId), ServiceId, Function, Args, Opts).
+
+-spec apply(Handle :: atom(), ServiceId :: atom(), Function :: atom(),
+ Args :: [term()], Opts :: couch_epi:apply_opts()) -> ok.
+
+apply(Handle, _ServiceId, Function, Args, Opts) ->
+ DispatchOpts = parse_opts(Opts),
+ Modules = providers(Handle, Function, length(Args), DispatchOpts),
+ dispatch(Handle, Modules, Function, Args, DispatchOpts).
+
+
+%% ------------------------------------------------------------------
+%% Codegeneration routines
+%% ------------------------------------------------------------------
+
+preamble() ->
+ "
+ -export([version/0, version/1]).
+ -export([providers/0, providers/2]).
+ -export([definitions/0, definitions/1]).
+ -export([dispatch/3]).
+ -export([callbacks/2]).
+
+ version() ->
+ [{Provider, version(Provider)} || Provider <- providers()].
+
+ definitions() ->
+ [{Provider, definitions(Provider)} || Provider <- providers()].
+
+ callbacks(Provider, Function) ->
+ [].
+
+ "
+ %% In addition to preamble we also generate following methods
+ %% dispatch(Module, Function, [A1, A2]) -> Module:Function(A1, A2);
+
+ %% version(Source1) -> "HASH";
+ %% version(Source) -> {error, {unknown, Source}}.
+
+ %% providers() -> [].
+ %% providers(Function, Arity) -> [].
+ %% definitions(Provider) -> [{Module, [{Fun, Arity}]}].
+ .
+
+generate(Handle, Defs) ->
+ DispatchFunForms = couch_epi_codegen:function(dispatchers(Defs)),
+ VersionFunForms = couch_epi_codegen:function(version_method(Defs)),
+
+ AllProvidersForms = all_providers_method(Defs),
+ ProvidersForms = couch_epi_codegen:function(providers_method(Defs)),
+ DefinitionsForms = couch_epi_codegen:function(definitions_method(Defs)),
+
+ Forms = couch_epi_codegen:scan(preamble())
+ ++ DispatchFunForms ++ VersionFunForms
+ ++ ProvidersForms ++ AllProvidersForms
+ ++ DefinitionsForms,
+
+ couch_epi_codegen:generate(Handle, Forms).
+
+all_providers_method(Defs) ->
+ Providers = couch_epi_codegen:format_term(defined_providers(Defs)),
+ couch_epi_codegen:scan("providers() -> " ++ Providers ++ ".").
+
+providers_method(Defs) ->
+ Providers = providers_by_function(Defs),
+ DefaultClause = "providers(_, _) -> [].",
+ lists:foldl(fun({{Fun, Arity}, Modules}, Clauses) ->
+ providers(Fun, Arity, Modules) ++ Clauses
+ end, [couch_epi_codegen:scan(DefaultClause)], Providers).
+
+providers(Function, Arity, Modules) ->
+ ArityStr = integer_to_list(Arity),
+ Mods = couch_epi_codegen:format_term(Modules),
+ Fun = atom_to_list(Function),
+ %% providers(Function, Arity) -> [Module];
+ couch_epi_codegen:scan(
+ "providers(" ++ Fun ++ "," ++ ArityStr ++ ") ->" ++ Mods ++ ";").
+
+dispatchers(Defs) ->
+ DefaultClause = "dispatch(_Module, _Fun, _Args) -> ok.",
+ fold_defs(Defs, [couch_epi_codegen:scan(DefaultClause)],
+ fun({_Source, Module, Function, Arity}, Acc) ->
+ dispatcher(Module, Function, Arity) ++ Acc
+ end).
+
+version_method(Defs) ->
+ DefaultClause = "version(S) -> {error, {unknown, S}}.",
+ lists:foldl(fun({Source, SrcDefs}, Clauses) ->
+ version(Source, SrcDefs) ++ Clauses
+ end, [couch_epi_codegen:scan(DefaultClause)], Defs).
+
+definitions_method(Defs) ->
+ DefaultClause = "definitions(S) -> {error, {unknown, S}}.",
+ lists:foldl(fun({Source, SrcDefs}, Clauses) ->
+ definition(Source, SrcDefs) ++ Clauses
+ end, [couch_epi_codegen:scan(DefaultClause)], Defs).
+
+definition(Source, Defs) ->
+ Src = atom_to_list(Source),
+ DefsStr = couch_epi_codegen:format_term(Defs),
+ couch_epi_codegen:scan("definitions(" ++ Src ++ ") -> " ++ DefsStr ++ ";").
+
+dispatcher(Module, Function, 0) ->
+ M = atom_to_list(Module),
+ Fun = atom_to_list(Function),
+
+ %% dispatch(Module, Function, []) -> Module:Function();
+ couch_epi_codegen:scan(
+ "dispatch(" ++ M ++ "," ++ Fun ++ ", []) ->"
+ ++ M ++ ":" ++ Fun ++ "();");
+dispatcher(Module, Function, Arity) ->
+ Args = args_string(Arity),
+ M = atom_to_list(Module),
+ Fun = atom_to_list(Function),
+ %% dispatch(Module, Function, [A1, A2]) -> Module:Function(A1, A2);
+ couch_epi_codegen:scan(
+ "dispatch(" ++ M ++ "," ++ Fun ++ ", [" ++ Args ++ "]) ->"
+ ++ M ++ ":" ++ Fun ++ "(" ++ Args ++ ");").
+
+args_string(Arity) ->
+ Vars = ["A" ++ integer_to_list(Seq) || Seq <- lists:seq(1, Arity)],
+ string:join(Vars, ", ").
+
+version(Source, SrcDefs) ->
+ Modules = [Module || {Module, _Exports} <- SrcDefs],
+ couch_epi_codegen:scan(
+ "version(" ++ atom_to_list(Source) ++ ") ->" ++ hash(Modules) ++ ";").
+
+
+
+%% ------------------------------------------------------------------
+%% Helper functions
+%% ------------------------------------------------------------------
+
+module_name(ServiceId) when is_list(ServiceId) ->
+ list_to_atom(string:join([atom_to_list(?MODULE), ServiceId], "_")).
+
+is_updated(Handle, Source, Modules) ->
+ Sig = hash(Modules),
+ try Handle:version(Source) of
+ {error, {unknown, Source}} -> true;
+ {error, Reason} -> throw(Reason);
+ Sig -> false;
+ _ -> true
+ catch
+ error:undef -> true;
+ Class:Reason ->
+ throw({Class, {Source, Reason}})
+ end.
+
+save(Handle, Source, Modules) ->
+ CurrentDefs = get_current_definitions(Handle),
+ Definitions = definitions(Source, Modules),
+ NewDefs = lists:keystore(Source, 1, CurrentDefs, Definitions),
+ generate(Handle, NewDefs).
+
+definitions(Source, Modules) ->
+ Blacklist = [{module_info, 0}, {module_info, 1}],
+ SrcDefs = [{M, M:module_info(exports) -- Blacklist} || M <- Modules],
+ {Source, SrcDefs}.
+
+get_current_definitions(Handle) ->
+ try Handle:definitions()
+ catch error:undef -> []
+ end.
+
+defined_providers(Defs) ->
+ [Source || {Source, _} <- Defs].
+
+%% Defs = [{Source, [{Module, [{Fun, Arity}]}]}]
+fold_defs(Defs, Acc, Fun) ->
+ lists:foldl(fun({Source, SourceData}, Clauses) ->
+ lists:foldl(fun({Module, Exports}, ExportsAcc) ->
+ lists:foldl(fun({Function, Arity}, InAcc) ->
+ Fun({Source, Module, Function, Arity}, InAcc)
+ end, [], Exports) ++ ExportsAcc
+ end, [], SourceData) ++ Clauses
+ end, Acc, Defs).
+
+providers_by_function(Defs) ->
+ Providers = fold_defs(Defs, [],
+ fun({_Source, Module, Function, Arity}, Acc) ->
+ [{{Function, Arity}, Module} | Acc]
+ end
+ ),
+ Dict = lists:foldl(fun({K, V}, Acc) ->
+ dict:append(K, V, Acc)
+ end, dict:new(), Providers),
+ dict:to_list(Dict).
+
+
+hash(Modules) ->
+ VSNs = [couch_epi_util:module_version(M) || M <- lists:usort(Modules)],
+ couch_epi_util:hash(VSNs).
+
+dispatch(_Handle, _Modules, _Func, _Args, #opts{concurrent = true, pipe = true}) ->
+ throw({error, {incompatible_options, [concurrent, pipe]}});
+dispatch(Handle, Modules, Function, Args,
+ #opts{pipe = true, ignore_errors = true}) ->
+ lists:foldl(fun(Module, Acc) ->
+ try
+ Handle:dispatch(Module, Function, Acc)
+ catch _:_ ->
+ Acc
+ end
+ end, Args, Modules);
+dispatch(Handle, Modules, Function, Args,
+ #opts{pipe = true}) ->
+ lists:foldl(fun(Module, Acc) ->
+ Handle:dispatch(Module, Function, Acc)
+ end, Args, Modules);
+dispatch(Handle, Modules, Function, Args, #opts{} = Opts) ->
+ [do_dispatch(Handle, Module, Function, Args, Opts) || Module <- Modules].
+
+do_dispatch(Handle, Module, Function, Args,
+ #opts{concurrent = true, ignore_errors = true}) ->
+ spawn(fun() ->
+ (catch Handle:dispatch(Module, Function, Args))
+ end);
+do_dispatch(Handle, Module, Function, Args,
+ #opts{ignore_errors = true}) ->
+ (catch Handle:dispatch(Module, Function, Args));
+do_dispatch(Handle, Module, Function, Args,
+ #opts{concurrent = true}) ->
+ spawn(fun() -> Handle:dispatch(Module, Function, Args) end);
+do_dispatch(Handle, Module, Function, Args, #opts{}) ->
+ Handle:dispatch(Module, Function, Args).
+
+
+parse_opts(Opts) ->
+ parse_opts(Opts, #opts{}).
+
+parse_opts([ignore_errors|Rest], #opts{} = Acc) ->
+ parse_opts(Rest, Acc#opts{ignore_errors = true});
+parse_opts([pipe|Rest], #opts{} = Acc) ->
+ parse_opts(Rest, Acc#opts{pipe = true});
+parse_opts([concurrent|Rest], #opts{} = Acc) ->
+ parse_opts(Rest, Acc#opts{concurrent = true});
+parse_opts([ignore_providers|Rest], #opts{} = Acc) ->
+ parse_opts(Rest, Acc#opts{ignore_providers = true});
+parse_opts([], Acc) ->
+ Acc.
+
+providers(Handle, Function, Arity, #opts{ignore_providers = true}) ->
+ try
+ Handle:providers(Function, Arity)
+ catch
+ error:undef -> []
+ end;
+providers(Handle, Function, Arity, #opts{}) ->
+ Handle:providers(Function, Arity).
+
+remove_from_definitions(Defs, Source) ->
+ case lists:keytake(Source, 1, Defs) of
+ {value, {Source, Value}, Rest} ->
+ {Value, Rest};
+ false ->
+ {[], Defs}
+ end.
+
+%% ------------------------------------------------------------------
+%% Tests
+%% ------------------------------------------------------------------
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+foo(A1, A2) ->
+ {A1, A2}.
+
+bar() ->
+ [].
+
+basic_test() ->
+ Module = foo_bar_dispatcher,
+ add(Module, app1, [?MODULE]),
+
+ ?assert(is_list(Module:version(app1))),
+
+ Defs1 = lists:usort(Module:definitions()),
+ ?assertMatch([{app1, [{?MODULE, _}]}], Defs1),
+ [{app1, [{?MODULE, Exports}]}] = Defs1,
+ ?assert(lists:member({bar, 0}, Exports)),
+
+ add(Module, app2, [?MODULE]),
+ Defs2 = lists:usort(Module:definitions()),
+ ?assertMatch([{app1, [{?MODULE, _}]}, {app2, [{?MODULE, _}]}], Defs2),
+
+ ?assertMatch([{app1, Hash}, {app2, Hash}], Module:version()),
+
+ ?assertMatch([], Module:dispatch(?MODULE, bar, [])),
+ ?assertMatch({1, 2}, Module:dispatch(?MODULE, foo, [1, 2])),
+
+ ok.
+
+-endif.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_server.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_server.erl b/src/couch_epi_server.erl
new file mode 100644
index 0000000..e325db7
--- /dev/null
+++ b/src/couch_epi_server.erl
@@ -0,0 +1,144 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_server).
+-behaviour(gen_server).
+-define(SERVER, ?MODULE).
+
+%% ------------------------------------------------------------------
+%% API Function Exports
+%% ------------------------------------------------------------------
+
+-export([start_link/0]).
+-export([subscribe/3, subscribe/4, unsubscribe/1, unsubscribe/2]).
+-export([notify/4, notify/5]).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Exports
+%% ------------------------------------------------------------------
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(epi_server_state, {subscriptions}).
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+start_link() ->
+ gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+subscribe(App, Key, MFA) ->
+ subscribe(?SERVER, App, Key, MFA).
+
+subscribe(Server, App, Key, {_M, _F, _A} = MFA) ->
+ gen_server:call(Server, {subscribe, App, Key, MFA}).
+
+unsubscribe(Subscription) ->
+ unsubscribe(?SERVER, Subscription).
+
+unsubscribe(Server, Subscription) ->
+ gen_server:call(Server, {unsubscribe, Subscription}).
+
+notify(App, Key, OldData, Data) ->
+ notify(?SERVER, App, Key, OldData, Data).
+
+notify(Server, App, Key, OldData, Data) ->
+ gen_server:cast(Server, {notify, App, Key, OldData, Data}).
+
+
+%% ------------------------------------------------------------------
+%% gen_server Function Definitions
+%% ------------------------------------------------------------------
+
+init(_Args) ->
+ State = #epi_server_state{subscriptions = dict:new()},
+ {ok, State}.
+
+handle_call({subscribe, App, Key, MFA}, {Pid, _Tag},
+ #epi_server_state{subscriptions = Subscriptions0} = State0) ->
+ {Subscription, Subscriptions1} = add(Pid, Subscriptions0, App, Key, MFA),
+ State1 = State0#epi_server_state{subscriptions = Subscriptions1},
+ {reply, {ok, Subscription}, State1};
+handle_call({unsubscribe, Subscription}, _From,
+ #epi_server_state{subscriptions = Subscriptions0} = State0) ->
+ Subscriptions1 = remove(Subscriptions0, Subscription),
+ State1 = State0#epi_server_state{subscriptions = Subscriptions1},
+ {reply, ok, State1};
+handle_call(_Request, _From, State) ->
+ {stop, normal, State}.
+
+handle_cast({notify, App, Key, OldData, Data},
+ #epi_server_state{subscriptions = Subscriptions} = State) ->
+ Subscribers = subscribers(Subscriptions, App, Key),
+ notify_subscribers(Subscribers, App, Key, OldData, Data),
+ {noreply, State};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({'DOWN', MonitorRef, Type, Object, Info},
+ #epi_server_state{subscriptions = Subscriptions0} = State0) ->
+ Subscriptions1 = remove(Subscriptions0, MonitorRef),
+ State1 = State0#epi_server_state{subscriptions = Subscriptions1},
+ {noreply, State1};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+subscribers(Subscriptions, App, Key) ->
+ case dict:find({App, Key}, Subscriptions) of
+ error ->
+ [];
+ {ok, Subscribers} ->
+ Subscribers
+ end.
+
+add(Pid, Subscriptions, App, Key, MFA) ->
+ Subscription = erlang:monitor(process, Pid),
+ {Subscription, dict:append({App, Key}, {Subscription, MFA}, Subscriptions)}.
+
+remove(Subscriptions, SubscriptionId) ->
+ case find(Subscriptions, SubscriptionId) of
+ {App, Key} ->
+ demonitor(SubscriptionId, [flush]),
+ delete_subscriber(Subscriptions, App, Key, SubscriptionId);
+ _ ->
+ Subscriptions
+ end.
+
+find(Subscriptions, SubscriptionId) ->
+ dict:fold(fun(Key, Subscribers, Acc) ->
+ case [ok || {Id, _MFA} <- Subscribers, Id =:= SubscriptionId] of
+ [_] ->
+ Key;
+ [] ->
+ Acc
+ end
+ end, not_found, Subscriptions).
+
+delete_subscriber(Subscriptions, App, Key, SubscriptionId) ->
+ dict:update({App, Key}, fun(Subscribers) ->
+ [{Id, MFA} || {Id, MFA} <- Subscribers, Id =/= SubscriptionId]
+ end, Subscriptions).
+
+notify_subscribers(Subscribers, App, Key, OldData, Data) ->
+ [M:F(App, Key, OldData, Data, A) || {_Id, {M, F, A}} <- Subscribers].
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_sup.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_sup.erl b/src/couch_epi_sup.erl
new file mode 100644
index 0000000..5e44d1b
--- /dev/null
+++ b/src/couch_epi_sup.erl
@@ -0,0 +1,38 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init([]) ->
+ {ok, { {one_for_one, 5, 10}, [?CHILD(couch_epi_server, worker)]} }.
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/src/couch_epi_util.erl
----------------------------------------------------------------------
diff --git a/src/couch_epi_util.erl b/src/couch_epi_util.erl
new file mode 100644
index 0000000..1c39aa5
--- /dev/null
+++ b/src/couch_epi_util.erl
@@ -0,0 +1,24 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_util).
+
+-export([module_version/1, hash/1]).
+
+module_version(Module) ->
+ Attributes = Module:module_info(attributes),
+ {vsn, VSNs} = lists:keyfind(vsn, 1, Attributes),
+ VSNs.
+
+hash(Term) ->
+ <<SigInt:128/integer>> = crypto:hash(md5, term_to_binary(Term)),
+ io_lib:format("\"~.36B\"",[SigInt]).
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/test/couch_epi_data_source_tests.erl
----------------------------------------------------------------------
diff --git a/test/couch_epi_data_source_tests.erl b/test/couch_epi_data_source_tests.erl
new file mode 100644
index 0000000..f5d701f
--- /dev/null
+++ b/test/couch_epi_data_source_tests.erl
@@ -0,0 +1,90 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_data_source_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+-define(DATA_FILE1, ?ABS_PATH("test/fixtures/app_data1.cfg")).
+-define(DATA_FILE2, ?ABS_PATH("test/fixtures/app_data2.cfg")).
+
+-record(ctx, {file, handle, pid}).
+
+setup() ->
+ Key = {test_app, descriptions},
+ File = ?tempfile(),
+ {ok, _} = file:copy(?DATA_FILE1, File),
+ {ok, Pid} = couch_epi_data_source:start_link(
+ test_app, {epi_key, Key}, {file, File}, [{interval, 100}]),
+ ok = couch_epi_data_source:wait(Pid),
+ #ctx{
+ pid = Pid,
+ file = File,
+ handle = couch_epi_data_gen:get_handle(Key)}.
+
+
+teardown(#ctx{pid = Pid, file = File}) ->
+ file:delete(File),
+ couch_epi_data_source:stop(Pid),
+ catch meck:unload(compile),
+ ok.
+
+
+epi_data_source_reload_test_() ->
+ {
+ "data_source reload tests",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun ensure_reload_if_manually_triggered/1,
+ fun ensure_reload_if_changed/1,
+ fun ensure_no_reload_when_no_change/1
+ ]
+ }
+ }.
+
+ensure_reload_if_manually_triggered(#ctx{pid = Pid, file = File}) ->
+ ?_test(begin
+ ok = meck:new(compile, [passthrough, unstick]),
+ ok = meck:expect(compile, forms, fun(_, _) -> {error, reload} end),
+ {ok, _} = file:copy(?DATA_FILE2, File),
+ Result = couch_epi_data_source:reload(Pid),
+ ?assertMatch({error,{badmatch,{error,reload}}}, Result)
+ end).
+
+ensure_reload_if_changed(#ctx{file = File, handle = Handle}) ->
+ ?_test(begin
+ ?assertMatch(
+ [[{type,counter},{desc,foo}]],
+ couch_epi_data_gen:get(Handle, [complex, key, 1])),
+ {ok, _} = file:copy(?DATA_FILE2, File),
+ timer:sleep(150),
+ ?assertMatch(
+ [[{type,counter},{desc,bar}]],
+ couch_epi_data_gen:get(Handle, [complex, key, 2]))
+ end).
+
+ensure_no_reload_when_no_change(#ctx{handle = Handle}) ->
+ ok = meck:new(compile, [passthrough, unstick]),
+ ok = meck:expect(compile, forms, fun(_, _) ->
+ {error, compile_should_not_be_called} end),
+ ?_test(begin
+ ?assertMatch(
+ [[{type,counter},{desc,foo}]],
+ couch_epi_data_gen:get(Handle, [complex, key, 1])),
+ timer:sleep(200),
+ ?assertMatch(
+ [],
+ couch_epi_data_gen:get(Handle, [complex, key, 2]))
+ end).
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/test/couch_epi_functions_tests.erl
----------------------------------------------------------------------
diff --git a/test/couch_epi_functions_tests.erl b/test/couch_epi_functions_tests.erl
new file mode 100644
index 0000000..c12b326
--- /dev/null
+++ b/test/couch_epi_functions_tests.erl
@@ -0,0 +1,126 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_functions_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+-define(MODULE1(Name), "
+ -export([foo/2, bar/0, inc/1]).
+ foo(A1, A2) ->
+ {A1, A2}.
+
+ bar() ->
+ [].
+
+ inc(A) ->
+ A + 1.
+").
+
+-define(MODULE2(Name), "
+ -export([baz/1, inc/1]).
+ baz(A1) ->
+ A1.
+
+ inc(A) ->
+ A + 1.
+").
+
+setup() ->
+ setup([{interval, 100}]).
+
+setup(Opts) ->
+ ServiceId = my_service,
+ Module = my_test_module,
+ ok = generate_module(Module, ?MODULE1(Module)),
+ {ok, Pid} = couch_epi_functions:start_link(
+ test_app, {epi_key, ServiceId}, {modules, [Module]}, Opts),
+ ok = couch_epi_functions:wait(Pid),
+ {Pid, Module, ServiceId, couch_epi_functions_gen:get_handle(ServiceId)}.
+
+teardown({Pid, Module, _, Handle}) ->
+ code:purge(Module),
+ %%code:purge(Handle), %% FIXME temporary hack
+ couch_epi_functions:stop(Pid),
+ catch meck:unload(compile),
+ ok.
+
+generate_module(Name, Body) ->
+ Tokens = couch_epi_codegen:scan(Body),
+ couch_epi_codegen:generate(Name, Tokens).
+
+temp_atom() ->
+ {A, B, C} = erlang:now(),
+ list_to_atom(lists:flatten(io_lib:format("module~p~p~p", [A, B, C]))).
+
+
+epi_functions_test_() ->
+ {
+ "functions reload tests",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun ensure_reload_if_changed/1,
+ fun ensure_no_reload_when_no_change/1
+ ]
+ }
+ }.
+
+epi_functions_manual_reload_test_() ->
+ {
+ "functions manual reload tests",
+ {
+ foreach,
+ fun() -> setup([{interval, 10000}]) end,
+ fun teardown/1,
+ [
+ fun ensure_reload_if_manually_triggered/1
+ ]
+ }
+ }.
+
+ensure_reload_if_manually_triggered({Pid, Module, _ServiceId, _Handle}) ->
+ ?_test(begin
+ ok = generate_module(Module, ?MODULE2(Module)),
+ ok = meck:new(compile, [passthrough, unstick]),
+ ok = meck:expect(compile, forms, fun(_, _) -> {error, reload} end),
+ Result = couch_epi_functions:reload(Pid),
+ ?assertMatch({error,{badmatch,{error,reload}}}, Result)
+ end).
+
+ensure_reload_if_changed({_Pid, Module, ServiceId, Handle}) ->
+ ?_test(begin
+ ?assertMatch(
+ [{1, 2}],
+ couch_epi_functions_gen:apply(ServiceId, foo, [1, 2], [])),
+ ok = generate_module(Module, ?MODULE2(Module)),
+ timer:sleep(150),
+ ?assertMatch(
+ [3],
+ couch_epi_functions_gen:apply(ServiceId, baz, [3], []))
+ end).
+
+ensure_no_reload_when_no_change({_Pid, Module, ServiceId, Handle}) ->
+ ok = meck:new(compile, [passthrough, unstick]),
+ ok = meck:expect(compile, forms, fun(_, _) ->
+ {error, compile_should_not_be_called} end),
+ ?_test(begin
+ ?assertMatch(
+ [{1, 2}],
+ couch_epi_functions_gen:apply(ServiceId, foo, [1, 2], [])),
+ timer:sleep(200),
+ ?assertMatch(
+ [],
+ couch_epi_functions_gen:apply(ServiceId, baz, [3], []))
+ end).
http://git-wip-us.apache.org/repos/asf/couchdb-couch-epi/blob/32ba6e79/test/couch_epi_tests.erl
----------------------------------------------------------------------
diff --git a/test/couch_epi_tests.erl b/test/couch_epi_tests.erl
new file mode 100644
index 0000000..bba2ae6
--- /dev/null
+++ b/test/couch_epi_tests.erl
@@ -0,0 +1,413 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_epi_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+-define(DATA_FILE1, ?ABS_PATH("test/fixtures/app_data1.cfg")).
+-define(DATA_FILE2, ?ABS_PATH("test/fixtures/app_data2.cfg")).
+
+-export([notify_cb/5, save/3]).
+
+-record(ctx, {
+ file, data_handle, data_source,
+ functions_handle, functions_source, kv}).
+
+-define(TIMEOUT, 5000).
+
+-define(MODULE1(Name), "
+ -export([inc/2, fail/2]).
+
+ inc(KV, A) ->
+ Reply = A + 1,
+ couch_epi_tests:save(KV, inc1, Reply),
+ [KV, Reply].
+
+ fail(KV, A) ->
+ inc(KV, A).
+").
+
+-define(MODULE2(Name), "
+ -export([inc/2, fail/2]).
+
+ inc(KV, A) ->
+ Reply = A + 1,
+ couch_epi_tests:save(KV, inc2, Reply),
+ [KV, Reply].
+
+ fail(KV, _A) ->
+ couch_epi_tests:save(KV, inc2, check_error),
+ throw(check_error).
+").
+
+
+notify_cb(App, Key, OldData, Data, KV) ->
+ save(KV, is_called, {App, Key, OldData, Data}).
+
+setup() ->
+ error_logger:tty(false),
+
+ Key = {test_app, descriptions},
+ File = ?tempfile(),
+ {ok, _} = file:copy(?DATA_FILE1, File),
+ application:start(couch_epi),
+ {ok, DataPid} = couch_epi_data_source:start_link(
+ test_app, {epi_key, Key}, {file, File}, [{interval, 100}]),
+ ok = couch_epi_data_source:wait(DataPid),
+
+ ok = generate_module(provider1, ?MODULE1(provider1)),
+ ok = generate_module(provider2, ?MODULE2(provider2)),
+
+ {ok, FunctionsPid} = couch_epi_functions:start_link(
+ test_app, {epi_key, my_service}, {modules, [provider1, provider2]},
+ [{interval, 100}]),
+ ok = couch_epi_functions:wait(FunctionsPid),
+ KV = state_storage(),
+ #ctx{
+ file = File,
+ data_handle = couch_epi:get_handle(Key),
+ functions_handle = couch_epi:get_handle(my_service),
+ kv = KV,
+ data_source = DataPid,
+ functions_source = FunctionsPid}.
+
+setup(_Opts) ->
+ setup().
+
+teardown(_, #ctx{} = Ctx) ->
+ teardown(Ctx).
+
+teardown(#ctx{data_source = DataPid,
+ functions_source = FunctionsPid,
+ kv = KV, file = File}) ->
+ file:delete(File),
+ couch_epi_data_source:stop(DataPid),
+ couch_epi_functions:stop(FunctionsPid),
+ catch meck:unload(compile),
+ call(KV, stop),
+ application:stop(couch_epi),
+ ok.
+
+epi_config_update_test_() ->
+ Funs = [
+ fun ensure_notified_when_changed/2,
+ fun ensure_not_notified_when_no_change/2,
+ fun ensure_not_notified_when_unsubscribed/2
+ ],
+ {
+ "config update tests",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [make_case("Check notifications for: ", [module, file], Funs)]
+ }
+ }.
+
+epi_data_source_test_() ->
+ {
+ "epi data_source tests",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun check_dump/1,
+ fun check_get/1,
+ fun check_get_value/1,
+ fun check_by_key/1,
+ fun check_by_source/1,
+ fun check_keys/1,
+ fun check_subscribers/1
+ ]
+ }
+ }.
+
+
+epi_apply_test_() ->
+ {
+ "epi dispatch tests",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun check_pipe/1,
+ fun check_broken_pipe/1,
+ fun ensure_fail/1,
+ fun ensure_fail_pipe/1
+ ]
+ }
+ }.
+
+epi_subscription_test_() ->
+ {
+ "epi subscription tests",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun ensure_unsubscribe_when_caller_die/1
+ ]
+ }
+ }.
+
+apply_options_test_() ->
+ Funs = [fun ensure_apply_is_called/2],
+ make_case("Apply with options: ", valid_options_permutations(), Funs).
+
+
+make_case(Msg, P, Funs) ->
+ [{format_case_name(Msg, Case), [
+ {
+ foreachx, fun setup/1, fun teardown/2,
+ [
+ {Case, Fun} || Fun <- Funs
+ ]
+ }
+ ]} || Case <- P].
+
+format_case_name(Msg, Case) ->
+ lists:flatten(Msg ++ io_lib:format("~p", [Case])).
+
+valid_options_permutations() ->
+ [
+ [],
+ [ignore_errors],
+ [pipe],
+ [pipe, ignore_errors],
+ [concurrent],
+ [concurrent, ignore_errors]
+ ].
+
+ensure_notified_when_changed(file, #ctx{file = File} = Ctx) ->
+ ?_test(begin
+ Key = {test_app, descriptions},
+ subscribe(Ctx, test_app, Key),
+ update(file, Ctx),
+ timer:sleep(200),
+ ExpectedData = lists:usort([
+ {[complex, key, 1], [{type, counter}, {desc, updated_foo}]},
+ {[complex, key, 2], [{type, counter}, {desc, bar}]}
+ ]),
+ Result = get(Ctx, is_called),
+ ?assertMatch({ok, {test_app, Key, {data, _}, {data, _}}}, Result),
+ {ok, {test_app, Key, {data, OldData}, {data, Data}}} = Result,
+ ?assertMatch(ExpectedData, lists:usort(Data)),
+ ?assertMatch(
+ [{[complex, key, 1], [{type, counter}, {desc, foo}]}],
+ lists:usort(OldData))
+ end);
+ensure_notified_when_changed(module, #ctx{file = File} = Ctx) ->
+ ?_test(begin
+ subscribe(Ctx, test_app, my_service),
+ update(module, Ctx),
+ timer:sleep(200),
+ Result = get(Ctx, is_called),
+ Expected = {test_app, my_service,
+ {modules, [provider1, provider2]},
+ {modules, [provider1, provider2]}},
+ ?assertMatch({ok, Expected}, Result),
+ ok
+ end).
+
+ensure_not_notified_when_no_change(Case, #ctx{} = Ctx) ->
+ ?_test(begin
+ Key = {test_app, descriptions},
+ subscribe(Ctx, test_app, Key),
+ timer:sleep(200),
+ ?assertMatch(error, get(Ctx, is_called))
+ end).
+
+ensure_not_notified_when_unsubscribed(Case, #ctx{file = File} = Ctx) ->
+ ?_test(begin
+ Key = {test_app, descriptions},
+ SubscriptionId = subscribe(Ctx, test_app, Key),
+ couch_epi:unsubscribe(SubscriptionId),
+ timer:sleep(100),
+ update(Case, Ctx),
+ timer:sleep(200),
+ ?assertMatch(error, get(Ctx, is_called))
+ end).
+
+ensure_apply_is_called(Opts, #ctx{functions_handle = Handle, kv = KV} = Ctx) ->
+ ?_test(begin
+ couch_epi:apply(Handle, my_service, inc, [KV, 2], Opts),
+ maybe_wait(Opts),
+ ?assertMatch({ok, _}, get(Ctx, inc1)),
+ ?assertMatch({ok, _}, get(Ctx, inc2)),
+ ok
+ end).
+
+check_pipe(#ctx{functions_handle = Handle, kv = KV}) ->
+ ?_test(begin
+ Result = couch_epi:apply(Handle, my_service, inc, [KV, 2], [pipe]),
+ ?assertMatch([KV, 4], Result),
+ ok
+ end).
+
+check_broken_pipe(#ctx{functions_handle = Handle, kv = KV} = Ctx) ->
+ ?_test(begin
+ Result = couch_epi:apply(Handle, my_service, fail, [KV, 2], [pipe, ignore_errors]),
+ ?assertMatch([KV, 3], Result),
+ ?assertMatch([3, check_error], pipe_state(Ctx)),
+ ok
+ end).
+
+ensure_fail_pipe(#ctx{functions_handle = Handle, kv = KV}) ->
+ ?_test(begin
+ ?assertThrow(check_error,
+ couch_epi:apply(Handle, my_service, fail, [KV, 2], [pipe])),
+ ok
+ end).
+
+ensure_fail(#ctx{functions_handle = Handle, kv = KV}) ->
+ ?_test(begin
+ ?assertThrow(check_error,
+ couch_epi:apply(Handle, my_service, fail, [KV, 2], [])),
+ ok
+ end).
+
+ensure_unsubscribe_when_caller_die(#ctx{} = Ctx) ->
+ ?_test(begin
+ Key = {test_app, descriptions},
+ spawn(fun() ->
+ subscribe(Ctx, test_app, Key)
+ end),
+ %%update(file, Ctx),
+ timer:sleep(200),
+ ?assertMatch(error, get(Ctx, is_called))
+ end).
+
+
+pipe_state(Ctx) ->
+ Trace = [get(Ctx, inc1), get(Ctx, inc2)],
+ lists:usort([State || {ok, State} <- Trace]).
+
+check_dump(#ctx{data_handle = Handle}) ->
+ ?_test(begin
+ ?assertMatch(
+ [[{type, counter}, {desc, foo}]],
+ couch_epi:dump(Handle))
+ end).
+
+check_get(#ctx{data_handle = Handle}) ->
+ ?_test(begin
+ ?assertMatch(
+ [[{type, counter}, {desc, foo}]],
+ couch_epi:get(Handle, [complex,key, 1]))
+ end).
+
+check_get_value(#ctx{data_handle = Handle}) ->
+ ?_test(begin
+ ?assertMatch(
+ [{type, counter}, {desc, foo}],
+ couch_epi:get_value(Handle, test_app, [complex,key, 1]))
+ end).
+
+check_by_key(#ctx{data_handle = Handle}) ->
+ ?_test(begin
+ ?assertMatch(
+ [{[complex, key, 1],
+ [{test_app, [{type, counter}, {desc, foo}]}]}],
+ couch_epi:by_key(Handle)),
+ ?assertMatch(
+ [{test_app, [{type, counter}, {desc, foo}]}],
+ couch_epi:by_key(Handle, [complex, key, 1]))
+ end).
+
+check_by_source(#ctx{data_handle = Handle}) ->
+ ?_test(begin
+ ?assertMatch(
+ [{test_app,
+ [{[complex,key, 1], [{type, counter}, {desc, foo}]}]}],
+ couch_epi:by_source(Handle)),
+ ?assertMatch(
+ [{[complex,key, 1], [{type, counter}, {desc, foo}]}],
+ couch_epi:by_source(Handle, test_app))
+ end).
+
+check_keys(#ctx{data_handle = Handle}) ->
+ ?_assertMatch([[complex,key,1]], couch_epi:keys(Handle)).
+
+check_subscribers(#ctx{data_handle = Handle}) ->
+ ?_assertMatch([test_app], couch_epi:subscribers(Handle)).
+
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+generate_module(Name, Body) ->
+ Tokens = couch_epi_codegen:scan(Body),
+ couch_epi_codegen:generate(Name, Tokens).
+
+update(module, #ctx{}) ->
+ ok = generate_module(provider1, ?MODULE2(provider1));
+update(file, #ctx{file = File}) ->
+ {ok, _} = file:copy(?DATA_FILE2, File),
+ ok.
+
+subscribe(#ctx{kv = Kv}, App, Key) ->
+ {ok, Pid} = couch_epi:subscribe(App, Key, ?MODULE, notify_cb, Kv),
+ call(Kv, empty),
+ Pid.
+
+maybe_wait(Opts) ->
+ case lists:member(concurrent, Opts) of
+ true ->
+ timer:sleep(100);
+ false ->
+ ok
+ end.
+
+%% ------------
+%% State tracer
+
+save(Kv, Key, Value) ->
+ call(Kv, {set, Key, Value}).
+
+get(#ctx{kv = Kv}, Key) ->
+ call(Kv, {get, Key}).
+
+call(Server, Msg) ->
+ Ref = make_ref(),
+ Server ! {{Ref, self()}, Msg},
+ receive
+ {reply, Ref, Reply} ->
+ Reply
+ after ?TIMEOUT ->
+ {error, {timeout, Msg}}
+ end.
+
+reply({Ref, From}, Msg) ->
+ From ! {reply, Ref, Msg}.
+
+state_storage() ->
+ spawn_link(fun() -> state_storage(dict:new()) end).
+
+state_storage(Dict) ->
+ receive
+ {From, {set, Key, Value}} ->
+ reply(From, ok),
+ state_storage(dict:store(Key, Value, Dict));
+ {From, {get, Key}} ->
+ reply(From, dict:find(Key, Dict)),
+ state_storage(Dict);
+ {From, empty} ->
+ reply(From, ok),
+ state_storage(dict:new());
+ {From, stop} ->
+ reply(From, ok)
+ end.