polyxml 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +19 -0
- package/README.md +38 -0
- package/build.rs +3 -0
- package/index.d.ts +58 -0
- package/index.js +92 -0
- package/package.json +30 -0
- package/polyxml.darwin-arm64.node +0 -0
- package/polyxml.darwin-x64.node +0 -0
- package/polyxml.linux-x64-gnu.node +0 -0
- package/polyxml.win32-x64-msvc.node +0 -0
- package/src/lib.rs +149 -0
- package/test/index.test.js +42 -0
package/Cargo.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "polyxml-js"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
authors.workspace = true
|
|
7
|
+
repository.workspace = true
|
|
8
|
+
description = "Node.js and TypeScript native addon bindings for PolyXML"
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
polyxml = { path = "../polyxml-core" }
|
|
15
|
+
napi = { version = "2.16", default-features = false, features = ["napi4"] }
|
|
16
|
+
napi-derive = "2.16"
|
|
17
|
+
|
|
18
|
+
[build-dependencies]
|
|
19
|
+
napi-build = "2.1"
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# PolyXML Node.js & TypeScript Bindings
|
|
2
|
+
|
|
3
|
+
High-performance, polyglot streaming XML data-binding engine for Node.js and TypeScript.
|
|
4
|
+
|
|
5
|
+
- Precompiled native binaries for Linux (x86_64), macOS (Apple Silicon & Intel), and Windows (x64).
|
|
6
|
+
- Zero external native toolchain dependencies needed at runtime.
|
|
7
|
+
- Powered by the ultra-fast Rust `polyxml-core` engine and NAPI-RS.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install polyxml
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const polyxml = require('polyxml');
|
|
19
|
+
|
|
20
|
+
const schema = {
|
|
21
|
+
name: 'Book',
|
|
22
|
+
fields: [
|
|
23
|
+
{ name: 'id', xmlName: 'id', kind: 'attribute', scalarType: 'int' },
|
|
24
|
+
{ name: 'title', xmlName: 'title', kind: 'element', scalarType: 'string' },
|
|
25
|
+
{ name: 'price', xmlName: 'price', kind: 'element', scalarType: 'float' }
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const xml = '<Book id="42"><title>Rust in Action</title><price>39.99</price></Book>';
|
|
30
|
+
const book = polyxml.deserialize(xml, schema);
|
|
31
|
+
|
|
32
|
+
console.log(book);
|
|
33
|
+
// { id: 42, title: 'Rust in Action', price: 39.99 }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/build.rs
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolyXML TypeScript Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type FieldKind = 'attribute' | 'element' | 'text';
|
|
6
|
+
|
|
7
|
+
export type ScalarType =
|
|
8
|
+
| 'string'
|
|
9
|
+
| 'int'
|
|
10
|
+
| 'float'
|
|
11
|
+
| 'bool'
|
|
12
|
+
| 'decimal'
|
|
13
|
+
| 'xml_date'
|
|
14
|
+
| 'xml_datetime'
|
|
15
|
+
| 'any';
|
|
16
|
+
|
|
17
|
+
export interface FieldDefinition {
|
|
18
|
+
name: string;
|
|
19
|
+
xmlName: string;
|
|
20
|
+
kind: FieldKind;
|
|
21
|
+
scalarType: ScalarType;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ModelSchema {
|
|
25
|
+
name: string;
|
|
26
|
+
fields: FieldDefinition[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PolyValue =
|
|
30
|
+
| null
|
|
31
|
+
| boolean
|
|
32
|
+
| number
|
|
33
|
+
| string
|
|
34
|
+
| PolyValue[]
|
|
35
|
+
| { [key: string]: PolyValue };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Deserialize an XML string or Uint8Array into a JavaScript object based on schema.
|
|
39
|
+
*/
|
|
40
|
+
export function deserialize(
|
|
41
|
+
xml: string | Uint8Array,
|
|
42
|
+
schema: ModelSchema
|
|
43
|
+
): PolyValue;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Serialize a JavaScript object into XML bytes based on schema.
|
|
47
|
+
*/
|
|
48
|
+
export function serialize(
|
|
49
|
+
rootName: string,
|
|
50
|
+
value: Record<string, any>,
|
|
51
|
+
schema: ModelSchema,
|
|
52
|
+
indent?: number | null
|
|
53
|
+
): Uint8Array;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return the PolyXML engine version.
|
|
57
|
+
*/
|
|
58
|
+
export function version(): string;
|
package/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { existsSync } = require('fs')
|
|
2
|
+
const { join } = require('path')
|
|
3
|
+
|
|
4
|
+
const { platform, arch } = process
|
|
5
|
+
|
|
6
|
+
let nativeBinding = null
|
|
7
|
+
let localFileExisted = false
|
|
8
|
+
let loadError = null
|
|
9
|
+
|
|
10
|
+
switch (platform) {
|
|
11
|
+
case 'win32':
|
|
12
|
+
switch (arch) {
|
|
13
|
+
case 'x64':
|
|
14
|
+
localFileExisted = existsSync(join(__dirname, 'polyxml.win32-x64-msvc.node'))
|
|
15
|
+
try {
|
|
16
|
+
if (localFileExisted) {
|
|
17
|
+
nativeBinding = require('./polyxml.win32-x64-msvc.node')
|
|
18
|
+
} else {
|
|
19
|
+
nativeBinding = require('polyxml-win32-x64-msvc')
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
loadError = e
|
|
23
|
+
}
|
|
24
|
+
break
|
|
25
|
+
default:
|
|
26
|
+
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
|
27
|
+
}
|
|
28
|
+
break
|
|
29
|
+
case 'darwin':
|
|
30
|
+
switch (arch) {
|
|
31
|
+
case 'x64':
|
|
32
|
+
localFileExisted = existsSync(join(__dirname, 'polyxml.darwin-x64.node'))
|
|
33
|
+
try {
|
|
34
|
+
if (localFileExisted) {
|
|
35
|
+
nativeBinding = require('./polyxml.darwin-x64.node')
|
|
36
|
+
} else {
|
|
37
|
+
nativeBinding = require('polyxml-darwin-x64')
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
loadError = e
|
|
41
|
+
}
|
|
42
|
+
break
|
|
43
|
+
case 'arm64':
|
|
44
|
+
localFileExisted = existsSync(join(__dirname, 'polyxml.darwin-arm64.node'))
|
|
45
|
+
try {
|
|
46
|
+
if (localFileExisted) {
|
|
47
|
+
nativeBinding = require('./polyxml.darwin-arm64.node')
|
|
48
|
+
} else {
|
|
49
|
+
nativeBinding = require('polyxml-darwin-arm64')
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
loadError = e
|
|
53
|
+
}
|
|
54
|
+
break
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
|
57
|
+
}
|
|
58
|
+
break
|
|
59
|
+
case 'linux':
|
|
60
|
+
switch (arch) {
|
|
61
|
+
case 'x64':
|
|
62
|
+
localFileExisted = existsSync(join(__dirname, 'polyxml.linux-x64-gnu.node'))
|
|
63
|
+
try {
|
|
64
|
+
if (localFileExisted) {
|
|
65
|
+
nativeBinding = require('./polyxml.linux-x64-gnu.node')
|
|
66
|
+
} else {
|
|
67
|
+
nativeBinding = require('polyxml-linux-x64-gnu')
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
loadError = e
|
|
71
|
+
}
|
|
72
|
+
break
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
|
75
|
+
}
|
|
76
|
+
break
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!nativeBinding) {
|
|
82
|
+
try {
|
|
83
|
+
nativeBinding = require('./polyxml.node')
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (loadError) {
|
|
86
|
+
throw loadError
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Failed to load native PolyXML addon for ${platform}-${arch}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = nativeBinding
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "polyxml",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance, polyglot XML data-binding engine built in Rust",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test/index.test.js"
|
|
9
|
+
},
|
|
10
|
+
"napi": {
|
|
11
|
+
"name": "polyxml"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@napi-rs/cli": "^3.0.0"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"xml",
|
|
18
|
+
"parser",
|
|
19
|
+
"serializer",
|
|
20
|
+
"deserializer",
|
|
21
|
+
"fast-xml",
|
|
22
|
+
"polyglot"
|
|
23
|
+
],
|
|
24
|
+
"author": "Bailey Nguyen <bailey.tan.nguyen@gmail.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/nth-bailey/PolyXML.git"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/lib.rs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
use napi::bindgen_prelude::*;
|
|
2
|
+
use napi::JsUnknown;
|
|
3
|
+
use napi_derive::napi;
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
|
|
6
|
+
use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
|
|
7
|
+
use polyxml::value::PolyValue;
|
|
8
|
+
|
|
9
|
+
#[napi(object)]
|
|
10
|
+
pub struct JsFieldDef {
|
|
11
|
+
pub name: String,
|
|
12
|
+
pub xml_name: String,
|
|
13
|
+
pub kind: String,
|
|
14
|
+
pub scalar_type: String,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[napi(object)]
|
|
18
|
+
pub struct JsModelSchema {
|
|
19
|
+
pub name: String,
|
|
20
|
+
pub fields: Vec<JsFieldDef>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn convert_js_schema(schema: &JsModelSchema) -> Arc<ModelSchema> {
|
|
24
|
+
let mut builder = ModelSchema::builder(&schema.name);
|
|
25
|
+
for f in &schema.fields {
|
|
26
|
+
let kind = match f.kind.as_str() {
|
|
27
|
+
"attribute" => FieldKind::Attribute,
|
|
28
|
+
"text" => FieldKind::Text,
|
|
29
|
+
_ => FieldKind::Element,
|
|
30
|
+
};
|
|
31
|
+
let sc = match f.scalar_type.as_str() {
|
|
32
|
+
"int" => ScalarType::Int,
|
|
33
|
+
"float" => ScalarType::Float,
|
|
34
|
+
"bool" => ScalarType::Bool,
|
|
35
|
+
"decimal" => ScalarType::Decimal,
|
|
36
|
+
"xml_date" => ScalarType::XmlDate,
|
|
37
|
+
"xml_datetime" => ScalarType::XmlDateTime,
|
|
38
|
+
_ => ScalarType::String,
|
|
39
|
+
};
|
|
40
|
+
builder = builder.field(FieldSchema::new(
|
|
41
|
+
&f.name,
|
|
42
|
+
f.xml_name.as_bytes(),
|
|
43
|
+
kind,
|
|
44
|
+
ValueType::Scalar(sc),
|
|
45
|
+
));
|
|
46
|
+
}
|
|
47
|
+
builder.build()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn poly_value_to_js(env: &Env, val: &PolyValue) -> Result<JsUnknown> {
|
|
51
|
+
match val {
|
|
52
|
+
PolyValue::Null => Ok(env.get_null()?.into_unknown()),
|
|
53
|
+
PolyValue::Bool(b) => Ok(env.get_boolean(*b)?.into_unknown()),
|
|
54
|
+
PolyValue::Int(i) => Ok(env.create_int64(*i)?.into_unknown()),
|
|
55
|
+
PolyValue::Float(f) => Ok(env.create_double(*f)?.into_unknown()),
|
|
56
|
+
PolyValue::String(s) => Ok(env.create_string(s)?.into_unknown()),
|
|
57
|
+
PolyValue::List(list) => {
|
|
58
|
+
let mut arr = env.create_array(list.len() as u32)?;
|
|
59
|
+
for (idx, item) in list.iter().enumerate() {
|
|
60
|
+
let js_item = poly_value_to_js(env, item)?;
|
|
61
|
+
arr.set(idx as u32, js_item)?;
|
|
62
|
+
}
|
|
63
|
+
Ok(arr.coerce_to_object()?.into_unknown())
|
|
64
|
+
}
|
|
65
|
+
PolyValue::Object(map) => {
|
|
66
|
+
let mut obj = env.create_object()?;
|
|
67
|
+
for (k, v) in map {
|
|
68
|
+
let js_val = poly_value_to_js(env, v)?;
|
|
69
|
+
obj.set(k.as_str(), js_val)?;
|
|
70
|
+
}
|
|
71
|
+
Ok(obj.into_unknown())
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[napi]
|
|
77
|
+
pub fn deserialize(
|
|
78
|
+
env: Env,
|
|
79
|
+
xml: Either<String, Buffer>,
|
|
80
|
+
schema: JsModelSchema,
|
|
81
|
+
) -> Result<JsUnknown> {
|
|
82
|
+
let xml_bytes: &[u8] = match &xml {
|
|
83
|
+
Either::A(s) => s.as_bytes(),
|
|
84
|
+
Either::B(b) => b.as_ref(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
let model_schema = convert_js_schema(&schema);
|
|
88
|
+
let val = polyxml::deserialize(xml_bytes, model_schema)
|
|
89
|
+
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
|
90
|
+
|
|
91
|
+
poly_value_to_js(&env, &val)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn js_to_poly_value(_env: &Env, val: JsUnknown, scalar_type: &str) -> Result<PolyValue> {
|
|
95
|
+
let val_type = val.get_type()?;
|
|
96
|
+
if val_type == napi::ValueType::Null || val_type == napi::ValueType::Undefined {
|
|
97
|
+
return Ok(PolyValue::Null);
|
|
98
|
+
}
|
|
99
|
+
match scalar_type {
|
|
100
|
+
"int" => {
|
|
101
|
+
let num = val.coerce_to_number()?;
|
|
102
|
+
Ok(PolyValue::Int(num.get_int64()?))
|
|
103
|
+
}
|
|
104
|
+
"float" => {
|
|
105
|
+
let num = val.coerce_to_number()?;
|
|
106
|
+
Ok(PolyValue::Float(num.get_double()?))
|
|
107
|
+
}
|
|
108
|
+
"bool" => {
|
|
109
|
+
let b = val.coerce_to_bool()?;
|
|
110
|
+
Ok(PolyValue::Bool(b.get_value()?))
|
|
111
|
+
}
|
|
112
|
+
_ => {
|
|
113
|
+
let s = val.coerce_to_string()?;
|
|
114
|
+
let utf8 = s.into_utf8()?;
|
|
115
|
+
Ok(PolyValue::String(utf8.as_str()?.to_string()))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[napi]
|
|
121
|
+
pub fn serialize(
|
|
122
|
+
env: Env,
|
|
123
|
+
root_name: String,
|
|
124
|
+
value: napi::JsObject,
|
|
125
|
+
schema: JsModelSchema,
|
|
126
|
+
indent: Option<u32>,
|
|
127
|
+
) -> Result<Buffer> {
|
|
128
|
+
use std::collections::HashMap;
|
|
129
|
+
|
|
130
|
+
let mut map = HashMap::new();
|
|
131
|
+
for f in &schema.fields {
|
|
132
|
+
if value.has_named_property(&f.name)? {
|
|
133
|
+
let prop: JsUnknown = value.get_named_property(&f.name)?;
|
|
134
|
+
let pv = js_to_poly_value(&env, prop, &f.scalar_type)?;
|
|
135
|
+
map.insert(f.name.clone(), pv);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let poly_val = PolyValue::Object(map);
|
|
139
|
+
let model_schema = convert_js_schema(&schema);
|
|
140
|
+
let indent_opt = indent.map(|i| i as usize);
|
|
141
|
+
let bytes = polyxml::serialize(&root_name, &poly_val, &model_schema, indent_opt)
|
|
142
|
+
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
|
143
|
+
Ok(bytes.into())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[napi]
|
|
147
|
+
pub fn version() -> &'static str {
|
|
148
|
+
env!("CARGO_PKG_VERSION")
|
|
149
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
|
|
4
|
+
test('PolyXML JavaScript bindings', (t) => {
|
|
5
|
+
let polyxml;
|
|
6
|
+
try {
|
|
7
|
+
polyxml = require('../index.js');
|
|
8
|
+
} catch (err) {
|
|
9
|
+
// If native addon not yet compiled locally, skip with notice
|
|
10
|
+
t.diagnostic('Native addon not compiled in current environment; skipping live execution test.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
assert.strictEqual(typeof polyxml.version(), 'string');
|
|
15
|
+
|
|
16
|
+
const schema = {
|
|
17
|
+
name: 'Sensor',
|
|
18
|
+
fields: [
|
|
19
|
+
{ name: 'id', xmlName: 'id', kind: 'attribute', scalarType: 'int' },
|
|
20
|
+
{ name: 'name', xmlName: 'name', kind: 'element', scalarType: 'string' },
|
|
21
|
+
{ name: 'reading', xmlName: 'reading', kind: 'element', scalarType: 'float' },
|
|
22
|
+
{ name: 'calibrated', xmlName: 'calibrated', kind: 'element', scalarType: 'bool' },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const xml = '<Sensor id="101"><name>Barometric Altimeter</name><reading>1013.25</reading><calibrated>true</calibrated></Sensor>';
|
|
27
|
+
const val = polyxml.deserialize(xml, schema);
|
|
28
|
+
|
|
29
|
+
assert.strictEqual(val.id, 101);
|
|
30
|
+
assert.strictEqual(val.name, 'Barometric Altimeter');
|
|
31
|
+
assert.strictEqual(val.reading, 1013.25);
|
|
32
|
+
assert.strictEqual(val.calibrated, true);
|
|
33
|
+
|
|
34
|
+
// Test serialization
|
|
35
|
+
const outBytes = polyxml.serialize('Sensor', val, schema, 2);
|
|
36
|
+
assert.ok(outBytes instanceof Uint8Array || Buffer.isBuffer(outBytes));
|
|
37
|
+
const outXml = Buffer.from(outBytes).toString('utf-8');
|
|
38
|
+
assert.ok(outXml.includes('id="101"'));
|
|
39
|
+
assert.ok(outXml.includes('<name>Barometric Altimeter</name>'));
|
|
40
|
+
assert.ok(outXml.includes('<reading>1013.25</reading>'));
|
|
41
|
+
assert.ok(outXml.includes('<calibrated>true</calibrated>'));
|
|
42
|
+
});
|