-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds initial functionality to the library, namely: * decorator definitions to be able to create Typescript classes that map to JSON:API types * serialisation and deserialisation routines for converting between JSON:API and Typescript class equivalents * A package structure that allows the library to be used from an Angular application Tests for all the above.
- Loading branch information
1 parent
0a75d5c
commit aa71719
Showing
33 changed files
with
1,768 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist/ | ||
node_modules/ |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# jsonapi-transformers | ||
|
||
This is a library for transforming between JSON:API responses and natural JSON objects. | ||
|
||
Specifically, it: | ||
|
||
* is a Typescript library | ||
* allows natural JSON objects to be represented as Typescript classes | ||
* supports serialisation of Typescript class instances to JSON:API and deserialisation back again JSON:API. | ||
|
||
# Todo | ||
|
||
- [x] Work out distribution pattern and consumption from within a TS application | ||
- [ ] Update tests and documented examples to be a neutral domain | ||
- [ ] Configure as a CircleCI private project | ||
- [ ] Install a code coverage tool and get test coverage to a reasonable level | ||
- [ ] Document all functions and types | ||
- [ ] Add references to other JSON:API libraries | ||
- [ ] PR and code review | ||
- [ ] v0.1 | ||
|
||
# Motivation | ||
|
||
The motivation for this library is manifold: | ||
|
||
* JSON:API is a relatively pleasant transmission format for JSON-over-HTTP; | ||
* ... however, we wouldn't want to use the JSON:API format in application-level JSON; | ||
* I've been doing a lot of Angular recently; | ||
* There's no Ember-level support for JSON:API in Angular; | ||
* I wanted a lightweight serialisation solution, not an ORM or something with networking (read: asynchrony) baked in; | ||
* Existing lightweight serialisation solutions like Yayson work perfectly well in many use-cases, but, I found, not so well in mine. | ||
* Angular is written in Typescript, which provides appropriate metaprogramming capabilities (and I wanted to experiment with them). | ||
|
||
# What can you do? | ||
|
||
Well, you can define Typescript classes, and have them serialise to JSON:API and deserialise from JSON:API. | ||
|
||
## Entities and attributes | ||
|
||
Imagine a Typescript interface representing the basics of an address: | ||
|
||
```typescript | ||
interface Address { | ||
houseNumber?: number; | ||
street: string; | ||
city: string; | ||
county?: string; | ||
} | ||
``` | ||
|
||
An example of this interface in JSON might be: | ||
|
||
|
||
```json | ||
{ | ||
"houseNumber": 29, | ||
"street": "Acacia Road", | ||
"city": "Nuttytown" | ||
} | ||
``` | ||
|
||
and the counterpart in JSON:API might look like this: | ||
|
||
```json | ||
{ | ||
"id": "29_acacia_road", | ||
"type": "addresses", | ||
"attributes": { | ||
"houseNumber": 29, | ||
"street": "Acacia Road", | ||
"city": "Nuttytown" | ||
} | ||
} | ||
``` | ||
|
||
So the question is, how can we map between the Typescript type and its JSON:API representation? | ||
|
||
This library uses Typescript decorators to provide this, and for a number of reasons we target classes, not interfaces. Here's our example again: | ||
|
||
```typescript | ||
@entity({ type: 'addresses' }) | ||
export class Address extends JsonapiEntity { | ||
@attribute() houseNumber?: number; | ||
@attribute() street: string; | ||
@attribute() city: string; | ||
@attribute() county?: string; | ||
} | ||
``` | ||
|
||
There are a few things to note: | ||
|
||
First, we used a Typescript class to define our type. It must extend `JsonapiEntity`, which ensures that our class provides the mandatory `id` and `type` attributes. | ||
|
||
Second, the `@entity` decorator binds our class to a JSON:API entity type. Note that `entity` is a function accepting a JSON object as its sole argument, and the `type` (as it appears in the JSON:API entity definition) must be explicitly provided. This is how `jsonapi-transformers` connects the class with its serialised form. | ||
|
||
Third, each class property that should be mapped to a JSON:API attribute uses the `@attribute` decorator. Again, this is a function accepting a JSON object as its sole argument. By default, the serialised attribute will have the same name as the class property (though this may be customised). | ||
|
||
## Relationships | ||
|
||
```typescript | ||
/** @todo */ | ||
``` | ||
|
||
## Customising JSON property names | ||
|
||
```typescript | ||
/** @todo */ | ||
``` | ||
|
||
## Handling unresolved identifiers | ||
|
||
```typescript | ||
/** @todo */ | ||
``` | ||
|
||
# Specific isses encountered with Yayson | ||
|
||
Yayson is perfectly fine, well-established, and better-supported than this library. You should almost certainly use it in favour of this library. Indeed, I am using it, at least for the time being... However, since I've mentioned that I hit some stumbling blocks for my use cases, I'll draw them out here. | ||
|
||
First, Yayson retains a single "store" of entities that it has deserialised (`sync`ed). The main implication of this is that the library encapulates its own state, and this is out of the developer's control. This library follows a functional pattern (pure functions, without internal state), so you can maintain your own state and pass it into the functions should you need it. | ||
|
||
Second, if Yayson cannot resolve a relationship to an entity, the information of that relationship (type/ID) is simply discarded. I wanted a little more control of that, so this library makes that possible. If you want to keep relationship identifiers around, you can do so (look for `UnresolvedIdentifiers`). |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
const del = require('del'); | ||
const gulp = require('gulp'); | ||
const jasmine = require('gulp-jasmine'); | ||
const ts = require('gulp-typescript'); | ||
|
||
gulp.task('build_tests', () => { | ||
return gulp.src('./spec/**/*.ts') | ||
.pipe(ts({ | ||
emitDecoratorMetadata: true, | ||
experimentalDecorators: true, | ||
lib: ['es2016'], | ||
moduleResolution: 'node', | ||
target: 'es5', | ||
})) | ||
.pipe(gulp.dest('./dist/out-tsc/spec')); | ||
}); | ||
|
||
gulp.task('test', ['build', 'build_tests'], () => { | ||
return gulp.src(['./dist/out-tsc/**/*.spec.js']) | ||
.pipe(jasmine({ | ||
verbose: false, | ||
includeStackTrace: true, | ||
})); | ||
}); | ||
|
||
gulp.task('build', () => { | ||
return gulp.src('./src/**/*.ts') | ||
.pipe(ts({ | ||
emitDecoratorMetadata: true, | ||
experimentalDecorators: true, | ||
lib: ['es2016'], | ||
moduleResolution: 'node', | ||
target: 'es5', | ||
})) | ||
.pipe(gulp.dest('./dist/out-tsc/src')); | ||
}); | ||
|
||
gulp.task('test:watch', [], () => { | ||
gulp.watch('src/**/*.ts', ['test']); | ||
gulp.watch('spec/**/*.ts', ['test']); | ||
}); | ||
|
||
gulp.task('clean', function () { | ||
return del(['dist/**/*']); | ||
}); |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './src'; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './src'; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"name": "jsonapi-transformers", | ||
"version": "1.0.0", | ||
"description": "Typescript-based JSON API serialisation/deserialisation", | ||
"main": "./index.ts", | ||
"types": "./index.d.ts", | ||
"scripts": { | ||
"build": "gulp clean && gulp build", | ||
"test": "gulp test", | ||
"test:watch": "gulp test:watch" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/junglebarry/jsonapi-transformers.git" | ||
}, | ||
"keywords": [ | ||
"Typescript", | ||
"JSON", | ||
"API", | ||
"serialisation" | ||
], | ||
"author": "David James Brooks", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/junglebarry/jsonapi-transformers/issues" | ||
}, | ||
"homepage": "https://github.com/junglebarry/jsonapi-transformers#readme", | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"@types/jasmine": "^2.5.46", | ||
"@types/node": "^7.0.8", | ||
"del": "^2.2.2", | ||
"gulp": "^3.9.1", | ||
"gulp-jasmine": "^2.4.2", | ||
"gulp-typescript": "^3.1.6", | ||
"typescript": "^2.2.1" | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { | ||
attribute, | ||
entity, | ||
relationship, | ||
toJsonApi, | ||
JsonapiEntity, | ||
} from '../../src'; | ||
|
||
import { | ||
Address, | ||
Person, | ||
} from '../test-data'; | ||
|
||
|
||
const address1: Address = new Address(); | ||
address1.id = 'address1'; | ||
address1.houseNumber = 8; | ||
address1.street = 'Acacia Road'; | ||
address1.city = 'Nuttytown'; | ||
address1.county = 'West Nutshire'; | ||
|
||
const address2: Address = new Address(); | ||
address2.id = 'address2'; | ||
address2.street = 'Mountain Drive'; | ||
address2.city = 'Gotham City'; | ||
|
||
const person1: Person = new Person(); | ||
person1.id = 'person1'; | ||
person1.firstName = 'Eric'; | ||
person1.surname = 'Wimp'; | ||
person1.address = address1; | ||
person1.oldAddresses = [address2]; | ||
|
||
describe('entity', () => { | ||
it('should respect instanceof', () => { | ||
expect(address1).toEqual(jasmine.any(Address)); | ||
expect(address2).toEqual(jasmine.any(Address)); | ||
expect(person1).toEqual(jasmine.any(Person)); | ||
}); | ||
|
||
it('should respect constructor names', () => { | ||
expect(address1.constructor.name).toEqual(Address.name); | ||
expect(address2.constructor.name).toEqual(Address.name); | ||
expect(person1.constructor.name).toEqual(Person.name); | ||
}); | ||
|
||
it('should pretty-print type names', () => { | ||
expect(address1.constructor.name).toEqual('Address'); | ||
expect(address2.constructor.name).toEqual('Address'); | ||
expect(person1.constructor.name).toEqual('Person'); | ||
}); | ||
|
||
it('should add a type property from the decorator definition', () => { | ||
expect(address1.type).toEqual('addresses'); | ||
expect(address2.type).toEqual('addresses'); | ||
expect(person1.type).toEqual('people'); | ||
}); | ||
|
||
it('should permit a natural JSON interpretation', () => { | ||
expect(address1).toEqual(jasmine.objectContaining({ | ||
id: 'address1', | ||
type: 'addresses', | ||
street: 'Acacia Road', | ||
city: 'Nuttytown', | ||
})); | ||
}); | ||
}); |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { | ||
isUnresolvedIdentifier, | ||
unresolvedIdentifier, | ||
JsonapiEntity, | ||
ResourceIdentifier, | ||
UnresolvedResourceIdentifier, | ||
} from '../../src'; | ||
|
||
describe('unresolved-identifiers', () => { | ||
|
||
class FakeJsonapiEntity extends JsonapiEntity { | ||
id = 'foo'; | ||
type = 'things'; | ||
} | ||
|
||
describe('isUnresolvedIdentifier', () => { | ||
it('should return true for a valid UnresolvedResourceIdentifier', () => { | ||
expect(isUnresolvedIdentifier(new UnresolvedResourceIdentifier('foo', 'things'))).toEqual(true); | ||
}); | ||
|
||
it('should return false for other subtypes of ResourceIdentifier', () => { | ||
expect(isUnresolvedIdentifier({ id: 'foo', type: 'things' })).toEqual(false); | ||
expect(isUnresolvedIdentifier(new FakeJsonapiEntity())).toEqual(false); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.