Skip to content

Commit 352217e

Browse files
committed
First commit
0 parents  commit 352217e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1216
-0
lines changed

.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["@babel/preset-typescript"]
3+
}

.editorconfig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# EditorConfig is awesome: https://EditorConfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
[*]
7+
indent_style = space
8+
indent_size = 2
9+
end_of_line = lf
10+
charset = utf-8
11+
trim_trailing_whitespace = true
12+
insert_final_newline = true

.github/workflows/test.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Tests
2+
on: [push, pull_request]
3+
jobs:
4+
build:
5+
runs-on: ubuntu-latest
6+
strategy:
7+
matrix:
8+
node: [ '12', '14', '16' ]
9+
name: Node ${{ matrix.node }} sample
10+
steps:
11+
- uses: actions/checkout@v2
12+
- name: Setup node
13+
uses: actions/setup-node@v2
14+
with:
15+
node-version: ${{ matrix.node }}
16+
- run: yarn install
17+
- run: yarn test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/node_modules

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Mònade srl
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 all
13+
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 THE
21+
SOFTWARE.

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
![Tests](https://github.com/monade/json-api-parser/actions/workflows/test.yml/badge.svg)
2+
3+
# @monade/json-api-parser
4+
5+
A parser for [JSON:API](https://jsonapi.org/) format that maps data to models using decorators, inspired by retrofit.
6+
7+
## Installation
8+
9+
```bash
10+
npm install https://github.com/monade/json-api-parser.git
11+
```
12+
13+
## Example usage
14+
15+
```typescript
16+
import { Attr, JSONAPI, Model, Rel } from "../src";
17+
18+
export const DateParser = (data: any) => new Date(data);
19+
20+
@JSONAPI("posts")
21+
export class Post extends Model {
22+
@Attr() name!: string;
23+
@Attr("description") content!: string;
24+
@Attr("created_at", { parser: DateParser }) createdAt!: Date;
25+
@Attr("active", { default: true }) enabled!: boolean;
26+
@Attr() missing!: boolean;
27+
28+
@Rel("user") author!: User;
29+
@Rel() reviewer!: User | null;
30+
}
31+
32+
@JSONAPI("users")
33+
class User extends Model {
34+
@Attr() firstName!: string;
35+
@Attr() lastName!: string;
36+
@Attr("created_at", { parser: DateParser }) createdAt!: Date;
37+
38+
@Rel() favouritePost!: Post;
39+
}
40+
```
41+
42+
## TODO
43+
* Documentation
44+
* Edge case tests
45+
* Release on NPM

dist/index.cjs.js

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
'use strict';
2+
3+
Object.defineProperty(exports, '__esModule', { value: true });
4+
5+
class Model {
6+
toJSON() {
7+
return { ...this
8+
};
9+
}
10+
11+
toFormData() {
12+
const data = this.toJSON();
13+
const formData = new FormData();
14+
15+
for (const key in data) {
16+
if (data[key] === null || data[key] === undefined) {
17+
continue;
18+
}
19+
20+
if (Array.isArray(data[key])) {
21+
for (const value of data[key]) {
22+
formData.append(key + "[]", value);
23+
}
24+
} else if (data[key] instanceof File) {
25+
formData.append(key, data[key], data[key].filename);
26+
} else {
27+
formData.append(key, data[key]);
28+
}
29+
}
30+
31+
return formData;
32+
}
33+
34+
}
35+
36+
function debug(...args) {
37+
debug.adapter(...args);
38+
}
39+
40+
debug.adapter = (...args) => console.warn(...args);
41+
42+
class Parser {
43+
static $registeredModels = [];
44+
static $registeredAttributes = [];
45+
static $registeredRelationships = [];
46+
resolved = {};
47+
48+
constructor(data, included = []) {
49+
this.data = data;
50+
this.included = included;
51+
}
52+
53+
run() {
54+
if (!this.data) {
55+
return null;
56+
}
57+
58+
const {
59+
data,
60+
included
61+
} = this;
62+
const fullIncluded = Array.isArray(data) ? [...data, ...included] : [data, ...included];
63+
return this.parse(data, fullIncluded);
64+
}
65+
66+
parse(data, included = []) {
67+
if (!data) {
68+
return null;
69+
}
70+
71+
if (Array.isArray(data)) {
72+
return this.parseList(data, included);
73+
} else if ("data" in data && !("id" in data)) {
74+
return this.parse(data.data, data.included || included);
75+
} else {
76+
return this.parseElement(data, included);
77+
}
78+
}
79+
80+
parseList(list, included) {
81+
return list.map(e => {
82+
return this.parseElement(e, included);
83+
});
84+
}
85+
86+
parseElement(element, included) {
87+
const uniqueKey = `${element.id}$${element.type}`;
88+
89+
if (this.resolved[uniqueKey]) {
90+
return this.resolved[uniqueKey];
91+
}
92+
93+
const loadedElement = Parser.load(element, included);
94+
const model = Parser.$registeredModels.find(e => e.type === loadedElement.type);
95+
const attrData = Parser.$registeredAttributes.find(e => e.klass === model?.klass);
96+
const relsData = Parser.$registeredRelationships.find(e => e.klass === model?.klass);
97+
const instance = new (model?.klass || Model)();
98+
this.resolved[uniqueKey] = instance;
99+
instance.id = loadedElement.id;
100+
101+
for (const key in loadedElement.attributes) {
102+
const parser = attrData?.attributes?.[key];
103+
104+
if (parser) {
105+
instance[parser.key] = parser.parser(loadedElement.attributes[key]);
106+
} else {
107+
instance[key] = loadedElement.attributes[key];
108+
debug(`Undeclared key "${key}" in "${loadedElement.type}"`);
109+
}
110+
}
111+
112+
if (attrData) {
113+
for (const key in attrData.attributes) {
114+
const parser = attrData.attributes[key];
115+
116+
if (!(parser.key in instance)) {
117+
if ("default" in parser) {
118+
instance[parser.key] = parser.default;
119+
} else {
120+
debug(`Missing attribute "${key}" in "${loadedElement.type}"`);
121+
}
122+
}
123+
}
124+
}
125+
126+
for (const key in loadedElement.relationships) {
127+
const relation = loadedElement.relationships[key];
128+
const parser = relsData?.attributes?.[key];
129+
130+
if (parser) {
131+
instance[parser.key] = parser.parser(this.parse(relation, included));
132+
} else {
133+
instance[key] = this.parse(relation, included);
134+
debug(`Undeclared relationship "${key}" in "${loadedElement.type}"`);
135+
}
136+
}
137+
138+
if (relsData) {
139+
for (const key in relsData.attributes) {
140+
const parser = relsData.attributes[key];
141+
142+
if (!(parser.key in instance)) {
143+
if ("default" in parser) {
144+
instance[parser.key] = parser.default;
145+
} else {
146+
debug(`Missing relationships "${key}" in "${loadedElement.type}"`);
147+
}
148+
}
149+
}
150+
}
151+
152+
return instance;
153+
}
154+
155+
static load(element, included) {
156+
const found = included.find(e => e.id == element.id && e.type === element.type);
157+
158+
if (!found) {
159+
debug(`Relationship with type ${element.type} with id ${element.id} not present in included`);
160+
}
161+
162+
return found || { ...element,
163+
$_partial: true
164+
};
165+
}
166+
167+
}
168+
169+
function Attr(sourceKey, options = {
170+
parser: v => v
171+
}) {
172+
return function _Attr(klass, key) {
173+
let model = Parser.$registeredAttributes.find(e => e.klass === klass.constructor);
174+
175+
if (!model) {
176+
model = {
177+
attributes: {},
178+
klass: klass.constructor
179+
};
180+
Parser.$registeredAttributes.push(model);
181+
}
182+
183+
const data = {
184+
parser: options.parser ?? (v => v),
185+
key
186+
};
187+
188+
if ("default" in options) {
189+
data.default = options.default;
190+
}
191+
192+
model.attributes[sourceKey ?? key] = data;
193+
};
194+
}
195+
196+
function Rel(sourceKey, options = {
197+
parser: v => v
198+
}) {
199+
return function _Rel(klass, key) {
200+
let model = Parser.$registeredRelationships.find(e => e.klass === klass.constructor);
201+
202+
if (!model) {
203+
model = {
204+
attributes: {},
205+
klass: klass.constructor
206+
};
207+
Parser.$registeredRelationships.push(model);
208+
}
209+
210+
model.attributes[sourceKey ?? key] = {
211+
parser: options.parser ?? (v => v),
212+
key,
213+
default: options.default
214+
};
215+
};
216+
}
217+
218+
function JSONAPI(type) {
219+
return function _JSONAPI(constructor) {
220+
Parser.$registeredModels.push({
221+
klass: constructor,
222+
type
223+
});
224+
};
225+
}
226+
227+
exports.Attr = Attr;
228+
exports.JSONAPI = JSONAPI;
229+
exports.Model = Model;
230+
exports.Parser = Parser;
231+
exports.Rel = Rel;
232+
exports.debug = debug;

0 commit comments

Comments
 (0)