Skip to content

Commit

Permalink
Merge pull request #32 from rhwilr/feature/multiple_transform_methods
Browse files Browse the repository at this point in the history
feat(variant): different transformer variants can be used
  • Loading branch information
rhwilr authored Feb 25, 2019
2 parents dd27e59 + 3cdb1bb commit 8a6ff04
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 33 deletions.
83 changes: 66 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ presentation and transformation layer for complex data output.
* [Default Include](#default-include)
* [Available Include](#available-include)
* [Parse available includes automatically](#parse-available-includes-automatically)
* [Transformer Variants](#transformer-variants)
* [EagerLoading](#eagerloading)
* [Serializers](#serializers)
* [PlainSerializer](#plainserializer)
Expand Down Expand Up @@ -179,12 +180,12 @@ return transform.collection(users, 'App/Transformers/UserTransformer')
*Note:* Passing the Transformer as the second argument will terminate the fluent
interface. If you want to chain more methods after the call to `collection` or
`item` you should only pass the first argument and then use the `transformWith`
method to define the transformer. See [Fluent Interface](#fluent-interface)
method to define the transformer. See [Fluent Interface](#fluent-interface).


### Including Data

When transforming a model you may want to include some additional data. For
When transforming a model, you may want to include some additional data. For
example, you may have a book model and want to include the author for the book
in the same resource. Include methods let you do just that.

Expand All @@ -195,11 +196,11 @@ Includes defined in the `defaultInclude` getter will always be included in the
returned data.

You have to specify the name of the include by returning an array of all
includes from the `defaultInclude` getter. Then you create an additional method
for each include, named like in the example: `include{Name}`
includes from the `defaultInclude` getter. Then you create an additional method
for each include, named like in the example: `include{Name}`.

The include method returns a new resource, that can either be an `item` or a
`collection`. See [Resources](#resources)
`collection`. See [Resources](#resources).

```js
class BookTransformer extends BumblebeeTransformer {
Expand All @@ -225,7 +226,8 @@ class BookTransformer extends BumblebeeTransformer {
module.exports = BookTransformer
```

*Note:* Just like in the transform method, you can also access to the `context` through the second argument.
*Note:* Just like in the transform method, you can also access to the `context`
through the second argument.

*Note:* If you have hyphen or underscore separated properties, you would name
the include function in camelCase. The conversion is done automatically.
Expand Down Expand Up @@ -266,20 +268,64 @@ To include this resource you call the `include()` method before transforming.
return transform.include('author').item(book, BookTransformer)
```

These includes can be nested with dot notation too, to include resources within other resources.
These includes can be nested with dot notation too, to include resources within
other resources.

```js
return transform.include('author,publisher.something').item(book, BookTransformer)
```


### Parse available includes automatically
#### Parse available includes automatically

In addition to the previously mentioned `include` method, you can also enable
`parseRequest` in the config file. Now bumblebee will automatically parse the
`?include=` GET parameter and include the requested resources.


### Transformer Variants

Sometimes you may want to transform some model in a slitely different way while
sill utilizing existing include methods. To use out book example, you may have
an api endpoint that returns a list of all books, but you don't want to include
the summary of the book in this response to save on data. However, when
requesting a single book you want the summary to be included.

You could define a separate transformer for this, but it would be much easier if
you could reuse the existing book transformer. This is where transform variants
come in play.

```js
class BookTransformer extends BumblebeeTransformer {
transform (book) {
return {
id: book.id,
title: book.title,
year: book.yr
}
}

transformWithSummary (book) {
return {
...this.transform(book),
summary: book.summary
}
}
}

module.exports = BookTransformer
```

We define a `transformWithSummary` method that calls our existing `transform`
method and adds the book summary to the result.

Now we can use this variant by specifing it as follows:

```js
return transform.collection(books, 'App/Transformers/BookTransformer.withSummary')
```



## EagerLoading

Expand Down Expand Up @@ -390,9 +436,9 @@ There is one major drawback to this serializer. It does not play nicely with met

Since you cannot mix an Array and Objects in JSON, the serializer has to add a
`data` property if you use metadata on a collection. The same is true if you use
pagination. This is why we do not recommend using the `PlainSerializer` when
using these features. But other than that, this serializer works great for small
and simple APIs.
pagination. This is why we do not recommend using `PlainSerializer` when using
these features. But other than that, this serializer works great for small and
simple APIs.


### DataSerializer
Expand Down Expand Up @@ -458,6 +504,7 @@ available on the `transform` object in the context and from `Bumblebee.create()`
- `paginate(data)`
- `meta(metadata)`
- `transformWith(transformer)`
- `usingVariant(variant)`
- `withContext(ctx)`
- `include(include)`
- `setSerializer(serializer)`
Expand All @@ -483,15 +530,17 @@ let transformed = await Bumblebee.create()
.toJSON()
```

You can use the same methods as in a controller. With one difference: If
you need the `context` inside the transformer, you have to set it with the
You can use the same methods as in a controller. With one difference: If you
need the `context` inside the transformer, you have to set it with the
`.withContext(ctx)` method since it is not automatically injected.



## Credits

Special thanks to the creator(s) of
[Fractal](https://fractal.thephpleague.com/), a PHP API transformer that was the
main inspiration for this package. Also, a huge thank goes to the creator(s) of
[AdonisJS](http://adonisjs.com/) for creating such an awesome framework.
Special thanks to the creator(s) of [Fractal], a PHP API transformer that was
the main inspiration for this package. Also, a huge thank goes to the creator(s)
of [AdonisJS] for creating such an awesome framework.

[Fractal]: https://fractal.thephpleague.com
[AdonisJS]: http://adonisjs.com
22 changes: 11 additions & 11 deletions src/Bumblebee/Manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ class Manager {
this._autoIncludeParents()
}

/**
* Allowes setting a custom recursion limit
*
* @param {*} limit
*/
setRecursionLimit (limit) {
this._recursionLimit = limit

return this
}

/**
* Create a serializer
*
Expand Down Expand Up @@ -148,17 +159,6 @@ class Manager {
parsed.forEach(this.requestedIncludes.add, this.requestedIncludes)
}

/**
* Allowes setting a custom recursion limit
*
* @param {*} limit
*/
setRecursionLimit (limit) {
this._recursionLimit = limit

return this
}

/**
* Parses the request object from the context and extracts the requested includes
*
Expand Down
50 changes: 48 additions & 2 deletions src/Bumblebee/Resources/ResourceAbstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ class ResourceAbstract {
* Constractor for the ResourceAbstract
* This allowes to set data and transformer while creating an instance
*/
constructor (data, transformer) {
constructor (data, trans) {
this.data = data
this.transformer = transformer
this.meta = null

let { transformer, variant } = this._separateTransformerAndVariation(trans)

this.transformer = transformer
this.variant = variant
}

/**
Expand Down Expand Up @@ -75,6 +79,48 @@ class ResourceAbstract {
getPagination () {
return this.pagination
}

/**
* Set the transformer variant to be used for this resource
*
* @param {Object} pagination
*/
setVariant (variant) {
if (variant) {
this.variant = variant
}

return this
}

/**
* Returns the transformer variant
*/
getVariant () {
return this.variant
}

/**
* When a transformer string is passed with a variation defined in dot-notation
* we will split the string into transformer and variant
*/
_separateTransformerAndVariation (transformerString) {
// This feature is only available when a string binding is used
if (typeof transformerString !== 'string') {
return { transformer: transformerString, variant: null }
}

let regex = /(.*)\.(.*)/

let matches = transformerString.match(regex)

// if the string did not contain a variation use the
// transformerString is used and the variation is set to null
let transformer = matches ? matches[1] : transformerString
let variant = matches ? matches[2] : null

return { transformer, variant }
}
}

module.exports = ResourceAbstract
28 changes: 27 additions & 1 deletion src/Bumblebee/Scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class Scope {

// get a transformer instance and tranform data
let transformerInstance = this._getTransformerInstance(transformer)
let transformedData = await transformerInstance.transform(await data, this._ctx)
let transformedData = await this._dispatchToTransformerVariant(transformerInstance, await data, this._ctx)

// if this transformer has includes defined,
// figure out which includes should be run and run requested includes
Expand Down Expand Up @@ -200,6 +200,32 @@ class Scope {
throw new Error('A transformer must be a function or a class extending TransformerAbstract')
}

/**
* Checks if any variants are defined and calls the corresponding transform method
*
* @param {*} transformerInstance
* @param {*} data
* @param {*} ctx
*/
async _dispatchToTransformerVariant (transformerInstance, data, ctx) {
let variant = this._resource.getVariant()

// if a variant was defined, we construct the name for the transform mehod
// otherwise, the default transformer method 'transform' is called
let transformMethodName = variant
? `transform${variant.charAt(0).toUpperCase()}${variant.slice(1)}`
: 'transform'

// Since the user can pass anything as a variant name, we need to
// validate that the transformer method exists.
if (!(transformerInstance[transformMethodName] instanceof Function)) {
throw new Error(`A transformer method '${transformMethodName}' could not be found in '${transformerInstance.constructor.name}'`)
}

// now we call the transformer method on the transformer and return the data
return transformerInstance[transformMethodName](data, this._ctx)
}

/**
* Check if the used transformer has any includes defined
*
Expand Down
2 changes: 1 addition & 1 deletion src/Bumblebee/TransformerAbstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class TransformerAbstract {
* Implementation required
*/
transform () {
throw new Error('You have to implement the method transform!')
throw new Error('You have to implement the method transform or specify a variant when calling the transformer!')
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/Bumblebee/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Bumblebee {
instance._data = data
instance._dataType = instance._determineDataType(data)
instance._transformer = transformer
instance._variant = null

// set pagination, context and meta properties to null
instance._pagination = null
Expand Down Expand Up @@ -143,6 +144,17 @@ class Bumblebee {
return this
}

/**
* Set the transformer variant
*
* @param {String} variant
*/
usingVariant (variant) {
this._variant = variant

return this
}

/**
* Allows you to set the adonis context if you are not
* using the 'transform' object from the http context.
Expand Down Expand Up @@ -230,6 +242,7 @@ class Bumblebee {

resourceInstance.setMeta(this._meta)
resourceInstance.setPagination(this._pagination)
resourceInstance.setVariant(this._variant)

return resourceInstance
}
Expand Down
Loading

0 comments on commit 8a6ff04

Please sign in to comment.