├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── docs
├── .nojekyll
├── 404.html
├── CNAME
├── _next
│ ├── data
│ │ └── 7of8QfVFcgerx6lX1Jut2
│ │ │ ├── async.json
│ │ │ ├── changelog.json
│ │ │ ├── getting-started.json
│ │ │ ├── index.json
│ │ │ ├── inheritance.json
│ │ │ ├── options.json
│ │ │ ├── pojo.json
│ │ │ └── projections.json
│ └── static
│ │ ├── 7of8QfVFcgerx6lX1Jut2
│ │ ├── _buildManifest.js
│ │ └── _ssgManifest.js
│ │ ├── chunks
│ │ ├── commons.cb82874e43d6dd0d0fae.js
│ │ ├── fd73fa5fb9f73f18227a47c23a9002adcbd74f44.626d6ae8e6ddf7156f39.js
│ │ ├── framework.9ec1f7868b3e9d138cdd.js
│ │ ├── main-ceab71ff4af1e6f937e6.js
│ │ ├── pages
│ │ │ ├── [slug]-70f9bba8d6d55adb0882.js
│ │ │ ├── _app-6e5ef8a34b8b345de256.js
│ │ │ ├── _error-cb301e4e9e10851ffa77.js
│ │ │ └── index-9a6041a3546e6cfb3411.js
│ │ ├── polyfills-11c8eba6a84e3fddec04.js
│ │ └── webpack-e067438c4cf4ef2ef178.js
│ │ └── css
│ │ ├── 50d98ba4398f391c4b76.css
│ │ └── adaccf006f5d2e27b0ac.css
├── assets
│ └── icons
│ │ ├── external-link.svg
│ │ ├── favicon.ico
│ │ ├── link.svg
│ │ └── scroll-down.svg
├── async.html
├── changelog.html
├── getting-started.html
├── index.html
├── inheritance.html
├── options.html
├── pojo.html
└── projections.html
├── lib
├── src
│ ├── decorator_factory.ts
│ ├── frontend_functions.ts
│ ├── index.ts
│ ├── json
│ │ ├── index.ts
│ │ └── json_type.ts
│ ├── metadata
│ │ ├── index.ts
│ │ ├── metadata_container.ts
│ │ └── metadata_manager.ts
│ ├── object_property_serializer.ts
│ ├── options.ts
│ ├── property_serializer.ts
│ ├── type_serializer.ts
│ ├── type_serializer_picker.ts
│ └── types
│ │ ├── constructor.ts
│ │ ├── index.ts
│ │ ├── provider.ts
│ │ ├── serializable.ts
│ │ └── util.ts
└── tsconfig.json
├── package-lock.json
├── package.json
├── samples
├── node_modules
│ └── serialazy
├── src
│ ├── async.ts
│ ├── getting-started.ts
│ ├── inheritance.ts
│ ├── options.ts
│ ├── pojo.ts
│ └── projections.ts
└── tsconfig.json
├── spec
├── node_modules
│ └── serialazy
├── src
│ ├── circular_deps_and_refs.spec.ts
│ ├── class_inheritance.spec.ts
│ ├── custom_prop_serializer.spec.ts
│ ├── custom_type_serializer.spec.ts
│ ├── decorator_options.spec.ts
│ ├── default_type_serializer.spec.ts
│ ├── frontend_functions.spec.ts
│ ├── mock
│ │ ├── circular_child.ts
│ │ └── circular_parent.ts
│ ├── pojo.spec.ts
│ ├── projections.spec.ts
│ ├── promise_resolving.spec.ts
│ └── run.ts
└── tsconfig.json
├── tsconfig.base.json
├── tslint.json
└── website
├── .babelrc
├── .gitignore
├── components
├── active-link.module.scss
├── active-link.tsx
├── contents.module.scss
├── contents.tsx
├── cover.module.scss
├── cover.tsx
├── external-link.tsx
├── footer.tsx
├── layout.tsx
└── meta.tsx
├── content
├── async.page.md
├── changelog.page.md
├── getting-started.page.md
├── index.ts
├── inheritance.page.md
├── options.page.md
├── pojo.page.md
└── projections.page.md
├── next-env.d.ts
├── out
├── package-info.ts
├── package-lock.json
├── package.json
├── pages
├── [slug].tsx
├── _app.tsx
├── index.tsx
└── markdown.module.scss
├── postcss.config.js
├── public
├── .nojekyll
├── CNAME
└── assets
│ └── icons
│ ├── external-link.svg
│ ├── favicon.ico
│ ├── link.svg
│ └── scroll-down.svg
├── shiki.d.ts
├── styles
└── index.scss
├── tailwind.config.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /lib/dist
3 | /samples/bin
4 | /spec/bin
5 |
6 | # IDEs
7 | .idea/
8 | .vscode/
9 |
10 | # OS Specific
11 | .DS_Store
12 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /docs
2 | /lib/src
3 | /node_modules
4 | /samples
5 | /spec
6 | /website
7 | **/tsconfig.json
8 | **/tslint.json
9 | tsconfig.base.json
10 |
11 | # IDEs
12 | .idea/
13 | .vscode/
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Changelog
3 | ---
4 |
5 | # Changelog
6 |
7 | v3.0.0
8 | ------
9 |
10 | * **[BREAKING]** Removed `@Serialize.Type()` and `@Serialize.Custom()` decorators.
11 | Now all type and property decoration is done by `@Serialize()`.
12 | * **[BREAKING]** `@Serialize()` accepts custom type serializer (`up` & `down`) and options as a single argument.
13 | * **[BREAKING]** Removed `TypeSerializer.discriminate()` (redundant, was never used)
14 | * Add `Serializable`, an abstract base class for serializables
15 | * Async serialization / deserialization (`deflate.resolve` and `inflate.resolve`)
16 | * Serializatio / deserialization to/from a POJO (`as` and `toPojo` options)
17 | * Add projections (`projection` and `fallbackToDefaultProjection` options)
18 | * Add optional `as` parameter to DeflateOptions. It allows to override a type of serializable
19 | (serialize as a different type)
20 | * Add options for `inflate` / `deflate`, pass them to `up` / `down` functions as second argument
21 | * Both `up` and `down` functions for custom type serializer are now optional
22 |
23 | v2.0.2
24 | ------
25 |
26 | * Refine class inheritance logic: Inheriting from property-bag serializable makes child class
27 | a property-bag serializable. Inheriting from serializable with custom type serializer doesn't
28 | make child class serializable. Fixes [#11](https://github.com/teq/serialazy/issues/11).
29 |
30 | v2.0.1
31 | ------
32 |
33 | * Update `PropertyBagMetadata.getTypeSerializer()`: `up` & `down` arguments are checked for being null/undefined
34 | before applying property serializers. Fixes [#6](https://github.com/teq/serialazy/issues/6).
35 |
36 | v2.0.0
37 | ------
38 |
39 | * **[BREAKING]** Removed `isSerializable`, `deepMerge` functions and `@Serialize.Skip()` decorator.
40 | * Add `@Serialize.Type()` decorator which allows to define custom serializers for types
41 | * `deflate` / `inflate` can accept primitives (string, number, boolean and their "boxed" variants, null, undefined)
42 |
43 | v1.3.1
44 | ------
45 |
46 | * Add `assertSerializable` functions which throws an error if target is not serializable class instance
47 | or serializable class constructor function.
48 | * Previously to be _serializable_ class should have serializers on its own properties (i.e. should have own metadata)
49 | with no respect to its ancestors. Now class is _serializable_ if it either has own serializers or any of its ancestors have serializers.
50 | * Using global symbol to access serializable's metadata.
51 | This fixes a bug when project dependencies introduce multiple instances of library
52 | and metadata defined in one version can't be accessed in another.
53 |
54 | v1.3.0
55 | ------
56 |
57 | * Add `deepMerge` function which performs a deep (recursive) property merge from serializable-like source object to serializable destination object
58 | * Add `isSerializable` function which allows to check if target is a serializable class instance or serializable class constructor function
59 | * Add class name to serialization / deserialization error message
60 |
61 | v1.2.3
62 | ------
63 |
64 | * `Serialize.Custom` decorator now accepts either serializer or serializer provider function
65 |
66 | v1.2.2
67 | ------
68 |
69 | * Fix a bug with circular module dependencies
70 |
71 | v1.2.1
72 | ------
73 |
74 | * Export JSON types
75 |
76 | v1.2.0
77 | ------
78 |
79 | * Child class inherits serializers from parent
80 | * Add support for `options` in custom serializers
81 | * Add `name` option which allows to map property to a different name
82 |
83 | v1.0.1
84 | ------
85 |
86 | Initial version features:
87 | * Default serializers for primitive types (string, number, boolean)
88 | * Support for optional / nullable properties
89 | * Recursive object tree serialization (circular references not handled yet)
90 | * Custom property serialization functions
91 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at vorteq@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Andrey Tselischev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Serialazy** is a serialization / data-mapping library
2 | which can be used to deflate / inflate TypeScript class instances as well as plain JS objects (POJO).
3 |
4 | See [Documentation](https://serialazy.teqlab.net) for more info.
5 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teq/serialazy/0427acf7d6634696fff098c2e36a6c5a3a326a0a/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
1 |
If one or more type serializer or deserializer (down or up functions) return a Promise value,\nit is possible to await until they are resolved with deflate.resolve and inflate.resolve.
\n
Unlike deflate and inflate, these functions return a Promise to serialized / deserialized value.
\n
Following example serializes User to its id and deserializes using async getUserFieldsById function.
[BREAKING] Removed @Serialize.Type() and @Serialize.Custom() decorators.\nNow all type and property decoration is done by @Serialize().
\n
[BREAKING]@Serialize() accepts custom type serializer (up & down) and options as a single argument.
\n
[BREAKING] Removed TypeSerializer.discriminate() (redundant, was never used)
\n
Add Serializable, an abstract base class for serializables
\n
Async serialization / deserialization (deflate.resolve and inflate.resolve)
\n
Serializatio / deserialization to/from a POJO (as and toPojo options)
\n
Add projections (projection and fallbackToDefaultProjection options)
\n
Add optional as parameter to DeflateOptions. It allows to override a type of serializable\n(serialize as a different type)
\n
Add options for inflate / deflate, pass them to up / down functions as second argument
\n
Both up and down functions for custom type serializer are now optional
\n
\n
v2.0.2
\n
\n
Refine class inheritance logic: Inheriting from property-bag serializable makes child class\na property-bag serializable. Inheriting from serializable with custom type serializer doesn't\nmake child class serializable. Fixes #11.
\n
\n
v2.0.1
\n
\n
Update PropertyBagMetadata.getTypeSerializer(): up & down arguments are checked for being null/undefined\nbefore applying property serializers. Fixes #6.
\n
\n
v2.0.0
\n
\n
[BREAKING] Removed isSerializable, deepMerge functions and @Serialize.Skip() decorator.
\n
Add @Serialize.Type() decorator which allows to define custom serializers for types
\n
deflate / inflate can accept primitives (string, number, boolean and their \"boxed\" variants, null, undefined)
\n
\n
v1.3.1
\n
\n
Add assertSerializable functions which throws an error if target is not serializable class instance\nor serializable class constructor function.
\n
Previously to be serializable class should have serializers on its own properties (i.e. should have own metadata)\nwith no respect to its ancestors. Now class is serializable if it either has own serializers or any of its ancestors have serializers.
\n
Using global symbol to access serializable's metadata.\nThis fixes a bug when project dependencies introduce multiple instances of library\nand metadata defined in one version can't be accessed in another.
\n
\n
v1.3.0
\n
\n
Add deepMerge function which performs a deep (recursive) property merge from serializable-like source object to serializable destination object
\n
Add isSerializable function which allows to check if target is a serializable class instance or serializable class constructor function
\n
Add class name to serialization / deserialization error message
\n
\n
v1.2.3
\n
\n
Serialize.Custom decorator now accepts either serializer or serializer provider function
\n
\n
v1.2.2
\n
\n
Fix a bug with circular module dependencies
\n
\n
v1.2.1
\n
\n
Export JSON types
\n
\n
v1.2.0
\n
\n
Child class inherits serializers from parent
\n
Add support for options in custom serializers
\n
Add name option which allows to map property to a different name
\n
\n
v1.0.1
\n
Initial version features:
\n
\n
Default serializers for primitive types (string, number, boolean)
\n
Support for optional / nullable properties
\n
Recursive object tree serialization (circular references not handled yet)
Property bag serializables inherit all property serializers from parent (property bag) class
\n
import{deflate,inflate,Serialize}from'serialazy';\n\nabstractclassShape{\n@Serialize() public x:number;\n@Serialize() public y:number;\n}\n\nclassRectangleextendsShape{\n@Serialize({name:'w'}) public width:number;\n@Serialize({name:'h'}) public height:number;\n}\n\nconstrect=Object.assign(newRectangle(),{\nx:1,y:2,width:5,height:3\n});\n\nconstserialized=deflate(rect);\n// serialized includes all props from Rectangle + Shape\nexpect(serialized).to.deep.equal({\nx:1,y:2,w:5,h:3\n});\n\nconstdeserialized=inflate(Rectangle,serialized);\n// deserialized includes all props from Rectangle + Shape\nexpect(deserialized).to.deep.equal(rect);\n
\n
\n
Note: Child class can shadow parent's property serializers.
\n
"},"__N_SSG":true}
--------------------------------------------------------------------------------
/docs/_next/static/7of8QfVFcgerx6lX1Jut2/_buildManifest.js:
--------------------------------------------------------------------------------
1 | self.__BUILD_MANIFEST = (function(a,b){return {__rewrites:[],"/":[a,b,"static\u002Fchunks\u002Fpages\u002Findex-9a6041a3546e6cfb3411.js"],"/_error":["static\u002Fchunks\u002Fpages\u002F_error-cb301e4e9e10851ffa77.js"],"/[slug]":[a,b,"static\u002Fchunks\u002Fpages\u002F[slug]-70f9bba8d6d55adb0882.js"],sortedPages:["\u002F","\u002F_app","\u002F_error","\u002F[slug]"]}}("static\u002Fchunks\u002Ffd73fa5fb9f73f18227a47c23a9002adcbd74f44.626d6ae8e6ddf7156f39.js","static\u002Fcss\u002Fadaccf006f5d2e27b0ac.css"));self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
--------------------------------------------------------------------------------
/docs/_next/static/7of8QfVFcgerx6lX1Jut2/_ssgManifest.js:
--------------------------------------------------------------------------------
1 | self.__SSG_MANIFEST=new Set(["\u002F","\u002F[slug]"]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
--------------------------------------------------------------------------------
/docs/_next/static/chunks/pages/[slug]-70f9bba8d6d55adb0882.js:
--------------------------------------------------------------------------------
1 | _N_E=(window.webpackJsonp_N_E=window.webpackJsonp_N_E||[]).push([[5],{WD4f:function(_,n,w){(window.__NEXT_P=window.__NEXT_P||[]).push(["/[slug]",function(){return w("eL0V")}])}},[["WD4f",0,2,1,3]]]);
--------------------------------------------------------------------------------
/docs/_next/static/chunks/pages/_app-6e5ef8a34b8b345de256.js:
--------------------------------------------------------------------------------
1 | _N_E=(window.webpackJsonp_N_E=window.webpackJsonp_N_E||[]).push([[6],{0:function(n,t,o){o("74v/"),n.exports=o("nOHt")},"74v/":function(n,t,o){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return o("cha2")}])},Gpft:function(n,t,o){},cha2:function(n,t,o){"use strict";o.r(t),o.d(t,"default",(function(){return c}));var e=o("q1tI"),p=o.n(e),u=(o("Gpft"),p.a.createElement);function c(n){var t=n.Component,o=n.pageProps;return u(t,o)}}},[[0,0,2,1]]]);
--------------------------------------------------------------------------------
/docs/_next/static/chunks/pages/_error-cb301e4e9e10851ffa77.js:
--------------------------------------------------------------------------------
1 | _N_E=(window.webpackJsonp_N_E=window.webpackJsonp_N_E||[]).push([[7],{"/0+H":function(e,t,n){"use strict";t.__esModule=!0,t.isInAmpMode=i,t.useAmp=function(){return i(a.default.useContext(o.AmpStateContext))};var r,a=(r=n("q1tI"))&&r.__esModule?r:{default:r},o=n("lwAK");function i(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.ampFirst,n=void 0!==t&&t,r=e.hybrid,a=void 0!==r&&r,o=e.hasQuery,i=void 0!==o&&o;return n||a&&i}},"/a9y":function(e,t,n){"use strict";var r=n("lwsE"),a=n("W8MJ"),o=n("7W2i"),i=n("a1gu"),u=n("Nsbk");function c(e){var t=function(){if("undefined"===typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"===typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=u(e);if(t){var a=u(this).constructor;n=Reflect.construct(r,arguments,a)}else n=r.apply(this,arguments);return i(this,n)}}var l=n("TqRt");t.__esModule=!0,t.default=void 0;var s=l(n("q1tI")),d=l(n("8Kt/")),f={400:"Bad Request",404:"This page could not be found",405:"Method Not Allowed",500:"Internal Server Error"};function p(e){var t=e.res,n=e.err;return{statusCode:t&&t.statusCode?t.statusCode:n?n.statusCode:404}}var h=function(e){o(n,e);var t=c(n);function n(){return r(this,n),t.apply(this,arguments)}return a(n,[{key:"render",value:function(){var e=this.props.statusCode,t=this.props.title||f[e]||"An unexpected error has occurred";return s.default.createElement("div",{style:y.error},s.default.createElement(d.default,null,s.default.createElement("title",null,e,": ",t)),s.default.createElement("div",null,s.default.createElement("style",{dangerouslySetInnerHTML:{__html:"body { margin: 0 }"}}),e?s.default.createElement("h1",{style:y.h1},e):null,s.default.createElement("div",{style:y.desc},s.default.createElement("h2",{style:y.h2},t,"."))))}}]),n}(s.default.Component);t.default=h,h.displayName="ErrorPage",h.getInitialProps=p,h.origGetInitialProps=p;var y={error:{color:"#000",background:"#fff",fontFamily:'-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},desc:{display:"inline-block",textAlign:"left",lineHeight:"49px",height:"49px",verticalAlign:"middle"},h1:{display:"inline-block",borderRight:"1px solid rgba(0, 0, 0,.3)",margin:0,marginRight:"20px",padding:"10px 23px 10px 0",fontSize:"24px",fontWeight:500,verticalAlign:"top"},h2:{fontSize:"14px",fontWeight:"normal",lineHeight:"inherit",margin:0,padding:0}}},"04ac":function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return n("/a9y")}])},"8Kt/":function(e,t,n){"use strict";n("lSNA");t.__esModule=!0,t.defaultHead=s,t.default=void 0;var r,a=function(e){if(e&&e.__esModule)return e;if(null===e||"object"!==typeof e&&"function"!==typeof e)return{default:e};var t=l();if(t&&t.has(e))return t.get(e);var n={},r=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if(Object.prototype.hasOwnProperty.call(e,a)){var o=r?Object.getOwnPropertyDescriptor(e,a):null;o&&(o.get||o.set)?Object.defineProperty(n,a,o):n[a]=e[a]}n.default=e,t&&t.set(e,n);return n}(n("q1tI")),o=(r=n("Xuae"))&&r.__esModule?r:{default:r},i=n("lwAK"),u=n("FYa8"),c=n("/0+H");function l(){if("function"!==typeof WeakMap)return null;var e=new WeakMap;return l=function(){return e},e}function s(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t=[a.default.createElement("meta",{charSet:"utf-8"})];return e||t.push(a.default.createElement("meta",{name:"viewport",content:"width=device-width"})),t}function d(e,t){return"string"===typeof t||"number"===typeof t?e:t.type===a.default.Fragment?e.concat(a.default.Children.toArray(t.props.children).reduce((function(e,t){return"string"===typeof t||"number"===typeof t?e:e.concat(t)}),[])):e.concat(t)}var f=["name","httpEquiv","charSet","itemProp"];function p(e,t){return e.reduce((function(e,t){var n=a.default.Children.toArray(t.props.children);return e.concat(n)}),[]).reduce(d,[]).reverse().concat(s(t.inAmpMode)).filter(function(){var e=new Set,t=new Set,n=new Set,r={};return function(a){var o=!0;if(a.key&&"number"!==typeof a.key&&a.key.indexOf("$")>0){var i=a.key.slice(a.key.indexOf("$")+1);e.has(i)?o=!1:e.add(i)}switch(a.type){case"title":case"base":t.has(a.type)?o=!1:t.add(a.type);break;case"meta":for(var u=0,c=f.length;u
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/assets/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teq/serialazy/0427acf7d6634696fff098c2e36a6c5a3a326a0a/docs/assets/icons/favicon.ico
--------------------------------------------------------------------------------
/docs/assets/icons/link.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/docs/assets/icons/scroll-down.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lib/src/decorator_factory.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_PROJECTION, MetadataManager } from "./metadata";
2 | import ObjectPropertySerializer from "./object_property_serializer";
3 | import { DecoratorOptions } from "./options";
4 | import { Constructor, isConstructor } from "./types/constructor";
5 | import TypeSerializer from "./type_serializer";
6 |
7 | /** Constructs type/property decorators */
8 | export default function DecoratorFactory(
9 | backend: string,
10 | options?: DecoratorOptions
11 | ) {
12 |
13 | let { projection } = options || {};
14 | projection = projection || DEFAULT_PROJECTION;
15 |
16 | function decorateProperty(proto: Object, propertyName: string, options: DecoratorOptions) {
17 |
18 | const propertySerializer = ObjectPropertySerializer(backend).create(proto, propertyName, options);
19 |
20 | MetadataManager.get(backend, projection)
21 | .getOrCreateMetaFor(proto)
22 | .addOwnPropertySerializer(propertySerializer);
23 |
24 | }
25 |
26 | function decorateType(ctor: Constructor, options: DecoratorOptions) {
27 |
28 | const customTypeSerializerProvider = () => {
29 | return { type: ctor, ...options } as TypeSerializer;
30 | };
31 |
32 | const proto = ctor.prototype;
33 |
34 | MetadataManager.get(backend, projection)
35 | .getOrCreateMetaFor(proto)
36 | .setOwnTypeSerializer(customTypeSerializerProvider);
37 |
38 | }
39 |
40 | return (protoOrCtor: Object | Constructor, propertyName?: string) => {
41 | if (isConstructor(protoOrCtor)) {
42 | decorateType(protoOrCtor, options);
43 | } else if (typeof protoOrCtor === 'object' && typeof propertyName === 'string') {
44 | decorateProperty(protoOrCtor, propertyName, options);
45 | } else {
46 | throw new Error('Unable to decorate: Target is not a property, nor a constructor');
47 | }
48 | };
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/lib/src/frontend_functions.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_PROJECTION } from "./metadata";
2 | import { DeflateOptions, InflateOptions } from './options';
3 | import { Constructor, isConstructor } from './types/constructor';
4 | import TypeSerializerPicker from './type_serializer_picker';
5 |
6 | export default function FrontendFunctions(backend: string) {
7 |
8 | /** Serialize given value */
9 | function deflate(
10 | serializable: TOriginal,
11 | options?: DeflateOptions
12 | ): TSerialized | Promise {
13 |
14 | let { as: ctor, projection } = options || {};
15 | projection = projection || DEFAULT_PROJECTION;
16 |
17 | if (serializable === null || serializable === undefined) {
18 |
19 | return serializable as null | undefined;
20 |
21 | } else {
22 |
23 | const picker = TypeSerializerPicker(backend, options);
24 | const { down, type } = isConstructor(ctor) ? picker.pickForType(ctor) : picker.pickForValue(serializable);
25 |
26 | if (!down) {
27 | throw new Error(
28 | `Unable to serialize an instance of "${type.name}" in projection: "${projection}". ` +
29 | 'Its serializer is not defined or doesn\'t have a "down" method'
30 | );
31 | }
32 |
33 | return down(serializable, options);
34 | }
35 |
36 | }
37 |
38 | /** Construct/deserialize given value */
39 | function inflate(
40 | ctor: Constructor,
41 | serialized: TSerialized,
42 | options?: InflateOptions
43 | ): TOriginal | Promise {
44 |
45 | let { projection } = options || {};
46 | projection = projection || DEFAULT_PROJECTION;
47 |
48 | if (!isConstructor(ctor)) {
49 | throw new Error('Expecting a constructor function');
50 | }
51 |
52 | const picker = TypeSerializerPicker(backend, options);
53 | const { up, type } = picker.pickForType(ctor);
54 |
55 | if (!up) {
56 | throw new Error(
57 | `Unable to deserialize an instance of "${type.name}" in projection: "${projection}". ` +
58 | 'Its serializer is not defined or doesn\'t have an "up" method'
59 | );
60 | }
61 |
62 | return up(serialized, options);
63 |
64 | }
65 |
66 | return {
67 | deflate,
68 | inflate
69 | };
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/lib/src/index.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 |
3 | export * from './json';
4 | export { Serializable } from './types';
5 |
--------------------------------------------------------------------------------
/lib/src/json/index.ts:
--------------------------------------------------------------------------------
1 | import DecoratorFactory from '../decorator_factory';
2 | import FrontendFunctions from '../frontend_functions';
3 | import { DEFAULT_PROJECTION, MetadataManager } from '../metadata';
4 | import { DecoratorOptions, DeflateOptions, InflateOptions } from '../options';
5 | import Constructor from "../types/constructor";
6 | import Util from '../types/util';
7 | import JsonType from "./json_type";
8 |
9 | const BACKEND_NAME = 'json';
10 |
11 | /**
12 | * Define serializer for given property or type
13 | * @param options Custom type serializer and/or other options
14 | * @returns Type/property decorator
15 | */
16 | export function Serialize(
17 | options?: DecoratorOptions
18 | ): (protoOrCtor: Object | Constructor, propertyName?: string) => void {
19 | return DecoratorFactory(BACKEND_NAME, options);
20 | }
21 |
22 | /**
23 | * Serialize given instance to a JSON-compatible value
24 | * @param serializable Serializable type instance
25 | * @param options Deflate options
26 | * @returns JSON-compatible value which can be safely passed to `JSON.serialize`
27 | */
28 | export function deflate(
29 | serializable: TOriginal,
30 | options?: DeflateOptions
31 | ): JsonType {
32 | const serializedValue = FrontendFunctions(BACKEND_NAME).deflate(serializable, options);
33 | if (Util.isPromise(serializedValue)) {
34 | throw new Error('Async-serializable type should be serialized with "deflate.resolve"');
35 | }
36 | return serializedValue as JsonType;
37 | }
38 |
39 | export namespace deflate {
40 |
41 | /**
42 | * Asynchronously serialize given instance to a JSON-compatible value.
43 | * Resulting promise resolves when all nested type/property serializers are resolved
44 | * @param serializable Async-serializable type instance
45 | * @param options Deflate options
46 | * @returns A promise which resolves to JSON-compatible type
47 | */
48 | export async function resolve(
49 | serializable: TOriginal,
50 | options?: DeflateOptions
51 | ): Promise {
52 | const serializedValue = FrontendFunctions(BACKEND_NAME).deflate(serializable, options);
53 | return Promise.resolve(serializedValue);
54 | }
55 |
56 | }
57 |
58 | /**
59 | * Construct/deserialize an instance from a JSON-compatible object
60 | * @param ctor Serializable type constructor function
61 | * @param serialized JSON-compatible object (e.g. returned from `JSON.parse`)
62 | * @param options Inflate options
63 | * @returns Serializable type instance
64 | */
65 | export function inflate(
66 | ctor: Constructor,
67 | serialized: JsonType,
68 | options?: InflateOptions
69 | ): TOriginal {
70 | const originalValue = FrontendFunctions(BACKEND_NAME).inflate(ctor, serialized, options);
71 | if (Util.isPromise(originalValue)) {
72 | throw new Error('Async-serializable type should be deserialized with "inflate.resolve"');
73 | }
74 | return originalValue as TOriginal;
75 | }
76 |
77 | export namespace inflate {
78 |
79 | /**
80 | * Asynchronously construct/deserialize an instance from a JSON-compatible object.
81 | * Resulting promise resolves when all nested type/property serializers are resolved
82 | * @param ctor Async-serializable type constructor function
83 | * @param serialized JSON-compatible object (e.g. returned from `JSON.parse`)
84 | * @param options Inflate options
85 | * @returns A promise which resolves to serializable type instance
86 | */
87 | export async function resolve(
88 | ctor: Constructor,
89 | serialized: JsonType,
90 | options?: InflateOptions
91 | ): Promise {
92 | const originalValue = FrontendFunctions(BACKEND_NAME).inflate(ctor, serialized, options);
93 | return Promise.resolve(originalValue);
94 | }
95 |
96 | }
97 |
98 | // Types
99 | export * from './json_type';
100 |
101 | // Define serializers for built-in types in default projection
102 |
103 | const metaManager = MetadataManager.get(BACKEND_NAME, DEFAULT_PROJECTION);
104 |
105 | if (!metaManager.getMetaFor(Boolean.prototype)) {
106 | Serialize({
107 | down: (original: any) => Util.expectBooleanOrNil(original),
108 | up: (serialized: any) => Util.expectBooleanOrNil(serialized)
109 | })(Boolean);
110 | }
111 |
112 | if (!metaManager.getMetaFor(Number.prototype)) {
113 | Serialize({
114 | down: (original: any) => Util.expectNumberOrNil(original),
115 | up: (serialized: any) => Util.expectNumberOrNil(serialized)
116 | })(Number);
117 | }
118 |
119 | if (!metaManager.getMetaFor(String.prototype)) {
120 | Serialize({
121 | down: (original: any) => Util.expectStringOrNil(original),
122 | up: (serialized: any) => Util.expectStringOrNil(serialized)
123 | })(String);
124 | }
125 |
--------------------------------------------------------------------------------
/lib/src/json/json_type.ts:
--------------------------------------------------------------------------------
1 |
2 | /** JSON primitive type */
3 | export type JsonPrimitive = string | number | boolean | null;
4 |
5 | /** JSON array */
6 | export interface JsonArray extends Array {}
7 |
8 | /** JSON object */
9 | export interface JsonObject { [prop: string]: JsonType; }
10 |
11 | /** JSON-compatible type */
12 | export type JsonType = JsonPrimitive | JsonArray | JsonObject;
13 |
14 | export default JsonType;
15 |
--------------------------------------------------------------------------------
/lib/src/metadata/index.ts:
--------------------------------------------------------------------------------
1 | export { default as MetadataContainer } from './metadata_container';
2 | export { default as MetadataManager } from './metadata_manager';
3 | export const DEFAULT_PROJECTION = 'default';
4 |
--------------------------------------------------------------------------------
/lib/src/metadata/metadata_container.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_PROJECTION } from '.';
2 | import { DeflateOrInflateOptions, InflateOptions } from '../options';
3 | import PropertySerializer from '../property_serializer';
4 | import Constructor from '../types/constructor';
5 | import Provider from '../types/provider';
6 | import Util from '../types/util';
7 | import TypeSerializer from '../type_serializer';
8 | import MetadataManager from './metadata_manager';
9 |
10 | /** Metadata container for serializables */
11 | export default class MetadataContainer {
12 |
13 | /**
14 | * Metadata version number.
15 | *
16 | * It is possible to mix multiple serialazy versions which came from different dependencies.
17 | * To make sure that we access a compatible version of metadata (or throw an error instead)
18 | * we use a metadata version number.
19 | *
20 | * It's not directly linked with package (NPM) version, but:
21 | * * Several consecutive major package versions can share the same metadata version.
22 | * (If there are chages in public API, but not in metadata format)
23 | * * Metadata version increase is a **breaking change**, so the major part
24 | * of package versions should be increased as well
25 | */
26 | public static VERSION = 3;
27 |
28 | /** Metadata instance version number */
29 | public readonly version = MetadataContainer.VERSION;
30 |
31 | /** Type constructor */
32 | public readonly ctor: Constructor;
33 |
34 | /** Type name */
35 | public readonly name: string;
36 |
37 | private typeSerializerProvider: Provider>;
38 | private propertySerializers = new Map>();
39 |
40 | public constructor(
41 | public readonly backend: string,
42 | public readonly projection: string,
43 | public readonly proto: Object,
44 | ) {
45 | this.ctor = proto.constructor as Constructor;
46 | this.name = this.ctor.name;
47 | }
48 |
49 | public setOwnTypeSerializer(
50 | typeSerializer: TypeSerializer | Provider>
51 | ) {
52 |
53 | if (this.typeSerializerProvider) {
54 | throw new Error(`Unable to re-define custom type serializer for "${this.name}"`);
55 | }
56 |
57 | this.typeSerializerProvider = typeof(typeSerializer) === 'function' ? typeSerializer : () => typeSerializer;
58 |
59 | }
60 |
61 | public getOwnTypeSerializer() {
62 | return this.typeSerializerProvider?.();
63 | }
64 |
65 | public addOwnPropertySerializer(propSerializer: PropertySerializer) {
66 |
67 | if (this.propertySerializers.has(propSerializer.propertyName)) {
68 | throw new Error(`Unable to redefine serializer for "${propSerializer.propertyName}" property of "${this.name}"`);
69 | }
70 |
71 | const conflict = Array.from(this.propertySerializers.values()).find(ps => ps.propertyTag === propSerializer.propertyTag);
72 | if (conflict) {
73 | throw new Error(
74 | `Unable to define serializer for "${propSerializer.propertyName}" property of "${this.name}": ` +
75 | `"${conflict.propertyTag}" tag already used by "${conflict.propertyName}" property`
76 | );
77 | }
78 |
79 | this.propertySerializers.set(propSerializer.propertyName, propSerializer);
80 |
81 | }
82 |
83 | public getOwnPropertySerializers() {
84 | return this.propertySerializers as ReadonlyMap>;
85 | }
86 |
87 | /** Check if it has own or inherited property serializers */
88 | public hasPropertySerializers() {
89 | return !!this.aggregatePropertySerializers({ fallbackToDefaultProjection: true });
90 | }
91 |
92 | /**
93 | * Get own type serializer or build type serializer
94 | * based on own and inherited property serializers
95 | */
96 | public getTypeSerializer(options?: DeflateOrInflateOptions): TypeSerializer {
97 |
98 | let { prioritizePropSerializers = false } = options || {};
99 |
100 | const typeSerializer = prioritizePropSerializers ?
101 | (this.buildPropertyBagSerializer(options) ?? this.getOwnTypeSerializer())
102 | : (this.getOwnTypeSerializer() ?? this.buildPropertyBagSerializer(options));
103 |
104 | return typeSerializer ?? { type: this.ctor };
105 |
106 | }
107 |
108 | /** Build type serializer based on own and inherited property serializers */
109 | private buildPropertyBagSerializer(options?: DeflateOrInflateOptions): TypeSerializer {
110 |
111 | const serializers = this.aggregatePropertySerializers(options);
112 | const { toPojo = false } = options as InflateOptions || {};
113 |
114 | if (serializers.size > 0) {
115 |
116 | return {
117 |
118 | type: this.ctor,
119 |
120 | down: (serializable: any) => {
121 |
122 | if (serializable === null || serializable === undefined) {
123 | return serializable;
124 | }
125 |
126 | const serialized = {};
127 |
128 | try {
129 | const results = Array.from(serializers.values()).map(serializer => serializer.down(serializable, serialized, options));
130 | if (results.some(result => Util.isPromise(result))) {
131 | return (() => Promise.all(results).then(() => serialized))();
132 | } else {
133 | return serialized;
134 | }
135 | } catch (error) {
136 | throw new Error(`Unable to serialize an instance of "${this.name}" in projection: "${this.projection}": ${error.message}`);
137 | }
138 |
139 | },
140 |
141 | up: (serialized: any) => {
142 |
143 | if (serialized === null || serialized === undefined) {
144 | return serialized;
145 | }
146 |
147 | const serializable: any = toPojo ? {} : new this.ctor();
148 |
149 | try {
150 | const results = Array.from(serializers.values()).map(serializer => serializer.up(serializable, serialized, options));
151 | if (results.some(result => Util.isPromise(result))) {
152 | return (() => Promise.all(results).then(() => serializable))();
153 | } else {
154 | return serializable;
155 | }
156 | } catch (error) {
157 | throw new Error(`Unable to deserialize an instance of "${this.name}" in projection: "${this.projection}": ${error.message}`);
158 | }
159 |
160 | }
161 |
162 | };
163 |
164 | }
165 |
166 | }
167 |
168 | /** Aggregate all property serializers: own and inherited */
169 | private aggregatePropertySerializers(options?: DeflateOrInflateOptions) {
170 |
171 | let { fallbackToDefaultProjection = true } = options || {};
172 |
173 | let serializers = new Map(this.propertySerializers); // clone
174 |
175 | const defaultMeta = MetadataManager.get(this.backend, DEFAULT_PROJECTION).getOwnMetaFor(this.proto);
176 |
177 | if (fallbackToDefaultProjection && defaultMeta) {
178 | serializers = new Map([
179 | ...defaultMeta.propertySerializers,
180 | ...serializers
181 | ]);
182 | }
183 |
184 | const inheritedMeta = MetadataManager.get(this.backend, this.projection).seekInheritedMetaFor(this.proto);
185 |
186 | if (inheritedMeta) {
187 | serializers = new Map([
188 | ...inheritedMeta.aggregatePropertySerializers(options),
189 | ...serializers
190 | ]);
191 | }
192 |
193 | return serializers as ReadonlyMap>;
194 |
195 | }
196 |
197 | }
198 |
--------------------------------------------------------------------------------
/lib/src/metadata/metadata_manager.ts:
--------------------------------------------------------------------------------
1 | import MetadataContainer from "./metadata_container";
2 |
3 | /** Used to access serializable type metadata */
4 | export default class MetadataManager {
5 |
6 | /** NOTE: Constructable via `get` factory method */
7 | private constructor(
8 | private readonly backend: string,
9 | private readonly projection: string
10 | ) {}
11 |
12 | private static key(backend: string, projection: string) {
13 | // There may be multiple "serialazy" instances in project from different dependencies.
14 | // We use global symbol to make sure that all of them can access the same metadata.
15 | return Symbol.for(`com.github.teq.serialazy.metadata.${backend}.${projection}`);
16 | }
17 |
18 | /** Cache for manager instances */
19 | private static instances = new Map();
20 |
21 | /**
22 | * Get metadata manager instance
23 | * @param backend Serialization backend
24 | * @param projection Serialization projection
25 | */
26 | public static get(backend: string, projection: string) {
27 |
28 | const key = MetadataManager.key(backend, projection);
29 |
30 | let instance = this.instances.get(key);
31 |
32 | if (!instance) {
33 | instance = new this(backend, projection);
34 | this.instances.set(key, instance);
35 | }
36 |
37 | return instance;
38 |
39 | }
40 |
41 | /** Set own metadata */
42 | public setOwnMetaFor(proto: Object, metadata: MetadataContainer): void {
43 | const key = MetadataManager.key(this.backend, this.projection);
44 | Reflect.defineMetadata(key, metadata, proto);
45 | }
46 |
47 | /** Get own metadata */
48 | public getOwnMetaFor(proto: Object): MetadataContainer {
49 |
50 | if (proto === null || proto === undefined) {
51 | throw new Error('Expecting prototype object to be not null/undefined');
52 | }
53 |
54 | const key = MetadataManager.key(this.backend, this.projection);
55 | const metadata: MetadataContainer = Reflect.getOwnMetadata(key, proto);
56 |
57 | if (metadata) {
58 | const version = metadata.version || 0;
59 | if (version !== MetadataContainer.VERSION) {
60 | throw new Error(
61 | `Metadata version mismatch (lib: ${MetadataContainer.VERSION}, meta: ${version}). ` +
62 | 'Seems like you\'re trying to use 2 or more incompatible versions of "serialazy"'
63 | );
64 | }
65 | }
66 |
67 | return metadata;
68 |
69 | }
70 |
71 | /** Get own or inherited metadata */
72 | public getMetaFor(proto: Object): MetadataContainer {
73 |
74 | let metadata = this.getOwnMetaFor(proto);
75 |
76 | if (!metadata) {
77 | const inheritedMetadata = this.seekInheritedMetaFor(proto);
78 | if (inheritedMetadata?.hasPropertySerializers()) {
79 | // No own metadata, but it inherits from a property-bag serializable.
80 | // Return a virtual (not persisted) metadata.
81 | metadata = new MetadataContainer(this.backend, this.projection, proto);
82 | }
83 | }
84 |
85 | return metadata;
86 |
87 | }
88 |
89 | /** Seek prototype chain for inherited metadata */
90 | public seekInheritedMetaFor(proto: Object): MetadataContainer {
91 |
92 | let metadata: MetadataContainer;
93 |
94 | while (proto && !metadata) {
95 | proto = Object.getPrototypeOf(proto);
96 | metadata = proto && this.getOwnMetaFor(proto);
97 | }
98 |
99 | return metadata;
100 |
101 | }
102 |
103 | /** Get or create a metadata container */
104 | public getOrCreateMetaFor(proto: Object): MetadataContainer {
105 |
106 | let ownMeta = this.getOwnMetaFor(proto);
107 |
108 | if (!ownMeta) {
109 | ownMeta = new MetadataContainer(this.backend, this.projection, proto);
110 | this.setOwnMetaFor(proto, ownMeta);
111 | }
112 |
113 | return ownMeta;
114 |
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/lib/src/object_property_serializer.ts:
--------------------------------------------------------------------------------
1 | import { DeflateOptions, DeflateOrInflateOptions, InflateOptions } from "./options";
2 | import PropertySerializer from "./property_serializer";
3 | import Util from './types/util';
4 | import TypeSerializer from "./type_serializer";
5 | import TypeSerializerPicker from "./type_serializer_picker";
6 |
7 | export interface PropertyBag {
8 | [prop: string]: T;
9 | }
10 |
11 | /** Returns a factory for object property serializers */
12 | function ObjectPropertySerializer(backend: string) {
13 |
14 | return { create };
15 |
16 | /** Returns a property serializer for serializables which serialize to object-like (property bag) structures */
17 | function create(
18 | proto: Object,
19 | propertyName: string,
20 | options?: TypeSerializer & ObjectPropertySerializer.Options
21 | ): PropertySerializer, any, string> {
22 |
23 | const customTypeSerializer = options as TypeSerializer;
24 | let { name: propertyTag, optional, nullable } = options || {};
25 | propertyTag = propertyTag || propertyName;
26 |
27 | function getTypeSerializer(options?: DeflateOrInflateOptions) {
28 |
29 | try {
30 | const picker = TypeSerializerPicker(backend, options);
31 | const defaultTypeSerializer = picker.pickForProp(proto, propertyName);
32 | return TypeSerializer.compile([defaultTypeSerializer, customTypeSerializer]);
33 | } catch (error) {
34 | const className = proto.constructor.name;
35 | throw new Error(`Unable to construct a type serializer for "${propertyName}" property of "${className}": ${error.message}`);
36 | }
37 |
38 | }
39 |
40 | function down(
41 | serializable: TOriginal,
42 | serialized: PropertyBag,
43 | options?: DeflateOptions
44 | ): void | Promise {
45 |
46 | const putProperty = (value: TSerialized) => {
47 | if (value !== undefined) {
48 | serialized[propertyTag] = value;
49 | }
50 | };
51 |
52 | try {
53 | const originalValue = (serializable as any)[propertyName];
54 | const serializedValue = getTypeSerializer(options).down(validate(originalValue), options);
55 | if (Util.isPromise(serializedValue)) {
56 | return (async () => putProperty(await serializedValue))();
57 | } else {
58 | putProperty(serializedValue as TSerialized);
59 | }
60 | } catch (error) {
61 | const message = formatError(`Unable to serialize property "${propertyName}"`);
62 | throw new Error(`${message}: ${error.message}`);
63 | }
64 |
65 | }
66 |
67 | function up(
68 | serializable: TOriginal,
69 | serialized: PropertyBag,
70 | options?: InflateOptions
71 | ): void | Promise {
72 |
73 | const putProperty = (value: TOriginal) => {
74 | value = validate(value);
75 | if (value !== undefined) {
76 | (serializable as any)[propertyName] = value;
77 | }
78 | };
79 |
80 | try {
81 | const serializedValue = serialized[propertyTag];
82 | const originalValue = getTypeSerializer(options).up(serializedValue, options);
83 | if (Util.isPromise(originalValue)) {
84 | return (async () => putProperty(await originalValue))();
85 | } else {
86 | putProperty(originalValue as TOriginal);
87 | }
88 | } catch (error) {
89 | const message = formatError(`Unable to deserialize property "${propertyName}"`);
90 | throw new Error(`${message}: ${error.message}`);
91 | }
92 |
93 | }
94 |
95 | function formatError(message: string) {
96 | return propertyName !== propertyTag ? `${message} (mapped to "${propertyTag}")` : message;
97 | }
98 |
99 | function validate(value: T): T {
100 |
101 | if (!optional && value === undefined) {
102 | const hint = (typeof(optional) !== 'boolean') ? 'Hint: make it optional' : null;
103 | throw new Error(`Value is undefined${ hint ? `; ${hint}` : ''}`);
104 | }
105 |
106 | if (!nullable && value === null) {
107 | const hint = (typeof(nullable) !== 'boolean') ? 'Hint: make it nullable' : null;
108 | throw new Error(`Value is null${ hint ? `; ${hint}` : ''}`);
109 | }
110 |
111 | return value;
112 |
113 | }
114 |
115 | return {
116 | propertyName,
117 | propertyTag,
118 | down,
119 | up
120 | };
121 |
122 | }
123 |
124 | }
125 |
126 | namespace ObjectPropertySerializer {
127 |
128 | /** Object property serializer options */
129 | export interface Options {
130 |
131 | /**
132 | * Indicates if property can be undefined
133 | * @remarks Applicable to properties
134 | * @defaultValue `false`
135 | */
136 | optional?: boolean;
137 |
138 | /**
139 | * Indicates if property can be null
140 | * @remarks Applicable to properties
141 | * @defaultValue `false`
142 | */
143 | nullable?: boolean;
144 |
145 | /**
146 | * When defined it forces to use different property name in serialized object
147 | * @remarks Applicable to properties
148 | */
149 | name?: string;
150 |
151 | }
152 |
153 | }
154 |
155 | export default ObjectPropertySerializer;
156 |
--------------------------------------------------------------------------------
/lib/src/options.ts:
--------------------------------------------------------------------------------
1 | import ObjectPropertySerializer from './object_property_serializer';
2 | import Constructor from "./types/constructor";
3 | import TypeSerializer from "./type_serializer";
4 |
5 | export interface SerializationOptions {
6 |
7 | /**
8 | * Projection name
9 | * @defaultValue `"default"`
10 | */
11 | projection?: string;
12 |
13 | /**
14 | * If type or property is not serializable in given projection
15 | * it tries to serialize/deserialize it in default projection
16 | * @defaultValue `true`
17 | */
18 | fallbackToDefaultProjection?: boolean;
19 |
20 | /**
21 | * Controls which serializer takes precedence:
22 | * - One which comes from a type (class decorator). It is the **default** behavior.
23 | * - Or one which built upon property serializers (own and inherited).
24 | * @defaultValue `false`
25 | */
26 | prioritizePropSerializers?: boolean;
27 |
28 | }
29 |
30 | export type DecoratorOptions =
31 | TypeSerializer &
32 | ObjectPropertySerializer.Options &
33 | Pick;
34 |
35 | export interface DeflateOptions extends SerializationOptions {
36 |
37 | /** Serialize instance as if it were of the given type */
38 | as?: Constructor;
39 |
40 | }
41 |
42 | export interface InflateOptions extends SerializationOptions {
43 |
44 | /**
45 | * Deserialize to plain javascript object (POJO)
46 | * @defaultValue `false`
47 | */
48 | toPojo?: boolean;
49 |
50 | }
51 |
52 | export type DeflateOrInflateOptions =
53 | DeflateOptions |
54 | InflateOptions;
55 |
--------------------------------------------------------------------------------
/lib/src/property_serializer.ts:
--------------------------------------------------------------------------------
1 | import { DeflateOptions, InflateOptions } from "./options";
2 |
3 | /** Reporesents a generic property serializer */
4 | interface PropertySerializer {
5 |
6 | /** Identifies property in `serializable` */
7 | readonly propertyName: string;
8 |
9 | /** Identifies property in `serialized` */
10 | readonly propertyTag: TTag;
11 |
12 | /** Serializes target property from `serializable` and writes value to `serialized` */
13 | down(
14 | serializable: TOriginal,
15 | serialized: TSerialized,
16 | options?: DeflateOptions
17 | ): void | Promise;
18 |
19 | /** Deserializes target property from `serialized` and writes value to `serializable` */
20 | up(
21 | serializable: TOriginal,
22 | serialized: TSerialized,
23 | options?: InflateOptions
24 | ): void | Promise;
25 |
26 | }
27 |
28 | export default PropertySerializer;
29 |
--------------------------------------------------------------------------------
/lib/src/type_serializer.ts:
--------------------------------------------------------------------------------
1 | import { DeflateOptions, InflateOptions } from './options';
2 | import Constructor from './types/constructor';
3 |
4 | /** Represents a generic type serializer */
5 | interface TypeSerializer {
6 |
7 | /**
8 | * Serializer function
9 | * @param original Original value
10 | * @param options Options passed to `deflate` function
11 | * @returns Serialized value
12 | */
13 | down?(
14 | original: TOriginal,
15 | options: DeflateOptions
16 | ): TSerialized | Promise;
17 |
18 | /**
19 | * Deserializer function
20 | * @param serialized Serialized value
21 | * @param options Options passed to `inflate` function
22 | * @returns Original value
23 | */
24 | up?(
25 | serialized: TSerialized,
26 | options: InflateOptions
27 | ): TOriginal | Promise;
28 |
29 | /**
30 | * Type of serializable
31 | * @defaultValue
32 | * * For types: Type constructor function
33 | * * For properties: Value of `design:type` metadata for given property
34 | */
35 | type?: Constructor;
36 |
37 | }
38 |
39 | namespace TypeSerializer {
40 |
41 | const hints = (
42 | 'Hints: Use a serializable type or provide a custom serializer. ' +
43 | 'Property type should be specified explicitly, details: https://github.com/Microsoft/TypeScript/issues/18995.'
44 | );
45 |
46 | /** Compile type serializer partials to a final type serializer */
47 | export function compile(
48 | partials: Array>
49 | ): TypeSerializer {
50 |
51 | const { down, up, type } = Object.assign({
52 | down: () => { throw new Error(`Serializer function ("down") for type "${typeName}" is not defined. ` + hints); },
53 | up: () => { throw new Error(`Deserializer function ("up") for type "${typeName}" is not defined. ` + hints); },
54 | type: undefined
55 | }, ...partials) as TypeSerializer;
56 |
57 | const typeName = type?.name || '';
58 |
59 | return { down, up, type };
60 |
61 | }
62 |
63 | }
64 |
65 | export default TypeSerializer;
66 |
--------------------------------------------------------------------------------
/lib/src/type_serializer_picker.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_PROJECTION, MetadataManager } from './metadata';
2 | import { DeflateOrInflateOptions } from './options';
3 | import { Constructor, isConstructor } from './types/constructor';
4 | import TypeSerializer from './type_serializer';
5 |
6 | /** Returns a helper which picks a type serializer for given value or type */
7 | export default function TypeSerializerPicker(
8 | backend: string,
9 | options?: DeflateOrInflateOptions
10 | ) {
11 |
12 | let { projection, fallbackToDefaultProjection = true } = options || {};
13 | projection = projection || DEFAULT_PROJECTION;
14 |
15 | /** Try to pick a (possibly partial) type serializer for given type */
16 | function pickForType(ctor: Constructor): TypeSerializer {
17 |
18 | if (!isConstructor(ctor)) {
19 | throw new Error('Expecting constructor function');
20 | }
21 |
22 | const proto = ctor.prototype as Object;
23 |
24 | let meta = MetadataManager.get(backend, projection).getMetaFor(proto);
25 |
26 | if (!meta && projection !== DEFAULT_PROJECTION && (fallbackToDefaultProjection || isBuiltInType(ctor))) {
27 | meta = MetadataManager.get(backend, DEFAULT_PROJECTION).getMetaFor(proto);
28 | }
29 |
30 | const typeSerializer = meta?.getTypeSerializer(options);
31 |
32 | return typeSerializer || { type: ctor };
33 |
34 | }
35 |
36 | /** Try to pick a (possibly partial) type serializer for given property */
37 | function pickForProp(proto: Object, propertyName: string): TypeSerializer {
38 |
39 | const ctor: Constructor = Reflect.getMetadata('design:type', proto, propertyName);
40 |
41 | if (ctor === undefined) {
42 | throw new Error('Unable to fetch type information. Hint: Enable TS options: "emitDecoratorMetadata" and "experimentalDecorators"');
43 | }
44 |
45 | return pickForType(ctor);
46 |
47 | }
48 |
49 | /** Try to pick a (possibly partial) type serializer for given value */
50 | function pickForValue(value: unknown): TypeSerializer {
51 |
52 | if (value === null || value === undefined) {
53 | throw new Error('Expecting value to be not null/undefined');
54 | }
55 |
56 | return pickForType(value.constructor as Constructor);
57 |
58 | }
59 |
60 | return {
61 | pickForType,
62 | pickForProp,
63 | pickForValue
64 | };
65 |
66 | }
67 |
68 | function isBuiltInType(ctor: Constructor) {
69 | return ([Boolean, Number, String] as Array>).includes(ctor);
70 | }
71 |
--------------------------------------------------------------------------------
/lib/src/types/constructor.ts:
--------------------------------------------------------------------------------
1 |
2 | /** Represents constructor function which require no arguments */
3 | export type Constructor = new () => T;
4 |
5 | /** Check if target is constructor function */
6 | export function isConstructor(target: unknown): target is Constructor {
7 | return typeof target === 'function' && target.prototype && target.prototype.constructor.name;
8 | }
9 |
10 | export default Constructor;
11 |
--------------------------------------------------------------------------------
/lib/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Constructor } from './constructor';
2 | export { default as Provider } from './provider';
3 | export { default as Serializable } from './serializable';
4 | export { default as Util } from './util';
5 |
--------------------------------------------------------------------------------
/lib/src/types/provider.ts:
--------------------------------------------------------------------------------
1 |
2 | /** Represents provider function */
3 | type Provider = (...args: any[]) => T;
4 |
5 | export default Provider;
6 |
--------------------------------------------------------------------------------
/lib/src/types/serializable.ts:
--------------------------------------------------------------------------------
1 | import { Constructor } from './constructor';
2 |
3 | /** Generic serializable */
4 | export default abstract class Serializable {
5 |
6 | /**
7 | * Create new instance
8 | * @param fields Instance fields
9 | */
10 | public static create(
11 | this: Constructor,
12 | fields?: T
13 | ): T {
14 | return Object.assign(new this(), fields);
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/lib/src/types/util.ts:
--------------------------------------------------------------------------------
1 |
2 | namespace Util {
3 |
4 | export function expectBooleanOrNil(maybeBoolean: any): boolean {
5 | if (typeof(maybeBoolean) === 'boolean') {
6 | return maybeBoolean;
7 | } else if (maybeBoolean instanceof Boolean) {
8 | return maybeBoolean.valueOf();
9 | } else if (maybeBoolean === null || maybeBoolean === undefined) {
10 | return maybeBoolean;
11 | } else {
12 | throw new Error(`Not a boolean (typeof: "${typeof(maybeBoolean)}", value: "${maybeBoolean}")`);
13 | }
14 | }
15 |
16 | export function expectNumberOrNil(maybeNumber: any): number {
17 | if (typeof(maybeNumber) === 'number') {
18 | return maybeNumber;
19 | } else if (maybeNumber instanceof Number) {
20 | return maybeNumber.valueOf();
21 | } else if (maybeNumber === null || maybeNumber === undefined) {
22 | return maybeNumber;
23 | } else {
24 | throw new Error(`Not a number (typeof: "${typeof(maybeNumber)}", value: "${maybeNumber}")`);
25 | }
26 | }
27 |
28 | export function expectStringOrNil(maybeString: any): string {
29 | if (typeof(maybeString) === 'string') {
30 | return maybeString;
31 | } else if (maybeString instanceof String) {
32 | return maybeString.valueOf();
33 | } else if (maybeString === null || maybeString === undefined) {
34 | return maybeString;
35 | } else {
36 | throw new Error(`Not a string (typeof: "${typeof(maybeString)}", value: "${maybeString}")`);
37 | }
38 | }
39 |
40 | /** Check if target is a promise */
41 | export function isPromise(target: unknown): target is Promise {
42 | return Promise.resolve(target) === target;
43 | }
44 |
45 | }
46 |
47 | export default Util;
48 |
--------------------------------------------------------------------------------
/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 |
5 | /* Basic Options */
6 | "declaration": true, /* Generates corresponding '.d.ts' file. */
7 | "sourceMap": false, /* Generates corresponding '.map' file. */
8 | "outDir": "dist", /* Redirect output structure to the directory. */
9 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
10 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "serialazy",
3 | "version": "3.0.0",
4 | "description": "TypeScript class serialization / data-mapping library",
5 | "keywords": [
6 | "serialize",
7 | "deserialize",
8 | "json",
9 | "mapping",
10 | "typescript"
11 | ],
12 | "homepage": "https://serialazy.teqlab.net",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/teq/serialazy.git"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/teq/serialazy/issues"
19 | },
20 | "main": "./lib/dist/index.js",
21 | "types": "./lib/dist/index.d.ts",
22 | "scripts": {
23 | "lint": "tslint -p ./lib && tslint -p ./spec && tslint -p ./samples && tslint -p ./website",
24 | "clean_lib": "rm -rf ./lib/dist",
25 | "clean_spec": "rm -rf ./spec/bin",
26 | "clean_samples": "rm -rf ./samples/bin",
27 | "clean_all": "npm run clean_lib && npm run clean_spec && npm run clean_samples",
28 | "build_lib": "cd ./lib && tsc",
29 | "build_spec": "cd ./spec && tsc",
30 | "build_samples": "cd ./samples && tsc",
31 | "build_all": "npm run build_lib && npm run build_spec && npm run build_samples",
32 | "test": "npm run build_lib && npm run build_spec && node ./spec/bin/run.js",
33 | "prepublishOnly": "npm run lint && npm run clean_all && npm run build_all && node ./spec/bin/run.js"
34 | },
35 | "author": "Andrey Tselischev ",
36 | "license": "MIT",
37 | "dependencies": {
38 | "reflect-metadata": "^0.1.13"
39 | },
40 | "devDependencies": {
41 | "@types/chai": "^4.0.4",
42 | "@types/chai-as-promised": "^7.1.0",
43 | "@types/mocha": "^5.2.5",
44 | "@types/node": "^12.12.27",
45 | "chai": "^4.1.2",
46 | "chai-as-promised": "^7.1.1",
47 | "mocha": "^7.1.1",
48 | "tslint": "^6.1.0",
49 | "typescript": "^3.7.5"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/samples/node_modules/serialazy:
--------------------------------------------------------------------------------
1 | ../../lib/dist/
--------------------------------------------------------------------------------
/samples/src/async.ts:
--------------------------------------------------------------------------------
1 | import { deflate, inflate, Serialize } from 'serialazy';
2 |
3 | import chai = require('chai');
4 | const { expect } = chai;
5 |
6 | function randDefer(value: T): Promise {
7 | const pause = Math.random() * 100;
8 | return new Promise((resolve) => setTimeout(() => resolve(value), pause));
9 | }
10 |
11 | async function getUserFieldsById(id: string) {
12 | if (id !== '') {
13 | throw new Error(`No such user: ${id}`);
14 | } else {
15 | return randDefer({
16 | id: '',
17 | email: 'john.doe@example.com',
18 | isAdmin: true
19 | });
20 | }
21 | }
22 |
23 | (async () => {
24 |
25 | @Serialize({
26 | down: (user: User) => user.id,
27 | up: async (id: string) => Object.assign(new User(), await getUserFieldsById(id))
28 | })
29 | class User {
30 | public id: string;
31 | public email: string;
32 | public isAdmin: boolean;
33 | }
34 |
35 | const user = Object.assign(new User(), {
36 | id: '',
37 | email: 'john.doe@example.com',
38 | isAdmin: true
39 | });
40 |
41 | const serialized = deflate(user);
42 | expect(serialized).to.equal('');
43 |
44 | const deserialized = await inflate.resolve(User, serialized);
45 | expect(deserialized).to.deep.equal({
46 | id: '',
47 | email: 'john.doe@example.com',
48 | isAdmin: true
49 | });
50 |
51 | })();
52 |
--------------------------------------------------------------------------------
/samples/src/getting-started.ts:
--------------------------------------------------------------------------------
1 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
2 |
3 | import chai = require('chai');
4 | const { expect } = chai;
5 |
6 | (() => { // Without Serializable
7 |
8 | // Position class serializes to a tuple: [number, number]
9 | @Serialize({
10 | down: (pos: Position) => [pos.x, pos.y],
11 | up: (tuple) => Object.assign(new Position(), { x: tuple[0], y: tuple[1] })
12 | })
13 | class Position {
14 | public x: number;
15 | public y: number;
16 | }
17 |
18 | // Shape is a "property bag" serializable
19 | class Shape {
20 | @Serialize() public name: string;
21 | }
22 |
23 | // Sphere inherits property serializers from Shape
24 | class Sphere extends Shape {
25 | @Serialize() public radius: number;
26 | @Serialize() public position: Position; // Position defined below
27 | }
28 |
29 | const sphere = Object.assign(new Sphere(), {
30 | name: 'sphere1',
31 | radius: 10,
32 | position: Object.assign(new Position(), {
33 | x: 3,
34 | y: 5
35 | })
36 | });
37 |
38 | const serialized = deflate(sphere);
39 |
40 | expect(serialized).to.deep.equal({
41 | name: 'sphere1',
42 | radius: 10,
43 | position: [3, 5]
44 | });
45 |
46 | const deserialized = inflate(Sphere, serialized);
47 |
48 | // Deserialized sphere should be identical with the original one
49 | expect(deserialized).to.deep.equal(sphere);
50 |
51 | })();
52 |
53 | (() => { // With Serializable
54 |
55 | // Position class serializes to a tuple: [number, number]
56 | @Serialize({
57 | down: (pos: Position) => [pos.x, pos.y],
58 | up: (tuple) => Object.assign(new Position(), { x: tuple[0], y: tuple[1] })
59 | })
60 | class Position extends Serializable {
61 | public x: number;
62 | public y: number;
63 | }
64 |
65 | // Shape is a "property bag" serializable
66 | class Shape extends Serializable {
67 | @Serialize() public name: string;
68 | }
69 |
70 | // Sphere inherits property serializers from Shape
71 | class Sphere extends Shape {
72 | @Serialize() public radius: number;
73 | @Serialize() public position: Position; // Position defined below
74 | }
75 |
76 | const sphere = Sphere.create({
77 | name: 'sphere1',
78 | radius: 10,
79 | position: Position.create({ x: 3, y: 5 })
80 | });
81 |
82 | const serialized = deflate(sphere);
83 |
84 | expect(serialized).to.deep.equal({
85 | name: 'sphere1',
86 | radius: 10,
87 | position: [3, 5]
88 | });
89 |
90 | const deserialized = inflate(Sphere, serialized);
91 |
92 | // Deserialized sphere should be identical with the original one
93 | expect(deserialized).to.deep.equal(sphere);
94 |
95 | })();
96 |
--------------------------------------------------------------------------------
/samples/src/inheritance.ts:
--------------------------------------------------------------------------------
1 | import { deflate, inflate, Serialize } from 'serialazy';
2 |
3 | import chai = require('chai');
4 | const { expect } = chai;
5 |
6 | abstract class Shape {
7 | @Serialize() public x: number;
8 | @Serialize() public y: number;
9 | }
10 |
11 | class Rectangle extends Shape {
12 | @Serialize({ name: 'w' }) public width: number;
13 | @Serialize({ name: 'h' }) public height: number;
14 | }
15 |
16 | const rect = Object.assign(new Rectangle(), {
17 | x: 1, y: 2, width: 5, height: 3
18 | });
19 |
20 | const serialized = deflate(rect);
21 | // serialized includes all props from Rectangle + Shape
22 | expect(serialized).to.deep.equal({
23 | x: 1, y: 2, w: 5, h: 3
24 | });
25 |
26 | const deserialized = inflate(Rectangle, serialized);
27 | // deserialized includes all props from Rectangle + Shape
28 | expect(deserialized).to.deep.equal(rect);
29 |
--------------------------------------------------------------------------------
/samples/src/options.ts:
--------------------------------------------------------------------------------
1 | import { deflate, inflate, Serialize } from 'serialazy';
2 |
3 | import chai = require('chai');
4 | const { expect } = chai;
5 |
6 | (() => { // "down"
7 |
8 | @Serialize({
9 | down: (coord: Coord, options) => [coord.x, coord.y]
10 | })
11 | class Coord {
12 | public x: number;
13 | public y: number;
14 | }
15 |
16 | })();
17 |
18 | (() => { // "up"
19 |
20 | @Serialize({
21 | up: (tuple: [number, number], { toPojo }) => Object.assign(
22 | toPojo ? {} : new Coord(),
23 | { x: tuple[0], y: tuple[1] }
24 | )
25 | })
26 | class Coord {
27 | public x: number;
28 | public y: number;
29 | }
30 |
31 | })();
32 |
33 | (() => { // "optional"
34 |
35 | class Book {
36 | @Serialize({ optional: true }) public isbn: string;
37 | }
38 |
39 | })();
40 |
41 | (() => { // "nullable"
42 |
43 | class Book {
44 | @Serialize({ nullable: true }) public isbn: string;
45 | }
46 |
47 | })();
48 |
49 | (() => { // "name"
50 |
51 | class Book {
52 | @Serialize({ name: 'summary' }) public description: string;
53 | }
54 |
55 | const book = Object.assign(new Book(), {
56 | description: 'A popular-science book on cosmology'
57 | });
58 |
59 | expect(deflate(book)).to.deep.equal({
60 | // NOTE: "description" mapped to "summary" in serialized object
61 | summary: 'A popular-science book on cosmology'
62 | });
63 |
64 | })();
65 |
66 | (() => { // "prioritizePropSerializers"
67 |
68 | @Serialize({ down: (coord: Coord) => [coord.x, coord.y] })
69 | class Coord {
70 | @Serialize() public x: number;
71 | @Serialize() public y: number;
72 | }
73 |
74 | const coord = Object.assign(new Coord(), { x: 1, y: 2 });
75 |
76 | const obj1 = deflate(coord);
77 | expect(obj1).to.deep.equal([1, 2]);
78 |
79 | const obj2 = deflate(coord, { prioritizePropSerializers: true });
80 | expect(obj2).to.deep.equal({ x: 1, y: 2 });
81 |
82 | })();
83 |
84 | (() => { // "as"
85 |
86 | class Foo {
87 | @Serialize() public id: number;
88 | }
89 |
90 | @Serialize({ down: (bar: Bar) => bar.id })
91 | class Bar {
92 | public id: number;
93 | }
94 |
95 | const foo = Object.assign(new Foo(), { id: 123 });
96 |
97 | expect(deflate(foo)).to.deep.equal({ id: 123 });
98 |
99 | expect(deflate(foo, { as: Bar })).to.equal(123);
100 |
101 | })();
102 |
103 | (() => { // "toPojo"
104 |
105 | class Foo {
106 | @Serialize() public id: number;
107 | }
108 |
109 | const foo1 = inflate(Foo, { id: 123 });
110 | expect(foo1.constructor).to.equal(Foo);
111 |
112 | const foo2 = inflate(Foo, { id: 123 }, { toPojo: true });
113 | expect(foo2.constructor).to.equal(Object);
114 |
115 | })();
116 |
--------------------------------------------------------------------------------
/samples/src/pojo.ts:
--------------------------------------------------------------------------------
1 | import { deflate, inflate, Serialize } from 'serialazy';
2 |
3 | import chai = require('chai');
4 | const { expect } = chai;
5 |
6 | @Serialize({
7 | down: (pos: Position) => [pos.x, pos.y],
8 | up: (tuple: number[], { toPojo }) => Object.assign(
9 | toPojo ? {} : new Position(),
10 | { x: tuple[0], y: tuple[1] }
11 | )
12 | })
13 | class Position {
14 | public x: number;
15 | public y: number;
16 | }
17 |
18 | class Shape {
19 | @Serialize({ name: 'n' }) public name: string;
20 | @Serialize({ name: 'p' }) public position: Position;
21 | }
22 |
23 | const shape: Shape = { // <- plain object, NOT Shape instance
24 | name: 'circle1',
25 | position: { x: 10, y: 20 }
26 | };
27 |
28 | const serialized = deflate(shape, { as: Shape });
29 | expect(serialized).to.deep.equal({
30 | n: 'circle1',
31 | p: [ 10, 20 ]
32 | });
33 |
34 | const deserialized = inflate(Shape, serialized, { toPojo: true });
35 | expect(deserialized.constructor).to.equal(Object); // <- plain object
36 | expect(deserialized).to.deep.equal(shape);
37 |
--------------------------------------------------------------------------------
/samples/src/projections.ts:
--------------------------------------------------------------------------------
1 | import { deflate, Serialize } from 'serialazy';
2 |
3 | import chai = require('chai');
4 | const { expect } = chai;
5 |
6 | (() => {
7 |
8 | class Position {
9 |
10 | @Serialize() // <- defined in "default" projection
11 | @Serialize({ projection: 'alt', name: 'col' })
12 | public x: number;
13 |
14 | @Serialize() // <- defined in "default" projection
15 | @Serialize({ projection: 'alt', name: 'row' })
16 | public y: number;
17 |
18 | }
19 |
20 | const pos = Object.assign(new Position(), { x: 1, y: 2 });
21 |
22 | const obj1 = deflate(pos);
23 | expect(obj1).to.deep.equal({ x: 1, y: 2 });
24 |
25 | const obj2 = deflate(pos, { projection: 'alt' });
26 | expect(obj2).to.deep.equal({ col: 1, row: 2 });
27 |
28 | })();
29 |
30 | (() => {
31 |
32 | @Serialize({ projection: 'api', down: (user: User) => user.id })
33 | class User {
34 | @Serialize() public id: string;
35 | @Serialize() public email: string;
36 | }
37 |
38 | const user = Object.assign(new User(), {
39 | id: "",
40 | email: 'john.doe@example.com',
41 | });
42 |
43 | expect(deflate(user)).to.deep.equal({
44 | id: "",
45 | email: 'john.doe@example.com',
46 | });
47 |
48 | expect(deflate(user, { projection: 'api' })).to.deep.equal("");
49 |
50 | })();
51 |
--------------------------------------------------------------------------------
/samples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 |
5 | /* Basic Options */
6 | "declaration": false, /* Generates corresponding '.d.ts' file. */
7 | "sourceMap": true, /* Generates corresponding '.map' file. */
8 | "outDir": "bin", /* Redirect output structure to the directory. */
9 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
10 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/spec/node_modules/serialazy:
--------------------------------------------------------------------------------
1 | ../../lib/dist/
--------------------------------------------------------------------------------
/spec/src/circular_deps_and_refs.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | const { expect } = chai;
4 |
5 | describe('circular dependencies and references', () => {
6 |
7 | it('can handle circular dependencies between node modules', () => {
8 |
9 | expect(() => require('./mock/circular_parent')).to.not.throw();
10 |
11 | });
12 |
13 | });
14 |
--------------------------------------------------------------------------------
/spec/src/class_inheritance.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | describe('class inheritance', () => {
8 |
9 | @Serialize({
10 | down: (point: Position) => `(${point.x},${point.y})`,
11 | up: (str: string) => {
12 | const match = str.match(/^\((\d+),(\d+)\)$/);
13 | if (!match) { throw new Error(`Invalid point: ${str}`); }
14 | const [, xStr, yStr] = match;
15 | return Position.create({ x: Number.parseInt(xStr), y: Number.parseInt(yStr) });
16 | }
17 | })
18 | class Position extends Serializable {
19 | public x: number;
20 | public y: number;
21 | }
22 |
23 | abstract class Shape extends Serializable {
24 | @Serialize() public position: Position;
25 | }
26 |
27 | class Rectangle extends Shape {
28 | @Serialize() public width: number;
29 | @Serialize() public height: number;
30 | }
31 |
32 |
33 | describe('when descendant define a type serializer', () => {
34 |
35 | it('should override all property serializers: own and inherited', () => {
36 |
37 | @Serialize({
38 | down: (rect: MyRectangle) => {
39 | return `(${rect.position.x},${rect.position.y}),(${rect.width},${rect.height})`;
40 | },
41 | up: (str: string) => {
42 | const match = str.match(/^\((\d+),(\d+)\)\,\((\d+),(\d+)\)$/);
43 | if (!match) { throw new Error(`Invalid rectangle: ${str}`); }
44 | const [, xStr, yStr, widthStr, heightStr] = match;
45 | return MyRectangle.create({
46 | position: Object.assign(new Position(), {
47 | x: Number.parseInt(xStr),
48 | y: Number.parseInt(yStr)
49 | }),
50 | width: Number.parseInt(widthStr),
51 | height: Number.parseInt(heightStr)
52 | });
53 | }
54 | })
55 | class MyRectangle extends Rectangle {}
56 |
57 | const rectangle = MyRectangle.create({
58 | position: Position.create({ x: 23, y: 34 }),
59 | width: 5,
60 | height: 6
61 | });
62 |
63 | const serialized = deflate(rectangle);
64 | expect(serialized).to.deep.equal('(23,34),(5,6)');
65 |
66 | const deserialized = inflate(MyRectangle, serialized);
67 | expect(deserialized).to.deep.equal(rectangle);
68 |
69 | });
70 |
71 | });
72 |
73 | describe('when descendant define property serializers', () => {
74 |
75 | describe('and ancestor(-s) has property serializers', () => {
76 |
77 | it('should fail to redefine own property serializers', () => {
78 |
79 | expect(() => {
80 | // tslint:disable-next-line:no-unused-variable
81 | class Person {
82 | @Serialize() @Serialize() public name: number;
83 | }
84 | }).to.throw('Unable to redefine serializer for "name" property of "Person"');
85 |
86 | });
87 |
88 | it('be able to shadow inherited property serializers', () => {
89 |
90 | class MyRectangle extends Rectangle {
91 |
92 | @Serialize({
93 | name: 'p',
94 | down: (pos: Position) => [pos.x, pos.y],
95 | up: ([x, y]) => Position.create({ x, y })
96 | })
97 | public position: Position;
98 |
99 | @Serialize({ name: 'w' })
100 | public width: number;
101 |
102 | @Serialize({ name: 'h' })
103 | public height: number;
104 |
105 | }
106 |
107 | const rectangle = MyRectangle.create({
108 | position: Position.create({ x: 2, y: 3 }),
109 | width: 5,
110 | height: 6
111 | });
112 |
113 | const serialized = deflate(rectangle);
114 | expect(serialized).to.deep.equal({ p: [2, 3], w: 5, h: 6 });
115 |
116 | const deserialized = inflate(MyRectangle, serialized);
117 | expect(deserialized).to.deep.equal(rectangle);
118 |
119 | });
120 |
121 | it('should produce a serializable inheriting all property serializers', () => {
122 |
123 | const rectangle = Rectangle.create({
124 | position: Position.create({ x: 23, y: 34 }),
125 | width: 5,
126 | height: 6
127 | });
128 |
129 | const serialized = deflate(rectangle);
130 | expect(serialized).to.deep.equal({
131 | position: '(23,34)',
132 | width: 5,
133 | height: 6
134 | });
135 |
136 | const deserialized = inflate(Rectangle, serialized);
137 | expect(deserialized).to.deep.equal(rectangle);
138 |
139 | });
140 |
141 | });
142 |
143 | });
144 |
145 | describe('when descendant doesn\'t define any serializers', () => {
146 |
147 | describe('and ancestor(-s) has property serializers', () => {
148 |
149 | it('should produce a serializable inheriting all property serializers', () => {
150 |
151 | class Circle extends Shape {
152 | public radius: number; // not serializable
153 | }
154 |
155 | const circle = Circle.create({
156 | position: Position.create({ x: 3, y: 4 }),
157 | radius: 10
158 | });
159 |
160 | const serialized = deflate(circle);
161 | expect(serialized).to.deep.equal({ position: '(3,4)' }); // no radius
162 |
163 | const deserialized = inflate(Circle, serialized);
164 | expect(deserialized).to.deep.equal({ position: { x: 3, y: 4 } }); // no radius
165 | expect(deserialized).to.be.instanceOf(Circle);
166 |
167 | });
168 |
169 | });
170 |
171 | describe('and ancestor has type serializers', () => {
172 |
173 | it('should produce a non-serializable', () => {
174 |
175 | class TaggedPoint extends Position {
176 | public tag: string;
177 | }
178 |
179 | const point = TaggedPoint.create({ x: 3, y: 4, tag: 'not serializable' });
180 | expect(() => deflate(point)).to.throw('Unable to serialize an instance of "TaggedPoint"');
181 |
182 | });
183 |
184 | });
185 |
186 | });
187 |
188 | });
189 |
--------------------------------------------------------------------------------
/spec/src/custom_prop_serializer.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | describe('custom property serializer', () => {
8 |
9 | it('overrides default/predefined type serializer of given property', () => {
10 |
11 | class Author extends Serializable {
12 | @Serialize() public name: string;
13 | }
14 |
15 | class Book extends Serializable {
16 | @Serialize() public title: string;
17 |
18 | @Serialize({ // should override default `Author` type serializer
19 | down: (author: Author) => author.name,
20 | up: (name: string) => Author.create({ name })
21 | })
22 | public author: Author;
23 |
24 | @Serialize({ // should override default `boolean` type serializer
25 | down: (val: boolean) => val ? 1 : 0,
26 | up: (val) => val ? true : false
27 | })
28 | public read: boolean;
29 | }
30 |
31 | const book = Book.create({
32 | read: false,
33 | title: 'Doctor Zhivago',
34 | author: Author.create({ name: 'Boris Pasternak' })
35 | });
36 |
37 | const serialized = deflate(book);
38 |
39 | expect(serialized).to.deep.equal({
40 | read: 0,
41 | title: 'Doctor Zhivago',
42 | author: 'Boris Pasternak'
43 | });
44 |
45 | const deserialized = inflate(Book, serialized);
46 |
47 | expect(deserialized).to.deep.equal(book);
48 |
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/spec/src/custom_type_serializer.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | describe('custom type serializer', () => {
8 |
9 | @Serialize({
10 | down: (val: Point) => `(${val.x},${val.y})`,
11 | up: (val) => {
12 | const match = val.match(/^\((\d+),(\d+)\)$/);
13 | if (!match) { throw new Error(`Invalid point: ${val}`); }
14 | const [, xStr, yStr] = match;
15 | return Point.create({ x: Number.parseInt(xStr), y: Number.parseInt(yStr) });
16 | }
17 | })
18 | class Point extends Serializable {
19 | public x: number;
20 | public y: number;
21 | }
22 |
23 | it('is able to serialize a type instance', () => {
24 | const point = Point.create({ x: 2, y: 3 });
25 | const serialized = deflate(point);
26 | expect(serialized).to.equal('(2,3)');
27 | });
28 |
29 | it('is able to deserialize a type instance', () => {
30 | const point = inflate(Point, '(4,5)');
31 | expect(point).to.be.instanceOf(Point);
32 | expect(point).to.deep.equal({ x: 4, y: 5 });
33 | });
34 |
35 | it('can\'t be re-defined', () => {
36 | expect(() => {
37 | Serialize({ down: null, up: null })(Point);
38 | }).to.throw('Unable to re-define custom type serializer for "Point"');
39 | });
40 |
41 | });
42 |
--------------------------------------------------------------------------------
/spec/src/decorator_options.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | describe('decorator options', () => {
8 |
9 | describe('"name" option', () => {
10 |
11 | describe('when option is empty/null/undefined (default)', () => {
12 |
13 | class Person extends Serializable {
14 | @Serialize({ name: undefined }) public name: string;
15 | @Serialize({ name: null }) public age: number;
16 | @Serialize({ name: '' }) public notes : string;
17 | }
18 |
19 | it('doesn\'t affect property name in resulting serialized object', () => {
20 | const person = Person.create({ name: 'Joe', age: 35, notes: 'None' });
21 | const serialized = deflate(person);
22 | expect(serialized).to.deep.equal({ name: 'Joe', age: 35, notes: 'None' });
23 | const deserialized = inflate(Person, serialized);
24 | expect(deserialized).to.deep.equal(person);
25 | });
26 |
27 | });
28 |
29 | describe('when option is a non-empty string', () => {
30 |
31 | class Person extends Serializable {
32 | @Serialize() public name: string;
33 | @Serialize({ name: 'years' }) public age: number;
34 | }
35 |
36 | it('overrides property name in resulting serialized object', () => {
37 | const person = Person.create({ name: 'John', age: 35 });
38 | const serialized = deflate(person);
39 | expect(serialized).to.deep.equal({ name: 'John', years: 35 });
40 | const deserialized = inflate(Person, serialized);
41 | expect(deserialized).to.deep.equal(person);
42 | });
43 |
44 | it('should be impossible to use the same name for multiple properties', () => {
45 | [
46 | () => {
47 | class Person {
48 | @Serialize() public age: number;
49 | @Serialize({ name: 'age' }) public years: number;
50 | }
51 | },
52 | () => {
53 | class Person {
54 | @Serialize({ name: 'age' }) public foo: number;
55 | @Serialize({ name: 'age' }) public bar: number;
56 | }
57 | }
58 | ].forEach(func => expect(func).to.throw('"age" tag already used'));
59 | });
60 |
61 | });
62 |
63 | });
64 |
65 | describe('"nullable" option', () => {
66 |
67 | describe('when value is null and option is false/undefined (default)', () => {
68 |
69 | class Pet extends Serializable {
70 | @Serialize() public name: string;
71 | }
72 |
73 | class Person extends Serializable {
74 | @Serialize() public age: number;
75 | @Serialize({ nullable: false }) public pet: Pet;
76 | }
77 |
78 | it('should fail to serialize', () => {
79 |
80 | const pet = Pet.create({ name: null });
81 | expect(
82 | () => deflate(pet)
83 | ).to.throw('Unable to serialize property "name": Value is null');
84 |
85 | const person = Person.create({ age: 35, pet: null });
86 | expect(
87 | () => deflate(person)
88 | ).to.throw('Unable to serialize property "pet": Value is null');
89 |
90 | });
91 |
92 | it('should fail to deserialize', () => {
93 |
94 | expect(
95 | () => inflate(Pet, { name: null })
96 | ).to.throw('Unable to deserialize property "name": Value is null');
97 |
98 | expect(
99 | () => inflate(Person, { age: 35, pet: null })
100 | ).to.throw('Unable to deserialize property "pet": Value is null');
101 |
102 | });
103 |
104 | });
105 |
106 | describe('when value is null and option is true', () => {
107 |
108 | class Pet extends Serializable {
109 | @Serialize({ nullable: true }) public name: string;
110 | }
111 |
112 | class Person extends Serializable {
113 | @Serialize({ nullable: true }) public age: number;
114 | @Serialize({ nullable: true }) public pet: Pet;
115 | }
116 |
117 | it('serializes to null', () => {
118 |
119 | const pet = Pet.create({ name: null });
120 | expect(
121 | deflate(pet)
122 | ).to.deep.equal({ name: null });
123 |
124 | const person = Person.create({ age: 35, pet: null });
125 | expect(
126 | deflate(person)
127 | ).to.deep.equal({ age: 35, pet: null });
128 |
129 | });
130 |
131 | it('deserializes to null', () => {
132 |
133 | const pet = inflate(Pet, { name: null });
134 | expect(pet).to.be.instanceOf(Pet);
135 | expect(pet).to.deep.equal({ name: null });
136 |
137 | const person = inflate(Person, { age: 35, pet: null });
138 | expect(person).to.be.instanceOf(Person);
139 | expect(person).to.deep.equal({ age: 35, pet: null });
140 |
141 | });
142 |
143 | });
144 |
145 | });
146 |
147 | describe('"optional" option', () => {
148 |
149 | describe('when value is undefined and option is false/undefined (default)', () => {
150 |
151 | class Pet extends Serializable {
152 | @Serialize() public name: string;
153 | }
154 |
155 | class Person extends Serializable {
156 | @Serialize() public age: number;
157 | @Serialize({ optional: false }) public pet: Pet;
158 | }
159 |
160 | it('should fail to serialize', () => {
161 |
162 | const pet = new Pet(); // name is undefined
163 | expect(
164 | () => deflate(pet)
165 | ).to.throw('Unable to serialize property "name": Value is undefined');
166 |
167 | const person = Person.create({ age: 35, pet: undefined });
168 | expect(
169 | () => deflate(person)
170 | ).to.throw('Unable to serialize property "pet": Value is undefined');
171 |
172 | });
173 |
174 | it('should fail to deserialize', () => {
175 |
176 | const petObj = {}; // name is undefined
177 | expect(
178 | () => inflate(Pet, petObj)
179 | ).to.throw('Unable to deserialize property "name": Value is undefined');
180 |
181 | const personObj = { age: 35 }; // pet is undefined
182 | expect(
183 | () => inflate(Person, personObj)
184 | ).to.throw('Unable to deserialize property "pet": Value is undefined');
185 |
186 | });
187 |
188 | });
189 |
190 | describe('when value is undefined and option is true', () => {
191 |
192 | class Pet extends Serializable {
193 | @Serialize({ optional: true }) public name: string;
194 | }
195 |
196 | class Person extends Serializable {
197 | @Serialize({ optional: true }) public age: number;
198 | @Serialize({ optional: true }) public pet: Pet;
199 | }
200 |
201 | it('serializes to undefined', () => {
202 |
203 | const pet = new Pet(); // name is undefined
204 | expect(
205 | deflate(pet)
206 | ).to.deep.equal({}); // name is not serialized
207 |
208 | const person = Person.create({ age: 35, pet: undefined });
209 | expect(
210 | deflate(person)
211 | ).to.deep.equal({ age: 35 }); // pet is not serialized
212 |
213 | });
214 |
215 | it('deserializes to undefined', () => {
216 |
217 | const pet = inflate(Pet, {}); // name is undefined
218 | expect(pet).to.be.instanceOf(Pet);
219 | expect(pet).to.deep.equal({});
220 |
221 | const person = inflate(Person, { age: 35 }); // pet is undefined
222 | expect(person).to.be.instanceOf(Person);
223 | expect(person).to.deep.equal({ age: 35 });
224 |
225 | });
226 |
227 | });
228 |
229 | });
230 |
231 | });
232 |
--------------------------------------------------------------------------------
/spec/src/default_type_serializer.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | describe('default type serializer', () => {
8 |
9 | describe('for boolean properties', () => {
10 |
11 | class Book extends Serializable {
12 | @Serialize() public read: boolean;
13 | }
14 |
15 | describe('when the value is a boolean', () => {
16 |
17 | describe('of primitive type', () => {
18 |
19 | it('serializes to a boolean primitive', () => {
20 | const book = Book.create({ read: true });
21 | const serialized = deflate(book);
22 | expect(serialized).to.deep.equal({ read: true });
23 | });
24 |
25 | it('deserializes to a boolean primitive', () => {
26 | const deserialized = inflate(Book, { read: false });
27 | expect(deserialized instanceof Book).to.equal(true);
28 | expect(deserialized).to.deep.equal({ read: false });
29 | });
30 |
31 | });
32 |
33 | describe('of object type', () => {
34 |
35 | it('serializes to a boolean primitive', () => {
36 | const book = Book.create({ read: new Boolean(true) });
37 | const serialized = deflate(book);
38 | expect(serialized).to.deep.equal({ read: true });
39 | });
40 |
41 | it('deserializes to a boolean primitive', () => {
42 | const deserialized = inflate(Book, { read: new Boolean(false) as boolean });
43 | expect(deserialized instanceof Book).to.equal(true);
44 | expect(deserialized).to.deep.equal({ read: false });
45 | });
46 |
47 | });
48 |
49 | });
50 |
51 | describe('when the value is a non-boolean', () => {
52 |
53 | it('should fail to serialize', () => {
54 | const book = Book.create({ read: new Date() as any });
55 | expect(() => deflate(book)).to.throw('Unable to serialize property "read": Not a boolean');
56 | });
57 |
58 | it('should fail to deserialize', () => {
59 | expect(() => inflate(Book, { read: new Date() as any })).to.throw('Unable to deserialize property "read": Not a boolean');
60 | });
61 |
62 | });
63 |
64 | });
65 |
66 | describe('for number properties', () => {
67 |
68 | class Person extends Serializable {
69 | @Serialize() public age: number;
70 | }
71 |
72 | describe('when the value is a number', () => {
73 |
74 | describe('of primitive type', () => {
75 |
76 | it('serializes to a number primitive', () => {
77 | const person = Person.create({ age: 40 });
78 | const serialized = deflate(person);
79 | expect(serialized).to.deep.equal({ age: 40 });
80 | });
81 |
82 | it('deserializes to a number primitive', () => {
83 | const deserialized = inflate(Person, { age: 45 });
84 | expect(deserialized instanceof Person).to.equal(true);
85 | expect(deserialized).to.deep.equal({ age: 45 });
86 | });
87 |
88 | });
89 |
90 | describe('of object type', () => {
91 |
92 | it('serializes to a number primitive', () => {
93 | const person = Person.create({ age: new Number(40) });
94 | const serialized = deflate(person);
95 | expect(serialized).to.deep.equal({ age: 40 });
96 | });
97 |
98 | it('deserializes to a number primitive', () => {
99 | const deserialized = inflate(Person, { age: new Number(45) as number });
100 | expect(deserialized instanceof Person).to.equal(true);
101 | expect(deserialized).to.deep.equal({ age: 45 });
102 | });
103 |
104 | });
105 |
106 | });
107 |
108 | describe('when the value is a non-number', () => {
109 |
110 | it('should fail to serialize', () => {
111 | const person = Person.create({ age: new Date() as any });
112 | expect(() => deflate(person)).to.throw('Unable to serialize property "age": Not a number');
113 | });
114 |
115 | it('should fail to deserialize', () => {
116 | expect(() => inflate(Person, { age: new Date() as any })).to.throw('Unable to deserialize property "age": Not a number');
117 | });
118 |
119 | });
120 |
121 | });
122 |
123 | describe('for string properties', () => {
124 |
125 | class Greeter extends Serializable {
126 | @Serialize() public message: string;
127 | }
128 |
129 | describe('when the value is a string', () => {
130 |
131 | describe('of primitive type', () => {
132 |
133 | it('serializes to a string literal', () => {
134 | const greeter = Greeter.create({ message: 'hello' });
135 | const serialized = deflate(greeter);
136 | expect(serialized).to.deep.equal({ message: 'hello' });
137 | });
138 |
139 | it('deserializes to a string literal', () => {
140 | const deserialized = inflate(Greeter, { message: 'hi' });
141 | expect(deserialized instanceof Greeter).to.equal(true);
142 | expect(deserialized).to.deep.equal({ message: 'hi' });
143 | });
144 |
145 | });
146 |
147 | describe('of object type', () => {
148 |
149 | it('serializes to a string literal', () => {
150 | const greeter = Greeter.create({ message: new String('hello') });
151 | const serialized = deflate(greeter);
152 | expect(serialized).to.deep.equal({ message: 'hello' });
153 | });
154 |
155 | it('deserializes to a string literal', () => {
156 | const deserialized = inflate(Greeter, { message: new String('hello') as string });
157 | expect(deserialized instanceof Greeter).to.equal(true);
158 | expect(deserialized).to.deep.equal({ message: 'hello' });
159 | });
160 |
161 | });
162 |
163 | });
164 |
165 | describe('when the value is a non-string', () => {
166 |
167 | it('should fail to serialize', () => {
168 | const greeter = Greeter.create({ message: new Date() as any });
169 | expect(() => deflate(greeter)).to.throw('Unable to serialize property "message": Not a string');
170 | });
171 |
172 | it('should fail to deserialize', () => {
173 | expect(() => inflate(Greeter, { message: new Date() as any })).to.throw('Unable to deserialize property "message": Not a string');
174 | });
175 |
176 | });
177 |
178 | });
179 |
180 | describe('for non-primitive properties', () => {
181 |
182 | const bookObj = {
183 | title: 'The Story of the Sealed Room',
184 | author: { name: 'Arthur Conan Doyle' }
185 | };
186 |
187 | describe('when a property is serializable', () => {
188 |
189 | class Author extends Serializable {
190 | @Serialize() public name: string;
191 | }
192 |
193 | class Book extends Serializable {
194 | @Serialize() public title: string;
195 | @Serialize() public author: Author;
196 | }
197 |
198 | const book = Book.create({
199 | title: 'The Story of the Sealed Room',
200 | author: Author.create({ name: 'Arthur Conan Doyle' })
201 | });
202 |
203 | it('serializes to JSON-compatible object', () => {
204 | const serialized = deflate(book);
205 | expect(serialized).to.deep.equal(bookObj);
206 | });
207 |
208 | it('deserializes from JSON-compatible object', () => {
209 | const deserialized = inflate(Book, bookObj);
210 | expect(deserialized instanceof Book).to.equal(true);
211 | expect(deserialized).to.deep.equal(book);
212 | });
213 |
214 | });
215 |
216 | describe('when a property is a non-serializable', () => {
217 |
218 | class Author extends Serializable {
219 | public name: string;
220 | }
221 |
222 | class Book extends Serializable {
223 | @Serialize() public title: string;
224 | @Serialize() public author: Author;
225 | }
226 |
227 | const book = Book.create({
228 | title: 'The Story of the Sealed Room',
229 | author: Author.create({ name: 'Arthur Conan Doyle' })
230 | });
231 |
232 | it('should fail to serialize', () => {
233 | expect(() => deflate(book)).to.throw('Serializer function ("down") for type "Author" is not defined.');
234 | });
235 |
236 | it('should fail to deserialize', () => {
237 | expect(() => inflate(Book, bookObj)).to.throw('Deserializer function ("up") for type "Author" is not defined.');
238 | });
239 |
240 | });
241 |
242 | });
243 |
244 | });
245 |
--------------------------------------------------------------------------------
/spec/src/frontend_functions.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | describe('frontend functions', () => {
8 |
9 | class Foo extends Serializable {
10 | @Serialize()
11 | public id: string;
12 | }
13 |
14 | class Bar extends Serializable {
15 | public id: string;
16 | }
17 |
18 | describe('deflate', () => {
19 |
20 | it('is able to serialize null/undefined', () => {
21 | expect(deflate(null)).to.equal(null);
22 | expect(deflate(undefined)).to.equal(undefined);
23 | });
24 |
25 | it('is able to serialize primitives', () => {
26 |
27 | expect(deflate(true)).to.be.a('boolean');
28 | expect(deflate(new Boolean(true))).to.be.a('boolean');
29 | expect(deflate(true)).to.equal(true);
30 | expect(deflate(new Boolean(true))).to.equal(true);
31 |
32 | expect(deflate(12)).to.be.a('number');
33 | expect(deflate(new Number(12))).to.be.a('number');
34 | expect(deflate(12)).to.equal(12);
35 | expect(deflate(new Number(12))).to.equal(12);
36 |
37 | expect(deflate('hello')).to.be.a('string');
38 | expect(deflate(new String('hello'))).to.be.a('string');
39 | expect(deflate('hello')).to.equal('hello');
40 | expect(deflate(new String('hello'))).to.equal('hello');
41 |
42 | });
43 |
44 | it('is able to serialize non-primitives which are serializable', () => {
45 | const foo = Foo.create({ id: 'foo' });
46 | expect(deflate(foo)).to.deep.equal({ id: 'foo' });
47 | });
48 |
49 | it('should fail to serialize non-primitives which are not serializable', () => {
50 | const bar = Bar.create({ id: 'bar' });
51 | expect(() => deflate(bar)).to.throw('Unable to serialize an instance of "Bar"');
52 | });
53 |
54 | });
55 |
56 | describe('inflate', () => {
57 |
58 | it('is able to deserialize null/undefined', () => {
59 | expect(inflate(Boolean, null)).to.equal(null);
60 | expect(inflate(Boolean, undefined)).to.equal(undefined);
61 | expect(inflate(Number, null)).to.equal(null);
62 | expect(inflate(Number, undefined)).to.equal(undefined);
63 | expect(inflate(String, null)).to.equal(null);
64 | expect(inflate(String, undefined)).to.equal(undefined);
65 | expect(inflate(Foo, null)).to.equal(null);
66 | expect(inflate(Foo, undefined)).to.equal(undefined);
67 | });
68 |
69 | it('is able to deserialize primitives', () => {
70 |
71 | expect(inflate(Boolean, true)).to.be.a('boolean');
72 | expect(inflate(Boolean, new Boolean(true) as boolean)).to.be.a('boolean');
73 | expect(inflate(Boolean, true)).to.equal(true);
74 | expect(inflate(Boolean, new Boolean(true) as boolean)).to.equal(true);
75 |
76 | expect(inflate(Number, 12)).to.be.a('number');
77 | expect(inflate(Number, new Number(12) as number)).to.be.a('number');
78 | expect(inflate(Number, 12)).to.equal(12);
79 | expect(inflate(Number, new Number(12) as number)).to.equal(12);
80 |
81 | expect(inflate(String, 'hello')).to.be.a('string');
82 | expect(inflate(String, new String('hello') as string)).to.be.a('string');
83 | expect(inflate(String, 'hello')).to.equal('hello');
84 | expect(inflate(String, new String('hello') as string)).to.equal('hello');
85 |
86 | });
87 |
88 | it('is able to deserialize non-primitives which are serializable', () => {
89 | const foo = inflate(Foo, { id: 'foo' });
90 | expect(foo).to.be.instanceOf(Foo);
91 | expect(foo).to.deep.equal({ id: 'foo' });
92 | });
93 |
94 | it('should fail to deserialize non-primitives which are not serializable', () => {
95 | expect(() => inflate(Bar, { id: 'bar' })).to.throw('Unable to deserialize an instance of "Bar"');
96 | });
97 |
98 | });
99 |
100 | });
101 |
--------------------------------------------------------------------------------
/spec/src/mock/circular_child.ts:
--------------------------------------------------------------------------------
1 | import { Serialize } from 'serialazy';
2 |
3 | import Parent from './circular_parent';
4 |
5 | export default class Child {
6 | @Serialize({ optional: true }) public parent: Parent;
7 | }
8 |
--------------------------------------------------------------------------------
/spec/src/mock/circular_parent.ts:
--------------------------------------------------------------------------------
1 | import { Serialize } from 'serialazy';
2 |
3 | import Child from './circular_child';
4 |
5 | export default class Parent {
6 | @Serialize() public child: Child;
7 | }
8 |
--------------------------------------------------------------------------------
/spec/src/pojo.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | describe('serialization/deserialization to/from a POJO ("as" and "toPojo" options)', () => {
8 |
9 | @Serialize({
10 | down: (coord: Coord) => [coord.x, coord.y],
11 | up: (tuple: number[], { toPojo }) => Object.assign(
12 | toPojo ? {} : new Coord(),
13 | { x: tuple[0], y: tuple[1] }
14 | )
15 | })
16 | class Coord extends Serializable {
17 | public x: number;
18 | public y: number;
19 | }
20 |
21 | class Descriptor extends Serializable {
22 | @Serialize() public id: number;
23 | @Serialize() public name: string;
24 | }
25 |
26 | abstract class Element extends Serializable {
27 | @Serialize({ name: 'dsc' }) public descriptor: Descriptor;
28 | @Serialize({ name: 'pos' }) public position: Coord;
29 | }
30 |
31 | class Vector extends Element {
32 | @Serialize({ name: 'dir' }) public direction: Coord;
33 | }
34 |
35 | const vecPojo = {
36 | descriptor: { id: 123, name: 'vec1' },
37 | position: { x: -10, y: -5 },
38 | direction: { x: 3, y: 7 }
39 | };
40 |
41 | const vecObj = {
42 | dsc: { id: 123, name: 'vec1' },
43 | pos: [-10, -5],
44 | dir: [3, 7]
45 | };
46 |
47 | it('is able to serialize POJO as if it were of type specified by "as" option', () => {
48 | const obj = deflate(vecPojo, { as: Vector });
49 | expect(obj).to.deep.equal(vecObj);
50 | });
51 |
52 | it('is able to deserialize class to a POJO when "toPojo" option is set', () => {
53 | const pojo = inflate(Vector, vecObj, { toPojo: true });
54 | expect(pojo).to.deep.equal(vecPojo);
55 | [pojo, pojo.descriptor, pojo.direction, pojo.position].forEach(arg => {
56 | expect(arg.constructor).to.equal(Object);
57 | });
58 | });
59 |
60 | });
61 |
--------------------------------------------------------------------------------
/spec/src/projections.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, JsonType, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | type Constructor = new () => T;
8 |
9 | describe('projection options', () => {
10 |
11 | const TEST_PROJECTION = 'test';
12 |
13 | describe('when applied to decorator', () => {
14 |
15 | describe('on a type', () => {
16 |
17 | interface Named {
18 | name: string;
19 | }
20 |
21 | function itAppliesInDefaultProjection(ctor: Constructor) {
22 |
23 | it('applies decorator in default projection', () => {
24 | const serializable = Object.assign(new ctor(), { name: 'joe' });
25 | expect(() => deflate(serializable, {
26 | projection: TEST_PROJECTION,
27 | fallbackToDefaultProjection: false
28 | })).to.throw();
29 | const serialized = deflate(serializable);
30 | expect(serialized).to.equal('joe');
31 | });
32 |
33 | }
34 |
35 | describe('when projection is undefined', () => {
36 |
37 | @Serialize({ down: (named: Named) => named.name })
38 | class Person implements Named {
39 | public name: string;
40 | }
41 |
42 | itAppliesInDefaultProjection(Person);
43 |
44 | });
45 |
46 | describe('when projection is set to undefined', () => {
47 |
48 | @Serialize({ projection: undefined, down: (named: Named) => named.name })
49 | class Person implements Named {
50 | public name: string;
51 | }
52 |
53 | itAppliesInDefaultProjection(Person);
54 |
55 | });
56 |
57 | describe('when projection is set to null', () => {
58 |
59 | @Serialize({ projection: null, down: (named: Named) => named.name })
60 | class Person implements Named {
61 | public name: string;
62 | }
63 |
64 | itAppliesInDefaultProjection(Person);
65 |
66 | });
67 |
68 | describe('when projection is set to empty string', () => {
69 |
70 | @Serialize({ projection: '', down: (named: Named) => named.name })
71 | class Person implements Named {
72 | public name: string;
73 | }
74 |
75 | itAppliesInDefaultProjection(Person);
76 |
77 | });
78 |
79 | describe('when projection is a non-empty string', () => {
80 |
81 | @Serialize({ projection: 'test', down: (named: Named) => named.name })
82 | class Person implements Named {
83 | public name: string;
84 | }
85 |
86 | it('applies decorator in given projection', () => {
87 | const serializable = Object.assign(new Person(), { name: 'joe' });
88 | expect(() => deflate(serializable)).to.throw();
89 | const serialized = deflate(serializable, { projection: TEST_PROJECTION });
90 | expect(serialized).to.equal('joe');
91 | });
92 |
93 | });
94 |
95 | });
96 |
97 | describe('on a property', () => {
98 |
99 | describe('when projection is undefined/null or empty string', () => {
100 |
101 | class Person extends Serializable {
102 |
103 | @Serialize()
104 | public id: number;
105 |
106 | @Serialize({ projection: undefined })
107 | public name: string;
108 |
109 | @Serialize({ projection: null })
110 | public address: string;
111 |
112 | @Serialize({ projection: '' })
113 | public email: string;
114 |
115 | }
116 |
117 | it('applies decorator in default projection', () => {
118 | const person = Person.create({
119 | id: 1, name: 'joe', address: 'unknown', email: 'joe@example.com'
120 | });
121 | expect(() => deflate(person, {
122 | projection: TEST_PROJECTION,
123 | fallbackToDefaultProjection: false
124 | })).to.throw();
125 | const serialized = deflate(person);
126 | expect(serialized).to.deep.equal({
127 | id: 1, name: 'joe', address: 'unknown', email: 'joe@example.com'
128 | });
129 | });
130 |
131 | });
132 |
133 | describe('when projection is a non-empty string', () => {
134 |
135 | class Person extends Serializable {
136 |
137 | @Serialize({ projection: TEST_PROJECTION })
138 | public id: number;
139 |
140 | @Serialize({ projection: TEST_PROJECTION })
141 | public name: string;
142 |
143 | }
144 |
145 | it('applies decorator in given projection', () => {
146 | const person = Person.create({ id: 1, name: 'joe' });
147 | expect(() => deflate(person)).to.throw();
148 | const serialized = deflate(person, { projection: TEST_PROJECTION });
149 | expect(serialized).to.deep.equal({ id: 1, name: 'joe' });
150 | });
151 |
152 | });
153 |
154 | });
155 |
156 | });
157 |
158 | describe('when applied to serialization functions', () => {
159 |
160 | @Serialize({
161 | down: (ts: Timestamp) => ts.date.toISOString(),
162 | up: (isoString: string) => new Timestamp(new Date(isoString))
163 | })
164 | @Serialize({
165 | projection: 'compact',
166 | down: (ts: Timestamp) => ts.date.getTime(),
167 | up: (unixTimeMs: number) => new Timestamp(new Date(unixTimeMs))
168 | })
169 | class Timestamp {
170 | public constructor(
171 | public date: Date
172 | ) {}
173 | }
174 |
175 | class JWT extends Serializable {
176 |
177 | @Serialize({ name: 'sub' })
178 | public subject: string;
179 |
180 | @Serialize({ name: 'iat' })
181 | public issuedAt: Timestamp;
182 |
183 | }
184 |
185 | class Person extends Serializable {
186 |
187 | @Serialize()
188 | @Serialize({ projection: 'compact', name: 'id' })
189 | public identifier: number;
190 |
191 | @Serialize()
192 | public name: string;
193 |
194 | }
195 |
196 | class User extends Person {
197 |
198 | @Serialize()
199 | public jwt: JWT;
200 |
201 | @Serialize()
202 | @Serialize({ projection: 'compact', name: 'lv' })
203 | public lastVisit: Timestamp;
204 |
205 | }
206 |
207 | const timestamp = new Timestamp(new Date());
208 |
209 | const user = User.create({
210 | identifier: 1,
211 | name: 'John Doe',
212 | jwt: JWT.create({
213 | subject: 'api',
214 | issuedAt: timestamp
215 | }),
216 | lastVisit: timestamp
217 | });
218 |
219 | const userDefaultProjection = {
220 | identifier: 1,
221 | name: 'John Doe',
222 | jwt: {
223 | sub: 'api',
224 | iat: timestamp.date.toISOString()
225 | },
226 | lastVisit: timestamp.date.toISOString()
227 | };
228 |
229 | const userCompactProjection = {
230 | id: 1,
231 | name: 'John Doe',
232 | jwt: {
233 | sub: 'api',
234 | iat: timestamp.date.getTime()
235 | },
236 | lv: timestamp.date.getTime()
237 | };
238 |
239 | describe('deflate', () => {
240 |
241 | function itSerializesInDefaultProjection(obj: JsonType) {
242 | it('performs serialization in default projection', () => {
243 | expect(obj).to.deep.equal(userDefaultProjection);
244 | });
245 | }
246 |
247 | describe('when projection is undefined', () => {
248 | const obj = deflate(user);
249 | itSerializesInDefaultProjection(obj);
250 | });
251 |
252 | describe('when projection is set to undefined', () => {
253 | const obj = deflate(user, { projection: undefined });
254 | itSerializesInDefaultProjection(obj);
255 | });
256 |
257 | describe('when projection is null', () => {
258 | const obj = deflate(user, { projection: null });
259 | itSerializesInDefaultProjection(obj);
260 | });
261 |
262 | describe('when projection is set to empty string', () => {
263 | const obj = deflate(user, { projection: '' });
264 | itSerializesInDefaultProjection(obj);
265 | });
266 |
267 | describe('when projection is a non-empty string', () => {
268 |
269 | const projection = 'compact';
270 |
271 | it('performs serialization in given projection', () => {
272 | const obj = deflate(user, { projection });
273 | expect(obj).to.deep.equal(userCompactProjection);
274 | });
275 |
276 | describe('when fallback to default projection is disabled', () => {
277 |
278 | it('doesn\'t fall back to default projection', () => {
279 | const obj = deflate(user, { projection, fallbackToDefaultProjection: false });
280 | expect(obj).to.deep.equal({
281 | id: 1,
282 | lv: timestamp.date.getTime()
283 | });
284 | });
285 |
286 | });
287 |
288 | });
289 |
290 | });
291 |
292 | describe('inflate', () => {
293 |
294 | function itDeserializesInDefaultProjection(instance: User) {
295 | it('performs deserialization in default projection', () => {
296 | expect(instance).to.deep.equal(user);
297 | });
298 | }
299 |
300 | describe('when projection is undefined', () => {
301 | const instance = inflate(User, userDefaultProjection);
302 | itDeserializesInDefaultProjection(instance);
303 | });
304 |
305 | describe('when projection is set to undefined', () => {
306 | const instance = inflate(User, userDefaultProjection, { projection: undefined });
307 | itDeserializesInDefaultProjection(instance);
308 | });
309 |
310 | describe('when projection is null', () => {
311 | const instance = inflate(User, userDefaultProjection, { projection: null });
312 | itDeserializesInDefaultProjection(instance);
313 | });
314 |
315 | describe('when projection is set to empty string', () => {
316 | const instance = inflate(User, userDefaultProjection, { projection: '' });
317 | itDeserializesInDefaultProjection(instance);
318 | });
319 |
320 | describe('when projection is a non-empty string', () => {
321 |
322 | const projection = 'compact';
323 |
324 | it('performs deserialization in given projection', () => {
325 | const instance = inflate(User, userCompactProjection, { projection });
326 | expect(instance).to.deep.equal(user);
327 | });
328 |
329 | describe('when fallback to default projection is disabled', () => {
330 |
331 | it('doesn\'t fallback to default projection', () => {
332 | const instance = inflate(User, userCompactProjection, { projection, fallbackToDefaultProjection: false });
333 | expect(instance).to.deep.equal({
334 | identifier: 1,
335 | lastVisit: timestamp
336 | });
337 | });
338 |
339 | });
340 |
341 | });
342 |
343 | });
344 |
345 | });
346 |
347 | });
348 |
--------------------------------------------------------------------------------
/spec/src/promise_resolving.spec.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 |
3 | import { deflate, inflate, Serializable, Serialize } from 'serialazy';
4 |
5 | const { expect } = chai;
6 |
7 | function randDefer(value: T): Promise {
8 | const pause = Math.random() * 100;
9 | return new Promise((resolve) => setTimeout(() => resolve(value), pause));
10 | }
11 |
12 | function isPromise(target: unknown): target is Promise {
13 | return Promise.resolve(target) === target;
14 | }
15 |
16 | describe('promise resolving', () => {
17 |
18 | const posSerializer = {
19 | down: (pos: Position) => `${pos.x},${pos.y}`,
20 | up: (str: string) => {
21 | const match = str.match(/^(\d+),(\d+)$/);
22 | if (!match) { throw new Error(`Invalid point: ${str}`); }
23 | const [, xStr, yStr] = match;
24 | return Position.create({ x: Number.parseInt(xStr), y: Number.parseInt(yStr) });
25 | }
26 | };
27 |
28 | const tagsSerializer = {
29 | down: (tags: string[]) => tags.join(','),
30 | up: (str: string) => str.split(',')
31 | };
32 |
33 | @Serialize(posSerializer)
34 | class Position extends Serializable {
35 | public x: number;
36 | public y: number;
37 | }
38 |
39 | class Particle extends Serializable {
40 | @Serialize()
41 | public position: Position;
42 | @Serialize(tagsSerializer)
43 | public tags: string[];
44 | }
45 |
46 | @Serialize({
47 | down: (pos: Position) => randDefer(posSerializer.down(pos)),
48 | up: (str: string) => randDefer(posSerializer.up(str))
49 | })
50 | class PositionAsync extends Serializable {
51 | public x: number;
52 | public y: number;
53 | }
54 |
55 | class ParticleAsync extends Serializable {
56 | @Serialize()
57 | public position: Position;
58 | @Serialize({
59 | down: (tags: string[]) => randDefer(tagsSerializer.down(tags)),
60 | up: (str: string) => randDefer(tagsSerializer.up(str))
61 | })
62 | public tags: string[];
63 | }
64 |
65 |
66 | context('when type serializes to a non-promise value', () => {
67 |
68 | const pos = Position.create({ x: 10, y: 20 });
69 | const posSerialized = '10,20';
70 |
71 | context('when used with resolve', () => {
72 |
73 | it('should return a promise', async () => {
74 | const serialazedPromise = deflate.resolve(pos);
75 | expect(isPromise(serialazedPromise)).to.equal(true);
76 | const serialized = await serialazedPromise;
77 | expect(serialized).to.equal(posSerialized);
78 | const deserializedPromise = inflate.resolve(Position, serialized);
79 | expect(isPromise(deserializedPromise)).to.equal(true);
80 | const deserialized = await deserializedPromise;
81 | expect(deserialized).to.deep.equal(pos);
82 | });
83 |
84 | });
85 |
86 | context('when used w/o resolve', () => {
87 |
88 | it('should return a non-promise value', () => {
89 | const serialized = deflate(pos) as any;
90 | expect(isPromise(serialized)).to.equal(false);
91 | expect(serialized).to.equal(posSerialized);
92 | const deserialized = inflate(Position, serialized);
93 | expect(isPromise(deserialized)).to.equal(false);
94 | expect(deserialized).to.deep.equal(pos);
95 | });
96 |
97 | });
98 |
99 | });
100 |
101 | context('when type serializes to a promise', () => {
102 |
103 | const pos = PositionAsync.create({ x: 10, y: 20 });
104 | const posSerialized = '10,20';
105 |
106 | context('when used with resolve', () => {
107 |
108 | it('should return a promise', async () => {
109 | const serialazedPromise = deflate.resolve(pos);
110 | expect(isPromise(serialazedPromise)).to.equal(true);
111 | const serialized = await serialazedPromise;
112 | expect(serialized).to.equal(posSerialized);
113 | const deserializedPromise = inflate.resolve(PositionAsync, serialized);
114 | expect(isPromise(deserializedPromise)).to.equal(true);
115 | const deserialized = await deserializedPromise;
116 | expect(deserialized).to.deep.equal(pos);
117 | });
118 |
119 | });
120 |
121 | context('when used w/o resolve', () => {
122 |
123 | it('should throw an error', () => {
124 | expect(() => deflate(pos)).to.throw('should be serialized with "deflate.resolve"');
125 | expect(() => inflate(PositionAsync, posSerialized)).to.throw('should be deserialized with "inflate.resolve"');
126 | });
127 |
128 | });
129 |
130 | });
131 |
132 | context('when property-bag doesn\'t contain props serializing to promises', () => {
133 |
134 | const part = Particle.create({
135 | position: Position.create({ x: 20, y: 30 }),
136 | tags: ['one', 'two']
137 | });
138 | const partSerialized = {
139 | position: '20,30',
140 | tags: 'one,two'
141 | };
142 |
143 | context('when used with resolve', () => {
144 |
145 | it('should return a promise', async () => {
146 | const serialazedPromise = deflate.resolve(part);
147 | expect(isPromise(serialazedPromise)).to.equal(true);
148 | const serialized = await serialazedPromise;
149 | expect(serialized).to.deep.equal(partSerialized);
150 | const deserializedPromise = inflate.resolve(Particle, serialized);
151 | expect(isPromise(deserializedPromise)).to.equal(true);
152 | const deserialized = await deserializedPromise;
153 | expect(deserialized).to.deep.equal(part);
154 | });
155 |
156 | });
157 |
158 | context('when used w/o resolve', () => {
159 |
160 | it('should return a non-promise value', () => {
161 | const serialized = deflate(part) as any;
162 | expect(isPromise(serialized)).to.equal(false);
163 | expect(serialized).to.deep.equal(partSerialized);
164 | const deserialized = inflate(Particle, serialized);
165 | expect(isPromise(deserialized)).to.equal(false);
166 | expect(deserialized).to.deep.equal(part);
167 | });
168 |
169 | });
170 |
171 | });
172 |
173 | context('when property-bag contains props serializing to promises', () => {
174 |
175 | const part = ParticleAsync.create({
176 | position: PositionAsync.create({ x: 20, y: 30 }),
177 | tags: ['one', 'two']
178 | });
179 | const partSerialized = {
180 | position: '20,30',
181 | tags: 'one,two'
182 | };
183 |
184 | context('when used with resolve', () => {
185 |
186 | it('should return a promise', async () => {
187 | const serialazedPromise = deflate.resolve(part);
188 | expect(isPromise(serialazedPromise)).to.equal(true);
189 | const serialized = await serialazedPromise;
190 | expect(serialized).to.deep.equal(partSerialized);
191 | const deserializedPromise = inflate.resolve(ParticleAsync, serialized);
192 | expect(isPromise(deserializedPromise)).to.equal(true);
193 | const deserialized = await deserializedPromise;
194 | expect(deserialized).to.deep.equal(part);
195 | });
196 |
197 | });
198 |
199 | context('when used w/o resolve', () => {
200 |
201 | it('should throw an error', () => {
202 | expect(() => deflate(part)).to.throw('should be serialized with "deflate.resolve"');
203 | expect(() => inflate(ParticleAsync, partSerialized)).to.throw('should be deserialized with "inflate.resolve"');
204 | });
205 |
206 | });
207 |
208 | });
209 |
210 | });
211 |
--------------------------------------------------------------------------------
/spec/src/run.ts:
--------------------------------------------------------------------------------
1 | import chai = require('chai');
2 | import chaiAsPromised = require('chai-as-promised');
3 | import fs = require('fs');
4 | import Mocha = require('mocha');
5 | import path = require('path');
6 |
7 | // register chai plugins
8 | chai.use(chaiAsPromised);
9 |
10 | function listFilesRecursive(fname: string) {
11 | let files: string[] = [];
12 | const stat = fs.lstatSync(fname);
13 | if (stat.isFile()) {
14 | files.push(fname);
15 | } else if (stat.isDirectory()) {
16 | fs.readdirSync(fname).forEach(nested => {
17 | files = files.concat(listFilesRecursive(path.join(fname, nested)));
18 | });
19 | }
20 | return files;
21 | }
22 |
23 | let mocha = new Mocha({ reporter: 'spec', timeout: 10000 });
24 |
25 | // search for spec files recursively
26 | listFilesRecursive(__dirname)
27 | .filter(fname => fname.endsWith('spec.js'))
28 | .forEach(fname => mocha.addFile(fname));
29 |
30 | mocha.run(failureCount => {
31 | process.exitCode = failureCount;
32 | });
33 |
--------------------------------------------------------------------------------
/spec/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 |
5 | /* Basic Options */
6 | "declaration": false, /* Generates corresponding '.d.ts' file. */
7 | "sourceMap": true, /* Generates corresponding '.map' file. */
8 | "outDir": "bin", /* Redirect output structure to the directory. */
9 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
10 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 |
4 | /* Basic Options */
5 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
6 | "module": "commonjs", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | // "lib": [], /* Specify library files to be included in the compilation: */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | // "outDir": "./", /* Redirect output structure to the directory. */
15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | "removeComments": false, /* Do not emit comments to output. */
17 | // "noEmit": true, /* Do not emit outputs. */
18 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
19 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
20 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
21 |
22 | /* Strict Type-Checking Options */
23 | "strict": true, /* Enable all strict type-checking options. */
24 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
25 | "strictNullChecks": false, /* Enable strict null checks. */
26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
28 |
29 | /* Additional Checks */
30 | // "noUnusedLocals": true, /* Report errors on unused locals. */
31 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
32 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
33 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
34 |
35 | /* Module Resolution Options */
36 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
37 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
38 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
39 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
40 | // "typeRoots": [], /* List of folders to include type definitions from. */
41 | // "types": [], /* Type declaration files to be included in compilation. */
42 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
43 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
44 |
45 | /* Source Map Options */
46 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
47 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
48 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
49 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
50 |
51 | /* Experimental Options */
52 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
53 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
54 |
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 |
4 | // TypeScript Specific
5 | "adjacent-overload-signatures": true,
6 | "member-access": [ true, "check-accessor", "check-constructor" ],
7 | "no-inferrable-types": true,
8 | "no-internal-module": true,
9 | "no-var-requires": true,
10 |
11 | // Functionality
12 | "curly": true,
13 | "no-console": [ true, "log", "error", "debug" ],
14 | "no-debugger": true,
15 | "no-eval": true,
16 | "no-unused-expression": true,
17 | "no-var-keyword": true,
18 |
19 | // Maintainability
20 | "eofline": true,
21 | "indent": [ true, "spaces" ],
22 | "linebreak-style": [ true, "LF" ],
23 | "no-trailing-whitespace": true,
24 |
25 | // Style
26 | "class-name": true,
27 | "comment-format": [ true, "check-space" ],
28 | "jsdoc-format": true,
29 | "ordered-imports": true,
30 | "semicolon": [ true, "always" ],
31 | "variable-name": [ true, "ban-keywords", "check-format" ],
32 | "whitespace": [ true,
33 | "check-branch", "check-decl", "check-module",
34 | "check-operator", "check-separator", "check-type", "check-typecast"
35 | ]
36 |
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/website/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | "babel-plugin-transform-typescript-metadata",
5 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
6 | ["@babel/plugin-proposal-class-properties", { "loose": true }]
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /node_modules
3 | /.pnp
4 | /.next/
5 | .pnp.js
6 |
--------------------------------------------------------------------------------
/website/components/active-link.module.scss:
--------------------------------------------------------------------------------
1 | .visiting {
2 | @apply font-bold;
3 | }
4 |
--------------------------------------------------------------------------------
/website/components/active-link.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 | import { ReactNode } from 'react';
5 |
6 | import styles from './active-link.module.scss';
7 |
8 | type Props = {
9 | href: string,
10 | match?: string,
11 | children?: ReactNode,
12 | };
13 |
14 | export function ActiveLink({ href, match, children }: Props) {
15 |
16 | const { asPath } = useRouter();
17 |
18 | const visiting = asPath === (match ?? href) || asPath.startsWith(`${match ?? href}#`);
19 |
20 | return (
21 |
22 |
26 | {children}
27 |
28 |
29 | );
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/website/components/contents.module.scss:
--------------------------------------------------------------------------------
1 |
2 | .contents {
3 |
4 | @apply leading-loose font-light;
5 |
6 | a {
7 | @apply no-underline;
8 | }
9 |
10 | &.inline {
11 |
12 | ul, li {
13 | display: inline;
14 | }
15 |
16 | li:not(:last-child):after {
17 | content: " • ";
18 | }
19 |
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/website/components/contents.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import Link from 'next/link';
3 |
4 | import { ActiveLink } from '../components/active-link';
5 | import { packageName, packageVersion } from '../package-info';
6 |
7 | import styles from './contents.module.scss';
8 |
9 | type Props = {
10 | inline?: boolean,
11 | };
12 |
13 | export function Contents({ inline }: Props) {
14 |
15 | return (
16 |
17 |