Skip to content

Commit

Permalink
fix: xml-js parses raw string instead of DOMParser object. (#10)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: result object now includes json and xml output.
  • Loading branch information
patrickmichalina authored Jan 29, 2019
1 parent 87365ed commit ba1d39e
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 62 deletions.
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ For more information about the TypeScript API checkout the [generated API docs](

- [x] Generate API with typings and docs from WSDL's and XSD's
- [x] Execute simple (parameter-less) requests
- [ ] Execute requests with paremeters
- [x] Execute requests with paremeters

## Node Installation
This package is designed to be run in both the browser and node environments.
Expand Down Expand Up @@ -88,13 +88,10 @@ device.api.Device.GetUsers()
.toPromise()
.then(res=> {
res.match({ // results are wrapped in a managed object for safer processing
ok: console.log, // successful response object
fail: r => console.log(r.status, r.statusMessage) // request failure object
ok: success => console.log(success.json), // successful response object
fail: railure => console.log(railure.status, railure.statusMessage) // request failure object
})
})

// output
// { User: { Username: 'admin', UserLevel: 'Administrator' } }
```

### Ad Hoc Usage
Expand All @@ -106,19 +103,16 @@ Device.GetUsers()
.run({
system: DEFAULT_NODE_ENV,
deviceUrl: 'http://192.168.1.11/onvif/device_service',
user: maybe({ // currenlty requires a wrapper object, will improve in the future
user: maybe({ // currently requires a wrapper object, will improve in the future
username: 'admin',
password: '1234'
})
})
.toPromise()
.then(res=> {
res.match({
ok: console.log, // successful response object
fail: r => console.log(r.status, r.statusMessage) // request failure object
ok: success => console.log(success.json), // successful response object
fail: railure => console.log(railure.status, railure.statusMessage) // request failure object
})
})

// output
// { User: { Username: 'admin', UserLevel: 'Administrator' } }
```
100 changes: 50 additions & 50 deletions src/soap/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { map } from 'rxjs/operators'
import { createUserToken } from './auth'
import { xml2json } from 'xml-js'

export interface IResultStructure<T> {
readonly json: T
readonly xml: string
}

export type IOnvifResult = IResult<IXmlContainer, ITransportPayoad>

export enum XMLNS {
S11 = 'xmlns:S11="http://www.w3.org/2003/05/soap-envelope"',
wsse = 'xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"',
Expand Down Expand Up @@ -41,32 +48,19 @@ export enum XMLNS {
wsa5 = 'xmlns:wsa5="http://www.w3.org/2005/08/addressing"'
}

export interface ITransportPayloadXml {
readonly body: Document
readonly statusMessage: string
readonly status: number
}

export type IOnvifNetworkResponse<T> = Observable<IResult<T, ITransportPayloadXml>>

const parseXml =
(parser: DOMParser) =>
(payload: ITransportPayoad): ITransportPayloadXml => {
// console.log(payload.body)
return {
...payload,
body: parser.parseFromString(payload.body, 'text/xml')
}
}
(xmlString: string): Document =>
parser.parseFromString(xmlString, 'text/xml')

const nsstr = () => Object.keys(XMLNS).map((k: any) => XMLNS[k])

export enum SOAP_NODE {
Header = 'S11:Header',
Envelope = 'S11:Envelope',
Body = 'S11:Body'
}

const nsstr = () => Object.keys(XMLNS).map((k: any) => XMLNS[k])

export const soapShell =
(rawBody: string) =>
(rawHeader?: string) =>
Expand All @@ -80,45 +74,73 @@ export const mapResponseXmlToJson =
<T>(node: string) =>
(source: Observable<IOnvifResult>) =>
source.pipe(
map(a => a.map(b => {
map(a => a.map<IResultStructure<T>>(b => {
const split = node.split(':')
const nodeKey = maybe(split[1]).valueOr(node)
const soapNsPrefix = b.documentElement.lookupPrefix(b.documentElement.namespaceURI)
const soapNsPrefix = b.xmlDocument.documentElement.lookupPrefix(b.xmlDocument.documentElement.namespaceURI)

const parsed = JSON.parse(xml2json(b as any, {
const parsed = JSON.parse(xml2json(b.xmlString, {
compact: true,
spaces: 2,
ignoreAttributes: true,
elementNameFn: d => maybe(d.split(':')[1]).valueOr(d)
}))

const resultObject = parsed['Envelope']
const json = parsed['Envelope']
? parsed['Envelope']['Body'][nodeKey]
: parsed[`${soapNsPrefix}:Envelope`][`${soapNsPrefix}:Body`][nodeKey]

return resultObject as T
return {
json,
xml: b.xmlString
}
})))

export const mapResponseObsToProperty =
<A, B, E>(propSelectFn: (sel: A) => B) =>
(source: Observable<IResult<A, E>>) =>
source.pipe(map(a => a.map(propSelectFn)))

type IOnvifResult = IResult<Document, ITransportPayloadXml>
export interface IXmlContainer {
readonly xmlString: string
readonly xmlDocument: Document
}

// tslint:disable-next-line:readonly-array
export const generateRequestElements = (reqNode: string) => (parameterNodes: string[]) => (...params: any[]) => {
return !params.length
? `<${reqNode} />`
: `<${reqNode}>${params.map((param, index) => {
const type = typeof param
const insertIntoRootNode = (inner: string) => `<${parameterNodes[index]}>${inner}</${parameterNodes[index]}>`
switch (type) {
case 'undefined': return ''
case 'boolean': return insertIntoRootNode(param)
case 'object': return insertIntoRootNode(Object.keys(param).reduce((acc, key) => {
const val = (param as any)[key]
return val
? (acc || '') + `<${key}>${(param as any)[key]}</${key}>`
: (acc || '')
}, ''))
default: return insertIntoRootNode(param)
}
}).join('')}</${reqNode}>`
}

export const createStandardRequestBody =
(body: string) =>
reader<IDeviceConfig, Observable<IOnvifResult>>(config => {
const gen = (body: string) => config.system.transport(body)(config.deviceUrl)
.pipe(map(parseXml(config.system.parser)))
.pipe(map(response => {
const xmlDocument = parseXml(config.system.parser)(response.body)
const tmp = (XMLNS.S11.split('=').pop() || '').replace(/"/g, '')
const subcode = maybe(response.body.getElementsByTagNameNS(tmp, 'Subcode').item(0)).flatMapAuto(a => a.textContent)
const reason = maybe(response.body.getElementsByTagNameNS(tmp, 'Reason').item(0)).flatMapAuto(a => a.textContent)
const subcode = maybe(xmlDocument.getElementsByTagNameNS(tmp, 'Subcode').item(0)).flatMapAuto(a => a.textContent)
const reason = maybe(xmlDocument.getElementsByTagNameNS(tmp, 'Reason').item(0)).flatMapAuto(a => a.textContent)

return response.status === 200 && !reason.valueOrUndefined()
? ok(response.body)
: fail<Document, ITransportPayloadXml>({
? ok<IXmlContainer, ITransportPayoad>({ xmlString: response.body, xmlDocument })
: fail<IXmlContainer, ITransportPayoad>({
...response,
statusMessage: (reason.valueOrUndefined() || subcode.valueOr(response.statusMessage)).trim()
})
Expand Down Expand Up @@ -146,25 +168,3 @@ export const createDeviceRequestBodyFromString =
export const createMediaRequestBodyFromString =
(key: string) =>
createSimpleRequestBodyFromString(`trt:${key}`)

// tslint:disable-next-line:readonly-array
export const generateRequestElements = (reqNode: string) => (parameterNodes: string[]) => (...params: any[]) => {
return !params.length
? `<${reqNode} />`
: `<${reqNode}>${params.map((param, index) => {
const type = typeof param
const insertIntoRootNode = (inner: string) => `<${parameterNodes[index]}>${inner}</${parameterNodes[index]}>`
switch (type) {
case 'undefined': return ''
case 'boolean': return insertIntoRootNode(param)
case 'object': return insertIntoRootNode(Object.keys(param).reduce((acc, key) => {
const val = (param as any)[key]
return val
? (acc || '') + `<${key}>${(param as any)[key]}</${key}>`
: (acc || '')
}, ''))
default: return insertIntoRootNode(param)
}
}).join('')}</${reqNode}>`
}

0 comments on commit ba1d39e

Please sign in to comment.