feat(xml-rpc): new lib to parse/format XML-RPC

This commit is contained in:
Julien Fontanet
2024-04-24 15:29:01 +02:00
committed by Pierre Donias
parent 798049c20e
commit f8fc4aa551
11 changed files with 701 additions and 0 deletions

128
@vates/xml-rpc/.USAGE.md Normal file
View File

@@ -0,0 +1,128 @@
### Formatting
```js
import { xmlRpcFormatter } from '@vates/xml-rpc/formatter'
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// → {
// name: 'methodCall',
// children: [
// {
// name: 'methodName',
// children: [ 'examples.getStateName' ]
// },
// {
// name: 'params',
// children: [
// {
// name: 'param',
// children: [ { name: 'value', children: [ '40' ] } ]
// }
// ]
// }
// ]
// }
// There is a dedicated method to format a JS value
xmlRpcFormatter.format_js(new Date(900684535000))
// → {
// name: 'dateTime.iso8601',
// children: [ '1998-07-17T14:08:55.000Z' ]
// }
```
Formatting XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { formatXml } from '@vates/xml/format'
// it can now be passed to the XML-RPC parser
const tree = xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// avoid any indentation or new lines as it will confuse many XML-RPC parsers.
const xml = formatXml(tree, { indent: 0 })
```
### Parsing
```js
import { xmlRpcParser } from '@vates/xml-rpc/parser'
xmlRpcParser.parse({
name: 'methodResponse',
children: [
{
name: 'params',
children: [
{
name: 'param',
children: [
{
name: 'value',
children: [
{
name: 'string',
children: ['South Dakota'],
},
],
},
],
},
],
},
],
})
// { params: [ 'South Dakota' ] }
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcParser.parse_array({
name: 'array',
children: [
{
name: 'data',
children: [
{
name: 'value',
children: [{ name: 'boolean', children: ['1'] }],
},
{
name: 'value',
children: [{ name: 'int', children: ['42'] }],
},
],
},
],
})
// [ true, 42 ]
```
Parsing XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { parseXml } from '@vates/xml/parse'
const xml = `
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>South Dakota</string></value>
</param>
</params>
</methodResponse>
`
const tree = parseXml(xml)
// it can now be passed to the XML-RPC parser
xmlRpcParser.parse(tree)
```

1
@vates/xml-rpc/.npmignore Symbolic link
View File

@@ -0,0 +1 @@
../../scripts/npmignore

161
@vates/xml-rpc/README.md Normal file
View File

@@ -0,0 +1,161 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/xml-rpc
[![Package Version](https://badgen.net/npm/v/@vates/xml-rpc)](https://npmjs.org/package/@vates/xml-rpc) ![License](https://badgen.net/npm/license/@vates/xml-rpc) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/xml-rpc)](https://bundlephobia.com/result?p=@vates/xml-rpc) [![Node compatibility](https://badgen.net/npm/node/@vates/xml-rpc)](https://npmjs.org/package/@vates/xml-rpc)
> Parse and format XML-RPC messages, do not handle transport or XML
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/xml-rpc):
```sh
npm install --save @vates/xml-rpc
```
## Usage
### Formatting
```js
import { xmlRpcFormatter } from '@vates/xml-rpc/formatter'
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// → {
// name: 'methodCall',
// children: [
// {
// name: 'methodName',
// children: [ 'examples.getStateName' ]
// },
// {
// name: 'params',
// children: [
// {
// name: 'param',
// children: [ { name: 'value', children: [ '40' ] } ]
// }
// ]
// }
// ]
// }
// There is a dedicated method to format a JS value
xmlRpcFormatter.format_js(new Date(900684535000))
// → {
// name: 'dateTime.iso8601',
// children: [ '1998-07-17T14:08:55.000Z' ]
// }
```
Formatting XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { formatXml } from '@vates/xml/format'
// it can now be passed to the XML-RPC parser
const tree = xmlRpcFormatter.format_methodCall({
methodName: 'examples.getStateName',
params: [40],
})
// avoid any indentation or new lines as it will confuse many XML-RPC parsers.
const xml = formatXml(tree, { indent: 0 })
```
### Parsing
```js
import { xmlRpcParser } from '@vates/xml-rpc/parser'
xmlRpcParser.parse({
name: 'methodResponse',
children: [
{
name: 'params',
children: [
{
name: 'param',
children: [
{
name: 'value',
children: [
{
name: 'string',
children: ['South Dakota'],
},
],
},
],
},
],
},
],
})
// { params: [ 'South Dakota' ] }
// There is a dedicated method per XML-RPC tag (`array`, `string`, etc.)
xmlRpcParser.parse_array({
name: 'array',
children: [
{
name: 'data',
children: [
{
name: 'value',
children: [{ name: 'boolean', children: ['1'] }],
},
{
name: 'value',
children: [{ name: 'int', children: ['42'] }],
},
],
},
],
})
// [ true, 42 ]
```
Parsing XML is out of this library's scope, but you can achieve it easily with
`@vates/xml`:
```js
import { parseXml } from '@vates/xml/parse'
const xml = `
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>South Dakota</string></value>
</param>
</params>
</methodResponse>
`
const tree = parseXml(xml)
// it can now be passed to the XML-RPC parser
xmlRpcParser.parse(tree)
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

104
@vates/xml-rpc/formatter.js Normal file
View File

@@ -0,0 +1,104 @@
'use strict'
function node(name, attributes, children) {
if (Array.isArray(attributes)) {
children = attributes
attributes = undefined
}
return { name, attributes, children }
}
class XmlRpcFormatter {
format_js(value) {
const type = typeof value
if (type === 'object') {
return value === null
? this.format_nil()
: value instanceof Date
? this['format_dateTime.iso8601'](value)
: Array.isArray(value)
? this.format_array(value)
: Buffer.isBuffer(value)
? this.format_base64(value)
: this.format_struct(value)
}
return this['format_' + type](value)
}
format_array(values) {
return node('array', [node('data', values.map(this.format_value, this))])
}
format_base64(buf) {
return node('base64', [buf.toString('base64')])
}
format_boolean(bool) {
return node('boolean', [bool ? '1' : '0'])
}
'format_dateTime.iso8601'(date) {
return node('dateTime.iso8601', [date.toISOString()])
}
format_double(str) {
return node('double', [String(str)])
}
format_fault(val) {
return node('fault', [this.format_value(val)])
}
format_i4(str) {
return node('i4', [String(str)])
}
format_int(str) {
return node('int', [String(str)])
}
format_member([name, value]) {
return node('member', [node('name', [name]), this.format_value(value)])
}
format_methodCall({ methodName, params }) {
return node('methodCall', [node('methodName', [methodName]), this.format_params(params)])
}
format_methodResponse({ fault, params }) {
return node('methodResponse', [fault !== undefined ? this.format_fault(fault) : this.format_params(params)])
}
format_nil() {
return node('nil')
}
format_number(num) {
return Number.isInteger(num) ? this.format_int(num) : this.format_double(num)
}
format_param(value) {
return node('param', [this.format_value(value)])
}
format_params(params) {
return node('params', params.map(this.format_param, this))
}
format_string(str) {
return str
}
format_struct(struct) {
return node('struct', Object.entries(struct).map(this.format_member, this))
}
format_value(value) {
return node('value', [this.format_js(value)])
}
}
exports.Formatter = XmlRpcFormatter
exports.xmlRpcFormatter = new XmlRpcFormatter()

View File

@@ -0,0 +1,33 @@
{
"private": false,
"name": "@vates/xml-rpc",
"description": "Parse and format XML-RPC messages, do not handle transport or XML",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/xml-rpc",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/xml-rpc",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.0.0",
"engines": {
"node": ">=12"
},
"devDependencies": {
"@vates/xml": "0.0.0",
"test": "^3.3.0"
},
"exports": {
"./formatter": "./formatter.js",
"./parser": "./parser.js"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
}
}

133
@vates/xml-rpc/parser.js Normal file
View File

@@ -0,0 +1,133 @@
'use strict'
function toValue(node) {
const obj = { __proto__: null }
for (const child of node.children) {
if (typeof child === 'string') {
return child
}
const { name } = child
if (Object.hasOwn(obj, name)) {
const prev = obj[name]
if (Array.isArray(obj, name)) {
prev.push(child)
} else {
obj[name] = [prev, child]
}
} else {
obj[name] = child
}
}
return obj
}
// https://en.wikipedia.org/wiki/XML-RPC#Data_types
class XmlRpcParser {
parse(node) {
return this['parse_' + node.name](node)
}
parse_array({ children: [data] }) {
return data.children.map(this.parse_value, this)
}
parse_base64({ children: [str] }) {
return Buffer.from(str, 'base64')
}
parse_boolean({ children: [str] }) {
return Boolean(Number(str))
}
'parse_dateTime.iso8601'({ children: [str] }) {
const date = new Date(str)
// date successfully parsed
if (!Number.isNaN(+date)) {
return date
}
// XML-RPC dates may use YYYYMMDD instead of YYYY-MM-DD which ECMAScript
// does not parse
return new Date(str.slice(0, 4) + '-' + str.slice(4, 6) + '-' + str.slice(6))
}
parse_double({ children: [str] }) {
return Number(str)
}
parse_fault({ children: [value] }) {
return this.parse_value(value)
}
parse_i4({ children: [str] }) {
return Number(str)
}
parse_int({ children: [str] }) {
return Number(str)
}
parse_member(member) {
member = toValue(member)
return [member.name.children[0], this.parse_value(member.value)]
}
parse_methodCall(methodCall) {
methodCall = toValue(methodCall)
return {
methodName: methodCall.methodName.children[0],
params: this.parse_params(methodCall.params),
}
}
parse_methodResponse(methodResponse) {
methodResponse = toValue(methodResponse)
const { fault } = methodResponse
if (fault !== undefined) {
return { fault: this.parse_fault(fault) }
}
return { params: this.parse_params(methodResponse.params) }
}
parse_nil() {
return null
}
parse_param(param) {
return this.parse_value(param.children[0])
}
parse_params(params) {
return params.children.map(this.parse_param, this)
}
parse_string({ children: [str] }) {
return str
}
parse_struct(struct) {
return Object.fromEntries(struct.children.map(this.parse_member, this))
}
parse_value({ children }) {
if (children.length === 0) {
return ''
}
const child = children[0]
// can be a plain string
if (typeof child === 'string') {
return child
}
return this.parse(child)
}
}
exports.XmlRpcParser = XmlRpcParser
exports.xmlRpcParser = new XmlRpcParser()

View File

@@ -0,0 +1,19 @@
'use strict'
const { formatXml } = require('@vates/xml/format')
const { join } = require('node:path')
const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { xmlRpcFormatter } = require('@vates/xml-rpc/formatter')
const assert = require('node:assert/strict')
const test = require('test')
const methodCall = require('./methodCall.js')
test(function () {
const xmlTree = parseXml(readFileSync(join(__dirname, 'methodCall.xml')))
const xmlRpc = xmlRpcFormatter.format_methodCall(methodCall)
assert.deepEqual(parseXml(formatXml(xmlRpc)), xmlTree)
})

View File

@@ -0,0 +1,16 @@
'use strict'
module.exports = {
methodName: 'examples.getStateName',
params: [
[1404, 'Something here'],
Buffer.from([121, 111, 117, 32, 99, 97, 110, 39, 116, 32, 114, 101, 97, 100, 32, 116, 104, 105, 115, 33]),
true,
new Date('1998-07-17T14:08:55.000Z'),
-12.53,
42,
'Hello world!',
{ foo: 1, bar: 2 },
null,
],
}

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value>
<array>
<data>
<value>
<int>1404</int>
</value>
<value>
Something here
</value>
</data>
</array>
</value>
</param>
<param>
<value>
<base64>eW91IGNhbid0IHJlYWQgdGhpcyE=</base64>
</value>
</param>
<param>
<value>
<boolean>1</boolean>
</value>
</param>
<param>
<value>
<dateTime.iso8601>1998-07-17T14:08:55.000Z</dateTime.iso8601>
</value>
</param>
<param>
<value>
<double>-12.53</double>
</value>
</param>
<param>
<value>
<int>42</int>
</value>
</param>
<param>
<value>
Hello world!
</value>
</param>
<param>
<value>
<struct>
<member>
<name>foo</name>
<value>
<int>1</int>
</value>
</member>
<member>
<name>bar</name>
<value>
<int>2</int>
</value>
</member>
</struct>
</value>
</param>
<param>
<value>
<nil />
</value>
</param>
</params>
</methodCall>

View File

@@ -0,0 +1,32 @@
'use strict'
const { join } = require('node:path')
const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { xmlRpcParser } = require('@vates/xml-rpc/parser')
const assert = require('node:assert/strict')
const test = require('test')
const methodCall = require('./methodCall.js')
test('parser.parse()', function () {
const xml = String(readFileSync(join(__dirname, 'methodCall.xml')))
const xmlRpc = parseXml(xml)
assert.deepEqual(xmlRpcParser.parse(xmlRpc), methodCall)
})
test('datetime without dashes nor milliseconds', function () {
assert.deepEqual(
xmlRpcParser.parse({ name: 'dateTime.iso8601', children: ['19980717T14:08:55Z'] }),
new Date('1998-07-17T14:08:55Z')
)
})
test('i4 support', function () {
assert.equal(xmlRpcParser.parse({ name: 'i4', children: ['42'] }), 42)
})
test('string support', function () {
assert.equal(xmlRpcParser.parse({ name: 'string', children: ['foo'] }), 'foo')
})

View File

@@ -28,5 +28,6 @@
<!--packages-start-->
- @vates/xml major
- @vates/xml-rpc major
<!--packages-end-->