Last active
March 30, 2022 20:36
-
-
Save ProGM/0211b8d7c42fa0b5de7a10455437123f to your computer and use it in GitHub Desktop.
New Model Parser for JSON::API. Used as a bases for: https://github.com/monade/json-api-parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Parser } from './parser'; | |
import 'models'; | |
const parsable = `{ | |
"data": [ | |
{ | |
"id": "2", "type": "posts", | |
"attributes": { "name": "My post", "ciao": null, "description": "ciao", "created_at": "2020-10-10T10:32:00Z" }, | |
"relationships": { "user": { "data": { "id": "3", "type": "users" } } } | |
} | |
], | |
"included": [ | |
{ | |
"id": "3", "type": "users", | |
"attributes": { "firstName": "Gino", "lastName": "Pino", "created_at": "2020-10-15T10:32:00Z" }, | |
"relationships": { "favouritePost": { "data": { "id": "2", "type": "posts" } } } | |
} | |
] | |
}` | |
const parsed = JSON.parse(parsable) | |
console.log(new Parser(parsed.data, parsed.included).run()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { JSONAPI, Attr, Rel, DateParser } from './parser'; | |
@JSONAPI('posts') | |
class Post extends Model { | |
@Attr() name!: string; | |
@Attr('description') content!: string; | |
@Attr('created_at', { parser: DateParser }) createdAt!: Date; | |
@Attr('active', { default: true }) enabled!: boolean; | |
@Attr() missing!: boolean; | |
@Rel('user') author!: User; | |
} | |
@JSONAPI('users') | |
class User extends Model { | |
@Attr() firstName!: string; | |
@Attr() lastName!: string; | |
@Attr('created_at', { parser: DateParser }) createdAt!: string; | |
@Rel() favouritePost!: Post; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface JSONModel { | |
id: string; | |
type: string; | |
attributes?: { [key: string]: any }; | |
relationships?: { [key: string]: JSONData }; | |
} | |
export interface JSONData { | |
data: JSONModel | JSONModel[]; | |
included: JSONModel[]; | |
} | |
export class Model { | |
id!: string; | |
toJSON(): any { | |
return { ...this }; | |
} | |
toFormData() { | |
const data = this.toJSON(); | |
const formData = new FormData(); | |
for (const key in data) { | |
if (data[key] !== null && data[key] !== undefined) { | |
if (Array.isArray(data[key])) { | |
for (const value of data[key]) { | |
formData.append(key + '[]', value); | |
} | |
} else if (data[key] instanceof File) { | |
formData.append(key, data[key], data[key].filename); | |
} else { | |
formData.append(key, data[key]); | |
} | |
} | |
} | |
return formData; | |
} | |
} | |
interface RegisteredProperty { | |
key: string; | |
default?: any; | |
parser: (value: any) => any; | |
} | |
interface RegisteredModel { | |
type: string; | |
klass: typeof Model; | |
} | |
interface RegisteredAttribute { | |
klass: any; | |
attributes: Record<string, RegisteredProperty>; | |
} | |
function debug(...args: any[]) { | |
console.warn(...args); | |
} | |
export class Parser { | |
static $registeredModels: RegisteredModel[] = []; | |
static $registeredAttributes: RegisteredAttribute[] = []; | |
static $registeredRelationships: RegisteredAttribute[] = []; | |
readonly resolved: Record<string, Model> = {}; | |
constructor(private data: JSONModel[] | JSONModel, private included: JSONModel[] = []) { | |
} | |
run<T>(): T | T[] | null { | |
if (!this.data) { | |
return null; | |
} | |
const { data, included } = this; | |
const fullIncluded = Array.isArray(data) ? [...data, ...included] : [data, ...included]; | |
return this.parse(data, fullIncluded); | |
} | |
private parse<T>(data: JSONData | JSONModel[] | JSONModel, included: JSONModel[] = []): T | T[] | null { | |
if (!this.data) { | |
return null; | |
} | |
if (Array.isArray(data)) { | |
return this.parseList(data, included) as T[]; | |
} else if ('data' in data && !('id' in data)) { | |
return this.parse(data.data, data.included || included); | |
} else { | |
return this.parseElement(data, included) as T; | |
} | |
} | |
parseList(list: JSONModel[], included: JSONModel[]) { | |
return list.map((e) => { | |
return this.parseElement(e, included); | |
}); | |
} | |
parseElement<T>(element: JSONModel, included: JSONModel[]): T { | |
const uniqueKey = `${element.id}$${element.type}`; | |
if (this.resolved[uniqueKey]) { | |
return this.resolved[uniqueKey] as any; | |
} | |
const loadedElement = Parser.load(element, included); | |
const model = Parser.$registeredModels.find(e => e.type === loadedElement.type); | |
const attrData = Parser.$registeredAttributes.find(e => e.klass === model?.klass); | |
const relsData = Parser.$registeredRelationships.find(e => e.klass === model?.klass); | |
const instance = new (model?.klass || Model)(); | |
this.resolved[uniqueKey] = instance; | |
instance.id = loadedElement.id; | |
for (const key in loadedElement.attributes) { | |
const parser = attrData?.attributes?.[key]; | |
if (parser) { | |
(instance as any)[parser.key] = parser.parser(loadedElement.attributes[key]); | |
} else { | |
(instance as any)[key] = loadedElement.attributes[key]; | |
debug(`Undeclared key "${key}" in "${loadedElement.type}"`) | |
} | |
} | |
if (attrData) { | |
for (const key in attrData.attributes) { | |
const parser: RegisteredProperty = attrData.attributes[key]; | |
if (!(parser.key in instance)) { | |
if ('default' in parser) { | |
(instance as any)[parser.key] = parser.default; | |
} else { | |
debug(`Missing attribute "${key}" in "${loadedElement.type}"`) | |
} | |
} | |
} | |
} | |
for (const key in loadedElement.relationships) { | |
const relation = loadedElement.relationships[key]; | |
const parser = relsData?.attributes?.[key]; | |
if (parser) { | |
(instance as any)[parser.key] = parser.parser(this.parse(relation, included)); | |
} else { | |
(instance as any)[key] = this.parse(relation, included); | |
debug(`Undeclared relationship "${key}" in "${loadedElement.type}"`) | |
} | |
} | |
if (relsData) { | |
for (const key in relsData.attributes) { | |
const parser: RegisteredProperty = relsData.attributes[key]; | |
if (!(parser.key in instance)) { | |
if ('default' in parser) { | |
(instance as any)[parser.key] = parser.default; | |
} else { | |
debug(`Missing relationships "${key}" in "${loadedElement.type}"`) | |
} | |
} | |
} | |
} | |
return instance as any; | |
} | |
static load(element: JSONModel, included: JSONModel[]) { | |
const found = included.find((e) => e.id == element.id && e.type === element.type); | |
if (!found) { | |
debug(`Relationship with type ${element.type} with id ${element.id} not present in included`); | |
} | |
return ( | |
found || { ...element, $_partial: true } | |
); | |
} | |
} | |
export function JSONAPI(type: string) { | |
return function _Model<T extends typeof Model>(constructor: T) { | |
Parser.$registeredModels.push({ | |
klass: constructor, | |
type, | |
}) | |
} | |
} | |
export function Attr(sourceKey?: string, options: { default?: any; parser?: (v: any) => any } = { parser: ((v) => v) }) { | |
return function _Attr<T extends Model>(klass: T, key: string) { | |
let model = Parser.$registeredAttributes.find(e => e.klass === klass.constructor) | |
if (!model) { | |
model = { attributes: {}, klass: klass.constructor }; | |
Parser.$registeredAttributes.push(model); | |
} | |
const data: RegisteredProperty = { | |
parser: options.parser ?? ((v) => v), key | |
}; | |
if ('default' in options) { | |
data.default = options.default; | |
} | |
model.attributes[sourceKey ?? key] = data; | |
} | |
} | |
export function Rel(sourceKey?: string, options: { default?: any; parser?: (v: any) => any } = { parser: ((v) => v) }) { | |
return function _Rel<T extends Model>(klass: T, key: string) { | |
let model = Parser.$registeredRelationships.find(e => e.klass === klass.constructor) | |
if (!model) { | |
model = { attributes: {}, klass: klass.constructor }; | |
Parser.$registeredRelationships.push(model); | |
} | |
model.attributes[sourceKey ?? key] = { | |
parser: options.parser ?? ((v) => v), key, default: options.default | |
}; | |
} | |
} | |
// FIXME: Use moment or date-fns | |
export const DateParser = (data: any) => new Date(data); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment