├── .github
└── workflows
│ └── elixir.yml
├── .travis.yml
├── CNAME
├── LICENSE
├── README.md
├── config
└── config.exs
├── index.html
├── man
├── rest.htm
├── rest_cowboy.htm
└── rest_kvs.htm
├── mix.exs
├── rebar.config
├── src
├── bpe
│ ├── beginEvent.erl
│ ├── boundaryEvent.erl
│ ├── endEvent.erl
│ ├── hist.erl
│ ├── messageEvent.erl
│ ├── process.erl
│ ├── sequenceFlow.erl
│ ├── serviceTask.erl
│ ├── step.erl
│ ├── timeout.erl
│ ├── ts.erl
│ ├── tx.erl
│ └── userTask.erl
├── erp
│ ├── Organization.erl
│ ├── Payment.erl
│ ├── money.erl
│ └── users.erl
├── rest.app.src
├── rest.erl
├── rest_cowboy.erl
└── rest_kvs.erl
└── sys.config
/.github/workflows/elixir.yml:
--------------------------------------------------------------------------------
1 | name: mix
2 | on: push
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - uses: erlef/setup-elixir@v1
9 | with:
10 | otp-version: 22.x
11 | elixir-version: 1.9.x
12 | - name: Dependencies
13 | run: |
14 | mix local.rebar --force
15 | mix local.hex --force
16 | mix deps.get
17 | - name: Compilation
18 | run: mix compile
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: erlang
2 | otp_release:
3 | - 21.0
4 | - 22.0
5 | script:
6 | - "curl -fsSL https://raw.github.com/synrc/mad/master/mad > mad"
7 | - "chmod +x mad"
8 | - "./mad dep com"
9 | - "rebar3 dialyzer"
10 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | rest.n2o.dev
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Dmitry Bushmelev, Synrc Research Center
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | Software may only be used for the great good and the true happiness of all sentient beings.
11 |
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
18 | THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | REST: framework with typed JSON
2 | ===============================
3 |
4 | [](https://github.com/synrc/rest/actions)
5 | [](https://travis-ci.com/synrc/rest)
6 | [](https://hex.pm/packages/rest)
7 |
8 | Features and Goals
9 | ------------------
10 |
11 | * Fastest possibe Record <-> Proplists transformations
12 | * Smallest REST framework in the world
13 | * ETS/KVS/Any storage selection by scaffolding
14 |
15 | We've achived first goal by providing parse_transform code generation
16 | for tuple transformations. And second requirement was achieved
17 | by not including routing bullshit and other uncertain features.
18 |
19 | Usage
20 | -----
21 |
22 | Just plug REST endpoint directly to your Cowboy router:
23 |
24 | ```erlang
25 | {"/rest/:resource", rest_cowboy, []},
26 | {"/rest/:resource/:id", rest_cowboy, []},
27 | ```
28 |
29 | Module
30 | ------
31 |
32 | Sample REST service implementation:
33 |
34 | ```erlang
35 | -module(users).
36 | -behaviour(rest).
37 | -compile({parse_transform, rest}).
38 | -include("users.hrl").
39 | -export([init/0, populate/1, exists/1, get/0, get/1, post/1, delete/1]).
40 | -rest_record(user).
41 |
42 | init() -> ets:new(users, [public, named_table, {keypos, #user.id}]).
43 | populate(Users) -> ets:insert(users, Users).
44 | exists(Id) -> ets:member(users, wf:to_list(Id)).
45 | get() -> ets:tab2list(users).
46 | get(Id) -> [User] = ets:lookup(users, wf:to_list(Id)), User.
47 | delete(Id) -> ets:delete(users, wf:to_list(Id)).
48 | post(#user{} = User) -> ets:insert(users, User);
49 | post(Data) -> post(from_json(Data, #user{})).
50 | ```
51 |
52 | Usage
53 | -----
54 |
55 | ```sh
56 | $ curl -i -X POST -d "id=vlad" localhost:8005/rest/users
57 | $ curl -i -X POST -d "id=doxtop" localhost:8005/rest/users
58 | $ curl -i -X GET localhost:8005/rest/users
59 | $ curl -i -X PUT -d "id=5HT" localhost:8005/rest/users/vlad
60 | $ curl -i -X GET localhost:8005/rest/users/5HT
61 | $ curl -i -X DELETE localhost:8005/rest/users/5HT
62 | ```
63 |
64 | Credits
65 | -------
66 |
67 | * Dmitry Bushmelev — ETS
68 | * Maxim Sokhatsky — KVS
69 |
70 | OM A HUM
71 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :kvs,
4 | dba: :kvs_mnesia,
5 | dba_st: :kvs_stream,
6 | schema: [:kvs, :kvs_stream, :bpe_metainfo]
7 |
8 | config :rest,
9 | logger_level: :debug,
10 | logger: [{:handler, :synrc, :logger_std_h,
11 | %{level: :debug,
12 | id: :synrc,
13 | module: :logger_std_h,
14 | config: %{type: :file, file: 'bpe.log'},
15 | formatter: {:logger_formatter,
16 | %{template: [:time,' ',:pid,' ',:msg,'\n'],
17 | single_line: true,}}}}]
18 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | REST
10 |
11 |
12 |
13 |
14 |
20 |
21 |
22 | REST
23 |
24 |
55 |
56 |
57 | ETS JSON
58 | Simple table-oriented service implementation:
59 |
60 |
61 | -module(users).
62 | -behaviour(rest).
63 | -compile({parse_transform, rest}).
64 | -include("users.hrl").
65 | -export([init/0, populate/1, exists/1, get/0, get/1, post/1, delete/1]).
66 | -rest_record(user).
67 |
68 | init() -> ets:new(users, [public, named_table, {keypos, #user.id}]).
69 | populate(Users) -> ets:insert(users, Users).
70 | exists(Id) -> ets:member(users, wf:to_list(Id)).
71 | get() -> ets:tab2list(users).
72 | get(Id) -> [User] = ets:lookup(users, wf:to_list(Id)), User.
73 | delete(Id) -> ets:delete(users, wf:to_list(Id)).
74 | post(#user{} = User) -> ets:insert(users, User);
75 | post(Data) -> post(from_json(Data, #user{})).
76 |
77 |
78 |
79 |
80 | METHODS
81 |
82 | $ curl -i -X POST -d "id=vlad" localhost:8005/rest/users
83 | $ curl -i -X POST -d "id=doxtop" localhost:8005/rest/users
84 | $ curl -i -X GET localhost:8005/rest/users
85 | $ curl -i -X PUT -d "id=5HT" localhost:8005/rest/users/vlad
86 | $ curl -i -X GET localhost:8005/rest/users/5HT
87 | $ curl -i -X DELETE localhost:8005/rest/users/5HT
88 |
89 |
90 |
91 | KVS JSON
92 | Automatiс chain-oriented API service implementation. Plug your Erlang HRL schema to
93 | mix.exs
94 |
95 | {:bpe, "~> 4.9.18"},
96 | {:erp, "~> 0.10.3"},
97 |
98 | or rebar.config:
99 |
100 | {bpe, ".*", {git, "git://github.com/synrc/bpe", {tag,"master"}}},
101 | {erp, ".*", {git, "git://github.com/erpuno/erp", {tag,"master"}}},
102 |
103 |
104 |
105 | ERP JSON
106 | Retrieve ERP organizational structure:
107 |
108 | $ curl -X GET http://localhost:8005/rest/kvs/0/erp/group
109 | {"\/erp\/group":[{"name":"Quanterall","url":"quanterall.com",
110 | "location":[],"type":[]}]}
111 |
112 | Retrive all invoice payments for Stamp project of FinaTech company:
113 |
114 | $ curl -X GET http://localhost:8005/rest/kvs/0/plm/FinaTech-Stamps/income
115 | {"\/plm\/FinaTech-Stamps\/income":[{"invoice":"APR-2018-PAY-FTST","account":[],
116 | "subaccount":[],"volume":{"fraction":0,"digits":12000},"price":{"fraction":0,
117 | "digits":1},"instrument":"USD","type":"crypto","from":[],"to":[]},{"invoice":
118 | "AUG-2018-PAY-FTST","account":[],"subaccount":[],"volume":{"fraction":0,
119 | "digits":12000},"price":{"fraction":0,"digits":1},"instrument":"USD","type":
120 | "crypto","from":[],"to":[]},{"invoice":"FEB-2018-PAY-FTST","account":[],
121 | "subaccount":[],"volume":{"fraction":0,"digits":7000},"price":{"fraction":0,
122 | "digits":1},"instrument":"USD","type":"crypto","from":[],"to":[]},{"invoice":
123 | "JAN-2018-PAY-FTST","account":[],"subaccount":[],"volume":{"fraction":0,"digits":
124 | 5000},"price":{"fraction":0,"digits":1},"instrument":"USD","type":"crypto","from":
125 | [],"to":[]},{"invoice":"JUL-2018-PAY-FTST","account":[],"subaccount":[],"volume":
126 | {"fraction":0,"digits":10000},"price":{"fraction":0,"digits":1},"instrument":
127 | "USD","type":"crypto","from":[],"to":[]},{"invoice":"JUN-2018-PAY-FTST",
128 | "account":[],"subaccount":[],"volume":{"fraction":0,"digits":10000},"price":
129 | {"fraction":0,"digits":1},"instrument":"USD","type":"crypto","from":[],"to":[]},
130 | {"invoice":"MAR-2018-PAY-FTST","account":[],"subaccount":[],"volume":
131 | {"fraction":0,"digits":10000},"price":{"fraction":0,"digits":1},"instrument":
132 | "USD","type":"crypto","from":[],"to":[]},{"invoice":"MAY-2018-PAY-FTST",
133 | "account":[],"subaccount":[],"volume":{"fraction":0,"digits":15000},
134 | "price":{"fraction":0,"digits":1},"instrument":"USD","type":"crypto",
135 | "from":[],"to":[]},{"invoice":"SEP-2018-PAY-FTST","account":[],"subaccount":
136 | [],"volume":{"fraction":0,"digits":15000},"price":{"fraction":0,"digits":1},
137 | "instrument":"USD","type":"crypto","from":[],"to":[]}]}
138 |
139 |
140 |
141 |
142 | BPE JSON
143 |
144 | Retrieve All History from Process 288117946539000:
145 |
146 | curl -X GET http://localhost:8005/rest/kvs/0/bpe/hist/288117946539000
147 | {"\/bpe\/hist\/288117946539000":[{"id":{"id":0,"proc":"288117946539000"},
148 | "container":"feed","feed_id":[],"prev":[],"next":[],"name":[],"task":"Created",
149 | "docs":[],"time":{"time":"{{2019,10,5},{21,21,44}}"}},{"id":{"id":1,"proc":
150 | "288117946539000"},"container":"feed","feed_id":[],"prev":[],"next":[],
151 | "name":[],"task":"Init","docs":[],"time":{"time":"{{2019,10,5},{21,21,50}}"}},
152 | {"id":{"id":2,"proc":"288117946539000"},"container":"feed","feed_id":[],
153 | "prev":[],"next":[],"name":[],"task":"Upload","docs":[],"time":{"time":
154 | "{{2019,10,5},{21,21,51}}"}},{"id":{"id":3,"proc":"288117946539000"},
155 | "container":"feed","feed_id":[],"prev":[],"next":[],"name":[],"task":
156 | "Payment","docs":[],"time":{"time":"{{2019,10,5},{21,21,51}}"}}]}
157 |
158 |
159 | Retrieve Step 2 from process 288117946539000:
160 |
161 | curl -X GET localhost:8005/rest/kvs/1/step,0,288117946539000/bpe/hist/288117946539000
162 | {"id":{"id":2,"proc":"288117946539000"},"container":"feed","feed_id":[],
163 | "prev":[],"next":[],"name":[],"task":"Upload","docs":[],"time":
164 | {"time":"{{2019,10,5},{21,21,51}}"}}
165 |
166 |
167 | Retrieve all processes:
168 |
169 | $ curl -X GET http://localhost:8005/rest/kvs/0/bpe/proc
170 | {"\/bpe\/proc":[{"id":"288117946539000","container":"feed","feed_id":[],
171 | "prev":[],"next":[],"name":"IBAN Account","feeds":[],"roles":[],"tasks":
172 | [{"name":"Created","module":"bpe_account","prompt":[],"etc":[]},{"name":
173 | "Init","module":"bpe_account","prompt":[],"roles":[],"etc":[]},{"name":
174 | "Upload","module":"bpe_account","prompt":[],"roles":[],"etc":[]},{"name":
175 | "Signatory","module":"bpe_account","prompt":[],"roles":[],"etc":[]},{"name":
176 | "Payment","module":"bpe_account","prompt":[],"roles":[],"etc":[]},{"name":
177 | "Process","module":"bpe_account","prompt":[],"roles":[],"etc":[]},{"name":
178 | "Final","module":"bpe_account","prompt":[],"etc":[]}],"events":[{"name":
179 | "PaymentReceived","module":[],"prompt":[],"etc":[],"payload":[],"timeout":[]},
180 | {"name":"*","module":[],"prompt":[],"etc":[],"payload":[],"timeout":
181 | {"spec":"{0,{10,0,10}}"},"timeDate":[],"timeDuration":[],"timeCycle":[]}],
182 | "hist":[],"flows":[{"name":[],"condition":[],"source":"Created","target":
183 | "Init"},{"name":[],"condition":[],"source":"Init","target":"Upload"},
184 | {"name":[],"condition":[],"source":"Upload","target":"Payment"},
185 | {"name":[],"condition":[],"source":"Payment","target":["Signatory",
186 | "Process"]},{"name":[],"condition":[],"source":"Process","target":
187 | ["Process","Final"]},{"name":[],"condition":[],"source":"Signatory",
188 | "target":["Process","Final"]}],"rules":[],"docs":[],"options":[],
189 | "task":"Created","timer":[],"notifications":"undefined","result":[],
190 | "started":{"time":"{{2019,10,5},{22,5,20}}"},"beginEvent":"Created",
191 | "endEvent":"Final"}]}
192 |
193 |
194 |
195 |
196 | MODULES
197 | Module rest is an Erlang/OTP application, while
198 | rest_cowboy and rest_kvs are the access/routing/gate/plugin-modules
199 | to other systems.
200 |
203 |
204 |
205 | CREDTIS
206 | - Dmitry Bushmelev
207 | - Maxim Sokhatsky
208 |
209 |
210 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/man/rest.htm:
--------------------------------------------------------------------------------
1 | REST
6 |
7 | REST
8 |
9 |
10 |
11 | INTRO
12 |
13 | The REST module.
14 |
15 |
16 | This module may refer to:
17 | MAN_MODULES
18 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/man/rest_cowboy.htm:
--------------------------------------------------------------------------------
1 | COWBOY
6 |
7 | COWBOY
8 |
9 |
10 |
11 | INTRO
12 |
13 | The COWBOY module.
14 |
15 |
16 | This module may refer to:
17 | MAN_MODULES
18 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/man/rest_kvs.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | KVS
7 |
8 |
9 |
14 |
15 | KVS
16 |
17 |
18 |
19 | INTRO
20 |
21 | The KVS module.
22 |
23 |
24 | This module may refer to:
25 | MAN_MODULES
26 |
27 |
28 |
31 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule REST.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :rest,
6 | version: "6.11.1",
7 | description: "REST erlang interface generator",
8 | deps: deps(),
9 | package: package()]
10 | end
11 |
12 | def application() do
13 | [
14 | mod: {:rest, []},
15 | applications: [:public_key,:asn1,:kernel,:stdlib,:ranch,:cowboy,:syntax_tools,:compiler,:rocksdb,:kvs, :erp, :bpe]
16 | ]
17 | end
18 |
19 | def deps, do: [ {:ex_doc, "~> 0.11", only: :dev},
20 | {:rocksdb, "~> 1.6.0"},
21 | {:bpe, "~> 6.11.0"},
22 | {:erp, "~> 1.11.0"},
23 | {:jsone, "~> 1.5.0"},
24 | {:cowboy, "~> 2.5.0"} ]
25 |
26 | defp package do
27 | [files: ~w(src LICENSE mix.exs README.md rebar.config),
28 | licenses: ["MIT"],
29 | links: %{"GitHub" => "https://github.com/synrc/rest"}]
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | {erl_opts, [debug_info]}.
2 | {deps_dir,"deps"}.
3 | {erl_first_files, ["rest.erl"]}.
4 | {deps, [{n2o, ".*", {git, "git://github.com/synrc/n2o", {tag,"master"}}},
5 | {kvs, ".*", {git, "git://github.com/synrc/kvs", {tag,"master"}}},
6 | {dec, ".*", {git, "git://github.com/erpuno/dec", {tag,"master"}}},
7 | {bpe, ".*", {git, "git://github.com/synrc/bpe", {tag,"master"}}},
8 | {erp, ".*", {git, "git://github.com/erpuno/erp", {tag,"master"}}},
9 | {jsone, ".*", {git, "git://github.com/sile/jsone", {tag,"master"}}},
10 | {syn, ".*", {git, "git://github.com/ostinelli/syn", {tag,"master"}}},
11 | {cowboy, ".*", {git, "git://github.com/voxoz/cowboy2", {tag,"master"}}}]}.
12 |
13 | {project_plugins, [rebar3_format]}.
14 | {format, [
15 | {files, ["src/*.erl", "test/*.erl"]},
16 | {formatter, otp_formatter},
17 | {options, #{ line_length => 108,
18 | paper => 250,
19 | spaces_around_fields => false,
20 | inlining => all,
21 | inline_clause_bodies => true,
22 | inline_expressions => true,
23 | inline_qualified_function_composition => true,
24 | inline_simple_funs => true,
25 | inline_items => all,
26 | inline_fields => true,
27 | inline_attributes => true
28 | }}]}.
29 |
--------------------------------------------------------------------------------
/src/bpe/beginEvent.erl:
--------------------------------------------------------------------------------
1 | -module(beginEvent).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(beginEvent).
6 | new() -> #beginEvent{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/boundaryEvent.erl:
--------------------------------------------------------------------------------
1 | -module(boundaryEvent).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(boundaryEvent).
6 | new() -> #boundaryEvent{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/endEvent.erl:
--------------------------------------------------------------------------------
1 | -module(endEvent).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(endEvent).
6 | new() -> #endEvent{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/hist.erl:
--------------------------------------------------------------------------------
1 | -module(hist).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(hist).
6 | new() -> #hist{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/messageEvent.erl:
--------------------------------------------------------------------------------
1 | -module(messageEvent).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(messageEvent).
6 | new() -> #messageEvent{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/process.erl:
--------------------------------------------------------------------------------
1 | -module(process).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(process).
6 | new() -> #process{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/sequenceFlow.erl:
--------------------------------------------------------------------------------
1 | -module(sequenceFlow).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(sequenceFlow).
6 | new() -> #sequenceFlow{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/serviceTask.erl:
--------------------------------------------------------------------------------
1 | -module(serviceTask).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(serviceTask).
6 | new() -> #serviceTask{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/step.erl:
--------------------------------------------------------------------------------
1 | -module(step).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(step).
6 | new() -> #step{}.
7 |
8 | uri({step,No,Proc}) when is_integer(Proc) -> {step,No,integer_to_list(Proc)};
9 | uri(X) -> X.
10 |
--------------------------------------------------------------------------------
/src/bpe/timeout.erl:
--------------------------------------------------------------------------------
1 | -module(timeout).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile(export_all).
4 | new() -> {timeout,{0,{0,0,0}}}.
5 | to_json(#timeout{spec=X}) -> [{<<"spec">>,iolist_to_binary(lists:flatten(io_lib:format("~p",[X])))}].
6 | from_json([{<<"spec">>,X}],_) -> #timeout{spec = rest:parse(X) };
7 | from_json([{spec,X}],_) -> #timeout{spec = rest:parse(X) }.
8 |
--------------------------------------------------------------------------------
/src/bpe/ts.erl:
--------------------------------------------------------------------------------
1 | -module(ts).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile(export_all).
4 | new() -> {ts,{{0,0,0},{0,0,0}}}.
5 | to_json(#ts{time=X}) -> [{<<"time">>,iolist_to_binary(lists:flatten(io_lib:format("~p",[X])))}].
6 | from_json([{<<"time">>,X}],_) -> #ts{time = rest:parse(X) };
7 | from_json([{time,X}],_) -> #ts{time = rest:parse(X) }.
8 |
--------------------------------------------------------------------------------
/src/bpe/tx.erl:
--------------------------------------------------------------------------------
1 | -module(tx).
2 | -include_lib("bpe/include/doc.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(tx).
6 | new() -> #tx{}.
7 |
--------------------------------------------------------------------------------
/src/bpe/userTask.erl:
--------------------------------------------------------------------------------
1 | -module(userTask).
2 | -include_lib("bpe/include/bpe.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(userTask).
6 | new() -> #userTask{}.
7 |
--------------------------------------------------------------------------------
/src/erp/Organization.erl:
--------------------------------------------------------------------------------
1 | -module('Organization').
2 | -include_lib("erp/include/organization.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record('Organization').
6 | new() -> #'Organization'{}.
7 |
--------------------------------------------------------------------------------
/src/erp/Payment.erl:
--------------------------------------------------------------------------------
1 | -module('Payment').
2 | -include_lib("erp/include/payment.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record('Payment').
6 | new() -> #'Payment'{type=fiat}.
7 |
--------------------------------------------------------------------------------
/src/erp/money.erl:
--------------------------------------------------------------------------------
1 | -module(money).
2 | -include_lib("dec/include/dec.hrl").
3 | -compile({parse_transform, rest}).
4 | -compile(export_all).
5 | -rest_record(money).
6 | new() -> #money{}.
7 |
--------------------------------------------------------------------------------
/src/erp/users.erl:
--------------------------------------------------------------------------------
1 | -module(users).
2 | -compile({parse_transform, rest}).
3 | -record(user, {id,cn,name,type}).
4 | -export([init/0, populate/1, new/0, exists/1, get/0, get/1, post/1, delete/1]).
5 | -rest_record(user).
6 |
7 | new() -> #user{}.
8 | init() -> ets:new(users, [public, named_table, {keypos, #user.id}]).
9 | populate(Users) -> ets:insert(users, Users).
10 | exists(Id) -> X = ets:member(users, binary_to_list(Id)), io:format("Member: ~p~n",[X]), X.
11 | get() -> ets:tab2list(users).
12 | get(Id) -> [U] = ets:lookup(users, binary_to_list(Id)), io:format("User: ~p~n",[U]), U.
13 | delete(Id) -> ets:delete(users, binary_to_list(Id)).
14 | post(#user{} = User) -> ets:insert(users, User);
15 | post(Data) -> post(from_json(Data, #user{})).
16 |
--------------------------------------------------------------------------------
/src/rest.app.src:
--------------------------------------------------------------------------------
1 | {application, rest, [
2 | {description, "REST Yoctoframework"},
3 | {vsn, "6.11.1"},
4 | {applications, [public_key,asn1,kernel,stdlib,ranch,cowboy,syntax_tools,compiler,n2o]},
5 | {modules, []},
6 | {registered, []},
7 | {mod, { rest, []}},
8 | {env, []}
9 | ]}.
10 |
--------------------------------------------------------------------------------
/src/rest.erl:
--------------------------------------------------------------------------------
1 | -module(rest).
2 |
3 | -author('Dmitry Bushmelev').
4 |
5 | -behaviour(application).
6 |
7 | -behaviour(supervisor).
8 |
9 | -export([init/1, start/2, stop/1]).
10 |
11 | -export([behaviour_info/1,
12 | parse_transform/2,
13 | generate_to_json/3,
14 | binarize/1,
15 | generate_from_json/3,
16 | from_json/2,
17 | to_json/1,
18 | to_binary/1,
19 | parse/1,
20 | atomize/1]).
21 |
22 | stop(_State) -> ok.
23 |
24 | start(_StartType, _StartArgs) ->
25 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
26 |
27 | init([]) ->
28 | users:init(),
29 | kvs:join(),
30 | cowboy:start_clear(http,
31 | [{port, application:get_env(n2o, port, 8005)}],
32 | #{env => #{dispatch => points()}}),
33 | {ok, {{one_for_one, 5, 10}, []}}.
34 |
35 | points() ->
36 | cowboy_router:compile([{'_',
37 | [{"/rest/kvs/0/[...]", rest_kvs, []},
38 | {"/rest/kvs/1/:id/[...]", rest_kvs, []},
39 | {"/rest/:resource", rest_cowboy, []},
40 | {"/rest/:resource/:id", rest_cowboy, []}]}]).
41 |
42 | behaviour_info(callbacks) ->
43 | [{exists, 1},
44 | {get, 0},
45 | {get, 1},
46 | {post, 1},
47 | {delete, 1},
48 | {from_json, 2},
49 | {to_json, 1}];
50 | behaviour_info(_) -> undefined.
51 |
52 | parse_transform(Forms, _Options) ->
53 | RecordName = rest_record(Forms),
54 | RecordFields = record_fields(RecordName, Forms),
55 | Forms1 = generate({from_json, 2},
56 | RecordName,
57 | RecordFields,
58 | Forms),
59 | Forms2 = generate({to_json, 1},
60 | RecordName,
61 | RecordFields,
62 | Forms1),
63 | Forms2.
64 |
65 | rest_record([]) -> [];
66 | rest_record([{attribute, _, rest_record, RecordName}
67 | | _Forms]) ->
68 | RecordName;
69 | rest_record([_ | Forms]) -> rest_record(Forms).
70 |
71 | record_field({record_field, _, {atom, _, Field}},
72 | _cordName) ->
73 | % io:format("Case 1: ~p~n",[Field]),
74 | Field;
75 | record_field({record_field, _, {atom, _, Field}, Type},
76 | _cordName) ->
77 | % io:format("Case 2: ~p~n",[Field]),
78 | Field;
79 | record_field({typed_record_field,
80 | {record_field, _, {atom, _, Field}, _},
81 | Type},
82 | RecordName) ->
83 | Rec = allow(Type),
84 | put({RecordName, Field}, Rec),
85 | % case Rec of
86 | % undefined -> io:format("Case 3: ~p~n",[Field]);
87 | % _ -> io:format("Case 3: ~p, Link: ~p~n",[Field,Rec]) end,
88 | Field.
89 |
90 | allow({type, _, union, Components}) ->
91 | findType(Components);
92 | allow(Type) -> findType([Type]).
93 |
94 | findType([]) -> undefined;
95 | findType([{type, _, record, [{atom, _, X}]} | T]) -> X;
96 | findType([{remote_type,
97 | _,
98 | [{atom, _, _}, {atom, _, X}, _]}
99 | | T]) ->
100 | X;
101 | findType([H | T]) ->
102 | % io:format("Unknown Type: ~p~n",[H]),
103 | findType(T).
104 |
105 | record_fields(RecordName,
106 | [{attribute, _, record, {RecordName, Fields}}
107 | | _Forms]) ->
108 | [record_field(Field, RecordName) || Field <- Fields];
109 | record_fields(RecordName, [_ | Forms]) ->
110 | record_fields(RecordName, Forms);
111 | record_fields(__cordName, []) -> [].
112 |
113 | last_export_line(Exports) ->
114 | case lists:reverse(Exports) of
115 | [{_, Line, _, _} | _] -> Line;
116 | _ -> 0
117 | end.
118 |
119 | generate({FunName, _Arity} = Fun, Record, Fields,
120 | Forms) ->
121 | Exports = lists:filter(fun ({attribute,
122 | _,
123 | export,
124 | _}) ->
125 | true;
126 | (_) -> false
127 | end,
128 | Forms),
129 | case exported(Fun, Exports) of
130 | true -> Forms;
131 | false ->
132 | Line = last_export_line(Exports),
133 | Gen = list_to_atom("generate_" ++
134 | atom_to_list(FunName)),
135 | lists:flatten([(?MODULE):Gen(export(Form, Fun, Line),
136 | Record,
137 | Fields)
138 | || Form <- Forms])
139 | end.
140 |
141 | exported(Fun, Exports) ->
142 | lists:member(Fun,
143 | lists:flatten([E
144 | || {attribute, _, export, E} <- Exports])).
145 |
146 | field_var(Field) ->
147 | list_to_atom("V_" ++ atom_to_list(Field)).
148 |
149 | from_json_prelude(Line) ->
150 | {clause,
151 | Line,
152 | [{nil, Line}, {var, Line, 'Acc'}],
153 | [],
154 | [{var, Line, 'Acc'}]}.
155 |
156 | from_json_coda(Line) ->
157 | {clause,
158 | Line,
159 | [{cons, Line, {var, Line, '_'}, {var, Line, 'Json'}},
160 | {var, Line, 'Acc'}],
161 | [],
162 | [{call,
163 | Line,
164 | {atom, Line, from_json},
165 | [% {var, Line, 'Json'} % here is fix for recursive binarized preprocessing to raw X:from_json
166 | {call,
167 | Line,
168 | {remote,
169 | Line,
170 | {atom, Line, ?MODULE},
171 | {atom, Line, binarize}},
172 | [{var, Line, 'Json'}]},
173 | {var, Line, 'Acc'}]}]}.
174 |
175 | from_json_clauses(_, _, []) -> [];
176 | from_json_clauses(Line, Record, [Field | Fields]) ->
177 | [{clause,
178 | Line,
179 | [{cons,
180 | Line,
181 | {tuple,
182 | Line,
183 | [{bin,
184 | Line,
185 | [{bin_element,
186 | Line,
187 | {string, Line, atom_to_list(Field)},
188 | default,
189 | default}]},
190 | {var, Line, field_var(Field)}]},
191 | {var, Line, 'Json'}},
192 | {var, Line, 'Acc'}],
193 | [],
194 | [{call,
195 | Line,
196 | {atom, Line, from_json},
197 | [{var, Line, 'Json'},
198 | {record,
199 | Line,
200 | {var, Line, 'Acc'},
201 | Record,
202 | [{record_field,
203 | Line,
204 | {atom, Line, Field},
205 | {call,
206 | Line,
207 | {remote,
208 | Line,
209 | {atom, Line, ?MODULE},
210 | {atom, Line, from_json}},
211 | [{var, Line, field_var(Field)},
212 | {atom,
213 | Line,
214 | case get({Record, Field}) of
215 | undefined -> Record;
216 | FieldType -> FieldType
217 | end}]}}]}]}]}
218 | | from_json_clauses(Line, Record, Fields)].
219 |
220 | generate_from_json({eof, Line}, Record, Fields) ->
221 | [{function,
222 | Line,
223 | from_json,
224 | 2,
225 | [from_json_prelude(Line)] ++
226 | from_json_clauses(Line, Record, Fields) ++
227 | [from_json_coda(Line)]},
228 | {eof, Line + 1}];
229 | generate_from_json(Form, _, _) -> Form.
230 |
231 | export({attribute, LastExportLine, export, Exports},
232 | Fun, LastExportLine) ->
233 | {attribute, LastExportLine, export, [Fun | Exports]};
234 | export(Form, _, _) -> Form.
235 |
236 | to_json_cons(Line, []) -> {nil, Line};
237 | to_json_cons(Line, [Field | Fields]) ->
238 | {cons,
239 | Line,
240 | {tuple,
241 | Line,
242 | [{atom, Line, Field},
243 | {call,
244 | Line,
245 | {remote,
246 | Line,
247 | {atom, Line, ?MODULE},
248 | {atom, Line, to_json}},
249 | [{var, Line, field_var(Field)}]}]},
250 | to_json_cons(Line, Fields)}.
251 |
252 | generate_to_json({eof, Line}, Record, Fields) ->
253 | [{function,
254 | Line,
255 | to_json,
256 | 1,
257 | [{clause,
258 | Line,
259 | [{record,
260 | Line,
261 | Record,
262 | [{record_field,
263 | Line,
264 | {atom, Line, F},
265 | {var, Line, field_var(F)}}
266 | || F <- Fields]}],
267 | [],
268 | [to_json_cons(Line, Fields)]}]},
269 | {eof, Line + 1}];
270 | generate_to_json(Form, _, _) -> Form.
271 |
272 | from_json(<>, _) -> binary_to_list(Data);
273 | from_json({struct, Props}, X) -> from_json(Props, X);
274 | from_json([{Key, _} | _] = Props, X)
275 | when Key =/= struct ->
276 | X:from_json(binarize(Props), X:new());
277 | from_json(Any, X) -> Any.
278 |
279 | atomize([{Key, _} | _] = Props) when Key =/= struct ->
280 | lists:map(fun ({K, V}) when is_atom(K) -> {K, V};
281 | ({K, V}) when is_binary(K) ->
282 | {list_to_existing_atom(binary_to_list(K)), V}
283 | end,
284 | Props);
285 | atomize(X) -> X.
286 |
287 | binarize([{Key, _} | _] = Props) when Key =/= struct ->
288 | lists:map(fun ({K, V}) when is_atom(K) ->
289 | {list_to_binary(atom_to_list(K)), allowed_value(V)};
290 | ({K, V}) when is_binary(K) -> {K, allowed_value(V)}
291 | end,
292 | Props);
293 | binarize(X) -> X.
294 |
295 | allowed_value(X) when is_reference(X) -> [];
296 | allowed_value(X) -> X.
297 |
298 | to_json(X) when is_tuple(X) ->
299 | Module = hd(tuple_to_list(X)),
300 | Module:to_json(X);
301 | to_json(Data) ->
302 | case is_string(Data) of
303 | true -> rest:to_binary(Data);
304 | false -> json_match(Data)
305 | end.
306 |
307 | json_match([{_, _} | _] = Props) ->
308 | [{rest:to_binary(Key), to_json(Value)}
309 | || {Key, Value} <- Props];
310 | json_match([_ | _] = NonEmptyList) ->
311 | [to_json(X) || X <- NonEmptyList];
312 | json_match(Any) -> Any.
313 |
314 | is_char(C) ->
315 | is_integer(C) andalso C >= 0 andalso C =< 255.
316 |
317 | is_string([N | _] = PossibleString) when is_number(N) ->
318 | lists:all(fun is_char/1, PossibleString);
319 | is_string(_) -> false.
320 |
321 | to_binary(A) when is_atom(A) ->
322 | atom_to_binary(A, latin1);
323 | to_binary(B) when is_binary(B) -> B;
324 | to_binary(I) when is_integer(I) ->
325 | to_binary(integer_to_list(I));
326 | to_binary(F) when is_float(F) ->
327 | float_to_binary(F, [{decimals, 9}, compact]);
328 | to_binary(L) when is_list(L) -> iolist_to_binary(L).
329 |
330 | parse(String) when is_binary(String) ->
331 | parse(binary_to_list(String));
332 | parse(String) ->
333 | {ok, Tokens, _EndLine} = erl_scan:string(String ++ "."),
334 | {ok, AbsForm} = erl_parse:parse_exprs(Tokens),
335 | {value, Value, _Bs} = erl_eval:exprs(AbsForm,
336 | erl_eval:new_bindings()),
337 | Value.
338 |
--------------------------------------------------------------------------------
/src/rest_cowboy.erl:
--------------------------------------------------------------------------------
1 | -module(rest_cowboy).
2 |
3 | -author('Dmitry Bushmelev').
4 |
5 | -record(st,
6 | {resource_module = undefined :: atom(),
7 | resource_id = undefined :: binary()}).
8 |
9 | -export([init/2,
10 | rest_init/2,
11 | resource_exists/2,
12 | allowed_methods/2,
13 | content_types_provided/2,
14 | to_html/2,
15 | to_json/2,
16 | content_types_accepted/2,
17 | delete_resource/2,
18 | handle_urlencoded_data/2,
19 | handle_json_data/2]).
20 |
21 | init(Req, Opts) -> {cowboy_rest, Req, Opts}.
22 |
23 | -ifndef(REST_JSON).
24 |
25 | -define(REST_JSON,
26 | application:get_env(rest, json, jsone)).
27 |
28 | -endif.
29 |
30 | c(X) -> list_to_atom(binary_to_list(X)).
31 |
32 | rest_init(Req, _Opts) ->
33 | {Resource, Req1} = cowboy_req:binding(resource, Req),
34 | Module = case rest_module(Resource) of
35 | {ok, M} -> M;
36 | _ -> undefined
37 | end,
38 | {Id, Req2} = cowboy_req:binding(id, Req1),
39 | {Origin, Req3} = cowboy_req:header(<<"origin">>,
40 | Req2,
41 | <<"*">>),
42 | Req4 =
43 | cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>,
44 | Origin,
45 | Req3),
46 | io:format("REST INIT~p"),
47 | {ok,
48 | Req4,
49 | #st{resource_module = Module, resource_id = Id}}.
50 |
51 | resource_exists(#{bindings :=
52 | #{resource := Module, id := Id}} =
53 | Req,
54 | State) ->
55 | M = c(Module),
56 | io:format("EXISTS: ~p dymamic: ~p~n",
57 | [Id, M:exists(Id)]),
58 | {M:exists(Id), Req, State};
59 | resource_exists(#{bindings := #{resource := Module}} =
60 | Req,
61 | State) ->
62 | io:format("resource ~p: no-id~n", [Module]),
63 | {true, Req, State};
64 | resource_exists(#{bindings := #{id := _}} = Req,
65 | State) ->
66 | io:format("EXISTS id: true~n"),
67 | {true, Req, State}.
68 |
69 | allowed_methods(#{bindings := #{resource := _}} = Req,
70 | State) ->
71 | {[<<"GET">>, <<"POST">>], Req, State};
72 | allowed_methods(#{bindings :=
73 | #{resource := _, id := _}} =
74 | Req,
75 | State) ->
76 | {[<<"GET">>, <<"PUT">>, <<"DELETE">>], Req, State}.
77 |
78 | content_types_provided(#{bindings :=
79 | #{resource := Module}} =
80 | Req,
81 | State) ->
82 | {case erlang:function_exported(c(Module), to_html, 1) of
83 | false -> [{<<"application/json">>, to_json}];
84 | true ->
85 | [{<<"text/html">>, to_html},
86 | {<<"application/json">>, to_json}]
87 | end,
88 | Req,
89 | State}.
90 |
91 | to_html(#{bindings := #{resource := Module, id := Id}} =
92 | Req,
93 | State) ->
94 | M = c(Module),
95 | Body = case Id of
96 | undefined ->
97 | [M:to_html(Resource) || Resource <- M:get()];
98 | _ -> M:to_html(M:get(Id))
99 | end,
100 | Html = case erlang:function_exported(M, html_layout, 2)
101 | of
102 | true -> M:html_layout(Req, Body);
103 | false -> default_html_layout(Body)
104 | end,
105 | {Html, Req, State}.
106 |
107 | default_html_layout(Body) ->
108 | [<<"">>, Body, <<"">>].
109 |
110 | to_json(#{bindings := #{resource := Module, id := Id}} =
111 | Req,
112 | State) ->
113 | io:format("~p ~p ~p~n", [?FUNCTION_NAME, Module, Id]),
114 | M = c(Module),
115 | Struct = case Id of
116 | undefined ->
117 | [{M, [M:to_json(Resource) || Resource <- M:get()]}];
118 | _ -> M:to_json(M:get(Id))
119 | end,
120 | {iolist_to_binary((?REST_JSON):encode(Struct)),
121 | Req,
122 | State};
123 | to_json(#{bindings := #{resource := Module}} = Req,
124 | State) ->
125 | io:format("~p ~p~n", [?FUNCTION_NAME, Module]),
126 | M = c(Module),
127 | Struct = [{M,
128 | [M:to_json(Resource) || Resource <- M:get()]}],
129 | {iolist_to_binary((?REST_JSON):encode(Struct)),
130 | Req,
131 | State}.
132 |
133 | content_types_accepted(Req, State) ->
134 | {[{<<"application/x-www-form-urlencoded">>,
135 | handle_urlencoded_data},
136 | {<<"application/json">>, handle_json_data}],
137 | Req,
138 | State}.
139 |
140 | handle_urlencoded_data(#{bindings :=
141 | #{resource := Module}} =
142 | Req0,
143 | State) ->
144 | {ok, Data1, Req} =
145 | cowboy_req:read_urlencoded_body(Req0),
146 | io:format("FORM: ~p, Data1: ~p~n", [Module, Data1]),
147 | {handle_data(c(Module), [], Data1, Req), Req, State};
148 | handle_urlencoded_data(#{bindings :=
149 | #{resource := Module, id := Id}} =
150 | Req,
151 | State) ->
152 | {ok, Data, Req2} = cowboy_req:read_urlencoded_body(Req),
153 | io:format("FORM: ~p~n", [Data]),
154 | {handle_data(c(Module), Id, Data, Req), Req2, State}.
155 |
156 | handle_json_data(#{bindings := #{resource := Module}} =
157 | Req,
158 | State) ->
159 | {ok, Binary, Req2} = cowboy_req:read_body(Req),
160 | io:format("JSON: ~p~n", [Binary]),
161 | Data = case (?REST_JSON):decode(Binary) of
162 | {struct, Struct} -> Struct;
163 | S -> S
164 | end,
165 | {handle_data(c(Module), [], Data, Req), Req2, State};
166 | handle_json_data(#{bindings :=
167 | #{resource := Module, id := Id}} =
168 | Req,
169 | State) ->
170 | {ok, Binary, Req2} = cowboy_req:read_body(Req),
171 | io:format("JSON: ~p~n", [Binary]),
172 | Data = case (?REST_JSON):decode(Binary) of
173 | {struct, Struct} -> Struct;
174 | S -> S
175 | end,
176 | {handle_data(c(Module), Id, Data, Req), Req2, State}.
177 |
178 | handle_data(Mod, Id, Data, Req) ->
179 | io:format("handle_data(~p)~n", [{Mod, Id, Data, Req}]),
180 | Valid = case erlang:function_exported(Mod, validate, 2)
181 | of
182 | true -> Mod:validate(Id, Data);
183 | false -> default_validate(Mod, Id, Data, Req)
184 | end,
185 | io:format("Valid ~p Id ~p~n", [Valid, Id]),
186 | case {Valid, Id} of
187 | {false, _} -> false;
188 | {true, []} -> Mod:post(Data);
189 | {true, <<"undefined">>} -> Mod:post(Data);
190 | {true, _} ->
191 | case erlang:function_exported(Mod, put, 2) of
192 | true -> Mod:put(Id, Data);
193 | false -> default_put(Mod, Id, Data, Req)
194 | end
195 | end.
196 |
197 | default_put(Mod, Id, Data, Req) when is_map(Data) ->
198 | default_put(Mod, Id, maps:to_list(Data), Req);
199 | default_put(Mod, Id, Data, Req) ->
200 | NewRes = Mod:from_json(Data, Mod:get(Id)),
201 | NewId = proplists:get_value(id, Mod:to_json(NewRes)),
202 | io:format("Id ~p NewId ~p~n", [Id, NewId]),
203 | case Id =/= NewId of
204 | true when Id =:= [] -> skip;
205 | true -> Mod:delete(Id);
206 | false -> true
207 | end,
208 | Mod:post(NewRes).
209 |
210 | default_validate(Mod, Id, DataX, Req0)
211 | when is_map(DataX) ->
212 | default_validate(Mod, Id, maps:to_list(DataX), Req0);
213 | default_validate(Mod, Id, Data, Req0) ->
214 | Allowed = case erlang:function_exported(Mod,
215 | keys_allowed,
216 | 1)
217 | of
218 | true -> Mod:keys_allowed(proplists:get_keys(Data));
219 | false -> true
220 | end,
221 | validate_match(Mod,
222 | Id,
223 | Allowed,
224 | proplists:get_value(<<"id">>, Data)).
225 |
226 | validate_match(_Mod, [], true, []) -> false;
227 | validate_match(Mod, [], true, NewId) ->
228 | not Mod:exists(NewId);
229 | validate_match(_Mod, _Id, true, []) -> true;
230 | validate_match(_Mod, Id, true, Id) -> true;
231 | validate_match(Mod, _Id, true, NewId) ->
232 | not Mod:exists(NewId);
233 | validate_match(_, _, _, _) -> false.
234 |
235 | delete_resource(#{bindings :=
236 | #{resource := Module, id := []}} =
237 | Req,
238 | State) ->
239 | {[], Req, State};
240 | delete_resource(#{bindings :=
241 | #{resource := Module, id := Id}} =
242 | Req,
243 | State) ->
244 | M = c(Module),
245 | io:format("DELETE: ~p ~p ~p~n", [M, Id, M:delete(Id)]),
246 | {M:delete(Id), Req, State}.
247 |
248 | rest_module(Module) when is_binary(Module) ->
249 | rest_module(binary_to_list(Module));
250 | rest_module(Module) ->
251 | try M = list_to_existing_atom(Module),
252 | Info = proplists:get_value(attributes, M:module_info()),
253 | true = lists:member(rest,
254 | proplists:get_value(behaviour, Info)),
255 | {ok, M}
256 | catch
257 | error:Error -> {error, Error}
258 | end.
259 |
--------------------------------------------------------------------------------
/src/rest_kvs.erl:
--------------------------------------------------------------------------------
1 | -module(rest_kvs).
2 |
3 | -include_lib("kvs/include/kvs.hrl").
4 |
5 | -compile(export_all).
6 |
7 | -export([exists/1,
8 | exists/2,
9 | new/1,
10 | get/1,
11 | delete/2,
12 | post/2,
13 | post/3]).
14 |
15 | -export([init/2,
16 | resource_exists/2,
17 | allowed_methods/2,
18 | content_types_provided/2,
19 | to_html/2,
20 | to_json/2,
21 | content_types_accepted/2,
22 | delete_resource/2,
23 | handle_urlencoded_data/2,
24 | handle_json_data/2]).
25 |
26 | -ifndef(REST_JSON).
27 |
28 | -define(REST_JSON,
29 | application:get_env(rest, json, jsone)).
30 |
31 | -endif.
32 |
33 | c(X) when is_tuple(X) ->
34 | Module = hd(tuple_to_list(X)),
35 | Module:uri(X);
36 | c(X) -> binary_to_list(X).
37 |
38 | % kvs rest api
39 |
40 | new(Type) -> Type:new().
41 |
42 | exists(Mod) ->
43 | {X, _} = kvs:get(writer, c(Mod)),
44 | X == ok.
45 |
46 | exists(Mod, Id) ->
47 | {X, _} = kvs:get(c(Mod), c(Id)),
48 | X == ok.
49 |
50 | get(Mod) -> kvs:all(Mod).
51 |
52 | get(Mod, Id) ->
53 | {X, Y} = kvs:get(c(Mod), c(Id)),
54 | X == ok,
55 | Y.
56 |
57 | delete(Mod, Id) -> kvs:delete(Mod, Id).
58 |
59 | post(Mod, Resource) when is_tuple(Resource) ->
60 | kvs:append(Mod, Resource).
61 |
62 | post(Type, Mod, Data) when is_list(Data) ->
63 | post(Mod, Type:from_json(new(Type))).
64 |
65 | % cowboy rest api
66 |
67 | update_req(Req) ->
68 | #{bindings := Bindings, path_info := List} = Req,
69 | Req#{bindings =>
70 | Bindings#{resource =>
71 | list_to_binary("/" ++
72 | string:join(lists:map(fun (X) -> binary_to_list(X) end,
73 | List),
74 | "/"))}}.
75 |
76 | init(#{bindings := #{id := Id}} = Req, State) ->
77 | {cowboy_rest, update_req(Req), State};
78 | init(Req, State) ->
79 | {cowboy_rest, update_req(Req), State}.
80 |
81 | parse_id(Id) ->
82 | List = binary_to_list(Id),
83 | Parsed = case string:tokens(List, ",") of
84 | [X] -> Id;
85 | _ -> rest:parse(lists:concat(["{", List, "}"]))
86 | end.
87 |
88 | resource_exists(#{bindings :=
89 | #{resource := Module, id := Id}} =
90 | Req,
91 | State) ->
92 | {rest_kvs:exists(Module, parse_id(Id)), Req, State};
93 | resource_exists(#{bindings := #{resource := Module}} =
94 | Req,
95 | State) ->
96 | {rest_kvs:exists(Module), Req, State};
97 | resource_exists(#{bindings := #{id := _}} = Req,
98 | State) ->
99 | {true, Req, State}.
100 |
101 | allowed_methods(#{bindings := #{resource := _}} = Req,
102 | State) ->
103 | {[<<"GET">>, <<"POST">>], Req, State};
104 | allowed_methods(#{bindings :=
105 | #{resource := _, id := _}} =
106 | Req,
107 | State) ->
108 | {[<<"GET">>, <<"PUT">>, <<"DELETE">>], Req, State}.
109 |
110 | delete_resource(#{bindings :=
111 | #{resource := Module, id := []}} =
112 | Req,
113 | State) ->
114 | {[], Req, State};
115 | delete_resource(#{bindings :=
116 | #{resource := Module, id := Id}} =
117 | Req,
118 | State) ->
119 | {rest_kvs:delete(Module, Id), Req, State}.
120 |
121 | content_types_provided(#{bindings :=
122 | #{resource := Module}} =
123 | Req,
124 | State) ->
125 | {case application:get_env(rest, html, false) of
126 | false -> [{<<"application/json">>, to_json}];
127 | true ->
128 | [{<<"text/html">>, to_html},
129 | {<<"application/json">>, to_json}]
130 | end,
131 | Req,
132 | State}.
133 |
134 | % TODO: HTML render broken!
135 |
136 | to_html(#{bindings := #{resource := Module, id := Id}} =
137 | Req,
138 | State) ->
139 | % Body = case Id of
140 | % Id when Id==[];Id==undefined -> [ rest_kvs:to_html(Module, Resource) || Resource <- rest_kvs:get(Module,Id) ];
141 | % _ -> rest_kvs:to_html(rest_kvs:get(Module,Id)) end,
142 | % Html = case application:get_env(rest,html_layout,false) of
143 | % true -> rest_kvs:html_layout(Module, Req, Body);
144 | % false -> default_html_layout(Body) end,
145 | {<<>>, Req, State}.
146 |
147 | default_html_layout(Body) ->
148 | [<<"">>, Body, <<"">>].
149 |
150 | % JSON seems fine
151 |
152 | to_json(#{bindings := #{resource := Module, id := Id}} =
153 | Req,
154 | State) ->
155 | {ok, Resource} = kvs:get(c(Module), c(parse_id(Id))),
156 | Type = element(1, Resource),
157 | {iolist_to_binary([(?REST_JSON):encode(rest:binarize(Type:to_json(Resource))),
158 | "\n"]),
159 | Req,
160 | State};
161 | to_json(#{bindings := #{resource := Module}} = Req,
162 | State) ->
163 | Fold = [begin
164 | M = element(1, Resource),
165 | rest:binarize(M:to_json(Resource))
166 | end
167 | || Resource <- kvs:all(c(Module))],
168 | {iolist_to_binary([(?REST_JSON):encode([{Module,
169 | Fold}]),
170 | "\n"]),
171 | Req,
172 | State}.
173 |
174 | content_types_accepted(Req, State) ->
175 | {[{<<"application/x-www-form-urlencoded">>,
176 | handle_urlencoded_data},
177 | {<<"application/json">>, handle_json_data}],
178 | Req,
179 | State}.
180 |
181 | handle_urlencoded_data(#{bindings :=
182 | #{resource := Module, id := Id}} =
183 | Req,
184 | State) ->
185 | {ok, Data, Req2} = cowboy_req:read_urlencoded_body(Req),
186 | io:format("FORM: ~p~n", [Data]),
187 | {handle_data(Module, Id, Data, Req), Req2, State};
188 | handle_urlencoded_data(#{bindings :=
189 | #{resource := Module}} =
190 | Req0,
191 | State) ->
192 | {ok, Data1, Req} =
193 | cowboy_req:read_urlencoded_body(Req0),
194 | io:format("FORM: ~p, Data1: ~p~n", [Module, Data1]),
195 | {handle_data(Module, [], Data1, Req), Req, State}.
196 |
197 | handle_json_data(#{bindings :=
198 | #{resource := Module, id := Id}} =
199 | Req,
200 | State) ->
201 | {ok, Binary, Req2} = cowboy_req:read_body(Req),
202 | io:format("JSON: ~p~n", [Binary]),
203 | Data = case (?REST_JSON):decode(Binary) of
204 | {struct, Struct} -> Struct;
205 | S -> S
206 | end,
207 | {handle_data(Module, Id, Data, Req), Req2, State};
208 | handle_json_data(#{bindings := #{resource := Module}} =
209 | Req,
210 | State) ->
211 | {ok, Binary, Req2} = cowboy_req:read_body(Req),
212 | io:format("JSON: ~p~n", [Binary]),
213 | Data = case (?REST_JSON):decode(Binary) of
214 | {struct, Struct} -> Struct;
215 | S -> S
216 | end,
217 | {handle_data(Module, [], Data, Req), Req2, State}.
218 |
219 | handle_data(Mod, Id, Data, Req) ->
220 | Type = proplists:get_value(<<"rec">>, Data),
221 | Valid = case application:get_env(rest, validate, false)
222 | of
223 | true -> rest_kvs:validate(Mod, Id, Data);
224 | false -> default_validate(Mod, Id, Data, Req)
225 | end,
226 | case {Valid, Id} of
227 | {false, _} -> false;
228 | {true, <<"undefined">>} ->
229 | rest_kvs:post(Type, Mod, Data);
230 | {true, _} ->
231 | case application:get_env(rest, custom_put, false) of
232 | true -> Type:put(Mod, Id, Data);
233 | false -> default_put(Type, Mod, Id, Data, Req)
234 | end
235 | end.
236 |
237 | validate(_, _, _) -> true.
238 |
239 | keys_allowed(_, _) -> true.
240 |
241 | default_put(Type, Mod, Id, Data, Req)
242 | when is_map(Data) ->
243 | default_put(Type, Mod, Id, maps:to_list(Data), Req);
244 | default_put(Type, Mod, Id, Data, Req) ->
245 | NewRes = Type:from_json(Data, rest_kvs:get(Mod, Id)),
246 | NewId = proplists:get_value(id, Type:to_json(NewRes)),
247 | case Id =/= NewId of
248 | true -> rest_kvs:delete(Mod, Id);
249 | false -> true
250 | end,
251 | rest_kvs:post(Type, Mod, NewRes).
252 |
253 | default_validate(Mod, Id, DataX, Req0)
254 | when is_map(DataX) ->
255 | default_validate(Mod, Id, maps:to_list(DataX), Req0);
256 | default_validate(Mod, Id, Data, Req0) ->
257 | Allowed = case application:get_env(rest,
258 | keys_allowed,
259 | false)
260 | of
261 | true ->
262 | rest_kvs:keys_allowed(c(Mod), proplists:get_keys(Data));
263 | false -> true
264 | end,
265 | validate_match(Mod,
266 | Id,
267 | Allowed,
268 | proplists:get_value(<<"id">>, Data)).
269 |
270 | validate_match(_Mod, [], true, []) -> false;
271 | validate_match(Mod, [], true, NewId) ->
272 | not rest_kvs:exists(Mod, NewId);
273 | validate_match(_Mod, _Id, true, []) -> true;
274 | validate_match(_Mod, Id, true, Id) -> true;
275 | validate_match(Mod, _Id, true, NewId) ->
276 | not rest_kvs:exists(Mod, NewId);
277 | validate_match(_, _, _, _) -> false.
278 |
--------------------------------------------------------------------------------
/sys.config:
--------------------------------------------------------------------------------
1 | [
2 | {n2o, [{port,8005},
3 | {igor,"deps/n2o/src/protos"},
4 | {app,rest},
5 | {formatter,n2o_bert},
6 | {protocols,[n2o_heart,n2o_nitro,n2o_ftp]},
7 | {session,n2o_session},
8 | {origin,<<"*">>},
9 | {pickler,n2o_secret},
10 | {event,pickle}]},
11 | {kvs, [{dba,kvs_mnesia},
12 | {dba_st,kvs_stream},
13 | {schema, [kvs, kvs_stream ]} ]}
14 | ].
15 |
--------------------------------------------------------------------------------