-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.coffee
294 lines (225 loc) · 8.2 KB
/
index.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
Joi = require 'joi'
_ = require 'lodash'
assert = require 'assert'
# NB!!!! ClientSide and ServerSide code
# TODO -> pack for web w/ webpack OR browserify
# export candidates -------------
# Event -> boolean
isContextEvent = (ev) ->
!(Joi.validate ev.type, baseSchemas.ctxType).error
reducers =
# dueDate
# [Event] -> Int
sumOfIncrements : (events) ->
_.reduce events, ((sum,ev) ->
if ev.type == 'incr-member-cap'
sum + ev.increment
else
sum
),0
# [Event] -> [ids] -> [ids]
sqashedMembers : (events, members=[]) ->
_.reduce events, ((acc,ev) ->
if ev.type == 'push-members'
# add
acc = _.union acc, ev.members
else if ev.type == 'pop-members'
# remove/pop
acc = _.remove acc, (mem) ->
mem in ev.members
acc
), members
validCtxTypes = ['account','trial','student']
baseSchemas =
'ctxType' : Joi.string().only validCtxTypes
'context' : Joi.object().keys
type: Joi.string().only validCtxTypes
'org' : Joi.object().keys
name: Joi.string().required()
phone: Joi.string()
addressLine1: Joi.string().required()
addressLine2: Joi.string()
city: Joi.string().required()
zip: Joi.string().required()
region: Joi.string()
country: Joi.string().required()
'user' : Joi.object().keys
id: Joi.string().required()
name: Joi.string().required()
# only whats needed here, can have MORE !!
orderStateSchema = Joi.object().keys {
memberCapacity: Joi.number().integer()
memberSet: Joi.array().items baseSchemas.user
context: baseSchemas.context
}
.with 'memberSet', 'memberCapacity'
# [{}]
eventSchemas =
# ----------------------------------------
# -------------- E V E N T S ------------
# ----------------------------------------
# make-sure-NO-overlapping-names
# since we are y = joiflattening inside SNAPSHOTS
# genesis
'account' : Joi.object().keys
type: Joi.string().required().only 'account'
org: baseSchemas.org # optional
'incr-member-cap' : Joi.object().keys
type: Joi.string().required().only 'incr-member-cap'
increment: Joi.number().integer()
'push-members' : Joi.object().keys
type: Joi.string().required().only 'push-members'
members: Joi.array().items baseSchemas.user
'pop-members' : Joi.object().keys
type: Joi.string().required().only 'pop-members'
members: Joi.array().items baseSchemas.user
# genesis
'student' : Joi.object().keys
type: Joi.string().required().only 'student'
university: Joi.string().required()
course: Joi.string().required()
finishingYear: Joi.number().integer()
# genesis
'trial' : Joi.object().keys
type: Joi.string().required().only 'trial'
days: Joi.number().integer().required() # FIXME Security!!! Not allow hacking this!!!
'cancel' : Joi.object().keys
type: Joi.string().required().only 'cancel'
msg: Joi.string().required()
'uncancel' : Joi.object().keys
type: Joi.string().required().only 'uncancel'
msg: Joi.string().required()
# TEST!
'noop' : Joi.any()
# challenge the STATE aka state !
checkOrderingRules = (event, precedingEvents, state) ->
#console.log "precedingEvents",precedingEvents
# SENTINEL helper
# context -> Either<Err,Void>
mustBelongToContextType = (contextType) ->
Joi.attempt contextType, baseSchemas.ctxType
errMsg = "context-type different than '#{contextType}' !"
if precedingEvents.length>0 and state?.context?.type != contextType
rule = _.some precedingEvents, (pEv) -> pEv.type == contextType
assert rule, errMsg
else if state?.context?.type != contextType
console.warn '>>>> STRANGE snap=',state
assert.fail errMsg
cancelled = ->
prevCancelEv = _.find precedingEvents.reverse(), (ev) ->
ev.type == 'cancel' or ev.type == 'uncancel'
if prevCancelEv
cancelled: prevCancelEv.type == 'cancel' or false
msg: prevCancelEv.cancelMsg
else
cancelled: state.cancelled
msg: state.cancelMsg
# SENTINEL helper
cannotConflictEarlierContext = (contextTypes) ->
_.each contextTypes, (t) ->
Joi.attempt t, baseSchemas.ctxType
precedingType = (_.find precedingEvents, isContextEvent)?.type
earliestContext = state?.context?.type or precedingType
if earliestContext and not (earliestContext in contextTypes)
assert.fail "Cannot [#{event.type}] now.\n
\\_ [context] missmatch since '#{earliestContext}' is our context!"
currCap = ->
(state.memberCapacity or 0) + (reducers.sumOfIncrements precedingEvents)
accumulatedMembers = ->
precedingMems = reducers.sqashedMembers precedingEvents
console.log "Calculated; precedingMems #{precedingMems}"
# NOTE above is not ROCK-SOLID !!! since each PREV could have .error={} by now
_.union precedingMems, event.members, state.memberSet
{
'account' : ->
cannotConflictEarlierContext ['account','trial']
'trial' : ->
cannotConflictEarlierContext ['trial']
'student' : ->
cannotConflictEarlierContext ['student']
'incr-member-cap' : ->
mustBelongToContextType 'account'
currCap_ = currCap()
console.log "Calculated; currCap #{currCap_}"
newCap = currCap_ + event.increment
console.log "Calculated; newCap #{newCap}"
accumulatedMembers_ = accumulatedMembers()
console.log "Calculated; accumulatedMembers #{accumulatedMembers_}"
errMsg = "Cannot [#{event.type}] now.
Cap try [#{newCap}] but mem-cnt is [#{accumulatedMembers_.length}]"
assert newCap >= accumulatedMembers_.length, errMsg
'push-members' : ->
# rule 1
mustBelongToContextType 'account'
console.log "state MEMz >", state.memberSet
currCap_ = currCap()
console.log "Calculated; currCap #{currCap_}"
accumulatedMembers_ = accumulatedMembers()
console.log "Calculated; accumulatedMembers #{accumulatedMembers_}"
errMsg = "Cannot [#{event.type}] now. OVERFLOW ! capacity = #{currCap_}"
assert currCap_ >= accumulatedMembers_.length, errMsg
'pop-members' : ->
# rule 1
mustBelongToContextType 'account'
# rule 2
currMembers = reducers.sqashedMembers precedingEvents, state.memberSet
# NOTE above is not ROCK-SOLID !!! since each PREV could have .error={} by now
diff = _.difference event.members, currMembers
errMsg = "Cannot [#{event.type}] now. Cannot pop NON-EXISTING members; #{diff}"
assert diff.length == 1, errMsg #NOTE Was diff.length == 0
'cancel' : ->
assert not cancelled().cancelled, 'Order is ALREADY cancelled !'
'uncancel': ->
assert cancelled().cancelled, 'Order dont NEED un-cancelling !'
}
# Sync
# state = {shop-state}
# data = [events..]
# returns VOID
# throws
# err = { .data=annotated-data-clone } instanceOf Error
validate = (state, data ) ->
# state
fstErr = null
errs = 0
sRes = Joi.validate state, orderStateSchema, { allowUnknown:yes }
if sRes.error
console.log "[[[state]]]: INVALID (joi) ;", sRes.error.message
throw sRes.error
# mutator/helper
appendErr = (event,err) ->
event.error = err
fstErr = fstErr or err
errs += 1
event
# mutates !
annotatedData = _.map data, (ev,idx) ->
lineId_ = "[#{ev.type}] #{idx}"
ev_ = _.cloneDeep ev
unless (_.has eventSchemas, ev.type)
err = new Error "BUG!!! Schema for event [#{lineId_}] missing!"
return appendErr ev,err
vRes = Joi.validate ev, eventSchemas[ev.type]
if vRes.error
console.log "#{lineId_}: INVALID (joi) ;", vRes.error.message
return appendErr ev, vRes.error
console.log "#{lineId_}: passed joi "
precedingEvents = data[0...idx]
checkers = checkOrderingRules ev,precedingEvents,state
unless (_.has checkers, ev.type)
err = new Error "BUG!!! Rules for event [#{lineId_}] missing!"
return appendErr ev,err
try
checkers[ev.type]()
console.log "#{lineId_}: passed rules check"
ev_ # no errors
catch err
console.log "#{lineId_}: FAILING rules check ;", err.message
return appendErr ev,err
if errs > 0
err = new Error "Found #{errs} issue(s) | 1st err; #{fstErr.message}"
err.data = annotatedData
throw err
module.exports =
validate: validate
reducers: reducers