{"_id":"@alxcube/xml-mapper","_rev":"1-6bb346afcf7332bdfd22d28630f0cb64","name":"@alxcube/xml-mapper","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@alxcube/xml-mapper","version":"1.0.0","description":"Library designed for mapping XML documents to JavaScript objects using a declarative builder with XPath expressions.","keywords":["xml","xml mapper","xml parser","xpath"],"type":"module","main":"./dist/xml-mapper.umd.cjs","module":"./dist/xml-mapper.js","types":"./dist/xml-mapper.d.ts","exports":{".":{"import":"./dist/xml-mapper.js","require":"./dist/xml-mapper.umd.cjs"}},"scripts":{"build":"tsc && vite build","test":"vitest --typecheck --run","test:code":"vitest --run","test:types":"vitest --typecheck.only --run","lint":"eslint ./src ./spec --ext .ts && npm run prettier","prettier":"prettier --write 'src/**/*.ts' && prettier --write 'spec/**/*.ts'"},"author":{"name":"Alexander Alexandrov","email":"alxcube@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/alxcube/xml-mapper.git"},"license":"MIT","devDependencies":{"@types/node":"^20.11.19","@typescript-eslint/eslint-plugin":"^7.0.1","@vitest/browser":"^1.3.1","@xmldom/xmldom":"^0.8.10","eslint":"^8.56.0","eslint-config-prettier":"^9.1.0","eslint-plugin-import":"^2.29.1","eslint-plugin-prettier":"^5.1.3","prettier":"^3.2.5","typescript":"^5.2.2","vite":"^5.1.4","vite-plugin-banner":"^0.7.1","vite-plugin-dts":"^3.7.2","vitest":"^1.3.1","webdriverio":"^8.32.3"},"dependencies":{"xpath":"^0.0.34"},"_id":"@alxcube/xml-mapper@1.0.0","gitHead":"8b9afa15ffc59140fbc7c84e5aabc2d6b9f1e1df","bugs":{"url":"https://github.com/alxcube/xml-mapper/issues"},"homepage":"https://github.com/alxcube/xml-mapper#readme","_nodeVersion":"18.18.2","_npmVersion":"9.8.1","dist":{"integrity":"sha512-fVwutozJtnlU/l3hhSzgboAZG8D2pvzcKufKHU9Xj3FAZAJtWFHRX6MwFsAlyjzwnXwAx/wtp259lWIEKCQzvA==","shasum":"89f6310459e6f7751e88d043c0835cd181b9bf0d","tarball":"https://registry.npmjs.org/@alxcube/xml-mapper/-/xml-mapper-1.0.0.tgz","fileCount":8,"unpackedSize":197720,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIFYVMV/czlRCNrqrBATFfIhAQCJayKzXGuJftnili/ruAiAZG85VdrFVI1K1Dkl49Wb//4haYaEHfrSKTAVPIuUlFg=="}]},"_npmUser":{"name":"alxcube","email":"alxcube@gmail.com"},"directories":{},"maintainers":[{"name":"alxcube","email":"alxcube@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/xml-mapper_1.0.0_1709549660514_0.1958270924376191"},"_hasShrinkwrap":false},"1.0.1":{"name":"@alxcube/xml-mapper","version":"1.0.1","description":"Library designed for mapping XML documents to JavaScript objects using a declarative builder with XPath expressions.","keywords":["xml","xml mapper","xml parser","xpath"],"type":"module","main":"./dist/xml-mapper.umd.cjs","module":"./dist/xml-mapper.js","types":"./dist/xml-mapper.d.ts","exports":{".":{"import":"./dist/xml-mapper.js","require":"./dist/xml-mapper.umd.cjs"}},"scripts":{"build":"tsc && vite build","test":"vitest --typecheck --run","test:code":"vitest --run","test:types":"vitest --typecheck.only --run","lint":"eslint ./src ./spec --ext .ts && npm run prettier","prettier":"prettier --write 'src/**/*.ts' && prettier --write 'spec/**/*.ts'"},"author":{"name":"Alexander Alexandrov","email":"alxcube@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/alxcube/xml-mapper.git"},"license":"MIT","devDependencies":{"@types/node":"^20.11.19","@typescript-eslint/eslint-plugin":"^7.0.1","@vitest/browser":"^1.3.1","@xmldom/xmldom":"^0.8.10","eslint":"^8.56.0","eslint-config-prettier":"^9.1.0","eslint-plugin-import":"^2.29.1","eslint-plugin-prettier":"^5.1.3","prettier":"^3.2.5","typescript":"^5.2.2","vite":"^5.1.4","vite-plugin-banner":"^0.7.1","vite-plugin-dts":"^3.7.2","vitest":"^1.3.1","webdriverio":"^8.32.3"},"dependencies":{"xpath":"^0.0.34"},"_id":"@alxcube/xml-mapper@1.0.1","gitHead":"38c308585a6179a2ead4c4c315a09555c6b785c5","bugs":{"url":"https://github.com/alxcube/xml-mapper/issues"},"homepage":"https://github.com/alxcube/xml-mapper#readme","_nodeVersion":"18.18.2","_npmVersion":"9.8.1","dist":{"integrity":"sha512-NST2JjCvm6/s+YGtEAQ9FP3uOrOHYe92WHtMkCB/6OfzwFfFIK1LQrwGuwsA/JWB3duQdFjzFeXSyPzVcPpIBg==","shasum":"83c88550f6be37e2f29fb5151f5f436dc1e9c9d6","tarball":"https://registry.npmjs.org/@alxcube/xml-mapper/-/xml-mapper-1.0.1.tgz","fileCount":8,"unpackedSize":197642,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDd4flMTLgbG1VUqDhq/WnZZIZfEsGRuWHjfUueJElpNAiEA3yfsLIA4EQXMCn2xeVNaMjT0KR3gj/xlsajih2pCULs="}]},"_npmUser":{"name":"alxcube","email":"alxcube@gmail.com"},"directories":{},"maintainers":[{"name":"alxcube","email":"alxcube@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/xml-mapper_1.0.1_1710094019820_0.3863312212411667"},"_hasShrinkwrap":false}},"time":{"created":"2024-03-04T10:54:20.409Z","1.0.0":"2024-03-04T10:54:20.711Z","modified":"2024-03-10T18:07:00.478Z","1.0.1":"2024-03-10T18:07:00.055Z"},"maintainers":[{"name":"alxcube","email":"alxcube@gmail.com"}],"description":"Library designed for mapping XML documents to JavaScript objects using a declarative builder with XPath expressions.","homepage":"https://github.com/alxcube/xml-mapper#readme","keywords":["xml","xml mapper","xml parser","xpath"],"repository":{"type":"git","url":"git+https://github.com/alxcube/xml-mapper.git"},"author":{"name":"Alexander Alexandrov","email":"alxcube@gmail.com"},"bugs":{"url":"https://github.com/alxcube/xml-mapper/issues"},"license":"MIT","readme":"# xml-mapper\n\n`xml-mapper` is a library designed for mapping XML documents to JavaScript objects\nusing a declarative builder with XPath expressions. It provides a type-safe approach\nto mapping XML data to JavaScript objects.\n\n## Features\n\n- Declarative mapping: Define mappings using a fluent API with XPath expressions.\n- Type-safe: Ensure type safety throughout the mapping process.\n- Flexibility: Offers support for complex data structures, including recursive mapping, enabling the handling of intricate data hierarchies.\n\n## Quick start\n\n### Installation\n\n`xml-mapper` requires [xpath](https://www.npmjs.com/package/xpath) package.\n\nFor use in environments without native DOM support, you can use [@xmldom/xmldom](https://www.npmjs.com/package/@xmldom/xmldom)\npackage.\n\n```shell\nnpm i @alxcube/xml-mapper xpath @xmldom/xmldom\n```\n\n### Usage example\n\n```ts\nimport { DOMParser } from \"@xmldom/xmldom\";\nimport { createObjectMapper, map } from \"@alxcube/xml-mapper\";\n\nconst xml = `\n<User id=\"123\" verified=\"false\">\n  <FirstName>John</FirstName>\n  <LastName>Doe</LastName>\n  <ContactData>\n    <Email>johndoe@example.com</Email>\n    <Phone>+1234567890</Phone>\n  </ContactData>\n  <Groups>\n    <Group id=\"1\">Registered</Group>\n    <Group id=\"2\">Customers</Group>\n  </Groups>\n  <RegistrationDate year=\"2024\" month=\"2\" day=\"28\"/>\n</User>\n`;\n\n/**\n * Example interface\n */\ninterface User {\n  id: number;\n  isVerified: boolean;\n  firstName: string;\n  lastName: string;\n  contacts?: {\n    email?: string;\n    phone?: string;\n  };\n  groups: { id: number; title: string }[];\n  registeredAt: Date;\n}\n\n// Define mapper function with 'createObjectMapper()'\nconst userMapper = createObjectMapper<User>({\n  id: map()\n    .toNode(\"/User/@id\") // Search for 'id' attribute in User element\n    .mandatory() // Make sure that reference node is present in xml.\n    .asNumber(), // Use attribute value as number,\n  isVerified: map().toNode(\"/User/@verified\").asBoolean().withDefault(false), // Assign default value in case of reference node is not found\n  firstName: map().toNode(\"/User/FirstName\").mandatory().asString(),\n  lastName: map().toNode(\"/User/LastName\").mandatory().asString(),\n  contacts: map()\n    .toNode(\"/User/ContactData\") // Search for Element node\n    .asObject({\n      email: map()\n        .toNode(\"Email\") // Nested objects xpath expression can be relative to reference element\n        .asString(),\n      phone: map().toNode(\"Phone\").asString(),\n    }),\n  groups: map()\n    .toNodesArray(\"/User/Groups/Group\") // Search for array of elements\n    .mandatory()\n    .asArray()\n    .ofObjects({\n      id: map().toNode(\"@id\").mandatory().asNumber(),\n      title: map().toNode(\".\").mandatory().asString(),\n    }),\n  registeredAt: map()\n    .toNode(\"/User/RegistrationDate\")\n    .mandatory()\n    .callback((node, select) => {\n      // Use custom callback for complex case: select attribute values, using xpath expression and return Date\n      const year = select(\"number(@year)\", node) as number;\n      const month = select(\"number(@month)\", node) as number;\n      const day = select(\"number(@day)\", node) as number;\n      return new Date(year, month - 1, day);\n    }),\n});\n\n// Parse XML\nconst doc = new DOMParser().parseFromString(xml);\n\n// Get User object from parsed Document, using created mapper\nconst user: User = userMapper(doc);\n\nconsole.log(user);\n```\n\n## Key Concepts:\n\n### `SingleNodeDataExtractorFn`\n\n```ts\nimport type { XPathSelect } from \"xpath\";\n\ninterface SingleNodeDataExtractorFn<DataExtractorReturnType> {\n  (node: Node, xpathSelect: XPathSelect): DataExtractorReturnType;\n}\n```\n\nA function that takes two parameters: the context DOM node\nand the `XPathSelect` interface from the `xpath` library. The purpose of this function\nis to return data of a specific type from the context node or its child nodes.\n\n### `SingleNodeDataExtractorFnFactory`\n\n```ts\ninterface SingleNodeDataExtractorFnFactory<DataExtractorReturnType> {\n  createNodeDataExtractor(): SingleNodeDataExtractorFn<DataExtractorReturnType>;\n}\n```\n\nAn interface whose method `createNodeDataExtractor()` returns a function of type\n`SingleNodeDataExtractorFn`.\n\n### `SingleNodeLookupFn`\n\n```ts\nimport type { XPathSelect } from \"xpath\";\n\ninterface SingleNodeLookupFn<NodeLookupResult extends Node | undefined> {\n  (contextNode: Node, xpathSelect: XPathSelect): NodeLookupResult;\n}\n```\n\nA function that takes two parameters: the context DOM node relative to which the\nsearch is performed and the `XPathSelect` interface from the `xpath` library.\nThe returned result is a new context node for data extraction or `undefined` if the\nsearched node is absent.\n\n### `NodesArrayDataExtractorFn`\n\n```ts\nimport type { XPathSelect } from \"xpath\";\n\ninterface NodesArrayDataExtractorFn<ArrayDataExtractorReturnType> {\n  (nodes: Node[], xpathSelect: XPathSelect): ArrayDataExtractorReturnType;\n}\n```\n\nA function that takes two parameters: an array of context DOM nodes and the `XPathSelect`\ninterface from the `xpath` library. The purpose of this function is to return data of\na specific type from the array of context nodes.\n\n### `NodesArrayDataExtractorFnFactory`\n\n```ts\ninterface NodesArrayDataExtractorFnFactory<ArrayDataExtractorReturnType> {\n  createNodesArrayDataExtractor(): NodesArrayDataExtractorFn<ArrayDataExtractorReturnType>;\n}\n```\n\nAn interface whose method `createNodesArrayDataExtractor()` returns a function of type\n`NodesArrayDataExtractorFn`.\n\n### `NodesArrayLookupFn`\n\n```ts\nimport type { XPathSelect } from \"xpath\";\n\ninterface NodesArrayLookupFn<NodesLookupResult extends Node[] | undefined> {\n  (contextNode: Node, xpathSelect: XPathSelect): NodesLookupResult;\n}\n```\n\nA function that takes two parameters: the context DOM node relative to which the search\nis performed and the `XPathSelect` interface from the `xpath` library. The returned\nresult is an array of context nodes for data extraction or undefined if the searched\nnodes are absent.\n\n### Binding\n\n_Binding_ is a conceptual term for a function of type `SingleNodeDataExtractorFn` that\ncombines context node/array _lookup_ and _data extraction_ from the results of such a\nsearch. _Bindings_ are constructed using the built-in builder. The `map()` function\nis used to build _bindings_, returning a `MappingBuilder` interface. In subsequent\nsteps, the type of search (single node / array of nodes), the type of return value,\netc., are chosen (see detailed description below).\n\n### Mapping\n\n_Mapping_ is a conceptual term for a function of type `SingleNodeDataExtractorFn` or\nobject of type `SingleNodeDataExtractorFnFactory`, being assigned to property of\n`ObjectBlueprint` object, and represents a mapping of such property to a value,\nwhich is result of executing data extractor.\n\n### `ObjectBlueprint`\n\n```ts\ntype ObjectBlueprint<T extends object> = {\n  [K in keyof T]:\n    | SingleNodeDataExtractorFnFactory<T[K]>\n    | SingleNodeDataExtractorFn<T[K]>;\n};\n```\n\nAn object whose property names correspond to the names of properties in the constructed\ninterface, and whose property values are either functions of type\n`SingleNodeDataExtractorFn` or objects of the `SingleNodeDataExtractorFnFactory`\ninterface. Typically, these are _bindings_ or objects of the\n`LookupToDataExtractorBindingBuilder` interface, which inherits\n`SingleNodeDataExtractorFnFactory`, created using the `map()` helper.\n\nTo create a mapper, the `createObjectMapper()` helper is used, which takes an\n`ObjectBlueprint` and returns a function – the _mapper_, similar in type to\n`SingleNodeDataExtractorFn`, except that the second parameter is optional and\ndefaults to `xpath.select`.\n\nTo build an object of the required interface, you need to pass a top-level context\nnode (typically, a `Document`, but other nodes are also allowed) to this function.\nIf necessary, a second parameter - the `XPathSelect` interface, can be passed,\nfor example, if the document contains namespaces – using `xpath.useNamespaces()`.\n\nUnder the hood, this _mapper_ function iterates through all the keys of the passed\n`ObjectBlueprint`, calling the `SingleNodeDataExtractorFn` functions lying under these\nkeys with the passed top-level node and `XPathSelect` interface as arguments, and\nwrites the returned values to the properties with the same names in the resulting\nobject. Thus, the output is an object constructed \"according to the blueprint.\"\n\n## Binding Workflow\n\nThe following steps are performed inside [binding](#binding):\n\n1. Lookup is performed to find reference node(s).\n2. 1. If reference node(s) is not found, and node is mandatory, `LookupError` is THROWN.\n   2. If reference node(s) is not found, and node is not mandatory, _default value_\n      is RETURNED.\n3. If reference node(s) was found, data extractor function is called with that node(s).\n4. If _extracted value_ is `undefined`, _default value_ is RETURNED.\n5. 1. If conversion callback is set, _extracted value_ is passed to it to get _converted\n      value_.\n   2. If _converted value_ is `undefined`, _default value_ is RETURNED.\n   3. _Converted value_ is RETURNED.\n6. _Extracted value_ is RETURNED.\n\nIn above steps, _default value_ is undefined, unless it was set using `.withDefault()`\nmethod.\n\n## Building mappings\n\n### Creating object mapper\n\nTo create object mapper function, `createObjectMapper()` helper is used:\n\n```ts\nimport type { XPathSelect } from \"xpath\";\n\ndeclare function createObjectMapper<ObjectType extends object>(\n  blueprint: ObjectBlueprint<ObjectType>\n): (node: Node, xpathSelect?: XPathSelect) => ObjectType;\n```\n\nIt takes single argument of type [ObjectBlueprint](#objectblueprint) and returns\nmapper function. This returned function accepts root context node (typically of\n`Document` type), and optionally `XPathSelect` interface. The latter is useful\nwhen namespaces are used in document, and `xpath` should be configured for namespaces\nsupport:\n\n```ts\nimport { DOMParser } from \"@xmldom/xmldom\";\nimport xpath from \"xpath\";\nimport { createObjectMapper, map } from \"@alxcube/xml-mapper\";\n\nconst xml = `<Link xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:href=\"https://example.com\"/>`;\nconst doc = new DOMParser().parseFromString(xml);\nconst mapper = createObjectMapper({\n  url: map().toNode(\"/Link@xlink:href\").asString(),\n});\nconst select = xpath.useNamespaces({\n  xlink: \"http://www.w3.org/1999/xlink\",\n});\nconst result = mapper(doc, select);\n```\n\n### Defining `ObjectBlueprint`\n\nFor object blueprint definition, `map()` helper is used.\n\nSingle mapping consists of two main steps: definition of reference node(s) lookup\nand definition of return type:\n\n```ts\nconst objectBlueprint = {\n  string: map()\n    .toNode(\"//Element/@id\") // Looks for attribute \"id\" in <Element> node\n    .asString(), // Returns attribute value as string\n  arrayOfNumbers: map()\n    .toNodesArray(\"//List/Item/@ordering\") // Looks for array of attributes \"ordering\"\n    .asArray()\n    .ofNumbers(), // Returns array of attributes values as array of numbers\n};\n```\n\nAfter return type is specified, instance of `LookupToDataExtractorBindingBuilder`\ninterface is returned. This interface extends `SingleNodeDataExtractorFnFactory`,\nso the `objectBlueprint` inferred type in above example will be as follows:\n\n```ts\ntype TypeofExampleObjectBlueprint = {\n  string: SingleNodeDataExtractorFnFactory<string | undefined>;\n  arrayOfNumbers: SingleNodeDataExtractorFnFactory<number[] | undefined>;\n};\n```\n\nand the result object type will be:\n\n```ts\ntype TypeOfExampleResultObject = {\n  string: string | undefined;\n  arrayOfNumbers: number[] | undefined;\n};\n```\n\n### Non-nullable types inference\n\nAs seen above, the default mapping inferred type may be `undefined`. But your mapped\ninterface probably have required members. There are two ways, that remove `undefined`\nfrom inferred types: _mandatory reference node(s) lookup_ and _default value_.\n\n#### Mandatory lookup\n\nYou can use `.mandatory()` method after setting lookup:\n\n```ts\ninterface NonNullableObject {\n  string: string;\n  arrayOfNumbers: number[];\n}\n\nconst blueprint: ObjectBlueprint<NonNullableObject> = {\n  string: map()\n    .toNode(\"/Path/To/Node\")\n    .mandatory() // Mandatory node lookup\n    .asString(),\n  arrayOfNumbers: map()\n    .toNodesArray(\"/Path/To/NodesArray\")\n    .mandatory() // Mandatory array of nodes lookup\n    .asArray()\n    .ofNumbers(),\n};\n```\n\nWhen lookup is made mandatory, an error will be thrown if reference node is not found.\nThis allows to exclude `undefined` from inferred type union, and guarantees that\nmapped value will not be `undefined`. It worth noting that when using custom data\nextraction callback, which has `undefined` in its return type union, the inferred\ntype of mapping will still have `undefined`, unless you set a default value.\n\n#### Default value\n\nThe other way to remove `undefined` from inferred mapping type union is using\n`.withDefault()` method:\n\n```ts\ninterface NonNullableObject {\n  string: string;\n  arrayOfNumbers: number[];\n}\n\nconst blueprint: ObjectBlueprint<NonNullableObject> = {\n  string: map().toNode(\"/Path/To/Node\").asString().withDefault(\"\"),\n  arrayOfNumbers: map()\n    .toNodesArray(\"/Path/To/NodesArray\")\n    .asArray()\n    .ofNumbers()\n    .withDefault([]),\n};\n```\n\nDefault value is returned when reference node(s) is not found, when extracted data is\n`undefined` or when converted data (see below) is `undefined`.\n\n### Converting extracted data\n\nYou can use `.withConversion()` method to convert extracted value to other type. This\nmethod accepts a conversion callback, which should accept data of data extractor return\ntype and return converted data.\n\nDefault value type, set after setting conversion callback, should be of same type as\nconversion callback returns.\n\nIf any default value was set before calling `.withConversion()` method, it is reset to\n`undefined`.\n\n```ts\ntype YesNo = \"yes\" | \"no\";\n\ninterface Example {\n  yesno: YesNo;\n  date?: Date;\n  num: number;\n}\n\nconst blueprint: ObjectBlueprint<Example> = {\n  yesno: map()\n    .toNode(\"/Path/To/Boolean\")\n    .asBoolean()\n    .withDefault(false) // this default value will be reset by .withConversion() call\n    .withConversion((val) => (val ? \"yes\" : \"no\"))\n    .withDefault(\"no\"), // default value should have type, compatible with converted value\n  date: map()\n    .toNode(\"/Path/To/Date/String\")\n    .asString()\n    .withConversion((strVal) => new Date(Date.parse(strVal))),\n  num: map()\n    .toNode(\"/Path/To/Exponential/Number\")\n    .asString()\n    .withConversion(parseFloat)\n    .withDefault(0),\n};\n```\n\n## Available mappings\n\n### Single node mappings\n\n#### asString()\n\nExtracts string value from any node.\n\n```ts\nmap().toNode(\"path\").asString();\n```\n\n---\n\n#### asNumber()\n\nExtracts number value from any node. Supported formats:\n\n- Numbers in decimal format: `0`, `-0`, `1`, `-1.23`;\n- Fractional numbers without an integer part: `.75`, `-.75`;\n- `Infinity` and `-Infinity` strings.\n\nWhen value is not numeric, returns `NaN`.\n\n```ts\nmap().toNode(\"path\").asNumber();\n```\n\n---\n\n#### asBoolean()\n\nExtracts boolean values. The following string values are cast to `false`:\n\n- `\"false\"` in any case;\n- `\"null\"` in any case;\n- empty string;\n- any numeric string, that equals to `0`: `\"0\"`, `\"-0\"`, `\"0.00\"`\n\nAll other non-empty string values are cast to `true`.\n\n```ts\nmap().toNode(\"path\").asBoolean();\n```\n\n---\n\n#### asObject()\n\nAccepts `ObjectBlueprint` as argument. XPath expressions of mappings in such blueprint\nmay be relative to reference (context) node.\n\n```ts\nmap()\n  .toNode(\"path/to/context/node\")\n  .asObject({\n    num: map().toNode(\"@numeric-attribute\").asNumber(),\n    str: map().toNode(\"ChildElement\").asString(),\n  });\n```\n\n#### asRecursiveObject()\n\nAccepts callback, which should return `ObjectBlueprint`. A single argument of type\n`RecursiveObjectFactoryScope`. You can use its `getDepth()` method inside callback\nto get recursion depth. This given argument should be passed to `.asRecursiveObject()`\nmethod in nested mapping definition.\n\n```ts\nconst xml = `\n<Root>\n    <Child>\n        <Title>Level 0</Title>\n        <Child>\n            <Title>Level 1</Title>\n            <Child>\n                <Title>Level 2</Title>\n            </Child>\n        </Child>    \n    </Child>\n</Root>\n`;\n\ninterface TestRecursion {\n  title: string;\n  level: number;\n  child?: TestRecursion;\n}\n\nconst blueprint: ObjectBlueprint<{ recursiveObject: TestRecursion }> = {\n  recursiveObject: map()\n    .toNode(\"/Root/Child\")\n    .mandatory()\n    .asRecursiveObject((recursion) => {\n      return {\n        title: map().toNode(\"Title\").mandatory().asString(),\n        level: map().constant(recursion.getDepth()),\n        child: map().toNode(\"Child\").asRecursiveObject(recursion),\n      };\n    })\n    .createNodeDataExtractor()(doc, xs),\n};\n```\n\n---\n\n#### callback() - Custom data extractor\n\nYou can pass callback to `.callback()` method to extract custom data. Inferred type\nof mapping becomes return type of callback, so when mapping required interface\nproperty, give a default value to mapping if callback may return `undefined`.\n\n```ts\nimport { isElement } from \"xpath\";\n\nmap()\n  .toNode(\"/Path/To/Date\")\n  .mandatory()\n  .callback((node, select) => {\n    if (!isElement(node)) {\n      return undefined;\n    }\n    const year = select(\"number(@year)\", node) as number;\n    const month = select(\"number(@month)\", node) as number;\n    const day = select(\"number(@day)\", node) as number;\n    return new Date(year, month - 1, day);\n  })\n  .withDefault(new Date());\n```\n\n### Array mappings\n\nThere are 2 ways of mapping arrays of nodes: array mapper and custom array callback.\n\nArray mapper internally uses `.map()` method of `Array`, calling\n[SingleNodeDataExtractorFn](#singlenodedataextractorfn) for each node in lookup result.\nThe result array is then filtered to eliminate all `undefined` values.\nFor mapping array, `.asArray()` method should be called after setting lookup.\n\n#### asArray().ofStrings()\n\nExtracts array of strings from array of nodes.\n\n```ts\nmap().toNodesArray(\"/Path/To/Nodes\").asArray().ofStrings();\n```\n\n---\n\n#### asArray().ofNumbers()\n\nExtracts array of numbers from array of nodes.\n\n```ts\nmap().toNodesArray(\"/Path/To/Nodes\").asArray().ofNumbers();\n```\n\n---\n\n#### asArray().ofBooleans()\n\nExtracts array of boolean values from array of nodes.\n\n```ts\nmap().toNodesArray(\"/Path/To/Nodes\").asArray().ofBooleans();\n```\n\n---\n\n#### asArray().ofObjects()\n\nAccepts `ObjectBlueprint` as argument and extracts array of objects of given shape.\n\n```ts\ninterface User {\n  id: number;\n  name: string;\n}\n\nmap()\n  .toNodesArray(\"/Path/To/Node\")\n  .asArray()\n  .ofObjects<User>({\n    id: map().toNode(\"@id\").mandatory().asNumber(),\n    name: map().toNode(\"Name\").mandatory().asString(),\n  });\n```\n\n---\n\n#### asArray().ofRecursiveObjects()\n\nAccepts callback, that should return `ObjectBlueprint`. Same rules as in single node\n`.asRecursiveObject()` mapping applied.\n\n```ts\nimport { DOMParser } from \"@xmldom/xmldom\";\n\nconst xml = `\n<Categories>\n    <Category id=\"1\">\n        <Name>Category 1</Name>\n    </Category>\n    <Category id=\"2\">\n        <Name>Category 2</Name>\n        <Subcategories>\n            <Category id=\"3\">\n                <Name>Category 3</Name>\n                <Subcategories>\n                    <Category id=\"4\">\n                        <Name>Category 4</Name>\n                    </Category>\n                    <Category id=\"5\">\n                        <Name>Category 5</Name>\n                    </Category>\n                </Subcategories>\n            </Category>\n        </Subcategories>\n    </Category>\n    <Category id=\"6\">\n        <Name>Category 6</Name>\n    </Category>\n</Categories>      \n`;\n\ninterface Category {\n  id: number;\n  name: string;\n  level: number;\n  subcategories?: Category[];\n}\n\nconst doc = new DOMParser.parseFromString(xml);\n\nconst mapper = createObjectMapper<{ categories: Category[] }>({\n  categories: map()\n    .toNodesArray(\"/Categories/Category\")\n    .asArray()\n    .ofRecursiveObjects((recursion) => ({\n      id: map().toNode(\"@id\").mandatory().asNumber(),\n      name: map().toNode(\"Name\").mandatory().asString(),\n      level: map().constant(recursion.getDepth()), // Use constant recursion depth\n      subcategories: map()\n        .toNodesArray(\"Subcategories/Category\")\n        .asArray()\n        .ofRecursiveObjects(recursion), // Close recursion\n    })),\n});\n\nconsole.log(mapper(doc));\n```\n\n---\n\n#### asArray().usingMapper()\n\nAccepts [SingleNodeDataExtractorFn](#singlenodedataextractorfn) callback and uses it\nto map nodes array.\n\n```ts\nimport xpath, { type, XPathSelect } from \"xpath\";\nimport { DOMParser } from \"@xmldom/xmldom\";\nimport { map } from \"@alxcube/xml-mapper\";\n\nconst xml = `\n<Dates>\n    <Date y=\"2024\" m=\"2\" d=\"25\" />\n    <Date y=\"2024\" m=\"2\" d=\"26\" />\n</Dates>\n`;\n\nconst doc = new DOMParser.parseFromString(xml);\n\nfunction getDateFromAttributes(node: Node, xpathSelect: XPathSelect): Date {\n  return new Date(\n    xpathSelect(\"number(@y)\", node) as number,\n    (xpathSelect(\"number(@m)\", node) as number) - 1,\n    xpathSelect(\"number(@d)\", node) as number\n  );\n}\n\nconst mapper = map()\n  .toNodesArray(\"/Dates/Date\")\n  .asArray()\n  .usingMapper(getDateFromAttributes)\n  .createNodeDataExtractor(); // Calling factory method explicitly to get SingleNodeDataExtractorFn\n\nconsole.log(mapper(doc, xpath.select));\n```\n\n---\n\n---\n\nAnother way of mapping array of nodes is custom callback, in which you can do whatever\nyou want with nodes.\n\n```ts\nimport { DOMParser } from \"@xmldom/xmldom\";\nimport xpath from \"xpath\";\nimport { type NodesArrayDataExtractorFn, map } from \"@alxcube/xml-mapper\";\n\nconst xml = `\n<Numbers>\n    <Number>1</Number>\n    <Number>2</Number>\n    <Number>3</Number>\n    <Number>4</Number> \n</Numbers>\n`;\nconst doc = new DOMParser().parseFromString(xml);\n\nconst sumExtractor: NodesArrayDataExtractorFn<number> = (nodes, xpathSelect) =>\n  nodes.reduce(\n    (sum, node) => sum + (xpathSelect(\"number(.)\", node) as number),\n    0\n  );\n\nconst mapper = map()\n  .toNodesArray(\"/Numbers/Number\")\n  .callback(sumExtractor)\n  .createNodeDataExtractor(); // Calling factory method explicitly to get SingleNodeDataExtractorFn\n\nconsole.log(mapper(doc, xpath.select)); // 10\n```\n\n## Debugging\n\nMappers, created `createObjectMapper()` helper throws special kind of errors -\n`MappingError`. This error objects are verbose and have failed mapping path in its\n`message` text. Additionally, there is `mappingPath` property, of type `(string | number)[]`,\nwhich is mapping path segments array, and `cause` property, which contains initial\nerror object.\n","readmeFilename":"README.md"}