Skip to content

Commit

Permalink
use getType instead of typeof, add context to errors
Browse files Browse the repository at this point in the history
  • Loading branch information
bitmage committed Feb 1, 2013
1 parent 8a245ec commit 34ba9e9
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 39 deletions.
6 changes: 4 additions & 2 deletions lib/chain.coffee
@@ -1,3 +1,5 @@
{getType} = require './util'

# execute a stack of services (similar to async.waterfall)
module.exports = (serviceName, input, stack, cb) ->

Expand All @@ -7,7 +9,7 @@ module.exports = (serviceName, input, stack, cb) ->
cb ||= ->

# validations
unless (typeof input) is 'object'
unless getType(input) is 'Object'
return cb (new Error "#{serviceName} requires an arguments object as the first argument."), {input: input}
unless Array.isArray(stack) and stack.length > 0
return cb()
Expand All @@ -22,7 +24,7 @@ module.exports = (serviceName, input, stack, cb) ->
# run next service
stack[index] args, (err, results) ->
results ||= {}
unless (typeof results) is 'object'
unless getType(results) is 'Object'
return cb (new Error "#{stack[index].serviceName or serviceName} must return an object."), {results: results}
return cb err, results if err
callNext index + 1, results
Expand Down
41 changes: 28 additions & 13 deletions lib/createServiceFilters.coffee
@@ -1,4 +1,4 @@
{addTo, compact, flatten} = require './util'
{getType, addTo, compact, flatten, merge} = require './util'
generateFilter = require './generateFilter'

# generate validations that will be added to the filter stack
Expand All @@ -13,7 +13,16 @@ generateValidations = (serviceName, name, types, required) ->
# perform lookup if arg is not present
if not args[name]?
t.lookup args, (err, result) ->
args[name] = result

if err
args =
reason: 'lookupFailed'
fieldName: name
serviceName: serviceName
args: args
else
args[name] = result

next err, args
else
next()
Expand All @@ -23,12 +32,14 @@ generateValidations = (serviceName, name, types, required) ->

# only check if it's not present but required
if not args[name]? and required
meta =
context =
reason: 'requiredField'
fieldName: name
serviceName: serviceName
args: args

error = new Error "#{serviceName} requires '#{name}' to be defined."
return next error, meta
return next error, context

else
return next()
Expand All @@ -41,21 +52,25 @@ generateValidations = (serviceName, name, types, required) ->
# continue if field isn't present
return next() unless args[name]

checkResult = (passed) ->
context =
fieldName: name
value: args[name]
serviceName: serviceName
args: args

assert = (passed, extraContext) ->
unless passed
meta =
reason: 'invalidValue'
fieldName: name
serviceName: serviceName
requiredType: t.typeName
merge context, {reason: 'invalidValue', requiredType: t.typeName}
merge context, extraContext

error = new Error "#{serviceName} requires '#{name}' to be a valid #{t.typeName}."
return next error, meta
return next error, context

else
return next()

# run type validation
t.validation args[name], checkResult, args
t.validation context, assert

# remove any lookups/validations that weren't defined
return compact stack
Expand Down Expand Up @@ -83,7 +98,7 @@ module.exports =
throw new Error "#{serviceName}.paramSpecs must contain '#{field}'." unless field in param.keys()

# find types and wrap in validations
if (typeof param.validation) is 'function'
if getType(param.validation) is 'Function'
param.validation

else
Expand Down
2 changes: 1 addition & 1 deletion lib/generateFilter.coffee
Expand Up @@ -6,7 +6,7 @@ module.exports = (name, service) ->

# merge the results of the filter
final = {}
merge final, args
merge final, args unless err
merge final, results

# call the next function in the stack
Expand Down
7 changes: 4 additions & 3 deletions lib/lookupArgumentFilters.coffee
@@ -1,14 +1,15 @@
{getType} = require './util'
createServiceFilters = require './createServiceFilters'

module.exports = (serviceName, serviceDef, jargon) ->
{generateDefaultValidations, generateValidationsFromParams} = createServiceFilters jargon

switch typeof serviceDef
switch getType serviceDef

when 'function'
when 'Function'
return []

when 'object'
when 'Object'
validations = []
{required, optional, params} = serviceDef

Expand Down
4 changes: 3 additions & 1 deletion lib/util.coffee
@@ -1,5 +1,7 @@
module.exports = util =

getType: (obj) -> Object.prototype.toString.call(obj).slice 8, -1

# works like concat, but modifies array
addTo: (arr, addition) ->
if Array.isArray addition
Expand Down Expand Up @@ -28,7 +30,7 @@ module.exports = util =

# merge source hash into target
merge: (target, source) ->
return target unless (typeof target) is 'object' and (typeof source) is 'object'
return target unless util.getType(target) is 'Object' and util.getType(source) is 'Object'
for name, value of source
target[name] = value
return target
6 changes: 3 additions & 3 deletions lib/wrapServicesInMiddleware.coffee
@@ -1,5 +1,5 @@
{getType} = require './util'
chain = require './chain'

lookupArgumentFilters = require './lookupArgumentFilters'

module.exports = (services, jargon) ->
Expand All @@ -10,7 +10,7 @@ module.exports = (services, jargon) ->

typeValidations = lookupArgumentFilters serviceName, serviceDef, jargon
service = serviceDef.service or serviceDef
throw new Error "Could not find function definition for service '#{serviceName}'." unless (typeof service) is 'function'
throw new Error "Could not find function definition for service '#{serviceName}'." unless getType(service) is 'Function'
service.serviceName = serviceName

# return wrapped service
Expand All @@ -27,7 +27,7 @@ module.exports = (services, jargon) ->
wrapper.prepend = (services) ->
if services and Array.isArray services
wrapper.callStack.unshift services...
else if (typeof services) is 'function'
else if getType(services) is 'Function'
wrapper.callStack.unshift services

# build up static portion of call stack
Expand Down
78 changes: 71 additions & 7 deletions sample/app/domain/auth/jargon.coffee
Expand Up @@ -3,17 +3,81 @@ mongoId = /[a-f0-9]{24}/

module.exports = [
typeName: 'String'
validation: (arg, assert) ->
assert typeof arg is 'string'
defaultArgs: ['email', 'password']
validation: ({value, fieldName}, assert) ->
assert typeof value is 'string', {message: "#{fieldName} is not a string."}
defaultArgs: ['email', 'password', 'body', 'subject']
,
typeName: 'SessionId'
validation: (arg, assert, {sessionId}) ->
assert sessionId? and (typeof arg is 'string') and arg.match redisId
validation: ({value}, assert) ->
assert value and (typeof value is 'string') and value.match redisId
defaultArgs: ['sessionId']
,
typeName: 'AccountId'
lookup: ({sessionId}, found) ->
if sessionId
found null, "#{sessionId}abcd1234"
else
found()

validation: ({value}, assert) ->
assert value and (typeof value is 'string') and value.match mongoId
defaultArgs: ['accountId']
,
typeName: 'MongoId'
validation: (arg, assert) ->
assert arg.toString().match mongoId
validation: ({value}, assert) ->
assert value.toString().match mongoId
defaultArgs: ['userId']
]

#module.exports =
#words: [
#name: 'email'
#displayName: 'Email'
#validate: 'String'
#,
#name: 'password'
#displayName: 'Password'
#validate: 'Password'
#,
#name: 'sessionId'
#displayName: 'SessionId'
#validate: 'RedisId'
#,
#name: 'accountId'
#displayName: 'AccountId'
#validate: 'MongoId'
#lookup: 'findAccountIdBySessionId'
#serverOnly: true
#,
#name: 'userId'
#displayName: 'UserId'
#validate: 'MongoId'
#]

#validations: [
#name: 'String'
#def: ({value, displayName}, assert) ->
#assert (typeof instance.value) is 'string'
#,
#name: 'Password'
#also: 'String'
#def: ({value, displayName}, assert) ->
#assert value.length > 6, message: "#{displayName} must be at least 6 letters long."
#,
#name: 'MongoId'
#also: 'String'
#def: ({value}, assert) ->
#assert value.toString().match mongoId
#,
#name: 'RedisId'
#also: 'String'
#def: ({value}, assert) ->
#assert value.toString().match redisId
#]

#lookups: [
#name: 'findAccountIdBySessionId'
#def: ({sessionId}, found) ->
#found "#{sessionId}abcd1234"
#serverOnly: true
#]
1 change: 1 addition & 0 deletions sample/app/domain/auth/policy.coffee
Expand Up @@ -9,6 +9,7 @@ module.exports =
'getRole'
'login'
'invalidReturn'
'sendEmail'
]
}

Expand Down
6 changes: 3 additions & 3 deletions sample/app/domain/auth/services/getRole.coffee
@@ -1,4 +1,4 @@
module.exports =
required: ['sessionId']
service: (args, done) ->
done null, {role: 'Supreme Commander'}
required: ['sessionId', 'accountId']
service: ({accountId}, done) ->
done null, {role: 'Supreme Commander', accountId: accountId}
6 changes: 6 additions & 0 deletions sample/app/domain/auth/services/sendEmail.coffee
@@ -0,0 +1,6 @@
module.exports =
required: ['email', 'subject']
optional: ['body']
service: ({email, body, subject}, done) ->
# send email
done()
39 changes: 33 additions & 6 deletions test/auth.coffee
Expand Up @@ -51,8 +51,8 @@ describe "full stack", ->

it 'should prevent dashboard from being accessed', (done) ->
@services.dashboard {}, (err, result) ->
should.exist err
err.should.eql new Error "filters/isLoggedIn requires 'sessionId' to be defined."
should.exist err?.message, 'expected error'
err.message.should.eql "filters/isLoggedIn requires 'sessionId' to be defined."
for field in ['reason', 'fieldName', 'serviceName']
Object.keys(result).should.include field
done()
Expand All @@ -66,14 +66,34 @@ describe "full stack", ->
describe 'getRole', ->
it 'should require sessionId', (done) ->
@services.getRole {}, (err, result) ->
should.exist err
err.should.eql new Error "getRole requires 'sessionId' to be defined."
should.exist err?.message, 'expected error'
err.message.should.eql "getRole requires 'sessionId' to be defined."
result.should.eql
reason: 'requiredField'
fieldName: 'sessionId'
serviceName: 'getRole'
args: {}
done()

it 'should validate stringyness', (done) ->
@services.sendEmail {email: [], subject: ''}, (err, result) ->
should.exist err?.message, 'expected error'
err.message.should.eql "sendEmail requires 'email' to be a valid String."
result.should.eql
fieldName: 'email'
value: []
serviceName: 'sendEmail'
args: {email: [], subject: ''}
reason: 'invalidValue'
requiredType: 'String'
message: 'email is not a string.'

done()

it 'should validate sessionId', (done) ->
@services.getRole {sessionId: 'foo'}, (err, result) ->
should.exist err
err.should.eql new Error "getRole requires 'sessionId' to be a valid SessionId."
should.exist err?.message, 'expected error'
err.message.should.eql "getRole requires 'sessionId' to be a valid SessionId."
for field in ['reason', 'fieldName', 'serviceName', 'requiredType']
Object.keys(result).should.include field
done()
Expand All @@ -85,6 +105,13 @@ describe "full stack", ->
result.role.should.eql 'Supreme Commander'
done()

it 'should lookup accountId', (done) ->
@services.getRole {sessionId: 'ab23ab23ab23ab23'}, (err, result) ->
should.not.exist err
console.log result
should.exist result?.accountId, 'expected result.accountId'
done()

describe 'printFilters', ->
it 'should work', (done) ->
printout = print @services
Expand Down

0 comments on commit 34ba9e9

Please sign in to comment.