├── .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 | 404: This page could not be found

404

This page could not be found.

-------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | serialazy.teqlab.net -------------------------------------------------------------------------------- /docs/_next/data/7of8QfVFcgerx6lX1Jut2/async.json: -------------------------------------------------------------------------------- 1 | {"pageProps":{"slug":"async","frontmatter":{"title":"Async Serialization"},"githubLink":"https://github.com/teq/serialazy/tree/main/website/content/async.page.md","markdown":"

Async Serialization

\n

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.

\n
import { deflate, inflate, Serialize } from 'serialazy';\nimport { getUserFieldsById } from './db';\n\n@Serialize({\n    down: (user: User) => user.id,\n    up: async (id: string) => Object.assign(new User(), await getUserFieldsById(id))\n})\nclass User {\n    public id: string;\n    public email: string;\n    public isAdmin: boolean;\n}\n\nconst user = Object.assign(new User(), {\n    id: '<unique_id>',\n    email: 'john.doe@example.com',\n    isAdmin: true\n});\n\nconst serialized = deflate(user);\nexpect(serialized).to.equal('<unique_id>');\n\nconst deserialized = await inflate.resolve(User, serialized);\nexpect(deserialized).to.deep.equal({\n    id: '<unique_id>',\n    email: 'john.doe@example.com',\n    isAdmin: true\n});\n
\n
\n

Note: All properties of given instance are serialized / deserialized in parallel with Promise.all().

\n
"},"__N_SSG":true} -------------------------------------------------------------------------------- /docs/_next/data/7of8QfVFcgerx6lX1Jut2/changelog.json: -------------------------------------------------------------------------------- 1 | {"pageProps":{"slug":"changelog","frontmatter":{"title":"Changelog"},"githubLink":"https://github.com/teq/serialazy/tree/main/website/content/changelog.page.md","markdown":"

Changelog

\n

v3.0.0

\n\n

v2.0.2

\n\n

v2.0.1

\n\n

v2.0.0

\n\n

v1.3.1

\n\n

v1.3.0

\n\n

v1.2.3

\n\n

v1.2.2

\n\n

v1.2.1

\n\n

v1.2.0

\n\n

v1.0.1

\n

Initial version features:

\n"},"__N_SSG":true} -------------------------------------------------------------------------------- /docs/_next/data/7of8QfVFcgerx6lX1Jut2/inheritance.json: -------------------------------------------------------------------------------- 1 | {"pageProps":{"slug":"inheritance","frontmatter":{"title":"Class Inheritance"},"githubLink":"https://github.com/teq/serialazy/tree/main/website/content/inheritance.page.md","markdown":"

Class Inheritance

\n

Property bag serializables inherit all property serializers from parent (property bag) class

\n
import { deflate, inflate, Serialize } from 'serialazy';\n\nabstract class Shape {\n    @Serialize() public x: number;\n    @Serialize() public y: number;\n}\n\nclass Rectangle extends Shape {\n    @Serialize({ name: 'w' }) public width: number;\n    @Serialize({ name: 'h' }) public height: number;\n}\n\nconst rect = Object.assign(new Rectangle(), {\n    x: 1, y: 2, width: 5, height: 3\n});\n\nconst serialized = deflate(rect);\n// serialized includes all props from Rectangle + Shape\nexpect(serialized).to.deep.equal({\n    x: 1, y: 2, w: 5, h: 3\n});\n\nconst deserialized = 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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/assets/icons/scroll-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 |
18 | 19 | 20 |

21 | { packageName } 22 | { packageVersion } 23 |

24 |
25 | 26 |
    27 |
  • Getting Started
  • 28 |
  • Options
  • 29 |
  • Class Inheritance
  • 30 |
  • Async Serialization
  • 31 |
  • Projections
  • 32 |
  • POJO
  • 33 |
  • Changelog
  • 34 |
35 | 36 |
37 | 38 | ); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /website/components/cover.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | 3 | @apply inline-block px-8 py-3 mx-4 my-2 rounded-full border-solid border-2 border-green-600 text-green-600; 4 | @apply transition duration-150 ease-in-out; 5 | 6 | &:hover { 7 | @apply text-black; 8 | } 9 | 10 | &.button_primary { 11 | 12 | @apply bg-green-600 text-white; 13 | 14 | &:hover { 15 | @apply bg-green-500 border-green-500 text-black; 16 | } 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /website/components/cover.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import Link from 'next/link'; 3 | 4 | import { packageDescription, packageName, packageVersion } from '../package-info'; 5 | import { ExternalLink } from './external-link'; 6 | 7 | import styles from './cover.module.scss'; 8 | 9 | export function Cover() { 10 | 11 | return ( 12 |
13 |
14 |

15 | {packageName} 16 | {packageVersion} 17 |

18 |

19 | {packageDescription} 20 |

21 |
    22 |
  • Class inheritance
  • 23 |
  • Recursive serialization
  • 24 |
  • Custom serializers for properties/types
  • 25 |
  • TypeScript-friendly API based on decorators
  • 26 |
  • ... and more 👇
  • 27 |
28 |
29 | 30 | GitHub 31 | 32 | 33 | NPM 34 | 35 | 36 | Getting Started 37 | 38 |
39 |
40 |
41 | ); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /website/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from 'react'; 2 | 3 | type Props = { 4 | href: string, 5 | newTab?: boolean, 6 | children?: ReactNode, 7 | }; 8 | 9 | export function ExternalLink({ href, newTab, children }: Props) { 10 | 11 | const anchor = 12 | typeof children !== 'string' && 13 | React.Children.count(children) === 1 && 14 | React.Children.only(children) as ReactElement; 15 | 16 | if (anchor && anchor.type === 'a') { 17 | 18 | return React.cloneElement(anchor, { 19 | href, 20 | target: newTab ? "_blank" : undefined, 21 | rel: "noopener noreferrer" 22 | }); 23 | 24 | } else { 25 | 26 | return ( 27 | 32 | { children } 33 | 34 | ); 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /website/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from './external-link'; 2 | 3 | type Props = { 4 | githubLink?: string, 5 | }; 6 | 7 | export function Footer({ githubLink }: Props) { 8 | 9 | const year = new Date().getFullYear(); 10 | 11 | return ( 12 |
13 | © 2017 - {year} Andrey Tselishchev 14 | Site powered by next.js 15 | { githubLink && 16 | 17 | 18 | Edit on GitHub 19 | 20 | } 21 |
22 | ); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /website/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { ReactNode } from 'react'; 3 | 4 | import { Cover } from './cover'; 5 | import { Meta } from './meta'; 6 | 7 | type Props = { 8 | children: ReactNode 9 | }; 10 | 11 | export function Layout({ children }: Props) { 12 | 13 | const { asPath } = useRouter(); 14 | 15 | return ( 16 | <> 17 | 18 |
19 | {asPath === '/' && } 20 |
21 |
{children}
22 | 23 | ); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /website/components/meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | import { packageDescription, packageName } from '../package-info'; 4 | 5 | type Props = { 6 | subtitle?: string 7 | }; 8 | 9 | export function Meta({ subtitle }: Props) { 10 | 11 | let title = subtitle ? `${packageName} - ${subtitle}` : packageName; 12 | 13 | return ( 14 | 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {/* */} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {/* */} 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /website/content/async.page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Async Serialization 3 | --- 4 | 5 | # Async Serialization 6 | 7 | If one or more type serializer or deserializer (`down` or `up` functions) return a `Promise` value, 8 | it is possible to await until they are resolved with `deflate.resolve` and `inflate.resolve`. 9 | 10 | Unlike `deflate` and `inflate`, these functions return a `Promise` to serialized / deserialized value. 11 | 12 | Following example serializes `User` to its `id` and deserializes using async `getUserFieldsById` function. 13 | 14 | ```ts 15 | import { deflate, inflate, Serialize } from 'serialazy'; 16 | import { getUserFieldsById } from './db'; 17 | 18 | @Serialize({ 19 | down: (user: User) => user.id, 20 | up: async (id: string) => Object.assign(new User(), await getUserFieldsById(id)) 21 | }) 22 | class User { 23 | public id: string; 24 | public email: string; 25 | public isAdmin: boolean; 26 | } 27 | 28 | const user = Object.assign(new User(), { 29 | id: '', 30 | email: 'john.doe@example.com', 31 | isAdmin: true 32 | }); 33 | 34 | const serialized = deflate(user); 35 | expect(serialized).to.equal(''); 36 | 37 | const deserialized = await inflate.resolve(User, serialized); 38 | expect(deserialized).to.deep.equal({ 39 | id: '', 40 | email: 'john.doe@example.com', 41 | isAdmin: true 42 | }); 43 | ``` 44 | 45 | > __Note:__ All properties of given instance are serialized / deserialized in parallel with `Promise.all()`. 46 | -------------------------------------------------------------------------------- /website/content/changelog.page.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /website/content/getting-started.page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | # Getting Started 6 | 7 | **Serialazy** is a serialization / data-mapping library 8 | which can be used to deflate/inflate TypeScript class instances as well as plain JS objects ([POJO](/pojo)). 9 | 10 | Features: 11 | - Default serializers for primitive types (string, number, boolean) 12 | - Support for [optional](/options#optional), [nullable](/options#nullable) and [mapped](/options#name) properties 13 | - Recursive serialization for nested data structures 14 | - Property serializers [inheritance](/inheritance) (from parent class to a child) 15 | - User defined serialization functions for properties and types 16 | - Alternative [projections](/projections) 17 | - [Async](/async) serialization 18 | - TypeScript-friendly API based on decorators 19 | 20 | > **⚠ Note:** Version 3.x.x introduces breaking [changes](/changelog#v300) compared to 2.x.x. 21 | 22 | ## Requirements 23 | 24 | Library makes use of TypeScript experimental feature which emits type metadata to the resulting JS. 25 | Make sure that you enabled `experimentalDecorators` and `emitDecoratorMetadata` in your `tsconfig.json`. 26 | 27 | ## Installation 28 | 29 | ```shell 30 | npm i --save serialazy 31 | ``` 32 | 33 | ## Basic usage 34 | 35 | ```ts 36 | import { deflate, inflate } from 'serialazy'; 37 | const serialized = deflate(serializable); 38 | const deserialized = inflate(SerializableType, serialized); 39 | ``` 40 | 41 | Where: 42 | - `serialized` is a JSON-compatible value which can be safely passed to `JSON.stringify` 43 | - `SerializableType` is a constructor function for serializable type 44 | - `serializable` is a primitive (string, number, boolean or their "boxed" variants, null, undefined), or a _non-primitive_ serializable 45 | 46 | There are 2 types of _non-primitive_ serializables: 47 | 48 | __1. A type with custom serializer__ 49 | 50 | Is a TS class decorated with `@Serialize()`. 51 | 52 | ```ts 53 | import { Serialize } from 'serialazy'; 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 { 61 | public x: number; 62 | public y: number; 63 | } 64 | ``` 65 | 66 | __2. A "property bag"__ 67 | 68 | Is a TS class having properties decorated with `@Serialize()`. 69 | 70 | - _Always_ serializes to a plain object 71 | - Can extend (inherit from) another property bag 72 | 73 | ```ts 74 | import { Serialize } from 'serialazy'; 75 | 76 | // Shape is a "property bag" serializable 77 | class Shape { 78 | @Serialize() public name: string; 79 | } 80 | 81 | // Sphere inherits property serializers from Shape 82 | class Sphere extends Shape { 83 | @Serialize() public radius: number; 84 | @Serialize() public position: Position; 85 | } 86 | ``` 87 | 88 | Above classes can be serialized / deserialized like this: 89 | 90 | ```ts 91 | import { deflate, inflate } from 'serialazy'; 92 | 93 | import chai = require('chai'); 94 | const { expect } = chai; 95 | 96 | const sphere = Object.assign(new Sphere(), { 97 | name: 'sphere1', 98 | radius: 10, 99 | position: Object.assign(new Position(), { 100 | x: 3, 101 | y: 5 102 | }) 103 | }); 104 | 105 | const serialized = deflate(sphere); 106 | 107 | expect(serialized).to.deep.equal({ 108 | name: 'sphere1', 109 | radius: 10, 110 | position: [3, 5] 111 | }); 112 | 113 | const deserialized = inflate(Sphere, serialized); 114 | 115 | // Deserialized sphere should be identical with the original one 116 | expect(deserialized).to.deep.equal(sphere); 117 | ``` 118 | 119 | ## Serializable base class 120 | 121 | Library provides an _optional_ abstract base class for serializables. 122 | Currently it only provides static factory method `create` which makes instance creation more compact 123 | and type-safe. Above example could look like this: 124 | 125 | ```ts 126 | import { Serializable, Serialize } from 'serialazy'; 127 | 128 | @Serialize({ ... }) 129 | class Position extends Serializable { ... } 130 | 131 | class Shape extends Serializable { ... } 132 | 133 | class Sphere extends Shape { ... } 134 | 135 | const sphere = Sphere.create({ 136 | name: 'sphere1', 137 | radius: 10, 138 | position: Position.create({ x:3, y: 5 }) 139 | }); 140 | 141 | ... 142 | ``` 143 | -------------------------------------------------------------------------------- /website/content/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import matter from 'gray-matter'; 3 | import path from 'path'; 4 | import rehypeAutolink from 'rehype-autolink-headings'; 5 | import rehypeShiki from 'rehype-shiki'; 6 | import rehypeSlug from 'rehype-slug'; 7 | import rehypeHtml from 'rehype-stringify'; 8 | import rehypeUrlInspector from '@jsdevtools/rehype-url-inspector'; 9 | import remarkParse from 'remark-parse'; 10 | import remark2rehype from 'remark-rehype'; 11 | import unified from 'unified'; 12 | 13 | import { repositoryUrl } from '../package-info'; 14 | 15 | const PAGES_DIR = path.resolve(process.cwd(), 'content'); 16 | 17 | export async function getPageSlugs(): Promise { 18 | const filenames = await fs.promises.readdir(PAGES_DIR); 19 | return filenames 20 | .map(filename => filename.match(/^(.+)\.page\.md$/)?.[1]) 21 | .filter((slug): slug is string => typeof slug === 'string'); 22 | } 23 | 24 | export async function getPageContent(slug: string) { 25 | 26 | const relPath = `content/${slug}.page.md`; 27 | const buffer = await fs.promises.readFile(path.resolve(process.cwd(), relPath)); 28 | 29 | const { data: frontmatter, content } = matter(buffer); 30 | 31 | const renderedMarkdown = await unified() 32 | .use(remarkParse) 33 | .use(remark2rehype) 34 | .use(rehypeSlug) 35 | .use(rehypeAutolink) 36 | .use(rehypeShiki) 37 | .use(rehypeUrlInspector, { 38 | inspectEach(urlMatch) { 39 | // Urls starting with "http(s)" treated as external and opened in new tab 40 | if (urlMatch.url.startsWith('http')) { 41 | console.log('!!!', urlMatch.url); 42 | urlMatch.node.properties = Object.assign(urlMatch.node.properties ?? {}, { 43 | target: '_blank', 44 | rel: ['nofollow', 'noopener', 'noreferrer'] 45 | }); 46 | } 47 | } 48 | }) 49 | .use(rehypeHtml) 50 | .process(content); 51 | 52 | return { 53 | slug, 54 | frontmatter, 55 | githubLink: `${repositoryUrl}/tree/main/website/${relPath}`, 56 | markdown: renderedMarkdown.toString() 57 | }; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /website/content/inheritance.page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Class Inheritance 3 | --- 4 | 5 | # Class Inheritance 6 | 7 | Property bag serializables inherit all property serializers from parent (property bag) class 8 | 9 | ```ts 10 | import { deflate, inflate, Serialize } from 'serialazy'; 11 | 12 | abstract class Shape { 13 | @Serialize() public x: number; 14 | @Serialize() public y: number; 15 | } 16 | 17 | class Rectangle extends Shape { 18 | @Serialize({ name: 'w' }) public width: number; 19 | @Serialize({ name: 'h' }) public height: number; 20 | } 21 | 22 | const rect = Object.assign(new Rectangle(), { 23 | x: 1, y: 2, width: 5, height: 3 24 | }); 25 | 26 | const serialized = deflate(rect); 27 | // serialized includes all props from Rectangle + Shape 28 | expect(serialized).to.deep.equal({ 29 | x: 1, y: 2, w: 5, h: 3 30 | }); 31 | 32 | const deserialized = inflate(Rectangle, serialized); 33 | // deserialized includes all props from Rectangle + Shape 34 | expect(deserialized).to.deep.equal(rect); 35 | ``` 36 | 37 | > __Note:__ Child class can shadow parent's property serializers. 38 | -------------------------------------------------------------------------------- /website/content/options.page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Options 3 | --- 4 | 5 | # Options 6 | 7 | `@Serialize()` decorator and `inflate` / `deflate` functions accept options 8 | which affect default serialization behaviour. 9 | 10 | ## Decorator options 11 | 12 | ### "down" 13 | 14 | > __Applicable to:__ `type` and `property` serializers 15 | 16 | Defines serializer function. `deflate` options passed as a second argument. 17 | 18 | ```ts 19 | @Serialize({ 20 | down: (coord: Coord, options) => [coord.x, coord.y] 21 | }) 22 | class Coord { 23 | public x: number; 24 | public y: number; 25 | } 26 | ``` 27 | 28 | ### "up" 29 | 30 | > __Applicable to:__ `type` and `property` serializers 31 | 32 | Defines deserializer function. `inflate` options passed as a second argument. 33 | 34 | ```ts 35 | @Serialize({ 36 | up: (tuple: [number, number], { toPojo }) => Object.assign( 37 | toPojo ? {} : new Coord(), 38 | { x: tuple[0], y: tuple[1] } 39 | ) 40 | }) 41 | class Coord { 42 | public x: number; 43 | public y: number; 44 | } 45 | ``` 46 | 47 | ### "type" 48 | 49 | > __Applicable to:__ `type` and `property` serializers 50 | 51 | Overrides the type of serializable. 52 | 53 | Default value: 54 | 55 | - For types: Type constructor function 56 | - For properties: Value of `design:type` [metadata](https://www.typescriptlang.org/docs/handbook/decorators.html#metadata) for given property 57 | 58 | ### "optional" 59 | 60 | > __Applicable to:__ `property` serializers | __Default:__ `false` 61 | 62 | Indicates if property can be `undefined`, otherwise `deflate` / `inflate` will throw. 63 | 64 | ```ts 65 | class Book { 66 | @Serialize({ optional: true }) public isbn: string; 67 | } 68 | ``` 69 | 70 | ### "nullable" 71 | 72 | > __Applicable to:__ `property` serializers | __Default:__ `false` 73 | 74 | Indicates if property can be `null`, otherwise `deflate` / `inflate` will throw. 75 | 76 | ```ts 77 | class Book { 78 | @Serialize({ nullable: true }) public isbn: string; 79 | } 80 | ``` 81 | 82 | ### "name" 83 | 84 | > __Applicable to:__ `property` serializers 85 | 86 | If defined it forces to use different property name in serialized object. 87 | 88 | ```ts 89 | class Book { 90 | @Serialize({ name: 'summary' }) public description: string; 91 | } 92 | 93 | const book = Object.assign(new Book(), { 94 | description: 'A popular-science book on cosmology' 95 | }); 96 | 97 | expect(deflate(book)).to.deep.equal({ 98 | // NOTE: "description" mapped to "summary" in serialized object 99 | summary: 'A popular-science book on cosmology' 100 | }); 101 | ``` 102 | 103 | ### "projection" (decorator option) 104 | 105 | > __Applicable to:__ `type` and `property` serializers | __Default:__ `"default"` 106 | 107 | Forces serializer to be defined in a given projection. 108 | 109 | See [Projections](/projections) section for more details. 110 | 111 | ## Serialization options 112 | 113 | ### "projection" (serialization option) 114 | 115 | > __Applicable to:__ `deflate` and `inflate` | __Default:__ `"default"` 116 | 117 | Forces serialization in given projection. 118 | 119 | See [Projections](/projections) section for more details. 120 | 121 | ### "fallbackToDefaultProjection" 122 | 123 | > __Applicable to:__ `deflate` and `inflate` | __Default:__ `true` 124 | 125 | If type or property is not serializable in given projection 126 | it tries to serialize / deserialize it in `"default"` projection. 127 | 128 | See [Projections](/projections) section for more details. 129 | 130 | 131 | ### "prioritizePropSerializers" 132 | 133 | > __Applicable to:__ `deflate` and `inflate` | __Default:__ `false` 134 | 135 | By default, if class has own serializer defined and its properties also have serializers (property bag), 136 | class own serializer takes precedence over property serializers. Set option to `true` to serialize instance 137 | as a property bag. For recursive serialization this option applied to all nested properties. 138 | 139 | ```ts 140 | @Serialize({ down: (coord: Coord) => [coord.x, coord.y] }) 141 | class Coord { 142 | @Serialize() public x: number; 143 | @Serialize() public y: number; 144 | } 145 | 146 | const coord = Object.assign(new Coord(), { x: 1, y: 2 }); 147 | 148 | const obj1 = deflate(coord); 149 | expect(obj1).to.deep.equal([1, 2]); 150 | 151 | const obj2 = deflate(coord, { prioritizePropSerializers: true }); 152 | expect(obj2).to.deep.equal({ x: 1, y: 2 }); 153 | ``` 154 | 155 | ### "as" 156 | 157 | > __Applicable to:__ `deflate` 158 | 159 | Serialize instance as if it were of the given type. It completely ignores type's own serializer 160 | or its property serializers (if any) and uses serializer from provided type. 161 | 162 | See also: [POJO](/pojo) 163 | 164 | ```ts 165 | class Foo { 166 | @Serialize() public id: number; 167 | } 168 | 169 | @Serialize({ down: (bar: Bar) => bar.id }) 170 | class Bar { 171 | public id: number; 172 | } 173 | 174 | const foo = Object.assign(new Foo(), { id: 123 }); 175 | 176 | expect(deflate(foo)).to.deep.equal({ id: 123 }); 177 | 178 | expect(deflate(foo, { as: Bar })).to.equal(123); 179 | ``` 180 | 181 | ### "toPojo" 182 | 183 | > __Applicable to:__ `inflate` 184 | 185 | Deserialize to plain javascript object. When deserializing a property bag, instead of creating a new 186 | instance of given type, it creates an empty JS object and writes all deserialized properties there. 187 | In case of recursive serialization it affects deserialization of all nested properties. 188 | 189 | See also: [POJO](/pojo) 190 | 191 | ```ts 192 | class Foo { 193 | @Serialize() public id: number; 194 | } 195 | 196 | const foo1 = inflate(Foo, { id: 123 }); 197 | expect(foo1.constructor).to.equal(Foo); 198 | 199 | const foo2 = inflate(Foo, { id: 123 }, { toPojo: true }); 200 | expect(foo2.constructor).to.equal(Object); 201 | ``` 202 | -------------------------------------------------------------------------------- /website/content/pojo.page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: POJO 3 | --- 4 | 5 | # POJO 6 | 7 | It may be preferrable in some cases to work with plain JS objects instead of class instances. 8 | 9 | It is possible with [`"as"`](/options#as) and [`"toPojo"`](/options#topojo) options 10 | for `deflate` / `inflate` functions respectively. 11 | We use class definition to define the shape of an object, but we never instantiate it: 12 | 13 | ```ts 14 | import { deflate, inflate, Serialize } from 'serialazy'; 15 | 16 | @Serialize({ 17 | down: (pos: Position) => [pos.x, pos.y], 18 | up: (tuple: number[], { toPojo }) => Object.assign( 19 | toPojo ? {} : new Position(), 20 | { x: tuple[0], y: tuple[1] } 21 | ) 22 | }) 23 | class Position { 24 | public x: number; 25 | public y: number; 26 | } 27 | 28 | class Shape { 29 | @Serialize({ name: 'n' }) public name: string; 30 | @Serialize({ name: 'p' }) public position: Position; 31 | } 32 | ``` 33 | 34 | Serialize plain JS object with [`"as"`](/options#as) option: 35 | 36 | ```ts 37 | const shape: Shape = { // <- plain object, NOT Shape instance 38 | name: 'circle1', 39 | position: { x: 10, y: 20 } 40 | }; 41 | 42 | const serialized = deflate(shape, { as: Shape }); 43 | expect(serialized).to.deep.equal({ 44 | n: 'circle1', 45 | p: [ 10, 20 ] 46 | }); 47 | ``` 48 | 49 | Deserialize to plain JS object with [`"toPojo"`](/options#topojo) option: 50 | 51 | ```ts 52 | const deserialized = inflate(Shape, serialized, { toPojo: true }); 53 | expect(deserialized.constructor).to.equal(Object); // <- plain object 54 | expect(deserialized).to.deep.equal(shape); 55 | ``` 56 | -------------------------------------------------------------------------------- /website/content/projections.page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Projections 3 | --- 4 | 5 | # Projections 6 | 7 | Projection is one of multiple variants of serialization for a given class. 8 | It might be useful when data should be presented in different formats when passed across different data channels. 9 | 10 | By default all serializers are defined and serialization / deserialization is done in projection called `"default"`. 11 | 12 | Serializers for alternative projections can be defined using `@Serialize()` decorator 13 | with [`"projection"`](/options#projection-decorator-option) option: 14 | 15 | ```ts 16 | import { deflate, inflate, Serialize } from 'serialazy'; 17 | 18 | class Position { 19 | 20 | @Serialize() // <- defined in "default" projection 21 | @Serialize({ projection: 'alt', name: 'col' }) 22 | public x: number; 23 | 24 | @Serialize() // <- defined in "default" projection 25 | @Serialize({ projection: 'alt', name: 'row' }) 26 | public y: number; 27 | 28 | } 29 | ``` 30 | 31 | and later serialized / deserialized using `deflate` / `inflate` 32 | in required [`"projection"`](/options#projection-serialization-option): 33 | 34 | ```ts 35 | const pos = Object.assign(new Position(), { x: 1, y: 2 }); 36 | 37 | const obj1 = deflate(pos); 38 | expect(obj1).to.deep.equal({ x: 1, y: 2 }); 39 | 40 | const obj2 = deflate(pos, { projection: 'alt' }); 41 | expect(obj2).to.deep.equal({ col: 1, row: 2 }); 42 | ``` 43 | 44 | Another example demonstrates that `User` serializes to its `id` when serialized in "api" projection. 45 | Note that it's not possible do deserialize it in "api" projection without defining corresponding `"up"` 46 | deserialization function, which can be [async](/async) to be able to query a database for that user. 47 | 48 | ```ts 49 | import { deflate, inflate, Serialize } from 'serialazy'; 50 | 51 | @Serialize({ projection: 'api', down: (user: User) => user.id }) 52 | class User { 53 | @Serialize() public id: string; 54 | @Serialize() public email: string; 55 | } 56 | 57 | const user = Object.assign(new User(), { 58 | id: "", 59 | email: 'john.doe@example.com', 60 | }); 61 | 62 | expect(deflate(user)).to.deep.equal({ 63 | id: "", 64 | email: 'john.doe@example.com', 65 | }); 66 | 67 | expect(deflate(user, { projection: 'api' })).to.deep.equal(""); 68 | ``` 69 | 70 | ## Fallback to "default" projection 71 | 72 | `"default"` projection has a special role. It is used as a fallback if type is not serializable in given projection. 73 | This behaviour can be disabled by setting [`"fallbackToDefaultProjection"`](/options#fallbacktodefaultprojection) to `false` 74 | when calling `deflate` / `inflate`. 75 | 76 | > __Note:__ Serializers for primitive types (string, number, boolean) are defined in `"default"` projection and 77 | always used as a fallback, regardless of the value of `"fallbackToDefaultProjection"`. 78 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /website/out: -------------------------------------------------------------------------------- 1 | ../docs -------------------------------------------------------------------------------- /website/package-info.ts: -------------------------------------------------------------------------------- 1 | 2 | // tslint:disable-next-line: no-var-requires 3 | const npmPackage = require('../package.json'); 4 | 5 | export const repositoryUrl = (npmPackage.repository.url as string).replace(/\.git$/, ''); 6 | 7 | export const packageName = npmPackage.name as string; 8 | 9 | export const packageDescription = npmPackage.description as string; 10 | 11 | export const packageVersion = npmPackage.version as string; 12 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serialazy-docs", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "rm -rf .next && rm -rf out/* && next build && next export", 7 | "start": "next start" 8 | }, 9 | "dependencies": { 10 | "@jsdevtools/rehype-url-inspector": "^2.0.2", 11 | "classnames": "^2.2.6", 12 | "gray-matter": "^4.0.2", 13 | "next": "^9.5.3", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "rehype-autolink-headings": "^5.0.1", 17 | "rehype-shiki": "0.0.9", 18 | "rehype-slug": "^4.0.1", 19 | "rehype-stringify": "^8.0.0", 20 | "remark-parse": "^9.0.0", 21 | "remark-rehype": "^8.0.0", 22 | "serialazy": "^3.0.0-rc.18", 23 | "unified": "^9.2.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/plugin-proposal-class-properties": "^7.10.4", 27 | "@babel/plugin-proposal-decorators": "^7.10.5", 28 | "@types/classnames": "^2.2.10", 29 | "@types/node": "^14.11.5", 30 | "@types/react": "^16.9.51", 31 | "@types/react-dom": "^16.9.8", 32 | "babel-plugin-transform-typescript-metadata": "^0.3.0", 33 | "postcss-preset-env": "^6.7.0", 34 | "sass": "^1.29.0", 35 | "tailwindcss": "^1.8.11", 36 | "tslint": "^6.1.3", 37 | "typescript": "^4.0.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /website/pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Contents } from '../components/contents'; 4 | import { Footer } from '../components/footer'; 5 | import { Layout } from '../components/layout'; 6 | import { Meta } from '../components/meta'; 7 | import { getPageContent, getPageSlugs } from '../content'; 8 | 9 | import styles from './markdown.module.scss'; 10 | 11 | export type Props = { 12 | slug: string, 13 | frontmatter: { [prop: string]: any }, 14 | githubLink: string, 15 | markdown: string, 16 | }; 17 | 18 | export default function PageView({ slug, frontmatter, githubLink, markdown }: Props) { 19 | 20 | return ( 21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 | 34 |
35 |
39 |
40 |
41 |
42 |
43 | ); 44 | 45 | } 46 | 47 | type Params = { 48 | params: { 49 | slug: string 50 | } 51 | }; 52 | 53 | export async function getStaticProps({ params: { slug } }: Params) { 54 | return { 55 | props: await getPageContent(slug) 56 | }; 57 | } 58 | 59 | export async function getStaticPaths() { 60 | const slugs = await getPageSlugs(); 61 | return { 62 | paths: slugs.map((slug) => ({ params: { slug } })), 63 | fallback: false, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import React from 'react'; 3 | 4 | import '../styles/index.scss'; 5 | 6 | export default function MyApp({ Component, pageProps }: AppProps) { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /website/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { getPageContent } from '../content'; 2 | import PageView, { Props } from './[slug]'; 3 | 4 | export default function IndexPage(props: Props) { 5 | return ; 6 | } 7 | 8 | export async function getStaticProps() { 9 | return { 10 | props: await getPageContent('getting-started') 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /website/pages/markdown.module.scss: -------------------------------------------------------------------------------- 1 | .markdown { 2 | 3 | @apply leading-relaxed; 4 | 5 | p, ul, ol, blockquote, pre { 6 | @apply my-6; 7 | } 8 | 9 | blockquote { 10 | @apply border-l-4 border-gray-300 text-gray-700 pl-4; 11 | } 12 | 13 | li { 14 | @apply list-disc list-inside; 15 | } 16 | 17 | pre { 18 | @apply overflow-x-scroll p-4; 19 | } 20 | 21 | code { 22 | @apply text-sm; 23 | } 24 | 25 | :not(pre) code { 26 | @apply py-05 px-2 bg-gray-300 rounded border-b border-r border-gray-400; 27 | } 28 | 29 | h1, h2, h3 { 30 | 31 | @apply mb-4 leading-snug border-b; 32 | 33 | :global(.icon-link) { 34 | @apply inline-block align-baseline bg-contain bg-no-repeat w-6 h-4 -ml-6; 35 | } 36 | 37 | &:hover :global(.icon-link) { 38 | background-image: url("/assets/icons/link.svg"); 39 | } 40 | 41 | } 42 | 43 | h1 { 44 | @apply text-4xl mt-12; 45 | } 46 | 47 | h2 { 48 | @apply text-3xl mt-10; 49 | } 50 | 51 | h3 { 52 | @apply text-2xl mt-8; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['tailwindcss', 'postcss-preset-env'], 3 | } 4 | -------------------------------------------------------------------------------- /website/public/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teq/serialazy/0427acf7d6634696fff098c2e36a6c5a3a326a0a/website/public/.nojekyll -------------------------------------------------------------------------------- /website/public/CNAME: -------------------------------------------------------------------------------- 1 | serialazy.teqlab.net -------------------------------------------------------------------------------- /website/public/assets/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/public/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teq/serialazy/0427acf7d6634696fff098c2e36a6c5a3a326a0a/website/public/assets/icons/favicon.ico -------------------------------------------------------------------------------- /website/public/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/public/assets/icons/scroll-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/shiki.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'rehype-shiki'; 2 | -------------------------------------------------------------------------------- /website/styles/index.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | 7 | body { 8 | @apply overflow-y-scroll; 9 | } 10 | 11 | a { 12 | 13 | @apply underline duration-200 transition-colors; 14 | 15 | &:hover { 16 | @apply text-success; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | removeDeprecatedGapUtilities: true, 4 | purgeLayersByDefault: true, 5 | }, 6 | purge: ['./@(components|content|pages)/**/*.tsx'], 7 | theme: { 8 | extend: { 9 | colors: { 10 | cover1: '#fdfab8', 11 | cover2: '#b8ffcb', 12 | success: '#0070f3', 13 | }, 14 | spacing: { 15 | '05': '0.125rem', 16 | } 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["dom", "dom.iterable", "esnext"], 11 | "allowJs": true, 12 | "noEmit": true, 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true 18 | }, 19 | "exclude": ["node_modules"], 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 21 | } 22 | --------------------------------------------------------------------------------