Skip to content

Commit

Permalink
Initial functionality (#1)
Browse files Browse the repository at this point in the history
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
junglebarry authored Aug 29, 2017
1 parent 0a75d5c commit aa71719
Show file tree
Hide file tree
Showing 33 changed files with 1,768 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
122 changes: 122 additions & 0 deletions README.markdown
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`).
45 changes: 45 additions & 0 deletions gulpfile.js
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/**/*']);
});
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src';
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src';
38 changes: 38 additions & 0 deletions package.json
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"
}
}
67 changes: 67 additions & 0 deletions spec/decorators/entity.spec.ts
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',
}));
});
});
26 changes: 26 additions & 0 deletions spec/jsonapi/unresolved-identifiers.spec.ts
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);
});
});
});
Loading

0 comments on commit aa71719

Please sign in to comment.