Skip to content
Newer
Older
100644 187 lines (170 sloc) 8.05 KB
d036073 BossDB is now its own project
Evan Miller authored
1 -module(boss_db_mock_controller).
2 -behaviour(gen_server).
3
4 -export([start_link/0, start_link/1]).
5
6 -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
7
8 start_link() ->
9 start_link([]).
10
11 start_link(Args) ->
12 gen_server:start_link({global, boss_db_mock}, ?MODULE, Args, []).
13
14 init(_Options) ->
15 {ok, [{dict:new(), 1}]}.
16
17 handle_call({find, Id}, _From, [{Dict, _IdCounter}|_] = State) ->
18 Reply = case dict:find(Id, Dict) of
19 {ok, Record} -> Record;
20 error -> undefined
21 end,
22 {reply, Reply, State};
23 handle_call({find, Type, Conditions, Max, Skip, SortBy, SortOrder}, _From, [{Dict, _IdCounter}|_] = State) ->
24 Records = do_find(Dict, Type, Conditions, Max, Skip, SortBy, SortOrder),
25 {reply, Records, State};
26 handle_call({count, Type, Conditions}, _From, [{Dict, _IdCounter}|_] = State) ->
9943a4d @evanmiller Revamp the boss_db:find/N API
evanmiller authored
27 Records = do_find(Dict, Type, Conditions, all, 0, id, ascending),
d036073 BossDB is now its own project
Evan Miller authored
28 {reply, length(Records), State};
29 handle_call({delete, Id}, _From, [{Dict, IdCounter}|OldState]) ->
30 {reply, ok, [{dict:erase(Id, Dict), IdCounter}|OldState]};
31 handle_call({counter, Id}, _From, [{Dict, _IdCounter}|_] = State) ->
32 Value = case dict:find(Id, Dict) of
33 {ok, Integer} when is_integer(Integer) ->
34 Integer;
35 _ ->
36 0
37 end,
38 {reply, Value, State};
39 handle_call({incr, Id, Amount}, _From, [{Dict, IdCounter}|OldState]) ->
40 NewValue = case dict:find(Id, Dict) of
41 {ok, OldValue} when is_integer(OldValue) ->
42 OldValue + Amount;
43 _ ->
44 Amount
45 end,
46 {reply, NewValue, [{dict:store(Id, NewValue, Dict), IdCounter}|OldState]};
47 handle_call({save_record, Record}, _From, [{Dict, IdCounter}|OldState]) ->
48 Type = element(1, Record),
5d08791 @evanmiller Require "model-NN" ID format for mock adapter
evanmiller authored
49 TypeString = atom_to_list(Type),
d036073 BossDB is now its own project
Evan Miller authored
50 {Id, IdCounter1} = case Record:id() of
093d9f0 @kevinmontuori ::uuid() is now a valid Id type
kevinmontuori authored
51 id -> case keytype(Record) of
ae8898b @kevinmontuori changed uuid libraries, updated rebar.config accordingly
kevinmontuori authored
52 uuid -> {lists:concat([Type, "-", uuid:to_string(uuid:uuid4())]), IdCounter};
093d9f0 @kevinmontuori ::uuid() is now a valid Id type
kevinmontuori authored
53 _ -> {lists:concat([Type, "-", IdCounter]), IdCounter + 1}
54 end;
5d08791 @evanmiller Require "model-NN" ID format for mock adapter
evanmiller authored
55 ExistingId ->
093d9f0 @kevinmontuori ::uuid() is now a valid Id type
kevinmontuori authored
56 case keytype(Record) of
57 uuid -> {ExistingId, IdCounter};
58 _ ->
59 [TypeString, IdNum] = string:tokens(ExistingId, "-"),
60 Max = case list_to_integer(IdNum) of
61 N when N > IdCounter -> N;
62 _ -> IdCounter
63 end,
64 {lists:concat([Type, "-", IdNum]), Max + 1}
65 end
d036073 BossDB is now its own project
Evan Miller authored
66 end,
67 NewAttributes = lists:map(fun
68 ({id, _}) ->
69 {id, Id};
70 ({Attr, {_, _, _} = Val}) ->
71 {Attr, calendar:now_to_datetime(Val)};
72 (Other) ->
73 Other
74 end, Record:attributes()),
75 NewRecord = Record:set(NewAttributes),
76 {reply, NewRecord, [{dict:store(Id, NewRecord, Dict), IdCounter1}|OldState]};
77 handle_call(push, _From, [{Dict, IdCounter}|_] = State) ->
78 {reply, ok, [{Dict, IdCounter}|State]};
79 handle_call(pop, _From, [_|OldState]) ->
80 {reply, ok, OldState};
81 handle_call(commit, _From, [NewState, _OldState|State]) ->
82 {reply, ok, [NewState|State]};
83 handle_call(dump, _From, [{Dict, _IdCounter}|_]=State) ->
84 {reply, dict:to_list(Dict), State}.
85
86 handle_cast(_, State) ->
87 {noreply, State}.
88
89 terminate(_Reason, _State) ->
90 ok.
91
92 code_change(_OldVsn, State, _Extra) ->
93 {ok, State}.
94
95 handle_info(_Info, State) ->
96 {noreply, State}.
97
093d9f0 @kevinmontuori ::uuid() is now a valid Id type
kevinmontuori authored
98
99 keytype(Record) ->
100 proplists:get_value(id, Record:attribute_types(), unspecified).
101
d036073 BossDB is now its own project
Evan Miller authored
102 do_find(Dict, Type, Conditions, Max, Skip, SortBy, SortOrder) ->
103 Tail = lists:nthtail(Skip,
104 lists:sort(fun(RecordA, RecordB) ->
105 AttributeA = sortable_attribute(RecordA, SortBy),
106 AttributeB = sortable_attribute(RecordB, SortBy),
107 case SortOrder of
9943a4d @evanmiller Revamp the boss_db:find/N API
evanmiller authored
108 ascending ->
d036073 BossDB is now its own project
Evan Miller authored
109 AttributeA < AttributeB;
9943a4d @evanmiller Revamp the boss_db:find/N API
evanmiller authored
110 descending ->
d036073 BossDB is now its own project
Evan Miller authored
111 AttributeA > AttributeB
112 end
113 end,
114 lists:map(fun({_, V}) -> V end,
115 dict:to_list(dict:filter(
116 fun(_Id, Record) when is_tuple(Record) ->
117 element(1, Record) =:= Type andalso
118 match_cond(Record, Conditions);
119 (_Id, _) ->
120 false
121 end, Dict))))),
122 case Max of all -> Tail; _ -> lists:sublist(Tail, Max) end.
123
124 match_cond(_Record, []) ->
125 true;
126 match_cond(Record, [{Key, 'equals', Value}|Rest]) ->
127 Record:Key() =:= Value andalso match_cond(Record, Rest);
128 match_cond(Record, [{Key, 'not_equals', Value}|Rest]) ->
129 Record:Key() =/= Value andalso match_cond(Record, Rest);
130 match_cond(Record, [{Key, 'in', Value}|Rest]) when is_list(Value) ->
131 lists:member(Record:Key(), Value) andalso match_cond(Record, Rest);
132 match_cond(Record, [{Key, 'not_in', Value}|Rest]) when is_list(Value) ->
133 (not lists:member(Record:Key(), Value)) andalso match_cond(Record, Rest);
134 match_cond(Record, [{Key, 'in', {Min, Max}}|Rest]) when Max >= Min ->
135 Record:Key() >= Min andalso Record:Key() =< Max andalso match_cond(Record, Rest);
136 match_cond(Record, [{Key, 'not_in', {Min, Max}}|Rest]) when Max >= Min ->
137 (not (Record:Key() >= Min andalso Record:Key() =< Max)) andalso match_cond(Record, Rest);
138 match_cond(Record, [{Key, 'gt', Value}|Rest]) ->
139 Record:Key() > Value andalso match_cond(Record, Rest);
140 match_cond(Record, [{Key, 'lt', Value}|Rest]) ->
141 Record:Key() < Value andalso match_cond(Record, Rest);
142 match_cond(Record, [{Key, 'ge', Value}|Rest]) ->
143 Record:Key() >= Value andalso match_cond(Record, Rest);
144 match_cond(Record, [{Key, 'le', Value}|Rest]) ->
145 Record:Key() =< Value andalso match_cond(Record, Rest);
146 match_cond(Record, [{Key, 'matches', "*"++Value}|Rest]) ->
147 re:run(Record:Key(), Value, [caseless]) =/= nomatch andalso match_cond(Record, Rest);
148 match_cond(Record, [{Key, 'matches', Value}|Rest]) ->
149 re:run(Record:Key(), Value, []) =/= nomatch andalso match_cond(Record, Rest);
150 match_cond(Record, [{Key, 'not_matches', "*"++Value}|Rest]) ->
151 re:run(Record:Key(), Value, [caseless]) =:= nomatch andalso match_cond(Record, Rest);
152 match_cond(Record, [{Key, 'not_matches', Value}|Rest]) ->
153 re:run(Record:Key(), Value, []) =:= nomatch andalso match_cond(Record, Rest);
154 match_cond(Record, [{Key, 'contains', Value}|Rest]) ->
155 lists:member(Value, string:tokens(to_string(Record:Key()), " ")) andalso match_cond(Record, Rest);
156 match_cond(Record, [{Key, 'not_contains', Value}|Rest]) ->
157 (not lists:member(Value, string:tokens(to_string(Record:Key()), " "))) andalso match_cond(Record, Rest);
158 match_cond(Record, [{Key, 'contains_all', Value}|Rest]) ->
159 Tokens = string:tokens(to_string(Record:Key()), " "),
160 lists:all(fun(Token) -> lists:member(Token, Tokens) end, Value) andalso match_cond(Record, Rest);
161 match_cond(Record, [{Key, 'not_contains_all', Value}|Rest]) ->
162 Tokens = string:tokens(to_string(Record:Key()), " "),
163 (not lists:all(fun(Token) -> lists:member(Token, Tokens) end, Value)) andalso match_cond(Record, Rest);
164 match_cond(Record, [{Key, 'contains_any', Value}|Rest]) ->
165 Tokens = string:tokens(to_string(Record:Key()), " "),
166 lists:any(fun(Token) -> lists:member(Token, Tokens) end, Value) andalso match_cond(Record, Rest);
167 match_cond(Record, [{Key, 'contains_none', Value}|Rest]) ->
168 Tokens = string:tokens(to_string(Record:Key()), " "),
169 (not lists:any(fun(Token) -> lists:member(Token, Tokens) end, Value)) andalso match_cond(Record, Rest).
170
171 to_string(Val) when is_binary(Val) -> binary_to_list(Val);
172 to_string(Val) -> Val.
173
174 sortable_attribute(Record, Attr) ->
175 case Record:Attr() of
176 {D, T} when is_tuple(D), is_tuple(T) ->
177 calendar:datetime_to_gregorian_seconds({D, T});
178 Now when is_tuple(Now) ->
179 calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(Now));
180 Bin when is_binary(Bin) ->
181 binary_to_list(Bin);
182 true -> 1;
183 false -> 0;
184 Other ->
185 Other
186 end.
Something went wrong with that request. Please try again.