Skip to content

Commit

Permalink
Add support for multiple services (#3)
Browse files Browse the repository at this point in the history
Adds support for multiple services, addressing #2. Should be backwards compatible.
Key changes:
* name in constructor is now optional
* app.use(service, name, ...fns)
  • Loading branch information
bojand authored Feb 20, 2017
1 parent 2855c91 commit f2dfbfa
Show file tree
Hide file tree
Showing 9 changed files with 1,348 additions and 136 deletions.
54 changes: 47 additions & 7 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Represents a gRPC service
* [.env](#Mali+env) : <code>String</code>
* [.silent](#Mali+silent) : <code>Boolean</code>
* [.init(proto, name, options)](#Mali+init)
* [.use(name, ...fns)](#Mali+use)
* [.use(service, name, ...fns)](#Mali+use)
* [.onerror(err)](#Mali+onerror)
* [.start(port, creds)](#Mali+start) ⇒ <code>Object</code>
* [.close()](#Mali+close)
Expand All @@ -125,7 +125,7 @@ Create a gRPC service
| Param | Type | Description |
| --- | --- | --- |
| proto | <code>String</code> &#124; <code>Object</code> | Path to the protocol buffer definition file - Object specifying <code>root</code> directory and <code>file</code> to load - The static service proto object itself |
| name | <code>Object</code> | Name of the service. In case of proto path the name of the service as defined in the proto definition. In case of proto object the name of the constructor. |
| name | <code>Object</code> | Optional name of the service or an array of names. Otherwise all services are used. In case of proto path the name of the service as defined in the proto definition. In case of proto object the name of the constructor. |
| options | <code>Object</code> | Options to be passed to <code>grpc.load</code> |

**Example** *(Create service dynamically)*
Expand Down Expand Up @@ -183,19 +183,31 @@ app construction time for some reason.
| Param | Type | Description |
| --- | --- | --- |
| proto | <code>String</code> &#124; <code>Object</code> | Path to the protocol buffer definition file - Object specifying <code>root</code> directory and <code>file</code> to load - The static service proto object itself |
| name | <code>Object</code> | Name of the service. In case of proto path the name of the service as defined in the proto definition. In case of proto object the name of the constructor. |
| name | <code>Object</code> | Optional name of the service or an array of names. Otherwise all services are used. In case of proto path the name of the service as defined in the proto definition. In case of proto object the name of the constructor. |
| options | <code>Object</code> | Options to be passed to <code>grpc.load</code> |

<a name="Mali+use"></a>

#### mali.use(name, ...fns)
Define midelware and handlers
#### mali.use(service, name, ...fns)
Define middleware and handlers.
If <code>service</code> and name are given applies fns for that call under that service.
If <code>service</code> name is provided and matches one of the services defined in proto,
but no </code>name</code> is provided applies the fns as middleware as service level middleware
for all handlers in that service.
If <code>service</code> is provided and no <code>name</code> is provided, and service does not
match any of the service names in the proto, assumes <code>service</code> is actually rpc call
name. Uses <code>0</code>th property in internal services object. Useful for protos with only
one service.
If an <code>object</code> is provided, you can set middleware and handlers for all services.
If <code>object</code> provided but <code>0</code>th key does not match any of the services in
proto, assumes <code>0</code>th service. Useful for protos with only one service.

**Kind**: instance method of <code>[Mali](#Mali)</code>

| Param | Type | Description |
| --- | --- | --- |
| name | <code>String</code> &#124; <code>Object</code> | Name of the function as specified in the protocol buffer definition. or an object of name and handlers |
| service | <code>String</code> &#124; <code>Object</code> | Service name |
| name | <code>String</code> &#124; <code>function</code> | RPC name |
| ...fns | <code>function</code> &#124; <code>Array</code> | Middleware and/or handler |

**Example** *(Define handler for rpc function &#x27;fn1&#x27;)*
Expand All @@ -210,17 +222,45 @@ app.use('fn1', fn1)
app.use('fn2', mw1, mw2, fn2)
```

**Example** *(Define handler with middleware for rpc function &#x27;fn2&#x27; in service &#x27;Service2&#x27;)*

```js
app.use('Service2', 'fn2', mw1, mw2, fn2)
```

**Example** *(Using destructuring define handlers for rpc functions &#x27;fn1&#x27; and &#x27;fn2&#x27;)*

```js
app.use({ fn1, fn2 })
```

**Example** *(Apply middleware to all handers for a service)*

```js
app.use('Service1', mw1)
```

**Example** *(Using destructuring define handlers for rpc functions &#x27;fn1&#x27; and &#x27;fn2&#x27;)*

```js
// fn2 has middleware mw1 and mw2
app.use({ fn1, fn2: [mw1, mw2, fn2] })
app.use({ MyService: { fn1, fn2: [mw1, mw2, fn2] } })
```

**Example** *(Multiple services using object notation)*

```js
app.use(mw1) // global for all services
app.use('Service1', mw2) // applies to all Service1 handers
app.use({
Service1: {
sayGoodbye: handler1, // has mw1, mw2
sayHello: [ mw3, handler2 ] // has mw1, mw2, mw3
},
Service2: {
saySomething: handler3 // only has mw1
}
})
```

<a name="Mali+onerror"></a>
Expand Down
180 changes: 132 additions & 48 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const Context = require('./context')
const run = require('./run')
const mu = require('./utils')

const REMOVE_PROPS = ['grpc', 'middleware', 'handlers', 'servers', 'load', 'proto', 'service', 'methods']
const REMOVE_PROPS = ['grpc', 'middleware', 'handlers', 'servers', 'load', 'proto', 'services', 'methods']
const EE_PROPS = Object.getOwnPropertyNames(new Emitter())

/**
Expand All @@ -32,7 +32,7 @@ class Mali extends Emitter {
* @param {String|Object} proto - Path to the protocol buffer definition file
* - Object specifying <code>root</code> directory and <code>file</code> to load
* - The static service proto object itself
* @param {Object} name - Name of the service.
* @param {Object} name - Optional name of the service or an array of names. Otherwise all services are used.
* In case of proto path the name of the service as defined in the proto definition.
* In case of proto object the name of the constructor.
* @param {Object} options - Options to be passed to <code>grpc.load</code>
Expand All @@ -41,15 +41,15 @@ class Mali extends Emitter {
super()

this.grpc = grpc
this.middleware = [] // just the global middleware
this.middleware = {} // just the global middleware
this.handlers = {} // specific middleware and handlers
this.servers = []

// app options / settings
this.context = new Context()
this.env = process.env.NODE_ENV || 'development'

if (path && name) {
if (path) {
this.init(path, name, options)
}
}
Expand All @@ -60,7 +60,7 @@ class Mali extends Emitter {
* @param {String|Object} proto - Path to the protocol buffer definition file
* - Object specifying <code>root</code> directory and <code>file</code> to load
* - The static service proto object itself
* @param {Object} name - Name of the service.
* @param {Object} name - Optional name of the service or an array of names. Otherwise all services are used.
* In case of proto path the name of the service as defined in the proto definition.
* In case of proto object the name of the constructor.
* @param {Object} options - Options to be passed to <code>grpc.load</code>
Expand All @@ -70,78 +70,158 @@ class Mali extends Emitter {
this.load = _.isString(path) || (_.isObject(path) && path.root && path.file)
this.proto = this.load ? this.grpc.load(path, options) : path

let service
let descriptor
this.services = {}
this.methods = {}
if (this.load) {
descriptor = gi(this.proto)
let descriptor = gi(this.proto)
if (!descriptor) {
throw new Error(String.raw `Error parsing protocol buffer`)
}
const client = descriptor.client(name)
service = client ? client.service : null
let names = descriptor.serviceNames()
if (_.isString(name)) {
names = [name]
} else if (_.isArray(name)) {
names = _.intersection(name, names)
}
names.forEach(n => {
const client = descriptor.client(n)
const service = client ? client.service : null
if (service) {
this.services[n] = service
this.methods[n] = {}
this.middleware[n] = []
const methods = descriptor.methods(n)
methods.forEach(m => {
this.methods[n][_.camelCase(m.name)] = m
})
}
})
} else if (_.isObject(this.proto)) {
service = this.proto[name]
}

if (!_.isObject(service)) {
throw new Error(String.raw `${name} is not a service definition within the protocol buffer definition`)
}
let names = _.keys(this.proto)
if (_.isString(name)) {
names = [name]
} else if (_.isArray(name)) {
names = _.intersection(name, names)
}

this.service = service
if (this.load) {
const methods = descriptor.methods(name)
this.methods = {}
methods.forEach(m => {
this.methods[_.camelCase(m.name)] = m
_.forOwn(this.proto, (v, n) => {
if (_.isObject(v) && !_.isFunction(v) && names.indexOf(n) >= 0) {
this.services[n] = v
this.middleware[n] = []
this.methods[n] = mu.getMethodDescriptors(v)
}
})
} else {
this.methods = mu.getMethodDescriptors(service)
}
}

/**
* Define midelware and handlers
* @param {String|Object} name Name of the function as specified in the protocol buffer definition.
* or an object of name and handlers
* Define middleware and handlers.
* If <code>service</code> and name are given applies fns for that call under that service.
* If <code>service</code> name is provided and matches one of the services defined in proto,
* but no </code>name</code> is provided applies the fns as middleware as service level middleware
* for all handlers in that service.
* If <code>service</code> is provided and no <code>name</code> is provided, and service does not
* match any of the service names in the proto, assumes <code>service</code> is actually rpc call
* name. Uses <code>0</code>th property in internal services object. Useful for protos with only
* one service.
* If an <code>object</code> is provided, you can set middleware and handlers for all services.
* If <code>object</code> provided but <code>0</code>th key does not match any of the services in
* proto, assumes <code>0</code>th service. Useful for protos with only one service.
* @param {String|Object} service Service name
* @param {String|Function} name RPC name
* @param {Function|Array} fns - Middleware and/or handler
*
* @example <caption>Define handler for rpc function 'fn1'</caption>
* app.use('fn1', fn1)
*
* @example <caption>Define handler with middleware for rpc function 'fn2'</caption>
* app.use('fn2', mw1, mw2, fn2)
*
* @example <caption>Define handler with middleware for rpc function 'fn2' in service 'Service2'</caption>
* app.use('Service2', 'fn2', mw1, mw2, fn2)
*
* @example <caption>Using destructuring define handlers for rpc functions 'fn1' and 'fn2'</caption>
* app.use({ fn1, fn2 })
*
* @example <caption>Apply middleware to all handers for a service</caption>
* app.use('Service1', mw1)
*
* @example <caption>Using destructuring define handlers for rpc functions 'fn1' and 'fn2'</caption>
* // fn2 has middleware mw1 and mw2
* app.use({ fn1, fn2: [mw1, mw2, fn2] })
* app.use({ MyService: { fn1, fn2: [mw1, mw2, fn2] } })
*
* @example <caption>Multiple services using object notation</caption>
* app.use(mw1) // global for all services
* app.use('Service1', mw2) // applies to all Service1 handers
* app.use({
* Service1: {
* sayGoodbye: handler1, // has mw1, mw2
* sayHello: [ mw3, handler2 ] // has mw1, mw2, mw3
* },
* Service2: {
* saySomething: handler3 // only has mw1
* }
* })
*/
use (name, ...fns) {
if (_.isPlainObject(name)) {
for (var prop in name) {
if (name.hasOwnProperty(prop)) {
const mw = name[prop]
use (service, name, ...fns) {
if (_.isFunction(service)) {
_.forOwn(this.middleware, (v, sn) => {
this.middleware[sn] = _.concat(v, service, fns)
})
} else if (_.isPlainObject(service)) {
const testKey = _.keys(service)[0]
if (_.isFunction(service[testKey]) || _.isArray(service[testKey])) {
const serviceName = _.keys(this.services)[0]
_.forOwn(service, (mw, mwName) => {
if (_.isFunction(mw)) {
this.use(prop, mw)
this.use(serviceName, mwName, mw)
} else if (_.isArray(mw)) {
this.use(prop, ...mw)
this.use(serviceName, mwName, ...mw)
} else {
throw new TypeError(`Handler for ${prop} is not a function or array`)
throw new TypeError(`Handler for ${mwName} is not a function or array`)
}
}
})
} else if (_.isPlainObject(service[testKey])) {
_.forOwn(service, (def, serviceName) => {
_.forOwn(def, (mw, mwName) => {
if (_.isFunction(mw)) {
this.use(serviceName, mwName, mw)
} else if (_.isArray(mw)) {
this.use(serviceName, mwName, ...mw)
} else {
throw new TypeError(`Handler for ${mwName} is not a function or array`)
}
})
})
} else {
throw new TypeError(`Invalid type for handler for ${testKey}`)
}
} else if (_.isFunction(name)) {
this.middleware = _.concat(this.middleware, name, fns)
} else {
if (this.handlers[name]) {
throw new Error(String.raw `Handler for ${name} already defined`)
let serviceName = service
if (!_.isString(name)) {
fns.unshift(name)
const sNames = _.keys(this.services)
if (sNames.indexOf(service) >= 0) {
this.middleware[service] = _.concat(this.middleware[service], fns)
return
} else {
name = serviceName
serviceName = _.keys(this.services)[0]
}
}
if (!this.methods[name]) {
throw new Error(String.raw `Unknown method: ${name}`)
if (!this.services[serviceName]) {
throw new Error(String.raw `Unknown service ${serviceName}`)
}
this.handlers[name] = _.concat(this.middleware, fns)
if (!this.handlers[serviceName]) {
this.handlers[serviceName] = {}
}
if (this.handlers[serviceName][name]) {
throw new Error(String.raw `Handler for ${name} already defined for service ${serviceName}`)
}
if (!this.methods[serviceName][name]) {
throw new Error(String.raw `Unknown method: ${name} for service ${serviceName}`)
}
this.handlers[serviceName][name] = _.concat(this.middleware[serviceName], fns)
}
}

Expand Down Expand Up @@ -188,12 +268,16 @@ class Mali extends Emitter {

const method = this.load ? 'addProtoService' : 'addService'

const composed = {}
_.forOwn(this.handlers, (v, k) => {
composed[k] = this.callback(this.methods[k], v)
_.forOwn(this.services, (s, sn) => {
const composed = {}
const methods = this.methods[sn]
const handlers = this.handlers[sn]
_.forOwn(handlers, (v, k) => {
composed[k] = this.callback(methods[k], v)
})
server[method](s, composed)
})

server[method](this.service, composed)
server.bind(port, creds)

server.start()
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
"pify": "^2.3.0"
},
"devDependencies": {
"ava": "^0.17.0",
"ava": "^0.18.1",
"babel-eslint": "^7.1.1",
"google-protobuf": "^3.1.1",
"highland": "^3.0.0-beta.3",
"jsdoc-to-markdown": "^2.0.1",
"jsdoc-to-markdown": "^3.0.0",
"md-wrap-code": "^0.1.1",
"standard": "^8.5.0",
"test-console": "^1.0.0"
Expand Down
Loading

0 comments on commit f2dfbfa

Please sign in to comment.