├── .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 | [![Actions Status](https://github.com/synrc/rest/workflows/mix/badge.svg)](https://github.com/synrc/rest/actions) 5 | [![Build Status](https://travis-ci.com/synrc/rest.svg?branch=master)](https://travis-ci.com/synrc/rest) 6 | [![Hex pm](http://img.shields.io/hexpm/v/rest.svg?style=flat)](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 | 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 | --------------------------------------------------------------------------------