├── .gitignore ├── server.json ├── tests ├── resources │ ├── PlanetTransformer.cfc │ ├── Planet.cfc │ ├── Country.cfc │ ├── Author.cfc │ ├── Book.cfc │ ├── BookWithNullIncludesTransformer.cfc │ ├── CountryTransformer.cfc │ ├── SpecializedSerializerBookTransformer.cfc │ ├── DefaultIncludesAuthorTransformer.cfc │ ├── AuthorTransformer.cfc │ ├── DefaultIncludesBookTransformer.cfc │ └── BookTransformer.cfc ├── runner.cfm ├── Application.cfc └── specs │ ├── unit │ ├── serializers │ │ ├── SimpleSerializerTest.cfc │ │ ├── DataSerializerTest.cfc │ │ ├── ResultsMapSerializerTest.cfc │ │ └── XMLSerializerTest.cfc │ ├── BuilderTest.cfc │ ├── ManagerTest.cfc │ ├── resources │ │ ├── ResourceTest.cfc │ │ ├── ItemTest.cfc │ │ └── CollectionTest.cfc │ ├── TransformerTest.cfc │ └── ScopeTest.cfc │ └── integration │ ├── FractalBuilderTest.cfc │ └── FractalTest.cfc ├── .all-contributorsrc ├── .travis.yml ├── box.json ├── models ├── resources │ ├── Item.cfc │ ├── Collection.cfc │ └── AbstractResource.cfc ├── serializers │ ├── SimpleSerializer.cfc │ ├── DataSerializer.cfc │ ├── ResultsMapSerializer.cfc │ └── XMLSerializer.cfc ├── Manager.cfc ├── Builder.cfc ├── transformers │ └── AbstractTransformer.cfc └── Scope.cfc ├── ModuleConfig.cfc └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | /testbox 2 | /tests/results 3 | /tests/resources/app/coldbox 4 | /node_modules 5 | /modules -------------------------------------------------------------------------------- /server.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "cfengine":"adobe@10" 4 | }, 5 | "web":{ 6 | "http":{ 7 | "port":"49652" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/resources/PlanetTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | function transform( planet ) { 4 | return { 5 | "id" = planet.getId(), 6 | "name" = planet.getName() 7 | }; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /tests/resources/Planet.cfc: -------------------------------------------------------------------------------- 1 | component accessors="true" { 2 | 3 | property name="id"; 4 | property name="name"; 5 | 6 | function init( struct args = {} ) { 7 | for ( var arg in args ) { 8 | if ( structKeyExists( variables, "set#arg#" ) || structKeyExists( this, "set#arg#" ) ) { 9 | invoke( this, "set#arg#", { 1 = args[ arg ] } ); 10 | } 11 | } 12 | return this; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /tests/resources/Country.cfc: -------------------------------------------------------------------------------- 1 | component accessors="true" { 2 | 3 | property name="id"; 4 | property name="name"; 5 | property name="planet"; 6 | 7 | function init( struct args = {} ) { 8 | for ( var arg in args ) { 9 | if ( structKeyExists( variables, "set#arg#" ) || structKeyExists( this, "set#arg#" ) ) { 10 | invoke( this, "set#arg#", { 1 = args[ arg ] } ); 11 | } 12 | } 13 | return this; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /tests/resources/Author.cfc: -------------------------------------------------------------------------------- 1 | component accessors="true" { 2 | 3 | property name="id"; 4 | property name="name"; 5 | property name="birthdate"; 6 | property name="country"; 7 | property name="books"; 8 | 9 | function init( struct args = {} ) { 10 | for ( var arg in args ) { 11 | if ( structKeyExists( variables, "set#arg#" ) || structKeyExists( this, "set#arg#" ) ) { 12 | invoke( this, "set#arg#", { 1 = args[ arg ] } ); 13 | } 14 | } 15 | return this; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tests/resources/Book.cfc: -------------------------------------------------------------------------------- 1 | component accessors="true" { 2 | 3 | property name="id"; 4 | property name="title"; 5 | property name="year"; 6 | property name="author"; 7 | property name="isClassic" default="false"; 8 | 9 | function init( struct args = {} ) { 10 | for ( var arg in args ) { 11 | if ( structKeyExists( variables, "set#arg#" ) || structKeyExists( this, "set#arg#" ) ) { 12 | invoke( this, "set#arg#", { 1 = args[ arg ] } ); 13 | } 14 | } 15 | return this; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/resources/BookWithNullIncludesTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | variables.availableIncludes = [ "author" ]; 4 | 5 | function transform( book ) { 6 | return { 7 | "id" = book.getId(), 8 | "title" = book.getTitle(), 9 | "year" = book.getYear() 10 | }; 11 | } 12 | 13 | function includeAuthor( book ) { 14 | return item( 15 | javacast( "null", "" ), 16 | new tests.resources.AuthorTransformer().setManager( manager ) 17 | ); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /tests/resources/CountryTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | variables.availableIncludes = [ "planet" ]; 4 | 5 | function transform( country ) { 6 | return { 7 | "id" = country.getId(), 8 | "name" = country.getName() 9 | }; 10 | } 11 | 12 | function includePlanet( country ) { 13 | return item( country.getPlanet(), function( planet ) { 14 | return { 15 | "id" = planet.getId(), 16 | "name" = planet.getName() 17 | }; 18 | } ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /tests/resources/SpecializedSerializerBookTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | variables.defaultIncludes = [ "author" ]; 4 | 5 | function transform( book ) { 6 | return { 7 | "id" = book.getId(), 8 | "title" = book.getTitle(), 9 | "year" = book.getYear() 10 | }; 11 | } 12 | 13 | function includeAuthor( book ) { 14 | return item( book.getAuthor(), function( author ) { 15 | return { 16 | "name" = author.getName() 17 | }; 18 | }, new cffractal.models.serializers.SimpleSerializer() ); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /tests/resources/DefaultIncludesAuthorTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | function init( withDefaultCountry = false ) { 4 | if ( withDefaultCountry ) { 5 | variables.defaultIncludes = [ "country" ]; 6 | } 7 | else { 8 | variables.availableIncludes = [ "country" ]; 9 | } 10 | } 11 | 12 | function transform( author ) { 13 | return { 14 | "name" = author.getName() 15 | }; 16 | } 17 | 18 | function includeCountry( author ) { 19 | return item( author.getCountry(), new tests.resources.CountryTransformer().setManager( manager ) ); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tests/resources/AuthorTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | variables.availableIncludes = [ "country", "books" ]; 4 | 5 | function transform( author ) { 6 | return { 7 | "name" = author.getName() 8 | }; 9 | } 10 | 11 | function includeBooks( author ) { 12 | return collection( 13 | author.getBooks(), 14 | new tests.resources.BookTransformer().setManager( manager ), 15 | new cffractal.models.serializers.ResultsMapSerializer() 16 | ); 17 | } 18 | 19 | function includeCountry( author ) { 20 | return item( author.getCountry(), new tests.resources.CountryTransformer().setManager( manager ) ); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /tests/runner.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "cffractal", 3 | "projectOwner": "coldbox-modules", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "elpete", 12 | "name": "Eric Peterson", 13 | "avatar_url": "https://avatars2.githubusercontent.com/u/2583646?v=3", 14 | "profile": "https://github.com/elpete", 15 | "contributions": [ 16 | "code", 17 | "doc", 18 | "example", 19 | "test" 20 | ] 21 | }, 22 | { 23 | "login": "jclausen", 24 | "name": "Jon Clausen", 25 | "avatar_url": "https://avatars0.githubusercontent.com/u/5255645?v=3", 26 | "profile": "http://silowebworks.com", 27 | "contributions": [ 28 | "bug", 29 | "design", 30 | "doc" 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tests/Application.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | this.name = "ColdBoxTestingSuite" & hash(getCurrentTemplatePath()); 4 | this.sessionManagement = true; 5 | this.setClientCookies = true; 6 | this.sessionTimeout = createTimeSpan( 0, 0, 15, 0 ); 7 | this.applicationTimeout = createTimeSpan( 0, 0, 15, 0 ); 8 | 9 | testsPath = getDirectoryFromPath( getCurrentTemplatePath() ); 10 | this.mappings[ "/tests" ] = testsPath; 11 | rootPath = REReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); 12 | this.mappings[ "/root" ] = rootPath; 13 | this.mappings[ "/cffractal" ] = rootPath; 14 | this.mappings[ "/testingModuleRoot" ] = listDeleteAt( rootPath, listLen( rootPath, '\/' ), "\/" ); 15 | this.mappings[ "/app" ] = testsPath & "resources/app"; 16 | this.mappings[ "/coldbox" ] = testsPath & "resources/app/coldbox"; 17 | this.mappings[ "/testbox" ] = rootPath & "/testbox"; 18 | } 19 | -------------------------------------------------------------------------------- /tests/resources/DefaultIncludesBookTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | variables.resourceKey = "book"; 4 | variables.defaultIncludes = [ "author", "bookCount" ]; 5 | 6 | function init( withDefaultCountry = false ) { 7 | variables.withDefaultCountry = arguments.withDefaultCountry; 8 | return this; 9 | } 10 | 11 | function transform( book ) { 12 | return { 13 | "id" = book.getId(), 14 | "title" = book.getTitle(), 15 | "year" = book.getYear() 16 | }; 17 | } 18 | 19 | function includeAuthor( book ) { 20 | return item( 21 | book.getAuthor(), 22 | new tests.resources.DefaultIncludesAuthorTransformer( withDefaultCountry ).setManager( manager ) 23 | ); 24 | } 25 | 26 | function includeBookCount( book ) { 27 | return 4; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: required 3 | jdk: 4 | - openjdk8 5 | cache: 6 | directories: 7 | - $HOME/.CommandBox 8 | env: 9 | matrix: 10 | - ENGINE=lucee@5 11 | - ENGINE=lucee@4.5 12 | - ENGINE=adobe@2018 13 | - ENGINE=adobe@2016 14 | - ENGINE=adobe@11 15 | - ENGINE=adobe@10 16 | before_install: 17 | - curl -fsSl https://downloads.ortussolutions.com/debs/gpg | sudo apt-key add - 18 | - sudo echo "deb http://downloads.ortussolutions.com/debs/noarch /" | sudo tee -a /etc/apt/sources.list.d/commandbox.list 19 | install: 20 | - sudo apt-get update && sudo apt-get --assume-yes install commandbox 21 | - box install 22 | before_script: 23 | - box server start cfengine=$ENGINE port=8500 24 | script: 25 | - box testbox run runner='http://127.0.0.1:8500/tests/runner.cfm' 26 | after_success: 27 | - box install commandbox-semantic-release 28 | - box config set endpoints.forgebox.APIToken=${FORGEBOX_TOKEN} 29 | - box semantic-release 30 | notifications: 31 | email: false 32 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"cffractal", 3 | "version":"8.1.1", 4 | "author":"Eric Peterson", 5 | "location":"forgeboxStorage", 6 | "homepage":"https://github.com/coldbox-modules/cffractal", 7 | "documentation":"https://github.com/coldbox-modules/cffractal", 8 | "repository":{ 9 | "type":"git", 10 | "URL":"https://github.com/coldbox-modules/cffractal" 11 | }, 12 | "bugs":"https://github.com/coldbox-modules/cffractal/issues", 13 | "slug":"cffractal", 14 | "shortDescription":"Transform business models to JSON data structures. Based on the Fractal PHP library.", 15 | "description":"Transform business models to JSON data structures. Based on the Fractal PHP library.", 16 | "type":"modules", 17 | "dependencies":{}, 18 | "devDependencies":{ 19 | "coldbox":"^4.3.0+188", 20 | "testbox":"2.4.*" 21 | }, 22 | "installPaths":{ 23 | "testbox":"testbox/", 24 | "coldbox":"tests/resources/app/coldbox/" 25 | }, 26 | "scripts":{}, 27 | "ignore":[ 28 | "**/.*", 29 | "test", 30 | "tests" 31 | ], 32 | "testbox":{ 33 | "runner":"http://localhost:49652/tests/runner.cfm", 34 | "reporter":"json", 35 | "verbose":"false", 36 | "watchPaths":"/**/*.cfc" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/specs/unit/serializers/SimpleSerializerTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | function run() { 3 | describe( "simple serializer", function() { 4 | it( "serializes the data", function() { 5 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 6 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 7 | mockItem.$( "process" ).$args( mockScope ).$results( { "foo" = "bar" } ); 8 | var serializer = new cffractal.models.serializers.SimpleSerializer(); 9 | expect( serializer.data( mockItem, mockScope ) ) 10 | .toBe( { "foo" = "bar" } ); 11 | } ); 12 | 13 | it( "serializes the metadata", function() { 14 | var pagingData = { "pagination" = { "maxrows" = 50, "page" = 1, "pages" = 3, "totalRecords" = 112 } }; 15 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 16 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 17 | mockItem.$( "getMeta", pagingData, false ); 18 | var serializer = new cffractal.models.serializers.SimpleSerializer(); 19 | expect( serializer.meta( mockItem ) ).toBe( pagingData ); 20 | } ); 21 | } ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /models/resources/Item.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Item 3 | * @package cffractal.models.resources 4 | * @description Defines how to convert single items in to serializable data. 5 | */ 6 | component extends="cffractal.models.resources.AbstractResource" { 7 | 8 | /** 9 | * Processes the conversion of a resource to serializable data. 10 | * Also processes any default or requested includes. 11 | * 12 | * @scope A Fractal scope instance. Used to determinal requested 13 | * includes and handle nesting identifiers. 14 | * 15 | * @returns The transformed data. 16 | */ 17 | function process( scope ) { 18 | var transformedItem = processItem( 19 | scope, 20 | isNull( data ) ? javacast( "null", "" ) : data 21 | ); 22 | 23 | arrayEach( postTransformationCallbacks, function( callback ) { 24 | transformedItem = paramNull( 25 | callback( 26 | transformedItem, 27 | isNull( data ) ? javacast( "null", "" ) : data, 28 | this 29 | ), 30 | scope.getNullDefaultValue() 31 | ); 32 | } ); 33 | 34 | return transformedItem; 35 | } 36 | 37 | /** 38 | * Returns false as an item is never paged. 39 | * 40 | * @returns false 41 | */ 42 | function hasPagingData() { 43 | return false; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /tests/resources/BookTransformer.cfc: -------------------------------------------------------------------------------- 1 | component extends="cffractal.models.transformers.AbstractTransformer" { 2 | 3 | variables.resourceKey = "book"; 4 | variables.availableIncludes = [ "author", "isClassic" ]; 5 | 6 | function init( sortKeys = true, authorTransformer = new tests.resources.AuthorTransformer() ) { 7 | variables.sortKeys = arguments.sortKeys; 8 | variables.authorTransformer = arguments.authorTransformer; 9 | } 10 | 11 | function transform( book ) { 12 | if ( sortKeys ) { 13 | return { 14 | "id" = book.getId(), 15 | "title" = book.getTitle(), 16 | "year" = book.getYear(), 17 | "isClassic" = book.getIsClassic() 18 | }; 19 | } 20 | else { 21 | var hashMap = createObject( "java", "java.util.LinkedHashMap" ).init(); 22 | hashMap[ "year" ] = "1960"; 23 | hashMap[ "title" ] = "To Kill a Mockingbird"; 24 | hashMap[ "id" ] = 1; 25 | hashMap[ "isClassic" ] = book.getIsClassic(); 26 | return hashMap; 27 | } 28 | } 29 | 30 | function includeAuthor( book ) { 31 | return item( 32 | book.getAuthor(), 33 | variables.authorTransformer.setManager( manager ) 34 | ); 35 | } 36 | 37 | function includeIsClassic( book ) { 38 | return book.getIsClassic(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/specs/unit/serializers/DataSerializerTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | function run() { 3 | describe( "data serializer", function() { 4 | it( "serializes the data", function() { 5 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 6 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 7 | mockItem.$( "process" ).$args( mockScope ).$results( { "foo" = "bar" } ); 8 | var serializer = new cffractal.models.serializers.DataSerializer(); 9 | expect( serializer.data( mockItem, mockScope ) ) 10 | .toBe( { "data" = { "foo" = "bar" } } ); 11 | } ); 12 | 13 | it( "serializes the metadata", function() { 14 | var pagingData = { "pagination" = { "maxrows" = 50, "page" = 1, "pages" = 3, "totalRecords" = 112 } }; 15 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 16 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 17 | mockItem.$( "getMeta", pagingData, false ); 18 | var serializer = new cffractal.models.serializers.DataSerializer(); 19 | expect( serializer.meta( mockItem, mockScope, {} ) ) 20 | .toBe( { "meta" = pagingData } ); 21 | } ); 22 | } ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /models/resources/Collection.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Collection 3 | * @package cffractal.models.resources 4 | * @description Defines how to convert collections in to serializable data. 5 | */ 6 | component extends="cffractal.models.resources.AbstractResource" { 7 | 8 | /** 9 | * Processes the conversion of a resource to serializable data. 10 | * Also processes any default or requested includes. 11 | * 12 | * @scope A Fractal scope instance. Used to determinal requested 13 | * includes and handle nesting identifiers. 14 | * 15 | * @returns The transformed data. 16 | */ 17 | function process( scope ) { 18 | if ( isNull( data ) ) { 19 | return []; 20 | } 21 | 22 | var transformedDataArray = []; 23 | 24 | arrayEach( data, function( value ){ 25 | var transformedItem = processItem( scope, value ); 26 | for ( var callback in postTransformationCallbacks ) { 27 | transformedItem = paramNull( 28 | callback( 29 | transformedItem, 30 | isNull( value ) ? javacast( "null", "" ) : value, 31 | this 32 | ), 33 | scope.getNullDefaultValue() 34 | ); 35 | } 36 | arrayAppend( 37 | transformedDataArray, 38 | transformedItem 39 | ); 40 | } ); 41 | 42 | return transformedDataArray; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /tests/specs/unit/BuilderTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "wirebox integration", function() { 5 | it( "accepts wirebox slugs for a transformer", function() { 6 | var builder = prepareMock( 7 | new cffractal.models.Builder( 8 | new cffractal.models.Manager( 9 | new cffractal.models.serializers.SimpleSerializer(), 10 | new cffractal.models.serializers.DataSerializer() 11 | ) 12 | ) 13 | ); 14 | 15 | var mockWireBox = createStub().$( "getInstance", function( item ) { return item; } ); 16 | 17 | builder.$property( propertyName = "wirebox", mock = mockWireBox ); 18 | 19 | builder 20 | .item( { id = 1, name = "John" } ) 21 | .withTransformer( "MyWireBoxSlug" ) 22 | .convert(); 23 | 24 | var wireboxCallLog = mockWireBox.$callLog(); 25 | expect( wireboxCallLog ).toBeStruct(); 26 | expect( wireboxCallLog ).toHaveKey( "getInstance" ); 27 | expect( mockWirebox.$once( "getInstance" ) ).toBeTrue(); 28 | var getInstanceCallLog = wireboxCallLog.getInstance; 29 | expect( getInstanceCallLog ).toBeArray(); 30 | expect( getInstanceCallLog ).toHaveLength( 1 ); 31 | expect( getInstanceCallLog[ 1 ][ 1 ] ).toBe( "MyWireBoxSlug" ); 32 | } ); 33 | } ); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | this.name = "cffractal"; 4 | this.author = "Eric Peterson"; 5 | this.autoMapModels = false; 6 | this.cfmapping = "cffractal"; 7 | this.webUrl = "https://github.com/elpete/cffractal"; 8 | 9 | function configure() { 10 | settings = { 11 | defaultItemSerializer = "SimpleSerializer", 12 | defaultCollectionSerializer = "ResultsMapSerializer", 13 | nullDefaultValue = {} 14 | }; 15 | } 16 | 17 | function onLoad() { 18 | binder.map( "SimpleSerializer@cffractal" ).asSingleton() 19 | .to( "#moduleMapping#.models.serializers.SimpleSerializer" ); 20 | binder.map( "DataSerializer@cffractal" ).asSingleton() 21 | .to( "#moduleMapping#.models.serializers.DataSerializer" ); 22 | binder.map( "ResultsMapSerializer@cffractal" ).asSingleton() 23 | .to( "#moduleMapping#.models.serializers.ResultsMapSerializer" ); 24 | binder.map( "XMLSerializer@cffractal" ).asSingleton() 25 | .to( "#moduleMapping#.models.serializers.XMLSerializer" ); 26 | 27 | binder.map( "Manager@cffractal" ) 28 | .to( "#moduleMapping#.models.Manager" ) 29 | .asSingleton() 30 | .initArg( 31 | name = "itemSerializer", 32 | ref = "#moduleMapping#.models.serializers.#settings.defaultItemSerializer#" 33 | ) 34 | .initArg( 35 | name = "collectionSerializer", 36 | ref = "#moduleMapping#.models.serializers.#settings.defaultCollectionSerializer#" 37 | ) 38 | .initArg( 39 | name = "nullDefaultValue", 40 | value = settings.nullDefaultValue 41 | ); 42 | 43 | binder.map( "Builder@cffractal" ) 44 | .to( "#moduleMapping#.models.Builder" ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /models/serializers/SimpleSerializer.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name SimpleSerializer 3 | * @package cffractal.models.serializers 4 | * @description Does no further transformation to the data. 5 | */ 6 | component singleton { 7 | 8 | /** 9 | * Does no further transformation to the data. 10 | * 11 | * @resource The resource to serialize. 12 | * @scope A reference to the current Fractal scope. 13 | * 14 | * @returns The processed resource, unnested. 15 | */ 16 | function data( resource, scope ) { 17 | return resource.process( scope ); 18 | } 19 | 20 | /** 21 | * Decides how to nest the data under the given identifier. 22 | * 23 | * @resource The serializing resource. 24 | * @scope The current cffractal scope.. 25 | * @identifier The current identifier for the serialization process. 26 | * 27 | * @returns The scoped, serialized data. 28 | */ 29 | function scopeData( resource, scope, identifier ) { 30 | var serializedData = data( resource, scope ); 31 | 32 | if ( resource.hasPagingData() ) { 33 | resource.addMeta( "pagination", resource.getPagingData() ); 34 | } 35 | 36 | if ( resource.hasMeta() ) { 37 | meta( resource, scope, serializedData ); 38 | } 39 | 40 | return { "#listLast( identifier, "." )#" = serializedData }; 41 | } 42 | 43 | /** 44 | * Decides which key to use (if any) for the root of the serialized data. 45 | * 46 | * @data The serialized data. 47 | * @identifier The current identifier for the serialization process. 48 | * 49 | * @returns The scoped, serialized data. 50 | */ 51 | function scopeRootKey( data, identifier ) { 52 | return data; 53 | } 54 | 55 | /** 56 | * Returns the metadata nested under a meta key. 57 | * 58 | * @data The metadata for the response. 59 | * 60 | * @response The metadata nested under a "meta" key. 61 | */ 62 | function meta( resource, scope ) { 63 | return resource.getMeta(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /tests/specs/unit/serializers/ResultsMapSerializerTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | function run() { 3 | describe( "data serializer", function() { 4 | it( "serializes an item", function() { 5 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 6 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 7 | mockItem.$( "process" ).$args( mockScope ).$results( { "foo" = "bar" } ); 8 | var serializer = new cffractal.models.serializers.ResultsMapSerializer(); 9 | expect( serializer.data( mockItem, mockScope ) ) 10 | .toBe( { "foo" = "bar" } ); 11 | } ); 12 | 13 | it( "serializes a collection", function() { 14 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 15 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 16 | mockItem.$( "process" ).$args( mockScope ).$results( [ 17 | { "id" = 1, "foo" = "bar" }, 18 | { "id" = 2, "baz" = "qux" } 19 | ] ); 20 | var serializer = new cffractal.models.serializers.ResultsMapSerializer(); 21 | expect( serializer.data( mockItem, mockScope ) ) 22 | .toBe( { 23 | "results" = [ 1, 2 ], 24 | "resultsMap" = { 25 | 1 = { "id" = 1, "foo" = "bar" }, 26 | 2 = { "id" = 2, "baz" = "qux" } 27 | } 28 | } ); 29 | } ); 30 | 31 | it( "serializes the metadata", function() { 32 | var pagingData = { "pagination" = { "maxrows" = 50, "page" = 1, "pages" = 3, "totalRecords" = 112 } }; 33 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 34 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 35 | mockItem.$( "getMeta", pagingData, false ); 36 | var serializer = new cffractal.models.serializers.ResultsMapSerializer(); 37 | expect( serializer.meta( mockItem, mockScope, {} ) ) 38 | .toBe( { "meta" = pagingData } ); 39 | } ); 40 | } ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /models/serializers/DataSerializer.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name DataSerializer 3 | * @package cffractal.models.serializers 4 | * @description Nests the data under a "data" key in a struct. 5 | */ 6 | component singleton { 7 | 8 | /** 9 | * The key to use when scoping the data. 10 | */ 11 | property name="dataKey"; 12 | 13 | /** 14 | * The key to use when scoping the metadata. 15 | */ 16 | property name="metaKey"; 17 | 18 | function init( dataKey = "data", metaKey = "meta" ) { 19 | variables.dataKey = arguments.dataKey; 20 | variables.metaKey = arguments.metaKey; 21 | return this; 22 | } 23 | 24 | /** 25 | * Nests the data underneath a 'data' key. 26 | * 27 | * @resource The resource to serialize. 28 | * @scope A reference to the current Fractal scope. 29 | * 30 | * @returns The processed resource nested under a "data" key. 31 | */ 32 | function data( resource, scope ) { 33 | return { "data" = resource.process( scope ) }; 34 | } 35 | 36 | /** 37 | * Decides how to nest the data under the given identifier. 38 | * 39 | * @resource The serializing resource. 40 | * @scope The current cffractal scope.. 41 | * @identifier The current identifier for the serialization process. 42 | * 43 | * @returns The scoped, serialized data. 44 | */ 45 | function scopeData( resource, scope, identifier ) { 46 | var serializedData = data( resource, scope ); 47 | 48 | if ( resource.hasPagingData() ) { 49 | resource.addMeta( "pagination", resource.getPagingData() ); 50 | } 51 | 52 | if ( resource.hasMeta() ) { 53 | meta( resource, scope, serializedData ); 54 | } 55 | 56 | return { "#listLast( identifier, "." )#" = serializedData }; 57 | } 58 | 59 | /** 60 | * Decides which key to use (if any) for the root of the serialized data. 61 | * 62 | * @data The serialized data. 63 | * @identifier The current identifier for the serialization process. 64 | * 65 | * @returns The scoped, serialized data. 66 | */ 67 | function scopeRootKey( data, identifier ) { 68 | return data; 69 | } 70 | 71 | /** 72 | * Returns the metadata nested under a meta key. 73 | * 74 | * @data The metadata for the response. 75 | * 76 | * @response The metadata nested under a "meta" key. 77 | */ 78 | function meta( resource, scope, data ) { 79 | structAppend( data, { "#variables.metaKey#" = resource.getMeta() }, true ); 80 | return data; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /tests/specs/unit/ManagerTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function beforeAll() { 4 | variables.dataSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 5 | variables.fractal = new cffractal.models.Manager( dataSerializer, dataSerializer ); 6 | } 7 | 8 | function run() { 9 | describe( "cffractal manager", function() { 10 | it( "can be instantiated", function() { 11 | expect( fractal ).toBeInstanceOf( "cffractal.models.Manager" ); 12 | } ); 13 | 14 | describe( "create data", function() { 15 | it( "can create a root scope", function() { 16 | var resource = fractal.item( {}, function() {} ); 17 | var rootScope = fractal.createData( resource ); 18 | expect( rootScope ).toBeInstanceOf( "cffractal.models.Scope" ); 19 | expect( prepareMock( rootScope ).$getProperty( "identifier" ) ).toBe( "" ); 20 | } ); 21 | 22 | it( "can create a nested scope", function() { 23 | var resource = fractal.item( {}, function() {} ); 24 | var nestedScope = fractal.createData( 25 | resource = resource, 26 | identifier = "book" 27 | ); 28 | expect( nestedScope ).toBeInstanceOf( "cffractal.models.Scope" ); 29 | expect( prepareMock( nestedScope ).$getProperty( "identifier" ) ).toBe( "book" ); 30 | } ); 31 | } ); 32 | 33 | describe( "serializer", function() { 34 | it( "can set a custom item serializer", function() { 35 | var simpleSerializer = new cffractal.models.serializers.SimpleSerializer(); 36 | fractal.setItemSerializer( simpleSerializer ); 37 | expect( fractal.getItemSerializer() ) 38 | .toBeInstanceOf( "cffractal.models.serializers.SimpleSerializer" ); 39 | } ); 40 | 41 | it( "can set a custom collection serializer", function() { 42 | var resultsMapSerializer = new cffractal.models.serializers.ResultsMapSerializer(); 43 | fractal.setCollectionSerializer( resultsMapSerializer ); 44 | expect( fractal.getCollectionSerializer() ) 45 | .toBeInstanceOf( "cffractal.models.serializers.ResultsMapSerializer" ); 46 | } ); 47 | } ); 48 | 49 | describe( "null default value", function() { 50 | it( "defaults to an empty string", function() { 51 | expect( fractal.getNullDefaultValue() ).toBe( "" ); 52 | } ); 53 | 54 | it( "can set a new null default value", function() { 55 | fractal.setNullDefaultValue( {} ); 56 | expect( fractal.getNullDefaultValue() ).toBe( {} ); 57 | } ); 58 | 59 | it( "can set a the null default value as null", function() { 60 | fractal.setNullDefaultValue( javacast( "null", "" ) ); 61 | expect( fractal.getNullDefaultValue() ).toBeNull(); 62 | } ); 63 | } ); 64 | } ); 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /tests/specs/unit/resources/ResourceTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "resource test", function() { 5 | it( "processing with a transformer and includes", function() { 6 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 7 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 8 | mockScope.$property( propertyName = "includes", mock = [] ); 9 | mockScope.$property( propertyName = "excludes", mock = [] ); 10 | var mockTransformer = getMockBox().createMock( "cffractal.models.transformers.AbstractTransformer" ); 11 | mockTransformer.$( "transform", { "foo" = "bar" } ); 12 | mockTransformer.$( "hasIncludes", true ); 13 | mockTransformer.$( "processIncludes" ).$args( mockScope, { "foo" = "bar" } ).$results( [ { "baz" = "bam" } ] ); 14 | var item = new cffractal.models.resources.Item( { "foo" = "bar" }, mockTransformer, mockSerializer ); 15 | 16 | var transformedData = item.process( mockScope ); 17 | 18 | expect( transformedData ).toBe( { "foo" = "bar", "baz" = "bam" } ); 19 | } ); 20 | 21 | it( "can add any meta data to the resource", function() { 22 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 23 | var mockTransformer = getMockBox().createMock( "cffractal.models.transformers.AbstractTransformer" ); 24 | var item = new cffractal.models.resources.Item( { "foo" = "bar" }, mockTransformer, mockSerializer ); 25 | item.addMeta( "foo", "bar" ); 26 | item.addMeta( "baz", [ "a", "b", "c" ] ); 27 | expect( item.getMeta() ) 28 | .toBe( { "foo" = "bar", "baz" = [ "a", "b", "c" ] } ); 29 | } ); 30 | 31 | it( "can specify a specific serializer", function() { 32 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 33 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 34 | mockScope.$property( propertyName = "includes", mock = [] ); 35 | mockScope.$property( propertyName = "excludes", mock = [] ); 36 | var mockTransformer = getMockBox().createMock( "cffractal.models.transformers.AbstractTransformer" ); 37 | mockTransformer.$( "transform", { "foo" = "bar" } ); 38 | mockTransformer.$( "hasIncludes", true ); 39 | mockTransformer.$( "processIncludes" ).$args( mockScope, { "foo" = "bar" } ).$results( [ { "baz" = "bam" } ] ); 40 | var item = new cffractal.models.resources.Item( { "foo" = "bar" }, mockTransformer, mockSerializer ); 41 | 42 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.SimpleSerializer" ); 43 | expect( item.getSerializer() ).toBeInstanceOf( "cffractal.models.serializers.DataSerializer" ); 44 | item.setSerializer( mockSerializer ); 45 | expect( item.getSerializer() ).toBeInstanceOf( "cffractal.models.serializers.SimpleSerializer" ); 46 | var transformedData = item.process( mockScope ); 47 | 48 | expect( transformedData ).toBe( { "foo" = "bar", "baz" = "bam" } ); 49 | } ); 50 | } ); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /models/serializers/ResultsMapSerializer.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name ResultMapSerializer 3 | * @package cffractal.models.serializers 4 | * @description Nests the data under a "resultMap" key 5 | * as well as providing an array of ids. 6 | */ 7 | component singleton { 8 | 9 | /** 10 | * The primary key name for the dataset. 11 | */ 12 | property name="identitifer"; 13 | 14 | /** 15 | * The key to use when scoping the metadata. 16 | */ 17 | property name="metaKey"; 18 | 19 | /** 20 | * Creates a new ResultMapSerializer with the given identifier. 21 | * 22 | * @identifier The key name for the identifing piece of data. 23 | * This data will be stored in an array of results. 24 | * 25 | * @returns The serializer instance. 26 | */ 27 | function init( identifier = "id", metaKey = "meta" ) { 28 | variables.identifier = arguments.identifier; 29 | variables.metaKey = arguments.metaKey; 30 | return this; 31 | } 32 | 33 | /** 34 | * Returns an array of primary keys along with a map 35 | * of the data keyed by those same primary keys. 36 | * 37 | * @resource The resource to serialize. 38 | * @scope A reference to the current Fractal scope. 39 | * 40 | * @returns The processed resource ids and associated map. 41 | */ 42 | function data( resource, scope ) { 43 | var processedData = resource.process( scope ); 44 | 45 | if ( ! isArray( processedData ) ) { 46 | return processedData; 47 | } 48 | 49 | var ids = []; 50 | var map = {}; 51 | for ( var item in processedData ) { 52 | arrayAppend( ids, item[ identifier ] ); 53 | map[ item[ identifier ] ] = item; 54 | } 55 | 56 | return { 57 | "results" = ids, 58 | "resultsMap" = map 59 | }; 60 | } 61 | 62 | /** 63 | * Decides how to nest the data under the given identifier. 64 | * 65 | * @resource The serializing resource. 66 | * @scope The current cffractal scope.. 67 | * @identifier The current identifier for the serialization process. 68 | * 69 | * @returns The scoped, serialized data. 70 | */ 71 | function scopeData( resource, scope, identifier ) { 72 | var serializedData = data( resource, scope ); 73 | 74 | if ( resource.hasPagingData() ) { 75 | resource.addMeta( "pagination", resource.getPagingData() ); 76 | } 77 | 78 | if ( resource.hasMeta() ) { 79 | meta( resource, scope, serializedData ); 80 | } 81 | 82 | return { "#listLast( arguments.identifier, "." )#" = serializedData }; 83 | } 84 | 85 | /** 86 | * Decides which key to use (if any) for the root of the serialized data. 87 | * 88 | * @data The serialized data. 89 | * @identifier The current identifier for the serialization process. 90 | * 91 | * @returns The scoped, serialized data. 92 | */ 93 | function scopeRootKey( data, identifier ) { 94 | return data; 95 | } 96 | 97 | /** 98 | * Returns the metadata nested under a meta key. 99 | * 100 | * @data The metadata for the response. 101 | * 102 | * @response The metadata nested under a "meta" key. 103 | */ 104 | function meta( resource, scope, data ) { 105 | structAppend( data, { "#variables.metaKey#" = resource.getMeta() }, true ); 106 | return data; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v8.1.1 2 | ## 13 Feb 2020 — 17:49:55 UTC 3 | 4 | ### other 5 | 6 | + __\*:__ chore: Use OpenJDK instead of OracleJDK ([8a6b8ca](https://github.com/coldbox-modules/cffractal/commit/8a6b8ca2fb61e6b31e85adea67379f5163ab9981)) 7 | + __\*:__ chore: Use forgeboxStorage ([133118a](https://github.com/coldbox-modules/cffractal/commit/133118a6b37cc12ecf4ceab6e5fe4fc4fc71f48d)) 8 | 9 | 10 | # v8.1.0 11 | ## 28 May 2019 — 21:20:14 UTC 12 | 13 | ### feat 14 | 15 | + __XMLSerializer:__ Allow for a variable array separator in XML serialization other than ([1c25b18](https://github.com/coldbox-modules/cffractal/commit/1c25b18da3c608b558dbc5af261a6747bc8be259)) 16 | 17 | 18 | # v8.0.1 19 | ## 21 Feb 2019 — 13:50:14 UTC 20 | 21 | ### fix 22 | 23 | + __AbstractResource:__ Ignore non-struct data when excluding keys ([a9d1e04](https://github.com/coldbox-modules/cffractal/commit/a9d1e04035d61f888d966368778bea197c15cee9)) 24 | 25 | 26 | # v8.0.0 27 | ## 14 Jan 2019 — 21:13:12 UTC 28 | 29 | ### BREAKING 30 | 31 | + __Scope:__ Fix for includes not respecting custom serializers ([06114f0](https://github.com/coldbox-modules/cffractal/commit/06114f02576e8fb9fd958057255585aac9dc3fd0)) 32 | 33 | 34 | # v7.0.1 35 | ## 04 Jan 2019 — 20:34:53 UTC 36 | 37 | ### fix 38 | 39 | + __ResultsMapSerializer:__ Add meta back to serializer output 40 | ([d9594d0](https://github.com/coldbox-modules/cffractal/commit/d9594d0789852f0878d1e85f22fc3c302c5a35e7)) 41 | 42 | 43 | # v7.0.0 44 | ## 22 Aug 2018 — 15:04:57 UTC 45 | 46 | ### BREAKING 47 | 48 | + __Excludes:__ Add ability to specify excludes ([d89fa06](https://github.com/coldbox-modules/cffractal/commit/d89fa063faaf7b8a5ef1c84bc90c936159006d10)) 49 | 50 | ### chore 51 | 52 | + __ci:__ Replace flaky gpg key download with solid Ortus one 53 | ([754f451](https://github.com/coldbox-modules/cffractal/commit/754f451a44613819c7adc420c08965e3903d6117)) 54 | 55 | ### feat 56 | 57 | + __Transformers:__ Pass includes and excludes inside a transformer ([03ac095](https://github.com/coldbox-modules/cffractal/commit/03ac09510a7dce8279c405f4b6093fb7511487f8)) 58 | + __Transformers:__ Automatically remove unused available includes from the serialized output ([89f4938](https://github.com/coldbox-modules/cffractal/commit/89f4938efe7e5ffd4bf98d062c494dfec989e031)) 59 | + __Transformers:__ Allow for Transformer-level item and collection serializers ([448a07d](https://github.com/coldbox-modules/cffractal/commit/448a07dcc5854c0106d142b6bd78903c28e60b95)) 60 | + __Transformers:__ Add item callbacks when creating resources ([5e00c82](https://github.com/coldbox-modules/cffractal/commit/5e00c82360a98bb6ce7ca54b3d0a9b36c74483f1)) 61 | + __Transfomers:__ Simple values can be returned from includes ([67c1507](https://github.com/coldbox-modules/cffractal/commit/67c15076833c040c9bfcd6331d5ca4513fd1fe95)) 62 | 63 | ### fix 64 | 65 | + __Includes:__ Fix processing double includes 66 | ([ff33af5](https://github.com/coldbox-modules/cffractal/commit/ff33af5b23a7dd8786b49ea5e006ae762e48ada6)) 67 | 68 | ### other 69 | 70 | + __\*:__ Temporarily remove emoji due to ForgeBox support 71 | ([9cc3e37](https://github.com/coldbox-modules/cffractal/commit/9cc3e37dc6df3be5d5507a3e6993d83298435576)) 72 | 73 | 74 | # Changelog 75 | 76 | ## 6.0.0 77 | 78 | ### Breaking Changes 79 | 80 | + Custom `Serializers` now need to implement two additional methods: 81 | + `scopeData` — Decides how to nest the data under the given identifier. Most implementations will return the current data under the last identifier: `{ "#listLast( arguments.identifier, "." )#" = data };` 82 | + `scopeRootKey` — Decides which key to use (if any) for the root of the serialized data. 83 | + Transformers can set a `resourceKey` property to be used if the transformer is the root transformer in certain serializers. (`variables.resourceKey = "book";`) For instance, this property is used in the `XMLSerializer` to set the root node name. If no `resourceKey` is set, or a callback transfomer is used, a default `resourceKey` of `data` will be used. 84 | -------------------------------------------------------------------------------- /tests/specs/unit/resources/ItemTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | variables.simpleItem = { "foo" = "bar" }; 4 | 5 | function run() { 6 | describe( "item resources", function() { 7 | it( "can get the data from an item resource", function() { 8 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 9 | var item = new cffractal.models.resources.Item( simpleItem, function() {}, mockSerializer ); 10 | prepareMock( item ); 11 | expect( item.$getProperty( "data" ) ).toBe( simpleItem ); 12 | } ); 13 | 14 | it( "can add postTransformationCallbacks", function() { 15 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 16 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 17 | mockScope.$property( propertyName = "includes", mock = [] ); 18 | mockScope.$property( propertyName = "excludes", mock = [] ); 19 | mockScope.$( "getNullDefaultValue", {} ); 20 | var item = new cffractal.models.resources.Item( { 21 | "foo" = "bar" 22 | }, function( item ) { 23 | return { 24 | "foo" = item.foo & " " & item.foo 25 | }; 26 | }, mockSerializer ); 27 | 28 | var callbackCalled = false; 29 | 30 | item.addPostTransformationCallback( function( itemStruct, itemInstance, resource ) { 31 | expect( resource ).toBe( item ); 32 | expect( itemStruct.foo ).toBe( itemInstance.foo & " " & itemInstance.foo ); 33 | callbackCalled = true; 34 | return itemStruct; 35 | } ); 36 | 37 | item.process( mockScope ); 38 | 39 | expect( callbackCalled ).toBeTrue( "Callback was never called" ); 40 | } ); 41 | 42 | it( "it processes nulls in postTransformationCallbacks correctly using the manager null default value", function() { 43 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 44 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 45 | mockScope.$property( propertyName = "includes", mock = [] ); 46 | mockScope.$property( propertyName = "excludes", mock = [] ); 47 | mockScope.$( "getNullDefaultValue", {} ); 48 | var item = new cffractal.models.resources.Item( 49 | javacast( "null", "" ), 50 | function( item ) { 51 | return isNull( item ) ? javacast( "null", "" ) : item; 52 | }, 53 | mockSerializer 54 | ); 55 | 56 | var callbackCalled = false; 57 | 58 | item.addPostTransformationCallback( function( itemStruct, itemInstance ) { 59 | callbackCalled = true; 60 | return {}; 61 | } ); 62 | 63 | item.process( mockScope ); 64 | 65 | expect( callbackCalled ).toBeTrue( "Callback was never called" ); 66 | } ); 67 | 68 | describe( "can get the transformer from an item resource", function() { 69 | it( "works with a closure", function() { 70 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 71 | var item = new cffractal.models.resources.Item( simpleItem, function() {}, mockSerializer ); 72 | prepareMock( item ); 73 | expect( isClosure( item.$getProperty( "transformer" ) ) ).toBeTrue(); 74 | } ); 75 | 76 | it( "can work with components", function() { 77 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 78 | var transformerStub = getMockBox().createStub(); 79 | var item = new cffractal.models.resources.Item( simpleItem, transformerStub, mockSerializer ); 80 | prepareMock( item ); 81 | expect( item.$getProperty( "transformer" ) ).toBe( transformerStub ); 82 | } ); 83 | } ); 84 | } ); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /models/serializers/XMLSerializer.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name XMLSerializer 3 | * @package cffractal.models.serializers 4 | * @description Marshalls the data to XML. 5 | */ 6 | component singleton { 7 | 8 | /** 9 | * The key to use at the root of the XML object. 10 | */ 11 | property name="rootKey"; 12 | 13 | /** 14 | * The key to use when scoping the metadata. 15 | */ 16 | property name="metaKey"; 17 | 18 | 19 | /** 20 | * Do we alpha-sort the keys for XML output (default) or preserve the incoming order. 21 | * The default is to sort since using struct keys by default will come 22 | * out random, especially across different engines. 23 | */ 24 | property name="sortKeys"; 25 | 26 | /** 27 | * The separator key to use when serializing an array. Default "item" 28 | */ 29 | property name="itemKey"; 30 | 31 | function init( rootKey = "root", metaKey = "meta", sortKeys = true, itemKey = "item" ) { 32 | variables.rootKey = arguments.rootKey; 33 | variables.metaKey = arguments.metaKey; 34 | variables.sortKeys = arguments.sortKeys; 35 | variables.itemKey = arguments.itemKey; 36 | return this; 37 | } 38 | 39 | /** 40 | * Does no further transformation to the data. 41 | * 42 | * @resource The resource to serialize. 43 | * @scope A reference to the current Fractal scope. 44 | * 45 | * @returns The processed resource, unnested. 46 | */ 47 | function data( resource, scope ) { 48 | var xmlDoc = XMLNew(); 49 | xmlDoc.xmlRoot = XMLElemNew( xmlDoc, variables.rootKey ); 50 | populateNode( xmlDoc.xmlRoot, resource.process( scope ), xmlDoc ); 51 | return ToString( xmlDoc ); 52 | } 53 | 54 | /** 55 | * Decides how to nest the data under the given identifier. 56 | * 57 | * @resource The serializing resource. 58 | * @scope The current cffractal scope.. 59 | * @identifier The current identifier for the serialization process. 60 | * 61 | * @returns The scoped, serialized data. 62 | */ 63 | function scopeData( resource, scope, identifier ) { 64 | var data = resource.process( scope ); 65 | return { "#listLast( identifier, "." )#" = data }; 66 | } 67 | 68 | /** 69 | * Decides which key to use (if any) for the root of the serialized data. 70 | * 71 | * @data The serialized data. 72 | * @identifier The current identifier for the serialization process. 73 | * 74 | * @returns The scoped, serialized data. 75 | */ 76 | function scopeRootKey( data, identifier = "" ) { 77 | if ( identifier == "" ) { 78 | return data; 79 | } 80 | var xmlDoc = XMLParse( data ); 81 | var currentChildren = xmlDoc.xmlRoot.XmlChildren; 82 | var xmlData = XMLElemNew( xmlDoc, identifier ); 83 | arrayAppend( xmlData.XmlChildren, currentChildren, true ); 84 | arrayClear( xmlDoc.xmlRoot.XmlChildren ); 85 | arrayAppend( xmlDoc.xmlRoot.XmlChildren, xmlData ); 86 | return ToString( xmlDoc ); 87 | } 88 | 89 | /** 90 | * Returns the metadata nested under a meta key. 91 | * 92 | * @data The metadata for the response. 93 | * 94 | * @response The metadata nested under a "meta" key. 95 | */ 96 | function meta( resource, scope, data ) { 97 | var xmlDoc = XMLParse( data ); 98 | var metaNode = XMLElemNew( xmlDoc, variables.metaKey ); 99 | populateNode( metaNode, resource.getMeta(), xmlDoc ); 100 | arrayAppend( xmlDoc.XmlRoot.XmlChildren, metaNode ); 101 | return ToString( xmlDoc ); 102 | } 103 | 104 | private function populateNode( parent, contents, root ) { 105 | if ( isArray( contents ) ) { 106 | arrayEach( contents, function( item ) { 107 | var newNode = XMLElemNew( root, variables.itemKey ); 108 | populateNode( newNode, item, root ); 109 | arrayAppend( parent.XmlChildren, newNode ); 110 | } ); 111 | } 112 | else if ( isStruct( contents ) ) { 113 | var keys = structKeyArray( contents ); 114 | if ( variables.sortKeys ) { 115 | arraySort( keys, "textnocase" ); 116 | } 117 | arrayEach( keys, function( key ) { 118 | var newNode = XMLElemNew( root, key ); 119 | populateNode( newNode, contents[ key ], root ); 120 | arrayAppend( parent.XmlChildren, newNode ); 121 | } ); 122 | } 123 | else { 124 | parent.XmlText = contents; 125 | } 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /tests/specs/unit/resources/CollectionTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | variables.simpleCollection = [ 4 | { "foo" = "bar" }, 5 | { "baz" = "ban" } 6 | ]; 7 | 8 | function run() { 9 | describe( "collection resources", function() { 10 | it( "can get the data from an collection resource", function() { 11 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 12 | var collection = new cffractal.models.resources.Collection( simpleCollection, function() {}, mockSerializer ); 13 | prepareMock( collection ); 14 | expect( collection.$getProperty( "data" ) ).toBe( simpleCollection ); 15 | } ); 16 | 17 | it( "can add postTransformationCallbacks", function() { 18 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 19 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 20 | mockScope.$property( propertyName = "includes", mock = [] ); 21 | mockScope.$property( propertyName = "excludes", mock = [] ); 22 | mockScope.$( "getNullDefaultValue", {} ); 23 | var collection = new cffractal.models.resources.Collection( [ 24 | { "foo" = "bar" }, 25 | { "foo" = "baz" }, 26 | { "foo" = "qux" } 27 | ], function( item ) { 28 | return { 29 | "foo" = item.foo & " " & item.foo 30 | }; 31 | }, mockSerializer ); 32 | 33 | var callbackCalled = false; 34 | 35 | collection.addPostTransformationCallback( function( itemStruct, itemInstance, resource ) { 36 | expect( resource ).toBe( collection ); 37 | expect( itemStruct.foo ).toBe( itemInstance.foo & " " & itemInstance.foo ); 38 | callbackCalled = true; 39 | return itemStruct; 40 | } ); 41 | 42 | collection.process( mockScope ); 43 | 44 | expect( callbackCalled ).toBeTrue( "Callback was never called" ); 45 | } ); 46 | 47 | it( "it never calls the postTransformationCallbacks if the data is null", function() { 48 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 49 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 50 | mockScope.$property( propertyName = "includes", mock = [] ); 51 | mockScope.$property( propertyName = "excludes", mock = [] ); 52 | mockScope.$( "getNullDefaultValue", {} ); 53 | var collection = new cffractal.models.resources.Collection( 54 | javacast( "null", "" ), 55 | function( item ) { 56 | return isNull( item ) ? javacast( "null", "" ) : item; 57 | }, 58 | mockSerializer 59 | ); 60 | 61 | var callbackNeverCalled = true; 62 | 63 | collection.addPostTransformationCallback( function( itemStruct, itemInstance ) { 64 | callbackNeverCalled = false; 65 | return {}; 66 | } ); 67 | 68 | collection.process( mockScope ); 69 | 70 | expect( callbackNeverCalled ).toBeTrue( "Callback should never have been called" ); 71 | } ); 72 | 73 | describe( "can get the transformer from an collection resource", function() { 74 | it( "works with a closure", function() { 75 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 76 | var collection = new cffractal.models.resources.Collection( simpleCollection, function() {}, mockSerializer ); 77 | prepareMock( collection ); 78 | expect( isClosure( collection.$getProperty( "transformer" ) ) ).toBeTrue(); 79 | } ); 80 | 81 | it( "can work with components", function() { 82 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 83 | var transformerStub = getMockBox().createStub(); 84 | var collection = new cffractal.models.resources.Collection( simpleCollection, transformerStub, mockSerializer ); 85 | prepareMock( collection ); 86 | expect( collection.$getProperty( "transformer" ) ).toBe( transformerStub ); 87 | } ); 88 | } ); 89 | } ); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /models/Manager.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Manager 3 | * @package cffractal.models 4 | * @description The Manager component is responsible for kickstarting 5 | * the api transformation process. It creates the root 6 | * scope and serializes the transformed value. 7 | */ 8 | component singleton accessors="true" { 9 | 10 | /** 11 | * The WireBox injector 12 | */ 13 | property name="wirebox" inject="wirebox"; 14 | 15 | /** 16 | * The default item serializer. 17 | */ 18 | property name="itemSerializer"; 19 | 20 | /** 21 | * The default collection serializer. 22 | */ 23 | property name="collectionSerializer"; 24 | 25 | /** 26 | * The serializer instance. 27 | */ 28 | property name="nullDefaultValue"; 29 | 30 | /** 31 | * Create a new Manager instance. 32 | * 33 | * @itemSerializer The serializer to use to serialize items. 34 | * @collectionSerializer The serializer to use to serialize collections. 35 | * @nullDefaultValue The default value to use when encountering nulls. 36 | * 37 | * @returns The Fractal Manager. 38 | */ 39 | function init( itemSerializer, collectionSerializer, nullDefaultValue = "" ) { 40 | variables.itemSerializer = arguments.itemSerializer; 41 | variables.collectionSerializer = arguments.collectionSerializer; 42 | variables.nullDefaultValue = arguments.nullDefaultValue; 43 | return this; 44 | } 45 | 46 | function builder() { 47 | if ( structKeyExists( variables, "wirebox" ) ) { 48 | return wirebox.getInstance( 49 | name = "Builder@cffractal", 50 | initArguments = { 51 | manager = this 52 | } 53 | ); 54 | } 55 | else { 56 | return new cffractal.models.Builder( this ); 57 | } 58 | } 59 | 60 | /** 61 | * Creates a scope for a given resource and identifier. 62 | * 63 | * @resource A Fractal resource. 64 | * @includes A list of includes for the scope. Includes are 65 | * comma separated and use dots to designate 66 | * nested resources to be included. 67 | * @excludes A list of excludes for the scope. Excludes are 68 | * comma separated and use dots to designate 69 | * nested resources to be excluded. 70 | * @identifier The scope identifier defining the nesting level. 71 | * Defaults to the root level. 72 | * 73 | * @returns A Fractal scope primed with the given resource and identifier. 74 | */ 75 | function createData( resource, includes = "", excludes = "", identifier = "" ) { 76 | arguments.manager = this; 77 | return new cffractal.models.Scope( argumentCollection = arguments ); 78 | } 79 | 80 | /** 81 | * Returns a new item resource with the given data and transformer. 82 | * 83 | * @data The data or component to transform. 84 | * @transformer The transformer callback or component to use 85 | * transforming the above data. 86 | * @serializer A custom serializer for this resource. 87 | Defaults to the manager default for a item. 88 | * @itemCallback An optional callback to call after each item is serialized. 89 | * 90 | * @returns A new cffractal Item wrapping the given data and transformer. 91 | */ 92 | function item( 93 | data, 94 | transformer, 95 | serializer = variables.itemSerializer, 96 | itemCallback 97 | ) { 98 | return new cffractal.models.resources.Item( argumentCollection = arguments ); 99 | } 100 | 101 | /** 102 | * Returns a new collection resource with the given data and transformer. 103 | * 104 | * @data The data or component to transform. 105 | * @transformer The transformer callback or component to use 106 | * transforming the above data. 107 | * @serializer A custom serializer for this resource. 108 | * Defaults to the manager default for a collection. 109 | * @itemCallback An optional callback to call after each item is serialized. 110 | * 111 | * @returns A new cffractal Collection wrapping the given data and transformer. 112 | */ 113 | function collection( 114 | data, 115 | transformer, 116 | serializer = variables.collectionSerializer, 117 | itemCallback 118 | ) { 119 | return new cffractal.models.resources.Collection( argumentCollection = arguments ); 120 | } 121 | 122 | /** 123 | * Overload to the accessor, to ensure a struct default will not be copied by reference 124 | **/ 125 | function getNullDefaultValue() { 126 | return !isNull( variables.nullDefaultValue ) ? duplicate( variables.nullDefaultValue ) : javacast( "null", 0 ); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /models/Builder.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | /** 4 | * The WireBox injector 5 | */ 6 | property name="wirebox" inject="wirebox"; 7 | 8 | /** 9 | * The metadata struct to add to the resource. 10 | */ 11 | variables.meta = {}; 12 | 13 | /** 14 | * The post-transformation callbacks to add to the resource. 15 | */ 16 | variables.postTransformationCallbacks = []; 17 | 18 | /** 19 | * Creates a new builder instance for fluent fractal transformations. 20 | * 21 | * @manager The fractal manager instance. 22 | * 23 | * @returns The fractal builder. 24 | */ 25 | function init( manager ) { 26 | variables.manager = arguments.manager; 27 | variables.includes = ""; 28 | variables.excludes = ""; 29 | return this; 30 | } 31 | 32 | /** 33 | * Sets the item resource to be transformed. 34 | * 35 | * @data The model to transform. 36 | * 37 | * @returns The fractal builder. 38 | */ 39 | function item( data ) { 40 | variables.data = arguments.data; 41 | variables.resourceType = "item"; 42 | return this; 43 | } 44 | 45 | /** 46 | * Sets the collection resource to be transformed. 47 | * 48 | * @data The model to transform. 49 | * 50 | * @returns The fractal builder. 51 | */ 52 | function collection( data ) { 53 | variables.data = !isNull( arguments.data ) ? arguments.data : []; 54 | variables.resourceType = "collection"; 55 | return this; 56 | } 57 | 58 | /** 59 | * Sets the transformer to use. 60 | * If the transformer is a simple value, the Builder 61 | * will treat it as a WireBox binding. 62 | * 63 | * @transformer The transformer to use. 64 | * 65 | * @returns The fractal builder. 66 | */ 67 | function withTransformer( transformer ) { 68 | if ( isSimpleValue( arguments.transformer ) ) { 69 | arguments.transformer = wirebox.getInstance( arguments.transformer ); 70 | } 71 | variables.transformer = arguments.transformer; 72 | return this; 73 | } 74 | 75 | /** 76 | * Sets the serializer to use. 77 | * If the serializer is a simple value, the Builder 78 | * will treat it as a WireBox binding. 79 | * 80 | * @serializer The serializer to use. 81 | * 82 | * @returns The fractal builder. 83 | */ 84 | function withSerializer( serializer ) { 85 | if ( isSimpleValue( arguments.serializer ) ) { 86 | arguments.serializer = wirebox.getInstance( arguments.serializer ); 87 | } 88 | variables.serializer = arguments.serializer; 89 | return this; 90 | } 91 | 92 | /** 93 | * Sets the includes for the transformation. 94 | * 95 | * @includes The includes for the transformation. 96 | * 97 | * @returns The fractal builder. 98 | */ 99 | function withIncludes( includes ) { 100 | variables.includes = arguments.includes; 101 | return this; 102 | } 103 | 104 | /** 105 | * Sets the excludes for the transformation. 106 | * 107 | * @excludes The excludes for the transformation. 108 | * 109 | * @returns The fractal builder. 110 | */ 111 | function withExcludes( excludes ) { 112 | variables.excludes = arguments.excludes; 113 | return this; 114 | } 115 | 116 | /** 117 | * Sets the pagination metadata for the resource. 118 | * 119 | * @pagination The pagination metadata. 120 | * 121 | * @returns The fractal builder. 122 | */ 123 | function withPagination( pagination ) { 124 | withMeta( "pagination", pagination ); 125 | return this; 126 | } 127 | 128 | /** 129 | * Adds a key / value pair to the metadata for the resource. 130 | * 131 | * @key The metadata key. 132 | * @value The metadata value. 133 | * 134 | * @returns The fractal builder. 135 | */ 136 | function withMeta( key, value ) { 137 | meta[ key ] = value; 138 | return this; 139 | } 140 | 141 | /** 142 | * Add a callback to be called after each item is transformed. 143 | * 144 | * @callback The callback to run after each item has been transformed. 145 | * The callback will be passed the transformed data, the 146 | * original data, and the resource object as arguments. 147 | * 148 | * @returns The fractal builder 149 | */ 150 | function withItemCallback( callback ) { 151 | arrayAppend( postTransformationCallbacks, callback ); 152 | return this; 153 | } 154 | 155 | /** 156 | * Transforms the data using the set properties through the fractal manager. 157 | * 158 | * @returns The transformed data. 159 | */ 160 | function convert() { 161 | var resource = createResource(); 162 | for ( var callback in postTransformationCallbacks ) { 163 | resource.addPostTransformationCallback( callback ); 164 | } 165 | return manager.createData( resource, includes, excludes ).convert(); 166 | } 167 | 168 | /** 169 | * Transform the data through cffractal and then serialize it to JSON. 170 | * 171 | * @returns The transformed data as JSON. 172 | */ 173 | function toJSON() { 174 | return serializeJSON( convert() ); 175 | } 176 | 177 | /** 178 | * Creates a resource instance using the builder state, 179 | * complete with metadata. 180 | * 181 | * @returns The newly created resource instance. 182 | */ 183 | private function createResource() { 184 | return addMetadata( 185 | invoke( manager, resourceType, { 186 | data = data, 187 | transformer = transformer, 188 | serializer = getSerializer() 189 | } ) 190 | ); 191 | } 192 | 193 | /** 194 | * Adds metadata to a resource instance, 195 | * 196 | * @resource A resource instance 197 | * 198 | * @returns The resource instance with added metadata. 199 | */ 200 | private function addMetadata( resource ) { 201 | structEach( meta, function( key, value ) { 202 | resource.addMeta( key, value ); 203 | } ); 204 | return resource; 205 | } 206 | 207 | /** 208 | * Returns the specified serailzer or a default serializer, as needed. 209 | * 210 | * @returns The specified or default serializer. 211 | */ 212 | private function getSerializer() { 213 | if ( ! isNull( serializer ) ) { 214 | return serializer; 215 | } 216 | 217 | return resourceType == "item" ? 218 | manager.getItemSerializer() : 219 | manager.getCollectionSerializer(); 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /tests/specs/unit/TransformerTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "abstract transformers", function() { 5 | beforeEach( function() { 6 | variables.mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 7 | variables.mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 8 | mockFractal.$property( propertyName = "itemSerializer", mock = mockSerializer ); 9 | mockFractal.$property( propertyName = "collectionSerializer", mock = mockSerializer ); 10 | variables.transformer = new cffractal.models.transformers.AbstractTransformer(); 11 | transformer.setManager( mockFractal ); 12 | } ); 13 | 14 | it( "can be instantiated", function() { 15 | expect( transformer ) 16 | .toBeInstanceOf( "cffractal.models.transformers.AbstractTransformer" ); 17 | } ); 18 | 19 | it( "throws if the `transform()` method has not been implemented", function() { 20 | expect( function() { 21 | transformer.transform(); 22 | } ).toThrow( type = "MethodNotImplemented" ); 23 | } ); 24 | 25 | describe( "resource creation", function() { 26 | it( "can create new items", function() { 27 | makePublic( transformer, "item", "itemPublic" ); 28 | var item = transformer.itemPublic( {}, function() {} ); 29 | expect( item ).toBeInstanceOf( "cffractal.models.resources.Item" ); 30 | } ); 31 | 32 | it( "can add postTransformationCallbacks when creating items", function() { 33 | makePublic( transformer, "item", "itemPublic" ); 34 | var item = prepareMock( 35 | transformer.itemPublic( 36 | data = {}, 37 | transformer = function() {}, 38 | itemCallback = function() {} 39 | ) 40 | ); 41 | var callbacks = item.$getProperty( "postTransformationCallbacks" ); 42 | expect( callbacks ).toBeArray(); 43 | expect( callbacks ).toHaveLength( 1 ); 44 | } ); 45 | 46 | it( "creates an item with the transformer serializer if one has been specified", function() { 47 | makePublic( transformer, "item", "itemPublic" ); 48 | var itemA = transformer.itemPublic( {}, function() {} ); 49 | expect( itemA.getSerializer() ).toBe( mockSerializer ); 50 | 51 | var otherMockSerializer = getMockBox().createMock( "cffractal.models.serializers.SimpleSerializer" ); 52 | transformer.setSerializer( otherMockSerializer ); 53 | var itemB = transformer.itemPublic( {}, function() {} ); 54 | expect( itemB.getSerializer() ).toBe( otherMockSerializer ); 55 | } ); 56 | 57 | it( "can create new collections", function() { 58 | makePublic( transformer, "collection", "collectionPublic" ); 59 | var collection = transformer.collectionPublic( [ {}, {} ], function() {} ); 60 | expect( collection ).toBeInstanceOf( "cffractal.models.resources.Collection" ); 61 | } ); 62 | 63 | it( "can add postTransformationCallbacks when creating collections", function() { 64 | makePublic( transformer, "collection", "collectionPublic" ); 65 | var collection = prepareMock( 66 | transformer.collectionPublic( 67 | data = [ {}, {} ], 68 | transformer = function() {}, 69 | itemCallback = function() {} 70 | ) 71 | ); 72 | var callbacks = collection.$getProperty( "postTransformationCallbacks" ); 73 | expect( callbacks ).toBeArray(); 74 | expect( callbacks ).toHaveLength( 1 ); 75 | } ); 76 | 77 | it( "creates an collection with the transformer serializer if one has been specified", function() { 78 | makePublic( transformer, "collection", "collectionPublic" ); 79 | var collectionA = transformer.collectionPublic( [ {}, {} ], function() {} ); 80 | expect( collectionA.getSerializer() ).toBe( mockSerializer ); 81 | 82 | var otherMockSerializer = getMockBox().createMock( "cffractal.models.serializers.SimpleSerializer" ); 83 | transformer.setSerializer( otherMockSerializer ); 84 | var collectionB = transformer.collectionPublic( [ {}, {} ], function() {} ); 85 | expect( collectionB.getSerializer() ).toBe( otherMockSerializer ); 86 | } ); 87 | 88 | it( "can set a itemSerializer and a collectionSerializer separately for a transformer", function() { 89 | makePublic( transformer, "item", "itemPublic" ); 90 | makePublic( transformer, "collection", "collectionPublic" ); 91 | 92 | var itemA = transformer.itemPublic( {}, function() {} ); 93 | var collectionA = transformer.collectionPublic( [ {}, {} ], function() {} ); 94 | 95 | expect( itemA.getSerializer() ).toBe( mockSerializer ); 96 | expect( collectionA.getSerializer() ).toBe( mockSerializer ); 97 | 98 | var otherMockSerializer = getMockBox().createMock( "cffractal.models.serializers.SimpleSerializer" ); 99 | transformer.setItemSerializer( otherMockSerializer ); 100 | 101 | var itemB = transformer.itemPublic( {}, function() {} ); 102 | var collectionB = transformer.collectionPublic( [ {}, {} ], function() {} ); 103 | 104 | expect( itemB.getSerializer() ).toBe( otherMockSerializer ); 105 | expect( collectionB.getSerializer() ).toBe( mockSerializer ); 106 | } ); 107 | } ); 108 | 109 | it( "returns true if there are any default or available includes", function() { 110 | prepareMock( transformer ); 111 | expect( transformer.hasIncludes() ).toBeFalse(); 112 | transformer.$property( 113 | propertyName = "defaultIncludes", 114 | mock = [ "author" ] 115 | ); 116 | expect( transformer.hasIncludes() ).toBeTrue(); 117 | transformer.$property( 118 | propertyName = "defaultIncludes", 119 | mock = [] 120 | ); 121 | expect( transformer.hasIncludes() ).toBeFalse(); 122 | transformer.$property( 123 | propertyName = "availableIncludes", 124 | mock = [ "publisher" ] 125 | ); 126 | expect( transformer.hasIncludes() ).toBeTrue(); 127 | } ); 128 | 129 | it( "can process the includes of a transformer", function() { 130 | var mockScope = getMockBox().createMock( "cffractal.models.Scope" ); 131 | mockScope.$property( propertyName = "identifier", mock = "" ); 132 | mockScope.$property( propertyName = "includes", mock = [ "author" ] ); 133 | mockScope.$property( propertyName = "excludes", mock = [] ); 134 | mockScope.$( "requestedInclude" ).$args( "author" ).$results( true ); 135 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 136 | prepareMock( transformer ); 137 | transformer.$( "includeAuthor", { "foo" = "bar" } ); 138 | var mockChildScope = getMockBox().createMock( "cffractal.models.Scope" ); 139 | mockChildScope.$( "convert", { "foo" = "bar" } ); 140 | mockScope.$( "embedChildScope" ).$args( "author", { "foo" = "bar" } ).$results( mockChildScope ); 141 | transformer.$property( 142 | propertyName = "availableIncludes", 143 | mock = [ "author" ] 144 | ); 145 | 146 | var includedData = transformer.processIncludes( mockScope, mockItem ); 147 | expect( includedData ).toBe( [ { "foo" = "bar" } ] ); 148 | } ); 149 | } ); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /models/transformers/AbstractTransformer.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name AbstractTransformer 3 | * @package cffractal.models.transformers 4 | * @description Defines the common methods for transforming 5 | * resources, including processing includes, 6 | * and creating new resources. 7 | */ 8 | component accessors="true" { 9 | 10 | /** 11 | * A WireBox instance for ColdBox users to easily 12 | * retrieve new transformers for includes. 13 | */ 14 | property name="wirebox" inject="wirebox"; 15 | property name="manager" inject="Manager@cffractal"; 16 | 17 | /** 18 | * Define includes properties to allow for accessors 19 | **/ 20 | property name="defaultIncludes"; 21 | property name="availableIncludes"; 22 | 23 | /** 24 | * The key used to define the root level key for the serialized data. 25 | */ 26 | variables.resourceKey = "data"; 27 | 28 | /** 29 | * The array of default includes. 30 | * These includes are always return whether requested or not. 31 | * These are normally set by setting the `variables.defaultIncludes` 32 | * inside a concrete Transformer component. 33 | */ 34 | variables.defaultIncludes = []; 35 | 36 | /** 37 | * The array of available includes. 38 | * These includes are only returned if requested in the Fractal Manager. 39 | * These are normally set by setting the `variables.availableIncludes` 40 | * inside a concrete Transformer component. 41 | */ 42 | variables.availableIncludes = []; 43 | 44 | /** 45 | * @abstract 46 | * Defines the method for transforming a specific resource. 47 | * 48 | * @data The data or component to transform. 49 | * 50 | * @returns The transformed data. 51 | */ 52 | function transform( data ) { 53 | throw( 54 | type = "MethodNotImplemented", 55 | message = "The method `transform()` must be implemented in a subclass." 56 | ); 57 | } 58 | 59 | /** 60 | * Allows the user to set a custom fractal Manager 61 | **/ 62 | function setManager( required any manager ){ 63 | variables.manager = arguments.manager; 64 | return this; 65 | } 66 | 67 | /** 68 | * Allows the user to set a custom serializer for the transformer. 69 | * This method sets both the itemSerializer and the collectionSerializer. 70 | * 71 | * @serializer The default custom serializer to use for items and 72 | * collections created by this transformer. 73 | * 74 | * @returns The transfomer instance. 75 | */ 76 | function setSerializer( required any serializer ) { 77 | setItemSerializer( serializer ); 78 | setCollectionSerializer( serializer ); 79 | return this; 80 | } 81 | 82 | /** 83 | * Allows the user to set a custom item serializer for the transformer. 84 | * 85 | * @serializer The default custom serializer to use for items created by this transformer. 86 | * 87 | * @returns The transfomer instance. 88 | */ 89 | function setItemSerializer( required any serializer ) { 90 | variables.itemSerializer = arguments.serializer; 91 | return this; 92 | } 93 | 94 | /** 95 | * Allows the user to set a custom collection serializer for the transformer. 96 | * 97 | * @serializer The default custom serializer to use for collections created by this transformer. 98 | * 99 | * @returns The transfomer instance. 100 | */ 101 | function setCollectionSerializer( required any serializer ) { 102 | variables.collectionSerializer = arguments.serializer; 103 | return this; 104 | } 105 | 106 | /** 107 | * Determines if a transformer has any available or default includes. 108 | * 109 | * @returns True if a transformer has any available or default includes. 110 | */ 111 | function hasIncludes() { 112 | return ! arrayIsEmpty( variables.availableIncludes ) || 113 | ! arrayIsEmpty( variables.defaultIncludes ); 114 | } 115 | 116 | /** 117 | * Processes any available includes and returns the transformed data. 118 | * 119 | * @scope The current Fractal scope. Used to find request includes 120 | * and to set the current nesting identifier. 121 | * @data The data or component off of which to base the include. 122 | * 123 | * @returns An array of transformed data to merge in to the current scope. 124 | */ 125 | function processIncludes( scope, data ) { 126 | var scopedIncludes = filterIncludes( scope ); 127 | var includedData = []; 128 | 129 | var scopedExcludes = scope.getExcludes( scoped = true ); 130 | var allIncludes = scope.getIncludes(); 131 | var allExcludes = scope.getExcludes(); 132 | 133 | for ( var include in scopedIncludes ) { 134 | var resource = invoke( this, "include#include#", { 135 | 1 = data, 136 | 2 = scopedIncludes, 137 | 3 = scopedExcludes, 138 | 4 = allIncludes, 139 | 5 = allExcludes 140 | } ); 141 | if ( isSimpleValue( resource ) ) { 142 | resource = item( 143 | resource, 144 | function( item ) { return item; }, 145 | new cffractal.models.serializers.SimpleSerializer() 146 | ); 147 | } 148 | var childScope = scope.embedChildScope( include, resource ); 149 | arrayAppend( includedData, childScope.convert() ); 150 | } 151 | return includedData; 152 | } 153 | 154 | /** 155 | * Returns the resource key. 156 | * The resource key is used to define the root level key for the serialized data. 157 | * 158 | * @returns The current resource key. 159 | */ 160 | function getResourceKey() { 161 | return variables.resourceKey; 162 | } 163 | 164 | /** 165 | * Returns the resource key. 166 | * The resource key is used to define the root level key for the serialized data. 167 | * 168 | * @resourceKey The new key to use. 169 | * 170 | * @returns The Transformer instance. 171 | */ 172 | function setResourceKey( resourceKey ) { 173 | variables.resourceKey = arguments.resourceKey; 174 | return this; 175 | } 176 | 177 | /** 178 | * Filters all includes down to default includes and requested available includes. 179 | * 180 | * @scope The current Fractal scope. Used to determine if 181 | * an available includes was requested. 182 | * 183 | * @returns An array of includes to fetch for the transformer. 184 | */ 185 | public array function filterIncludes( scope ) { 186 | var filteredIncludes = duplicate( variables.defaultIncludes ); 187 | for ( var include in variables.availableIncludes ) { 188 | if ( scope.requestedInclude( include ) ) { 189 | arrayAppend( filteredIncludes, include ); 190 | } 191 | } 192 | var excludes = scope.getExcludes(); 193 | return arrayFilter( filteredIncludes, function( include ) { 194 | return ! scope.requestedExclude( include ); 195 | } ); 196 | } 197 | 198 | /** 199 | * Returns a new item resource with the given data and transformer. 200 | * Used primarily inside a includes method. 201 | * 202 | * @data The data or component to transform. 203 | * @transformer The transformer callback or component to use 204 | * transforming the above data. 205 | * @serializer A custom serializer for this resource. 206 | * @itemCallback An optional callback to call after each item is serialized. 207 | * 208 | * @returns A new cffractal Item wrapping the given data and transformer. 209 | */ 210 | private function item( data, transformer, serializer, itemCallback ) { 211 | if ( isNull( arguments.serializer ) && ! isNull( variables.itemSerializer ) ) { 212 | arguments.serializer = variables.itemSerializer; 213 | } 214 | return manager.item( argumentCollection = arguments ); 215 | } 216 | 217 | /** 218 | * Returns a new collection resource with the given data and transformer. 219 | * Used primarily inside a includes method. 220 | * 221 | * @data The data or component to transform. 222 | * @transformer The transformer callback or component to use 223 | * transforming the above data. 224 | * @serializer A custom serializer for this resource. 225 | * @itemCallback An optional callback to call after each item is serialized. 226 | * 227 | * @returns A new cffractal Collection wrapping the given data and transformer. 228 | */ 229 | private function collection( data, transformer, serializer, itemCallback ) { 230 | if ( isNull( arguments.serializer ) && ! isNull( variables.collectionSerializer ) ) { 231 | arguments.serializer = variables.collectionSerializer; 232 | } 233 | return manager.collection( argumentCollection = arguments ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /models/resources/AbstractResource.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name AbstractResource 3 | * @package cffractal.models.resources 4 | * @description Defines the common methods for processing 5 | * resources into serializable data. 6 | */ 7 | component accessors="true" { 8 | 9 | /** 10 | * The item to transform into serializable data. 11 | */ 12 | property name="data"; 13 | 14 | /** 15 | * The transformer component or callback used to transform the data. 16 | */ 17 | property name="transformer"; 18 | 19 | /** 20 | * The serializer used for this resource. 21 | */ 22 | property name="serializer"; 23 | 24 | /** 25 | * The collection of metadata for this resource. Default: {}. 26 | */ 27 | property name="meta"; 28 | 29 | /** 30 | * The paging data for this resource. 31 | */ 32 | property name="pagingData"; 33 | 34 | /** 35 | * An array of post-transformation callbacks to run 36 | * on each item after it has been transformed. 37 | */ 38 | variables.postTransformationCallbacks = []; 39 | 40 | /** 41 | * Creates a new cffractal resource. 42 | * 43 | * @data The data to be transformed into serializable data. 44 | * @transformer The transformer component or callback to 45 | * use to transform the data. 46 | * 47 | * @returns A Fractal resource. 48 | */ 49 | function init( data, transformer, serializer, meta = {}, itemCallback ) { 50 | variables.data = isNull( arguments.data ) ? javacast( "null", "" ) : arguments.data; 51 | variables.transformer = arguments.transformer; 52 | variables.serializer = arguments.serializer; 53 | variables.meta = arguments.meta; 54 | if ( ! isNull( itemCallback ) ) { 55 | addPostTransformationCallback( itemCallback ); 56 | } 57 | return this; 58 | } 59 | 60 | /** 61 | * @abstract 62 | * Processes the conversion of a resource to serializable data. 63 | * Also processes any default or requested includes. 64 | * 65 | * @scope A Fractal scope instance. Used to determinal requested 66 | * includes and handle nesting identifiers. 67 | * 68 | * @returns The transformed data. 69 | */ 70 | function process( scope ) { 71 | throw( 72 | type = "MethodNotImplemented", 73 | message = "The method `process()` must be implemented in a subclass." 74 | ); 75 | } 76 | 77 | /** 78 | * Processes the conversion of a single item to serializable data. 79 | * Also processes any default or requested includes. 80 | * 81 | * @scope A Fractal scope instance. Used to determinal requested 82 | * includes and handle nesting identifiers. 83 | * @item A single item instance to transform. 84 | * 85 | * @returns The transformed data. 86 | */ 87 | function processItem( scope, item ) { 88 | if ( isNull( item ) ) { 89 | return scope.getNullDefaultValue(); 90 | } 91 | 92 | var transformedData = transformData( transformer, item, scope ); 93 | 94 | if ( isNull( transformedData ) ) { 95 | return scope.getNullDefaultValue(); 96 | } 97 | 98 | transformedData = removeExcludes( 99 | transformedData, 100 | scope.filteredExcludes() 101 | ); 102 | 103 | if ( isClosure( transformer ) || isCustomFunction( transformer ) ) { 104 | return isNull( transformedData ) ? javacast( "null", "" ) : transformedData; 105 | } 106 | 107 | transformedData = removeUnusedAvailableIncludes( 108 | transformedData, 109 | transformer.getAvailableIncludes(), 110 | transformer.filterIncludes( scope ) 111 | ); 112 | 113 | if ( ! transformer.hasIncludes() ) { 114 | return isNull( transformedData ) ? javacast( "null", "" ) : transformedData; 115 | } 116 | 117 | var includedData = transformer.processIncludes( 118 | scope, 119 | item 120 | ); 121 | 122 | for ( var includedDataSet in includedData ) { 123 | structAppend( 124 | isNull( transformedData ) ? {} : transformedData, 125 | includedDataSet, 126 | true /* overwrite */ 127 | ); 128 | } 129 | 130 | return isNull( transformedData ) ? javacast( "null", "" ) : transformedData; 131 | } 132 | 133 | /** 134 | * Adds some data under a given identifier in the metadata. 135 | * 136 | * @key The key to nest the data under in the metadata scope. 137 | * @value The data to store under the given key. 138 | * 139 | * @returns The resource instance. 140 | */ 141 | function addMeta( key, value ) { 142 | variables.meta[ key ] = value; 143 | return this; 144 | } 145 | 146 | /** 147 | * Returns whether the resource has any metadata associated with it. 148 | * 149 | * @returns True if there are any metadata keys present. 150 | */ 151 | function hasMeta() { 152 | return ! structIsEmpty( variables.meta ); 153 | } 154 | 155 | /** 156 | * Returns whether any paging data has been set. 157 | * 158 | * @returns True if there is any paging data set. 159 | */ 160 | function hasPagingData() { 161 | return ! isNull( variables.pagingData ); 162 | } 163 | 164 | /** 165 | * Add a post transformation callback to run after transforming each item. 166 | * The value returned from the callback becomes the transformed item. 167 | * 168 | * @callback A callback to run after the resource has been transformed. 169 | * The callback will be passed the transformed data, the 170 | * original data, and the resource object as arguments. 171 | * 172 | * @returns The resource instance. 173 | */ 174 | function addPostTransformationCallback( callback ) { 175 | arrayAppend( postTransformationCallbacks, callback ); 176 | return this; 177 | } 178 | 179 | function getTransformerResourceKey() { 180 | return isClosure( variables.transformer ) ? 181 | "data" : 182 | variables.transformer.getResourceKey(); 183 | } 184 | 185 | /** 186 | * Handles the calling of the transformer, 187 | * whether a callback or a component. 188 | * 189 | * @transformer The callback or component to use to transform the item. 190 | * @item The item to transform. 191 | * 192 | * @returns The transformed data. 193 | */ 194 | private function transformData( transformer, item, scope ) { 195 | var scopedIncludes = scope.getIncludes( scoped = true ); 196 | var scopedExcludes = scope.getExcludes( scoped = true ); 197 | var allIncludes = scope.getIncludes(); 198 | var allExcludes = scope.getExcludes(); 199 | 200 | if ( isClosure( transformer ) || isCustomFunction( transformer ) ) { 201 | return transformer( 202 | isNull( item ) ? javacast( "null", "" ) : item, 203 | scopedIncludes, 204 | scopedExcludes, 205 | allIncludes, 206 | allExcludes 207 | ); 208 | } 209 | 210 | return transformer.transform( 211 | isNull( item ) ? javacast( "null", "" ) : item, 212 | scopedIncludes, 213 | scopedExcludes, 214 | allIncludes, 215 | allExcludes 216 | ); 217 | } 218 | 219 | /** 220 | * Returns the original value if it is not null. 221 | * Otherwise, returns the manager null default value. 222 | * 223 | * @returns The original value, if not null, or the manager default null value. 224 | */ 225 | private function paramNull( value, defaultValue ) { 226 | return isNull( value ) ? defaultValue : value; 227 | } 228 | 229 | /** 230 | * Removes any excluded keys from the transformed data. 231 | * 232 | * @transformedData The current transformed data structure. 233 | * @excludes The current filtered excludes list. 234 | * 235 | * @returns The transformed data without any excluded keys. 236 | */ 237 | private function removeExcludes( transformedData, excludes ) { 238 | if ( isStruct( transformedData ) && ! isObject( transformedData ) ) { 239 | for ( var exclude in excludes ) { 240 | structDelete( transformedData, exclude ); 241 | } 242 | } 243 | return transformedData; 244 | } 245 | 246 | /** 247 | * Removes any unused available includes keys from the transformed data. 248 | * 249 | * @transformedData The current transformed data structure. 250 | * @availableIncludes The available includes for the transformer. 251 | * @includes The current filtered includes list. 252 | * 253 | * @returns The transformed data without any unsued available includes keys. 254 | */ 255 | private function removeUnusedAvailableIncludes( transformedData, availableIncludes, includes ) { 256 | for ( var availableInclude in availableIncludes ) { 257 | if ( ! includes.contains( availableInclude ) ) { 258 | structDelete( transformedData, availableInclude ); 259 | } 260 | } 261 | return transformedData; 262 | } 263 | 264 | } 265 | -------------------------------------------------------------------------------- /tests/specs/unit/serializers/XMLSerializerTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | function run() { 3 | describe( "xml serializer", function() { 4 | beforeEach( function() { 5 | variables.XMLSerializer = new cffractal.models.serializers.XMLSerializer(); 6 | variables.fractal = new cffractal.models.Manager( XMLSerializer, XMLSerializer, {} ); 7 | } ); 8 | 9 | it( "with a callback transformer", function() { 10 | var book = new tests.resources.Book( { 11 | id = 1, 12 | title = "To Kill a Mockingbird", 13 | year = "1960" 14 | } ); 15 | var resource = fractal.item( book, function( book ) { 16 | return { 17 | "id" = book.getId(), 18 | "title" = book.getTitle(), 19 | "year" = book.getYear() 20 | }; 21 | } ); 22 | 23 | var scope = fractal.createData( resource ); 24 | expect( scope.convert() ).toMatch( '1To Kill a Mockingbird1960' ); 25 | } ); 26 | 27 | it( "with a custom transformer", function() { 28 | var book = new tests.resources.Book( { 29 | id = 1, 30 | title = "To Kill a Mockingbird", 31 | year = "1960" 32 | } ); 33 | 34 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 35 | 36 | var scope = fractal.createData( resource ); 37 | expect( scope.convert() ).toMatch( '1To Kill a Mockingbird1960' ); 38 | } ); 39 | 40 | describe( "has serialization options", function() { 41 | beforeEach( function() { 42 | variables.XMLSerializer = new cffractal.models.serializers.XMLSerializer( sortKeys = false ); 43 | variables.fractal = new cffractal.models.Manager( XMLSerializer, XMLSerializer, {} ); 44 | }); 45 | 46 | it( "can preserve the order of the keys", function() { 47 | var book = new tests.resources.Book( { 48 | id = 1, 49 | title = "To Kill a Mockingbird", 50 | year = "1960" 51 | } ); 52 | 53 | var resource = fractal.item( book, new tests.resources.BookTransformer( sortKeys = false ).setManager( fractal ) ); 54 | 55 | var scope = fractal.createData( resource ); 56 | expect( scope.convert() ).toMatch( '1960To Kill a Mockingbird1' ); 57 | } ); 58 | }); 59 | 60 | describe( "includes", function() { 61 | it( "ignores includes by default", function() { 62 | var book = new tests.resources.Book( { 63 | id = 1, 64 | title = "To Kill a Mockingbird", 65 | year = "1960", 66 | author = new tests.resources.Author( { 67 | id = 1, 68 | name = "Harper Lee", 69 | birthdate = createDate( 1926, 04, 28 ) 70 | } ) 71 | } ); 72 | 73 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 74 | 75 | var scope = fractal.createData( resource ); 76 | expect( scope.convert() ).toMatch( '1To Kill a Mockingbird1960' ); 77 | } ); 78 | 79 | it( "can parse an item with an includes", function() { 80 | var book = new tests.resources.Book( { 81 | id = 1, 82 | title = "To Kill a Mockingbird", 83 | year = "1960", 84 | author = new tests.resources.Author( { 85 | id = 1, 86 | name = "Harper Lee", 87 | birthdate = createDate( 1926, 04, 28 ) 88 | } ) 89 | } ); 90 | 91 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 92 | 93 | var scope = fractal.createData( resource = resource, includes = "author" ); 94 | expect( scope.convert() ).toMatch( 'Harper Lee1To Kill a Mockingbird1960' ); 95 | } ); 96 | 97 | it( "can parse an item with a default includes", function() { 98 | var book = new tests.resources.Book( { 99 | id = 1, 100 | title = "To Kill a Mockingbird", 101 | year = "1960", 102 | author = new tests.resources.Author( { 103 | id = 1, 104 | name = "Harper Lee", 105 | birthdate = createDate( 1926, 04, 28 ) 106 | } ) 107 | } ); 108 | 109 | var resource = fractal.item( book, new tests.resources.DefaultIncludesBookTransformer().setManager( fractal ) ); 110 | 111 | var scope = fractal.createData( resource ); 112 | expect( scope.convert() ).toMatch( 'Harper Lee41To Kill a Mockingbird1960' ); 113 | } ); 114 | 115 | it( "can parse an item with a nested includes", function() { 116 | var book = new tests.resources.Book( { 117 | id = 1, 118 | title = "To Kill a Mockingbird", 119 | year = "1960", 120 | author = new tests.resources.Author( { 121 | id = 1, 122 | name = "Harper Lee", 123 | birthdate = createDate( 1926, 04, 28 ), 124 | country = new tests.resources.Country( { 125 | id = 1, 126 | name = "United States", 127 | planet = new tests.resources.Planet( { 128 | id = 1, 129 | name = "Earth" 130 | } ) 131 | } ) 132 | } ) 133 | } ); 134 | 135 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 136 | 137 | var scope = fractal.createData( resource, "author.country" ); 138 | var expectedData = '1United StatesHarper Lee1To Kill a Mockingbird1960'; 139 | expect( scope.convert() ).toMatch( expectedData ); 140 | } ); 141 | 142 | it( "can parse an item with a deep nested includes", function() { 143 | var book = new tests.resources.Book( { 144 | id = 1, 145 | title = "To Kill a Mockingbird", 146 | year = "1960", 147 | author = new tests.resources.Author( { 148 | id = 1, 149 | name = "Harper Lee", 150 | birthdate = createDate( 1926, 04, 28 ), 151 | country = new tests.resources.Country( { 152 | id = 1, 153 | name = "United States", 154 | planet = new tests.resources.Planet( { 155 | id = 1, 156 | name = "Earth" 157 | } ) 158 | } ) 159 | } ) 160 | } ); 161 | 162 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 163 | 164 | var scope = fractal.createData( resource, "author.country.planet" ); 165 | var expectedData = '1United States1EarthHarper Lee1To Kill a Mockingbird1960'; 166 | expect( scope.convert() ).toMatch( expectedData ); 167 | } ); 168 | 169 | it( "can automatically includes the parent when grabbing a nested include", function() { 170 | var book = new tests.resources.Book( { 171 | id = 1, 172 | title = "To Kill a Mockingbird", 173 | year = "1960", 174 | author = new tests.resources.Author( { 175 | id = 1, 176 | name = "Harper Lee", 177 | birthdate = createDate( 1926, 04, 28 ), 178 | country = new tests.resources.Country( { 179 | id = 1, 180 | name = "United States" 181 | } ) 182 | } ) 183 | } ); 184 | 185 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 186 | 187 | var scope = fractal.createData( resource, "author.country" ); 188 | var expectedData = '1United StatesHarper Lee1To Kill a Mockingbird1960'; 189 | expect( scope.convert() ).toMatch( expectedData ); 190 | } ); 191 | } ); 192 | } ); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /models/Scope.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Scope 3 | * @package cffractal.models 4 | * @description The Scope component is responsible for encapsulating the api 5 | * transformation process for a single specific resource 6 | * and serializing the result. 7 | */ 8 | component accessors="true" { 9 | 10 | /** 11 | * A reference to the Fractal Manager. 12 | */ 13 | property name="manager"; 14 | 15 | /** 16 | * The specific resource to transform and serialize. 17 | */ 18 | property name="resource"; 19 | 20 | /** 21 | * The array of requested includes. 22 | */ 23 | property name="includes"; 24 | 25 | /** 26 | * The array of requested excludes. 27 | */ 28 | property name="excludes"; 29 | 30 | /** 31 | * The resource identifier for the specific resource. 32 | * Used to determine the correct nesting level. 33 | */ 34 | property name="identifier"; 35 | 36 | /** 37 | * Returns a new scoped resource at a given identifier. 38 | * 39 | * @manager A reference to a Fractal manager that has the needed includes. 40 | * @resource The specific resoruce to transform and serialize. 41 | * @includes A list of includes for the scope. Includes are 42 | * comma separated and use dots to designate 43 | * nested resources to be included. 44 | * @excludes A list of excludes for the scope. Excludes are 45 | * comma separated and use dots to designate 46 | * nested resources to be excluded. 47 | * @identifier Optional. The resource identifier for the specific resource. Default: "". 48 | * 49 | * @returns A scoped resource instance. 50 | */ 51 | function init( manager, resource, includes = "", excludes = "", identifier = "" ) { 52 | variables.manager = arguments.manager; 53 | variables.resource = arguments.resource; 54 | variables.identifier = arguments.identifier; 55 | 56 | parseExcludes( arguments.excludes ); 57 | parseIncludes( arguments.includes ); 58 | 59 | return this; 60 | } 61 | 62 | /** 63 | * Return the includes for the scope. 64 | * 65 | * @scoped If true, only pass the current level of includes with no nesting, 66 | * 67 | * @returns An array of includes. 68 | */ 69 | function getIncludes( scoped = false ) { 70 | if ( ! scoped ) { 71 | return variables.includes; 72 | } 73 | 74 | var scopedIncludes = []; 75 | for ( var include in variables.includes ) { 76 | var scopedInclude = include; 77 | if ( identifier != "" && find( ".", scopedInclude ) <= 0 ) { 78 | continue; 79 | } 80 | if ( identifier != "" ) { 81 | scopedInclude = replaceNoCase( scopedInclude, identifier & ".", "" ); 82 | } 83 | if ( scopedInclude != "" && find( ".", scopedInclude ) <= 0 ) { 84 | arrayAppend( scopedIncludes, listFirst( scopedInclude, "." ) ); 85 | } 86 | } 87 | return scopedIncludes; 88 | } 89 | 90 | /** 91 | * Return the excludes for the scope. 92 | * 93 | * @scoped If true, only pass the current level of excludes with no nesting, 94 | * 95 | * @returns An array of excludes. 96 | */ 97 | function getExcludes( scoped = false ) { 98 | if ( ! scoped ) { 99 | return variables.excludes; 100 | } 101 | 102 | var scopedExcludes = []; 103 | for ( var exclude in variables.excludes ) { 104 | if ( identifier == "" ) { 105 | if ( find( ".", exclude ) <= 0 ) { 106 | arrayAppend( scopedExcludes, exclude ); 107 | } 108 | } 109 | else { 110 | if ( find( ".", exclude ) <= 0 ) { 111 | continue; 112 | } 113 | var scopedExclude = replaceNoCase( exclude, identifier & ".", "" ); 114 | if ( find( ".", scopedExclude ) <= 0 ) { 115 | arrayAppend( scopedExcludes, scopedExclude ); 116 | } 117 | } 118 | } 119 | 120 | return scopedExcludes; 121 | } 122 | 123 | /** 124 | * Create a new Scope with a given scope identifier. 125 | * 126 | * @identifier The resource identifier for the specific resource. 127 | * @resource The specific child resoruce to transform and serialize. 128 | * 129 | * @returns A scoped resource instance. 130 | */ 131 | function embedChildScope( identifier, resource ) { 132 | arguments.identifier = combineIdentifiers( variables.identifier, arguments.identifier ); 133 | arguments.includes = arrayToList( includes ); 134 | arguments.excludes = arrayToList( excludes ); 135 | return manager.createData( argumentCollection = arguments ); 136 | } 137 | 138 | /** 139 | * Converts the scoped resource to a struct. 140 | * The scoped resource is processed through the 141 | * assigned transformer and serializer. 142 | * 143 | * @returns The transformed and serialized data. 144 | */ 145 | function convert() { 146 | var serializer = resource.getSerializer(); 147 | 148 | if ( identifier != "" ) { 149 | return serializer.scopeData( resource, this, identifier ); 150 | } 151 | 152 | var serializedData = serializer.data( resource, this ); 153 | 154 | serializedData = serializer.scopeRootKey( serializedData, resource.getTransformerResourceKey() ); 155 | 156 | if ( resource.hasPagingData() ) { 157 | resource.addMeta( "pagination", resource.getPagingData() ); 158 | } 159 | 160 | if ( resource.hasMeta() ) { 161 | serializer.meta( resource, this, serializedData ); 162 | } 163 | 164 | return serializedData; 165 | } 166 | 167 | /** 168 | * Converts the scoped resource to json. 169 | * The scoped resource is processed through the 170 | * assigned transformer and serializer. 171 | * 172 | * @returns The transformed and serialized data as json. 173 | */ 174 | function toJSON() { 175 | return serializeJSON( convert() ); 176 | } 177 | 178 | /** 179 | * Returns the manager's null default value. 180 | * 181 | * @returns The null default value. 182 | */ 183 | function getNullDefaultValue() { 184 | return variables.manager.getNullDefaultValue(); 185 | } 186 | 187 | /** 188 | * Returns if an include is requested. 189 | * 190 | * @needle The include to see if it is requested. 191 | * 192 | * @returns True, if the include is requested. 193 | */ 194 | function requestedInclude( needle ) { 195 | if ( identifier != "" ) { 196 | needle = "#identifier#.#needle#"; 197 | } 198 | 199 | for ( var include in includes ) { 200 | if ( compareNoCase( needle, include ) == 0 ) { 201 | return true; 202 | } 203 | } 204 | 205 | return false; 206 | } 207 | 208 | /** 209 | * Returns if an exclude is requested. 210 | * 211 | * @needle The exclude to see if it is requested. 212 | * 213 | * @returns True, if the exclude is requested. 214 | */ 215 | function requestedExclude( needle ) { 216 | if ( identifier != "" ) { 217 | needle = "#identifier#.#needle#"; 218 | } 219 | 220 | for ( var exclude in excludes ) { 221 | if ( compareNoCase( needle, exclude ) == 0 ) { 222 | return true; 223 | } 224 | } 225 | 226 | return false; 227 | } 228 | 229 | /** 230 | * Return the excludes for the specified level. 231 | * 232 | * @returns The excludes minus the identifier (if any). 233 | */ 234 | function filteredExcludes() { 235 | var collection = []; 236 | for ( var exclude in excludes ) { 237 | if ( identifier == "" ) { 238 | arrayAppend( collection, exclude ); 239 | } 240 | else { 241 | arrayAppend( collection, replaceNoCase( exclude, identifier & ".", "" ) ); 242 | } 243 | } 244 | return arrayFilter( collection, function( item ) { 245 | return item != ""; 246 | } ); 247 | } 248 | 249 | /** 250 | * Parse the list of includes, including parent includes not specified. 251 | * 252 | * @includes A list of includes. 253 | * 254 | * @returns The Scope instance. 255 | */ 256 | private function parseIncludes( includes ) { 257 | variables.includes = listToArray( arguments.includes ); 258 | addParentIncludes(); 259 | return this; 260 | } 261 | 262 | /** 263 | * Parse the list of excludes,. 264 | * 265 | * @excludes A list of excludes. 266 | * 267 | * @returns The Scope instance. 268 | */ 269 | private function parseExcludes( excludes ) { 270 | variables.excludes = listToArray( arguments.excludes ); 271 | return this; 272 | } 273 | 274 | /** 275 | * Add parent includes not specified in the list of includes. 276 | * 277 | * When a nested resource is specified like `author.country` make 278 | * sure the includes has each parent as well (`author` in this case). 279 | */ 280 | private function addParentIncludes() { 281 | // we create a temporary array to store the additional includes in 282 | // because you cannot concurrently loop over and modify an array. 283 | var parentIncludes = []; 284 | for ( var include in includes ) { 285 | var scopes = listToArray( include, "." ); 286 | if ( arrayLen( scopes ) <= 1 ) { 287 | continue; 288 | } 289 | var scopesToAdd = arraySlice( scopes, 1, arrayLen( scopes ) - 1 ); 290 | for ( var i = 1; i <= arrayLen( scopesToAdd ); i++ ) { 291 | arrayAppend( parentIncludes, arrayToList( arraySlice( scopesToAdd, 1, i ), "." ) ); 292 | } 293 | } 294 | for ( var parentInclude in parentIncludes ) { 295 | if ( ! arrayContains( variables.includes, parentInclude ) ) { 296 | arrayAppend( variables.includes, parentInclude ); 297 | } 298 | } 299 | } 300 | 301 | private function combineIdentifiers( identifierA, identifierB ) { 302 | var combinedIdentifier = identifierA & "." & identifierB; 303 | if ( left( combinedIdentifier, 1 ) == "." ) { 304 | return mid( combinedIdentifier, 2, len( combinedIdentifier ) - 1 ); 305 | } 306 | return combinedIdentifier; 307 | } 308 | 309 | } 310 | -------------------------------------------------------------------------------- /tests/specs/unit/ScopeTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "scope", function() { 5 | it( "can be instantiated", function() { 6 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 7 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 8 | expect( new cffractal.models.Scope( mockFractal, mockItem ) ) 9 | .toBeInstanceOf( "cffractal.models.Scope" ); 10 | } ); 11 | 12 | describe( "converting data to struct", function() { 13 | describe( "converting a single item", function() { 14 | it( "with a callback transformer", function() { 15 | var data = { "foo" = "bar" }; 16 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 17 | mockItem.$property( propertyName = "transformer", mock = function( data ) { return data; } ); 18 | mockItem.$property( propertyName = "data", mock = data ); 19 | mockItem.$property( propertyName = "meta", mock = {} ); 20 | 21 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 22 | mockSerializer.$( "data", { "data" = data } ); 23 | mockItem.$property( propertyName = "serializer", mock = mockSerializer ); 24 | 25 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 26 | 27 | var scope = new cffractal.models.Scope( mockFractal, mockItem ); 28 | expect( scope.convert() ).toBe( { "data" = data } ); 29 | } ); 30 | 31 | it( "with a custom transformer", function() { 32 | var data = { "foo" = "bar" }; 33 | var mockTransformer = getMockBox().createMock( "cffractal.models.transformers.AbstractTransformer" ); 34 | mockTransformer.$property( propertyName = "resourceKey", mock = "data" ); 35 | mockTransformer.$( "transform", data ); 36 | mockTransformer.$( "hasIncludes", false ); 37 | 38 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 39 | mockItem.$property( propertyName = "transformer", mock = mockTransformer ); 40 | mockItem.$property( propertyName = "data", mock = data ); 41 | mockItem.$property( propertyName = "meta", mock = {} ); 42 | 43 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 44 | mockSerializer.$( "data", { "data" = data } ); 45 | mockItem.$property( propertyName = "serializer", mock = mockSerializer ); 46 | 47 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 48 | 49 | var scope = new cffractal.models.Scope( mockFractal, mockItem ); 50 | expect( scope.convert() ).toBe( { "data" = data } ); 51 | } ); 52 | } ); 53 | 54 | describe( "converting a collection", function() { 55 | it( "with a callback transformer", function() { 56 | var data = [ { "foo" = "bar" }, { "baz" = "ban" } ]; 57 | var mockCollection = getMockBox().createMock( "cffractal.models.resources.Collection" ); 58 | mockCollection.$property( 59 | propertyName = "transformer", 60 | mock = function( data ) { return data; } 61 | ); 62 | mockCollection.$property( propertyName = "data", mock = data ); 63 | mockCollection.$property( propertyName = "meta", mock = {} ); 64 | 65 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 66 | mockSerializer.$( "data", { "data" = data } ); 67 | mockCollection.$property( propertyName = "serializer", mock = mockSerializer ); 68 | 69 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 70 | 71 | var scope = new cffractal.models.Scope( mockFractal, mockCollection ); 72 | expect( scope.convert() ).toBe( { "data" = data } ); 73 | } ); 74 | 75 | it( "with a custom transformer", function() { 76 | var data = [ { "foo" = "bar" }, { "baz" = "ban" } ]; 77 | var mockTransformer = getMockBox().createMock( "cffractal.models.transformers.AbstractTransformer" ); 78 | mockTransformer.$property( propertyName = "resourceKey", mock = "data" ); 79 | mockTransformer.$( "transform" ).$args( { "foo" = "bar" } ).$results( { "foo" = "bar" } ); 80 | mockTransformer.$( "transform" ).$args( { "baz" = "ban" } ).$results( { "baz" = "ban" } ); 81 | mockTransformer.$( "hasIncludes", false ); 82 | 83 | var mockCollection = getMockBox().createMock( "cffractal.models.resources.Collection" ); 84 | mockCollection.$property( propertyName = "transformer", mock = mockTransformer ); 85 | mockCollection.$property( propertyName = "data", mock = data ); 86 | mockCollection.$property( propertyName = "meta", mock = {} ); 87 | 88 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 89 | mockSerializer.$( "data", { "data" = data } ); 90 | mockCollection.$property( propertyName = "serializer", mock = mockSerializer ); 91 | 92 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 93 | 94 | var scope = new cffractal.models.Scope( mockFractal, mockCollection ); 95 | expect( scope.convert() ).toBe( { "data" = data } ); 96 | } ); 97 | } ); 98 | } ); 99 | 100 | describe( "converting data to json", function() { 101 | describe( "converting a single item", function() { 102 | it( "with a callback transformer", function() { 103 | var data = { "foo" = "bar" }; 104 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 105 | mockItem.$property( propertyName = "transformer", mock = function( data ) { return data; } ); 106 | mockItem.$property( propertyName = "data", mock = data ); 107 | mockItem.$property( propertyName = "meta", mock = {} ); 108 | 109 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 110 | mockSerializer.$( "data", { "data" = data } ); 111 | mockItem.$property( propertyName = "serializer", mock = mockSerializer ); 112 | 113 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 114 | 115 | var scope = new cffractal.models.Scope( mockFractal, mockItem ); 116 | expect( scope.toJSON() ).toBe( serializeJSON( { "data" = data } ) ); 117 | } ); 118 | 119 | it( "with a custom transformer", function() { 120 | var data = { "foo" = "bar" }; 121 | var mockTransformer = getMockBox().createMock( "cffractal.models.transformers.AbstractTransformer" ); 122 | mockTransformer.$property( propertyName = "resourceKey", mock = "data" ); 123 | mockTransformer.$( "transform", data ); 124 | mockTransformer.$( "hasIncludes", false ); 125 | 126 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 127 | mockItem.$property( propertyName = "transformer", mock = mockTransformer ); 128 | mockItem.$property( propertyName = "data", mock = data ); 129 | mockItem.$property( propertyName = "meta", mock = {} ); 130 | 131 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 132 | mockSerializer.$( "data", { "data" = data } ); 133 | mockItem.$property( propertyName = "serializer", mock = mockSerializer ); 134 | 135 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 136 | 137 | var scope = new cffractal.models.Scope( mockFractal, mockItem ); 138 | expect( scope.toJSON() ).toBe( serializeJSON( { "data" = data } ) ); 139 | } ); 140 | } ); 141 | 142 | describe( "converting a collection", function() { 143 | it( "with a callback transformer", function() { 144 | var data = [ { "foo" = "bar" }, { "baz" = "ban" } ]; 145 | var mockCollection = getMockBox().createMock( "cffractal.models.resources.Collection" ); 146 | mockCollection.$property( 147 | propertyName = "transformer", 148 | mock = function( data ) { return data; } 149 | ); 150 | mockCollection.$property( propertyName = "data", mock = data ); 151 | mockCollection.$property( propertyName = "meta", mock = {} ); 152 | 153 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 154 | mockSerializer.$( "data", { "data" = data } ); 155 | mockCollection.$property( propertyName = "serializer", mock = mockSerializer ); 156 | 157 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 158 | 159 | var scope = new cffractal.models.Scope( mockFractal, mockCollection ); 160 | expect( scope.toJSON() ).toBe( serializeJSON( { "data" = data } ) ); 161 | } ); 162 | 163 | it( "with a custom transformer", function() { 164 | var data = [ { "foo" = "bar" }, { "baz" = "ban" } ]; 165 | var mockTransformer = getMockBox().createMock( "cffractal.models.transformers.AbstractTransformer" ); 166 | mockTransformer.$property( propertyName = "resourceKey", mock = "data" ); 167 | mockTransformer.$( "transform" ).$args( { "foo" = "bar" } ).$results( { "foo" = "bar" } ); 168 | mockTransformer.$( "transform" ).$args( { "baz" = "ban" } ).$results( { "baz" = "ban" } ); 169 | mockTransformer.$( "hasIncludes", false ); 170 | 171 | var mockCollection = getMockBox().createMock( "cffractal.models.resources.Collection" ); 172 | mockCollection.$property( propertyName = "transformer", mock = mockTransformer ); 173 | mockCollection.$property( propertyName = "data", mock = data ); 174 | mockCollection.$property( propertyName = "meta", mock = {} ); 175 | 176 | var mockSerializer = getMockBox().createMock( "cffractal.models.serializers.DataSerializer" ); 177 | mockSerializer.$( "data", { "data" = data } ); 178 | mockCollection.$property( propertyName = "serializer", mock = mockSerializer ); 179 | 180 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 181 | 182 | var scope = new cffractal.models.Scope( mockFractal, mockCollection ); 183 | expect( scope.toJSON() ).toBe( serializeJSON( { "data" = data } ) ); 184 | } ); 185 | } ); 186 | } ); 187 | 188 | it( "can create a new scope with a given identifier", function() { 189 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 190 | mockFractal.$( "createData" ); 191 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 192 | var mockCollection = getMockBox().createMock( "cffractal.models.resources.Collection" ); 193 | var scope = new cffractal.models.Scope( mockFractal, mockItem ); 194 | 195 | scope.embedChildScope( "author", mockCollection ); 196 | 197 | var callLog = mockFractal.$callLog(); 198 | expect( callLog ).toHaveKey( "createData" ); 199 | var createDataCallLog = mockFractal.$callLog().createData; 200 | expect( createDataCallLog ).toBeArray(); 201 | expect( createDataCallLog ).toHaveLength( 1 ); 202 | expect( createDataCallLog[ 1 ] ).toHaveKey( "includes" ); 203 | expect( createDataCallLog[ 1 ].includes ).toBe( "" ); 204 | expect( createDataCallLog[ 1 ] ).toHaveKey( "resource" ); 205 | expect( createDataCallLog[ 1 ].resource ).toBe( mockCollection ); 206 | expect( createDataCallLog[ 1 ] ).toHaveKey( "identifier" ); 207 | expect( createDataCallLog[ 1 ].identifier ).toBe( "author" ); 208 | } ); 209 | 210 | describe( "parseIncludes", function() { 211 | it( "can set a list of includes for the transformed data", function() { 212 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 213 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 214 | var scope = new cffractal.models.Scope( mockFractal, mockItem ); 215 | makePublic( scope, "parseIncludes", "parseIncludesPublic" ); 216 | prepareMock( scope ); 217 | 218 | scope.parseIncludesPublic( "author,publisher" ); 219 | 220 | expect( scope.$getProperty( "includes" ) ).toBe( [ "author", "publisher" ] ); 221 | } ); 222 | 223 | it( "automatically includes parent scopes", function() { 224 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 225 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 226 | var scope = new cffractal.models.Scope( mockFractal, mockItem, "author.country.planet" ); 227 | 228 | prepareMock( scope ); 229 | expect( scope.$getProperty( "includes" ) ).toBe( [ "author.country.planet", "author", "author.country" ] ); 230 | } ); 231 | } ); 232 | 233 | describe( "requestedInclude", function() { 234 | it( "returns true if an include was request", function() { 235 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 236 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 237 | var scope = new cffractal.models.Scope( mockFractal, mockItem, "author" ); 238 | 239 | expect( scope.requestedInclude( "author" ) ).toBeTrue(); 240 | expect( scope.requestedInclude( "publisher" ) ).toBeFalse(); 241 | } ); 242 | 243 | it( "prepends the scope identifier if passed", function() { 244 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 245 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 246 | 247 | var scope = new cffractal.models.Scope( 248 | manager = mockFractal, 249 | resource = mockItem, 250 | includes = "author.country", 251 | identifier = "author" 252 | ); 253 | 254 | expect( scope.requestedInclude( "country" ) ).toBeTrue(); 255 | } ); 256 | 257 | it( "can handle deeply nested includes", function() { 258 | var mockFractal = getMockBox().createMock( "cffractal.models.Manager" ); 259 | var mockItem = getMockBox().createMock( "cffractal.models.resources.Item" ); 260 | 261 | var scope = new cffractal.models.Scope( 262 | manager = mockFractal, 263 | resource = mockItem, 264 | includes = "author.country.provinces.districts.localities", 265 | identifier = "author.country.provinces" 266 | ); 267 | 268 | expect( scope.requestedInclude( "districts" ) ).toBeTrue(); 269 | } ); 270 | } ); 271 | } ); 272 | } 273 | 274 | } 275 | -------------------------------------------------------------------------------- /tests/specs/integration/FractalBuilderTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "all the pieces working together", function() { 5 | beforeEach( function() { 6 | variables.dataSerializer = new cffractal.models.serializers.DataSerializer(); 7 | variables.fractal = new cffractal.models.Manager( dataSerializer, dataSerializer ); 8 | } ); 9 | 10 | describe( "converting models", function() { 11 | describe( "converting items", function() { 12 | it( "with a callback transformer", function() { 13 | var book = new tests.resources.Book( { 14 | id = 1, 15 | title = "To Kill a Mockingbird", 16 | year = "1960" 17 | } ); 18 | 19 | var result = fractal.builder() 20 | .item( book ) 21 | .withTransformer( function( book ) { 22 | return { 23 | "id" = book.getId(), 24 | "title" = book.getTitle(), 25 | "year" = book.getYear() 26 | }; 27 | } ) 28 | .convert(); 29 | 30 | expect( result ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1}} ); 31 | } ); 32 | 33 | it( "with a custom transformer", function() { 34 | var book = new tests.resources.Book( { 35 | id = 1, 36 | title = "To Kill a Mockingbird", 37 | year = "1960" 38 | } ); 39 | 40 | var result = fractal.builder() 41 | .item( book ) 42 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 43 | .convert(); 44 | 45 | expect( result ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1}} ); 46 | } ); 47 | 48 | it( "can use a special serializer for a resource", function() { 49 | var book = new tests.resources.Book( { 50 | id = 1, 51 | title = "To Kill a Mockingbird", 52 | year = "1960" 53 | } ); 54 | 55 | var result = fractal.builder() 56 | .item( book ) 57 | .withSerializer( new cffractal.models.serializers.SimpleSerializer() ) 58 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 59 | .convert(); 60 | 61 | expect( result ).toBe( {"year":1960,"title":"To Kill a Mockingbird","id":1} ); 62 | } ); 63 | 64 | describe( "includes", function() { 65 | it( "can parse an item with an includes", function() { 66 | var book = new tests.resources.Book( { 67 | id = 1, 68 | title = "To Kill a Mockingbird", 69 | year = "1960", 70 | author = new tests.resources.Author( { 71 | id = 1, 72 | name = "Harper Lee", 73 | birthdate = createDate( 1926, 04, 28 ) 74 | } ) 75 | } ); 76 | 77 | var result = fractal.builder() 78 | .item( book ) 79 | .withIncludes( "author" ) 80 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 81 | .convert(); 82 | 83 | expect( result ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"author":{"data":{"name":"Harper Lee"}}}} ); 84 | } ); 85 | 86 | it( "can parse an item with a default includes", function() { 87 | var book = new tests.resources.Book( { 88 | id = 1, 89 | title = "To Kill a Mockingbird", 90 | year = "1960", 91 | author = new tests.resources.Author( { 92 | id = 1, 93 | name = "Harper Lee", 94 | birthdate = createDate( 1926, 04, 28 ) 95 | } ) 96 | } ); 97 | 98 | var result = fractal.builder() 99 | .item( book ) 100 | .withTransformer( new tests.resources.DefaultIncludesBookTransformer().setManager( fractal ) ) 101 | .convert(); 102 | 103 | expect( result ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"bookCount":4,"author":{"data":{"name":"Harper Lee"}}}} ); 104 | } ); 105 | 106 | it( "can parse an item with a nested includes", function() { 107 | var book = new tests.resources.Book( { 108 | id = 1, 109 | title = "To Kill a Mockingbird", 110 | year = "1960", 111 | author = new tests.resources.Author( { 112 | id = 1, 113 | name = "Harper Lee", 114 | birthdate = createDate( 1926, 04, 28 ), 115 | country = new tests.resources.Country( { 116 | id = 1, 117 | name = "United States" 118 | } ) 119 | } ) 120 | } ); 121 | 122 | var result = fractal.builder() 123 | .item( book ) 124 | .withIncludes( "author,author.country" ) 125 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 126 | .convert(); 127 | 128 | var expectedData = { 129 | "data" = { 130 | "year" = 1960, 131 | "title" = "To Kill a Mockingbird", 132 | "id" = 1, 133 | "author" = { 134 | "data" = { 135 | "name" = "Harper Lee", 136 | "country" = { 137 | "data" = { 138 | "id" = 1, 139 | "name" = "United States" 140 | } 141 | } 142 | } 143 | } 144 | } 145 | }; 146 | expect( result ).toBe( expectedData ); 147 | } ); 148 | 149 | it( "can automatically includes the parent when grabbing a nested include", function() { 150 | var book = new tests.resources.Book( { 151 | id = 1, 152 | title = "To Kill a Mockingbird", 153 | year = "1960", 154 | author = new tests.resources.Author( { 155 | id = 1, 156 | name = "Harper Lee", 157 | birthdate = createDate( 1926, 04, 28 ), 158 | country = new tests.resources.Country( { 159 | id = 1, 160 | name = "United States" 161 | } ) 162 | } ) 163 | } ); 164 | 165 | var result = fractal.builder() 166 | .item( book ) 167 | .withIncludes( "author.country" ) 168 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 169 | .convert(); 170 | 171 | var expectedData = { 172 | "data" = { 173 | "year" = 1960, 174 | "title" = "To Kill a Mockingbird", 175 | "id" = 1, 176 | "author" = { 177 | "data" = { 178 | "name" = "Harper Lee", 179 | "country" = { 180 | "data" = { 181 | "id" = 1, 182 | "name" = "United States" 183 | } 184 | } 185 | } 186 | } 187 | } 188 | }; 189 | expect( result ).toBe( expectedData ); 190 | } ); 191 | 192 | it( "can handle an includes that returns a collection", function() { 193 | var author = new tests.resources.Author( { 194 | id = 1, 195 | name = "Harper Lee", 196 | birthdate = createDate( 1926, 04, 28 ), 197 | books = [ 198 | new tests.resources.Book( { 199 | id = 1, 200 | title = "To Kill a Mockingbird", 201 | year = "1960" 202 | } ), 203 | new tests.resources.Book( { 204 | id = 2, 205 | title = "Go Set a Watchman", 206 | year = "2015" 207 | } ) 208 | ] 209 | } ); 210 | 211 | var result = fractal.builder() 212 | .item( author ) 213 | .withIncludes( "books" ) 214 | .withTransformer( new tests.resources.AuthorTransformer().setManager( fractal ) ) 215 | .convert(); 216 | 217 | var expectedData = { 218 | "data" = { 219 | "name" = "Harper Lee", 220 | "books" = { 221 | "results" = [ 1, 2 ], 222 | "resultsMap" = { 223 | "1" = { 224 | "id" = 1, 225 | "year" = 1960, 226 | "title" = "To Kill a Mockingbird" 227 | }, 228 | "2" = { 229 | "id" = 2, 230 | "year" = 2015, 231 | "title" = "Go Set a Watchman" 232 | } 233 | } 234 | } 235 | } 236 | }; 237 | expect( result ).toBe( expectedData ); 238 | } ); 239 | } ); 240 | 241 | describe( "excludes", function() { 242 | it( "can ignore a default include", function() { 243 | var book = new tests.resources.Book( { 244 | id = 1, 245 | title = "To Kill a Mockingbird", 246 | year = "1960", 247 | author = new tests.resources.Author( { 248 | id = 1, 249 | name = "Harper Lee", 250 | birthdate = createDate( 1926, 04, 28 ) 251 | } ) 252 | } ); 253 | 254 | var result = fractal.builder() 255 | .item( book ) 256 | .withTransformer( new tests.resources.DefaultIncludesBookTransformer().setManager( fractal ) ) 257 | .withExcludes( "author" ) 258 | .convert(); 259 | 260 | expect( result ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"bookCount":4}} ); 261 | } ); 262 | } ); 263 | } ); 264 | 265 | describe( "converting collections", function() { 266 | it( "with a callback transformer", function() { 267 | var books = [ 268 | new tests.resources.Book( { 269 | id = 1, 270 | title = "To Kill a Mockingbird", 271 | year = "1960" 272 | } ), 273 | new tests.resources.Book( { 274 | id = 2, 275 | title = "A Tale of Two Cities", 276 | year = "1859" 277 | } ) 278 | ]; 279 | 280 | var result = fractal.builder() 281 | .collection( books ) 282 | .withTransformer( function( book ) { 283 | return { 284 | "id" = book.getId(), 285 | "title" = book.getTitle(), 286 | "year" = book.getYear() 287 | }; 288 | } ) 289 | .convert(); 290 | 291 | expect( result ).toBe( {"data":[{"year":1960,"title":"To Kill a Mockingbird","id":1},{"year":1859,"title":"A Tale of Two Cities","id":2}]} ); 292 | } ); 293 | 294 | it( "with a custom transformer", function() { 295 | var books = [ 296 | new tests.resources.Book( { 297 | id = 1, 298 | title = "To Kill a Mockingbird", 299 | year = "1960" 300 | } ), 301 | new tests.resources.Book( { 302 | id = 2, 303 | title = "A Tale of Two Cities", 304 | year = "1859" 305 | } ) 306 | ]; 307 | 308 | var result = fractal.builder() 309 | .collection( books ) 310 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 311 | .convert(); 312 | 313 | expect( result ).toBe( {"data":[{"year":1960,"title":"To Kill a Mockingbird","id":1},{"year":1859,"title":"A Tale of Two Cities","id":2}]} ); 314 | } ); 315 | 316 | describe( "pagination", function() { 317 | it( "returns pagination data in a meta field", function() { 318 | var books = [ 319 | new tests.resources.Book( { 320 | id = 1, 321 | title = "To Kill a Mockingbird", 322 | year = "1960" 323 | } ), 324 | new tests.resources.Book( { 325 | id = 2, 326 | title = "A Tale of Two Cities", 327 | year = "1859" 328 | } ) 329 | ]; 330 | 331 | var result = fractal.builder() 332 | .collection( books ) 333 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 334 | .withPagination( { 335 | "maxrows": 50, 336 | "page": 2, 337 | "pages": 3, 338 | "totalRecords": 112 339 | } ) 340 | .convert(); 341 | 342 | expect( result ).toBe( { 343 | "data": [ 344 | { 345 | "id": 1, 346 | "title": "To Kill a Mockingbird", 347 | "year": 1960 348 | }, 349 | { 350 | "id": 2, 351 | "title": "A Tale of Two Cities", 352 | "year": 1859 353 | } 354 | ], 355 | "meta": { 356 | "pagination": { 357 | "maxrows": 50, 358 | "page": 2, 359 | "pages": 3, 360 | "totalRecords": 112 361 | } 362 | } 363 | } ); 364 | } ); 365 | 366 | it( "returns pagination data in a meta field for the results map serializer", function() { 367 | var books = [ 368 | new tests.resources.Book( { 369 | id = 1, 370 | title = "To Kill a Mockingbird", 371 | year = "1960" 372 | } ), 373 | new tests.resources.Book( { 374 | id = 2, 375 | title = "A Tale of Two Cities", 376 | year = "1859" 377 | } ) 378 | ]; 379 | 380 | var result = fractal.builder() 381 | .collection( books ) 382 | .withSerializer( new cffractal.models.serializers.ResultsMapSerializer() ) 383 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 384 | .withPagination( { 385 | "maxrows": 50, 386 | "page": 2, 387 | "pages": 3, 388 | "totalRecords": 112 389 | } ) 390 | .convert(); 391 | 392 | expect( result ).toBe( { 393 | "results": [ 1, 2 ], 394 | "resultsMap": { 395 | "1": { 396 | "id": 1, 397 | "title": "To Kill a Mockingbird", 398 | "year": 1960 399 | }, 400 | "2": { 401 | "id": 2, 402 | "title": "A Tale of Two Cities", 403 | "year": 1859 404 | } 405 | }, 406 | "meta": { 407 | "pagination": { 408 | "maxrows": 50, 409 | "page": 2, 410 | "pages": 3, 411 | "totalRecords": 112 412 | } 413 | } 414 | } ); 415 | } ); 416 | } ); 417 | 418 | describe( "postTransformationCallbacks", function() { 419 | it( "can run a callback for each item in a collection", function() { 420 | var books = [ 421 | new tests.resources.Book( { 422 | id = 1, 423 | title = "To Kill a Mockingbird", 424 | year = "1960" 425 | } ), 426 | new tests.resources.Book( { 427 | id = 2, 428 | title = "A Tale of Two Cities", 429 | year = "1859" 430 | } ) 431 | ]; 432 | 433 | var result = fractal.builder() 434 | .collection( books ) 435 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 436 | .withItemCallback( function( transformed, original, resource ) { 437 | transformed.href = "/api/v1/books/#transformed.id#"; 438 | return transformed; 439 | } ) 440 | .convert(); 441 | 442 | expect( result ).toBe( { 443 | "data": [ 444 | { 445 | "id": 1, 446 | "title": "To Kill a Mockingbird", 447 | "year": 1960, 448 | "href": "/api/v1/books/1" 449 | }, 450 | { 451 | "id": 2, 452 | "title": "A Tale of Two Cities", 453 | "year": 1859, 454 | "href": "/api/v1/books/2" 455 | } 456 | ] 457 | } ); 458 | } ); 459 | } ); 460 | 461 | describe( "meta", function() { 462 | it( "returns meta data in a meta field", function() { 463 | var books = [ 464 | new tests.resources.Book( { 465 | id = 1, 466 | title = "To Kill a Mockingbird", 467 | year = "1960" 468 | } ), 469 | new tests.resources.Book( { 470 | id = 2, 471 | title = "A Tale of Two Cities", 472 | year = "1859" 473 | } ) 474 | ]; 475 | 476 | var result = fractal.builder() 477 | .collection( books ) 478 | .withTransformer( new tests.resources.BookTransformer().setManager( fractal ) ) 479 | .withMeta( "links", { 480 | "next": "https://example.com/api/v1/books/?page=3", 481 | "previous": "https://example.com/api/v1/books/?page=1" 482 | } ) 483 | .convert(); 484 | 485 | expect( result ).toBe( { 486 | "data": [ 487 | { 488 | "id": 1, 489 | "title": "To Kill a Mockingbird", 490 | "year": 1960 491 | }, 492 | { 493 | "id": 2, 494 | "title": "A Tale of Two Cities", 495 | "year": 1859 496 | } 497 | ], 498 | "meta": { 499 | "links": { 500 | "next": "https://example.com/api/v1/books/?page=3", 501 | "previous": "https://example.com/api/v1/books/?page=1" 502 | } 503 | } 504 | } ); 505 | } ); 506 | } ); 507 | } ); 508 | } ); 509 | } ); 510 | } 511 | 512 | } 513 | -------------------------------------------------------------------------------- /tests/specs/integration/FractalTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "all the pieces working together", function() { 5 | beforeEach( function() { 6 | variables.dataSerializer = new cffractal.models.serializers.DataSerializer(); 7 | variables.fractal = new cffractal.models.Manager( dataSerializer, dataSerializer, {} ); 8 | } ); 9 | 10 | describe( "null default values", function() { 11 | it( "returns the null default value if the resource is null for items", function() { 12 | var resource = fractal.item( javacast( "null", "" ), function( doesntMatter ) { 13 | return { 14 | "should" = "never", 15 | "get" = "called" 16 | }; 17 | } ); 18 | 19 | var scope = fractal.createData( resource ); 20 | expect( scope.convert() ).toBe( {"data":{}} ); 21 | } ); 22 | 23 | it( "returns an empty array if the resource is null for collections", function() { 24 | var resource = fractal.collection( javacast( "null", "" ), function( doesntMatter ) { 25 | return { 26 | "should" = "never", 27 | "get" = "called" 28 | }; 29 | } ); 30 | 31 | var scope = fractal.createData( resource ); 32 | expect( scope.convert() ).toBe( {"data":[]} ); 33 | } ); 34 | 35 | it( "returns the null default value if the transformed result is null", function() { 36 | var book = new tests.resources.Book( { 37 | id = 1, 38 | title = "To Kill a Mockingbird", 39 | year = "1960" 40 | } ); 41 | 42 | var resource = fractal.item( book, function( book ) { 43 | return javacast( "null", "" ); 44 | } ); 45 | 46 | var scope = fractal.createData( resource ); 47 | expect( scope.convert() ).toBe( {"data":{}} ); 48 | } ); 49 | 50 | it( "returns the null default value if the result from a postTransformationCallback is null for items", function() { 51 | var book = new tests.resources.Book( { 52 | id = 1, 53 | title = "To Kill a Mockingbird", 54 | year = "1960" 55 | } ); 56 | 57 | var resource = fractal.item( book, function( book ) { 58 | return { 59 | "id" = book.getId(), 60 | "title" = book.getTitle(), 61 | "year" = book.getYear() 62 | }; 63 | } ); 64 | 65 | resource.addPostTransformationCallback( function( book ) { 66 | return javacast( "null", "" ); 67 | } ); 68 | 69 | var scope = fractal.createData( resource ); 70 | expect( scope.convert() ).toBe( {"data":{}} ); 71 | } ); 72 | 73 | it( "returns the null default value if the result from a postTransformationCallback is null for collections", function() { 74 | var books = [ 75 | new tests.resources.Book( { 76 | id = 1, 77 | title = "To Kill a Mockingbird", 78 | year = "1960" 79 | } ), 80 | new tests.resources.Book( { 81 | id = 2, 82 | title = "A Tale of Two Cities", 83 | year = "1859" 84 | } ) 85 | ]; 86 | var resource = fractal.collection( books, function( book ) { 87 | return { 88 | "id" = book.getId(), 89 | "title" = book.getTitle(), 90 | "year" = book.getYear() 91 | }; 92 | } ); 93 | 94 | resource.addPostTransformationCallback( function( book ) { 95 | return javacast( "null", "" ); 96 | } ); 97 | 98 | var scope = fractal.createData( resource ); 99 | expect( scope.convert() ).toBe( {"data":[{},{}]} ); 100 | } ); 101 | 102 | it( "returns the null default value for an include that returns null", function() { 103 | var book = new tests.resources.Book( { 104 | id = 1, 105 | title = "To Kill a Mockingbird", 106 | year = "1960", 107 | author = new tests.resources.Author( { 108 | id = 1, 109 | name = "Harper Lee", 110 | birthdate = createDate( 1926, 04, 28 ) 111 | } ) 112 | } ); 113 | 114 | var resource = fractal.item( book, new tests.resources.BookWithNullIncludesTransformer().setManager( fractal ) ); 115 | 116 | var scope = fractal.createData( resource = resource, includes = "author" ); 117 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"author":{"data":{}}}} ); 118 | } ); 119 | } ); 120 | 121 | describe( "converting models", function() { 122 | describe( "converting items", function() { 123 | it( "with a callback transformer", function() { 124 | var book = new tests.resources.Book( { 125 | id = 1, 126 | title = "To Kill a Mockingbird", 127 | year = "1960" 128 | } ); 129 | var resource = fractal.item( book, function( book ) { 130 | return { 131 | "id" = book.getId(), 132 | "title" = book.getTitle(), 133 | "year" = book.getYear() 134 | }; 135 | } ); 136 | 137 | var scope = fractal.createData( resource ); 138 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1}} ); 139 | } ); 140 | 141 | it( "with a custom transformer", function() { 142 | var book = new tests.resources.Book( { 143 | id = 1, 144 | title = "To Kill a Mockingbird", 145 | year = "1960" 146 | } ); 147 | 148 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 149 | 150 | var scope = fractal.createData( resource ); 151 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1}} ); 152 | } ); 153 | 154 | it( "can use a special serializer for a resource", function() { 155 | var book = new tests.resources.Book( { 156 | id = 1, 157 | title = "To Kill a Mockingbird", 158 | year = "1960" 159 | } ); 160 | 161 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 162 | resource.setSerializer( new cffractal.models.serializers.SimpleSerializer() ); 163 | 164 | var scope = fractal.createData( resource ); 165 | expect( scope.convert() ).toBe( {"year":1960,"title":"To Kill a Mockingbird","id":1} ); 166 | } ); 167 | 168 | describe( "includes", function() { 169 | it( "ignores includes by default", function() { 170 | var book = new tests.resources.Book( { 171 | id = 1, 172 | title = "To Kill a Mockingbird", 173 | year = "1960", 174 | author = new tests.resources.Author( { 175 | id = 1, 176 | name = "Harper Lee", 177 | birthdate = createDate( 1926, 04, 28 ) 178 | } ) 179 | } ); 180 | 181 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 182 | 183 | var scope = fractal.createData( resource ); 184 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1}} ); 185 | } ); 186 | 187 | it( "can parse an item with an includes", function() { 188 | var book = new tests.resources.Book( { 189 | id = 1, 190 | title = "To Kill a Mockingbird", 191 | year = "1960", 192 | author = new tests.resources.Author( { 193 | id = 1, 194 | name = "Harper Lee", 195 | birthdate = createDate( 1926, 04, 28 ) 196 | } ) 197 | } ); 198 | 199 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 200 | 201 | var scope = fractal.createData( resource = resource, includes = "author" ); 202 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"author":{"data":{"name":"Harper Lee"}}}} ); 203 | } ); 204 | 205 | it( "can handle primitives returned directly from an include", function() { 206 | var book = new tests.resources.Book( { 207 | id = 1, 208 | title = "To Kill a Mockingbird", 209 | year = "1960", 210 | author = new tests.resources.Author( { 211 | id = 1, 212 | name = "Harper Lee", 213 | birthdate = createDate( 1926, 04, 28 ) 214 | } ), 215 | isClassic = false 216 | } ); 217 | 218 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 219 | 220 | var scope = fractal.createData( resource = resource, includes = "isClassic" ); 221 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"isClassic":false}} ); 222 | } ); 223 | 224 | it( "removes any availableIncludes keys that are not included from the serialized output", function() { 225 | var book = new tests.resources.Book( { 226 | id = 1, 227 | title = "To Kill a Mockingbird", 228 | year = "1960", 229 | author = new tests.resources.Author( { 230 | id = 1, 231 | name = "Harper Lee", 232 | birthdate = createDate( 1926, 04, 28 ) 233 | } ), 234 | isClassic = false 235 | } ); 236 | 237 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 238 | 239 | var scope = fractal.createData( resource = resource ); 240 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1}} ); 241 | } ); 242 | 243 | it( "can parse an item with a default includes", function() { 244 | var book = new tests.resources.Book( { 245 | id = 1, 246 | title = "To Kill a Mockingbird", 247 | year = "1960", 248 | author = new tests.resources.Author( { 249 | id = 1, 250 | name = "Harper Lee", 251 | birthdate = createDate( 1926, 04, 28 ), 252 | country = new tests.resources.Country( { 253 | id = 1, 254 | name = "United States", 255 | planet = new tests.resources.Planet( { 256 | id = 1, 257 | name = "Earth" 258 | } ) 259 | } ) 260 | } ) 261 | } ); 262 | 263 | var resource = fractal.item( book, new tests.resources.DefaultIncludesBookTransformer().setManager( fractal ) ); 264 | 265 | var scope = fractal.createData( resource ); 266 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"bookCount":4,"author":{"data":{"name":"Harper Lee"}}}} ); 267 | } ); 268 | 269 | it( "can use a special serializer for an include", function() { 270 | var book = new tests.resources.Book( { 271 | id = 1, 272 | title = "To Kill a Mockingbird", 273 | year = "1960", 274 | author = new tests.resources.Author( { 275 | id = 1, 276 | name = "Harper Lee", 277 | birthdate = createDate( 1926, 04, 28 ) 278 | } ) 279 | } ); 280 | 281 | var resource = fractal.item( book, new tests.resources.SpecializedSerializerBookTransformer().setManager( fractal ) ); 282 | 283 | var scope = fractal.createData( resource ); 284 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"author":{"name":"Harper Lee"}}} ); 285 | } ); 286 | 287 | it( "can parse an item with a nested includes", function() { 288 | var book = new tests.resources.Book( { 289 | id = 1, 290 | title = "To Kill a Mockingbird", 291 | year = "1960", 292 | author = new tests.resources.Author( { 293 | id = 1, 294 | name = "Harper Lee", 295 | birthdate = createDate( 1926, 04, 28 ), 296 | country = new tests.resources.Country( { 297 | id = 1, 298 | name = "United States", 299 | planet = new tests.resources.Planet( { 300 | id = 1, 301 | name = "Earth" 302 | } ) 303 | } ) 304 | } ) 305 | } ); 306 | 307 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 308 | 309 | var scope = fractal.createData( resource, "author.country" ); 310 | var expectedData = { 311 | "data" = { 312 | "year" = 1960, 313 | "title" = "To Kill a Mockingbird", 314 | "id" = 1, 315 | "author" = { 316 | "data" = { 317 | "name" = "Harper Lee", 318 | "country" = { 319 | "data" = { 320 | "id" = 1, 321 | "name" = "United States" 322 | } 323 | } 324 | } 325 | } 326 | } 327 | }; 328 | expect( scope.convert() ).toBe( expectedData ); 329 | } ); 330 | 331 | it( "can parse an item with a deep nested includes", function() { 332 | var book = new tests.resources.Book( { 333 | id = 1, 334 | title = "To Kill a Mockingbird", 335 | year = "1960", 336 | author = new tests.resources.Author( { 337 | id = 1, 338 | name = "Harper Lee", 339 | birthdate = createDate( 1926, 04, 28 ), 340 | country = new tests.resources.Country( { 341 | id = 1, 342 | name = "United States", 343 | planet = new tests.resources.Planet( { 344 | id = 1, 345 | name = "Earth" 346 | } ) 347 | } ) 348 | } ) 349 | } ); 350 | 351 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 352 | 353 | var scope = fractal.createData( resource, "author.country.planet" ); 354 | var expectedData = { 355 | "data" = { 356 | "year" = 1960, 357 | "title" = "To Kill a Mockingbird", 358 | "id" = 1, 359 | "author" = { 360 | "data" = { 361 | "name" = "Harper Lee", 362 | "country" = { 363 | "data" = { 364 | "id" = 1, 365 | "name" = "United States", 366 | "planet" = { 367 | "data" = { 368 | "id" = 1, 369 | "name" = "Earth" 370 | } 371 | } 372 | } 373 | } 374 | } 375 | } 376 | } 377 | }; 378 | expect( scope.convert() ).toBe( expectedData ); 379 | } ); 380 | 381 | it( "can automatically includes the parent when grabbing a nested include", function() { 382 | var book = new tests.resources.Book( { 383 | id = 1, 384 | title = "To Kill a Mockingbird", 385 | year = "1960", 386 | author = new tests.resources.Author( { 387 | id = 1, 388 | name = "Harper Lee", 389 | birthdate = createDate( 1926, 04, 28 ), 390 | country = new tests.resources.Country( { 391 | id = 1, 392 | name = "United States" 393 | } ) 394 | } ) 395 | } ); 396 | 397 | var resource = fractal.item( book, new tests.resources.BookTransformer().setManager( fractal ) ); 398 | 399 | var scope = fractal.createData( resource, "author.country" ); 400 | var expectedData = { 401 | "data" = { 402 | "year" = 1960, 403 | "title" = "To Kill a Mockingbird", 404 | "id" = 1, 405 | "author" = { 406 | "data" = { 407 | "name" = "Harper Lee", 408 | "country" = { 409 | "data" = { 410 | "id" = 1, 411 | "name" = "United States" 412 | } 413 | } 414 | } 415 | } 416 | } 417 | }; 418 | expect( scope.convert() ).toBe( expectedData ); 419 | } ); 420 | 421 | it( "makes the scoped includes available in a closure transformer", function() { 422 | var book = new tests.resources.Book( { 423 | id = 1, 424 | title = "To Kill a Mockingbird", 425 | year = "1960", 426 | author = new tests.resources.Author( { 427 | id = 1, 428 | name = "Harper Lee", 429 | birthdate = createDate( 1926, 04, 28 ) 430 | } ) 431 | } ); 432 | 433 | var resource = fractal.item( book, function( book, includes ) { 434 | expect( isNull( includes ) ).toBeFalse( "includes should not be null" ); 435 | expect( includes ).toBeArray(); 436 | expect( includes ).toHaveLength( 2 ); 437 | expect( includes ).toBe( [ "year", "author" ] ); 438 | return {}; 439 | } ); 440 | 441 | fractal.createData( 442 | resource = resource, 443 | includes = "year,author.name" 444 | ).convert(); 445 | } ); 446 | 447 | it( "passes an empty array for scoped includes and all includes if there are no includes", function() { 448 | var book = new tests.resources.Book( { 449 | id = 1, 450 | title = "To Kill a Mockingbird", 451 | year = "1960", 452 | author = new tests.resources.Author( { 453 | id = 1, 454 | name = "Harper Lee", 455 | birthdate = createDate( 1926, 04, 28 ) 456 | } ) 457 | } ); 458 | 459 | var resource = fractal.item( book, function( book, includes ) { 460 | expect( isNull( includes ) ).toBeFalse( "includes should not be null" ); 461 | expect( includes ).toBeArray(); 462 | expect( includes ).toBeEmpty(); 463 | return {}; 464 | } ); 465 | 466 | fractal.createData( 467 | resource = resource, 468 | includes = "" 469 | ).convert(); 470 | } ); 471 | 472 | it( "makes the full includes available in a closure transformer", function() { 473 | var book = new tests.resources.Book( { 474 | id = 1, 475 | title = "To Kill a Mockingbird", 476 | year = "1960", 477 | author = new tests.resources.Author( { 478 | id = 1, 479 | name = "Harper Lee", 480 | birthdate = createDate( 1926, 04, 28 ) 481 | } ) 482 | } ); 483 | 484 | var resource = fractal.item( book, function( book, includes, excludes, allIncludes ) { 485 | expect( isNull( allIncludes ) ).toBeFalse( "allIncludes should not be null" ); 486 | expect( allIncludes ).toBeArray(); 487 | expect( allIncludes ).toHaveLength( 3 ); 488 | expect( allIncludes ).toBe( [ "year", "author.name", "author" ] ); 489 | return {}; 490 | } ); 491 | 492 | fractal.createData( 493 | resource = resource, 494 | includes = "year,author.name" 495 | ).convert(); 496 | } ); 497 | 498 | it( "makes the scoped includes available in a component transformer", function() { 499 | var book = new tests.resources.Book( { 500 | id = 1, 501 | title = "To Kill a Mockingbird", 502 | year = "1960", 503 | author = new tests.resources.Author( { 504 | id = 1, 505 | name = "Harper Lee", 506 | birthdate = createDate( 1926, 04, 28 ) 507 | } ) 508 | } ); 509 | 510 | var bookTransformer = createMock( "tests.resources.BookTransformer" ).setManager( fractal ); 511 | bookTransformer.$( "transform", {} ); 512 | var authorTransformer = createMock( "tests.resources.AuthorTransformer" ).setManager( fractal ); 513 | authorTransformer.$( "transform", {} ); 514 | bookTransformer.$property( propertyName = "authorTransformer", mock = authorTransformer ); 515 | var resource = fractal.item( book, bookTransformer ); 516 | fractal.createData( 517 | resource = resource, 518 | includes = "year,author.name" 519 | ).convert(); 520 | 521 | // Book Transformer 522 | var callLog = bookTransformer.$callLog(); 523 | expect( callLog ).toBeStruct(); 524 | expect( callLog ).toHaveKey( "transform" ); 525 | var transformCallLog = callLog.transform; 526 | expect( transformCallLog ).toBeArray(); 527 | var firstCall = transformCallLog[ 1 ]; 528 | expect( arrayLen( firstCall ) ).toBeGTE( 2, "At least two arguments should be passed to the transform function" ); 529 | var scopedIncludes = firstCall[ 2 ]; 530 | expect( scopedIncludes ).toBeArray(); 531 | expect( scopedIncludes ).toBe( [ "year", "author" ] ); 532 | 533 | // Author Transformer 534 | var callLog = authorTransformer.$callLog(); 535 | expect( callLog ).toBeStruct(); 536 | expect( callLog ).toHaveKey( "transform" ); 537 | var transformCallLog = callLog.transform; 538 | expect( transformCallLog ).toBeArray(); 539 | var firstCall = transformCallLog[ 1 ]; 540 | expect( arrayLen( firstCall ) ).toBeGTE( 2, "At least two arguments should be passed to the transform function" ); 541 | var scopedIncludes = firstCall[ 2 ]; 542 | expect( scopedIncludes ).toBeArray(); 543 | expect( scopedIncludes ).toBe( [ "name" ] ); 544 | } ); 545 | 546 | it( "makes the full includes available in a component transformer", function() { 547 | var book = new tests.resources.Book( { 548 | id = 1, 549 | title = "To Kill a Mockingbird", 550 | year = "1960", 551 | author = new tests.resources.Author( { 552 | id = 1, 553 | name = "Harper Lee", 554 | birthdate = createDate( 1926, 04, 28 ) 555 | } ) 556 | } ); 557 | 558 | var bookTransformer = createMock( "tests.resources.BookTransformer" ).setManager( fractal ); 559 | bookTransformer.$( "transform", {} ); 560 | var authorTransformer = createMock( "tests.resources.AuthorTransformer" ).setManager( fractal ); 561 | authorTransformer.$( "transform", {} ); 562 | bookTransformer.$property( propertyName = "authorTransformer", mock = authorTransformer ); 563 | var resource = fractal.item( book, bookTransformer ); 564 | fractal.createData( 565 | resource = resource, 566 | includes = "year,author.name" 567 | ).convert(); 568 | 569 | // Book Transformer 570 | var callLog = bookTransformer.$callLog(); 571 | expect( callLog ).toBeStruct(); 572 | expect( callLog ).toHaveKey( "transform" ); 573 | var transformCallLog = callLog.transform; 574 | expect( transformCallLog ).toBeArray(); 575 | var firstCall = transformCallLog[ 1 ]; 576 | expect( arrayLen( firstCall ) ).toBeGTE( 4, "At least four arguments should be passed to the transform function" ); 577 | var allIncludes = firstCall[ 4 ]; 578 | expect( allIncludes ).toBeArray(); 579 | expect( allIncludes ).toBe( [ "year", "author.name", "author" ] ); 580 | 581 | // Author Transformer 582 | var callLog = authorTransformer.$callLog(); 583 | expect( callLog ).toBeStruct(); 584 | expect( callLog ).toHaveKey( "transform" ); 585 | var transformCallLog = callLog.transform; 586 | expect( transformCallLog ).toBeArray(); 587 | var firstCall = transformCallLog[ 1 ]; 588 | expect( arrayLen( firstCall ) ).toBeGTE( 4, "At least four arguments should be passed to the transform function" ); 589 | var allIncludes = firstCall[ 4 ]; 590 | expect( allIncludes ).toBeArray(); 591 | expect( allIncludes ).toBe( [ "year", "author.name", "author" ] ); 592 | } ); 593 | } ); 594 | 595 | describe( "excludes", function() { 596 | it( "can ignore a default include", function() { 597 | var book = new tests.resources.Book( { 598 | id = 1, 599 | title = "To Kill a Mockingbird", 600 | year = "1960", 601 | author = new tests.resources.Author( { 602 | id = 1, 603 | name = "Harper Lee", 604 | birthdate = createDate( 1926, 04, 28 ) 605 | } ) 606 | } ); 607 | 608 | var resource = fractal.item( book, new tests.resources.DefaultIncludesBookTransformer().setManager( fractal ) ); 609 | 610 | var scope = fractal.createData( 611 | resource = resource, 612 | excludes = "author" 613 | ); 614 | expect( scope.convert() ).toBe( {"data":{"year":1960,"title":"To Kill a Mockingbird","id":1,"bookCount":4}} ); 615 | } ); 616 | 617 | it( "can ignore a nested default include", function() { 618 | var book = new tests.resources.Book( { 619 | id = 1, 620 | title = "To Kill a Mockingbird", 621 | year = "1960", 622 | author = new tests.resources.Author( { 623 | id = 1, 624 | name = "Harper Lee", 625 | birthdate = createDate( 1926, 04, 28 ), 626 | country = new tests.resources.Country( { 627 | id = 1, 628 | name = "United States", 629 | planet = new tests.resources.Planet( { 630 | id = 1, 631 | name = "Earth" 632 | } ) 633 | } ) 634 | } ) 635 | } ); 636 | 637 | var resource = fractal.item( book, new tests.resources.DefaultIncludesBookTransformer( withDefaultCountry = true ).setManager( fractal ) ); 638 | 639 | var scope = fractal.createData( 640 | resource = resource, 641 | excludes = "author.country" 642 | ); 643 | var expectedData = { 644 | "data" = { 645 | "year" = 1960, 646 | "title" = "To Kill a Mockingbird", 647 | "id" = 1, 648 | "author" = { 649 | "data" = { 650 | "name" = "Harper Lee" 651 | } 652 | }, 653 | "bookCount" = 4 654 | } 655 | }; 656 | expect( scope.convert() ).toBe( expectedData ); 657 | } ); 658 | 659 | it( "removes the excluded keys from the serialized output with a closure transformer", function() { 660 | var planet = new tests.resources.Planet( { 661 | id = 1, 662 | name = "Mercury" 663 | } ); 664 | 665 | var resource = fractal.item( planet, function( planet ) { 666 | return { 667 | id = planet.getId(), 668 | name = planet.getName() 669 | }; 670 | } ); 671 | 672 | var scope = fractal.createData( 673 | resource = resource, 674 | excludes = "name" 675 | ); 676 | 677 | expect( scope.convert() ).toBe( { "data" = { "id" = 1 } } ); 678 | } ); 679 | 680 | it( "removes the excluded keys from the serialized output with a component transformer", function() { 681 | var planet = new tests.resources.Planet( { 682 | id = 1, 683 | name = "Mercury" 684 | } ); 685 | 686 | var resource = fractal.item( 687 | planet, 688 | new tests.resources.PlanetTransformer() 689 | ); 690 | 691 | var scope = fractal.createData( 692 | resource = resource, 693 | excludes = "name" 694 | ); 695 | 696 | expect( scope.convert() ).toBe( { "data" = { "id" = 1 } } ); 697 | } ); 698 | 699 | it( "removes the nested excluded keys from the serialized output", function() { 700 | var book = new tests.resources.Book( { 701 | id = 1, 702 | title = "To Kill a Mockingbird", 703 | year = "1960", 704 | author = new tests.resources.Author( { 705 | id = 1, 706 | name = "Harper Lee", 707 | birthdate = createDate( 1926, 04, 28 ), 708 | country = new tests.resources.Country( { 709 | id = 1, 710 | name = "United States", 711 | planet = new tests.resources.Planet( { 712 | id = 1, 713 | name = "Earth" 714 | } ) 715 | } ) 716 | } ) 717 | } ); 718 | 719 | var resource = fractal.item( 720 | book, 721 | new tests.resources.DefaultIncludesBookTransformer( withDefaultCountry = true ).setManager( fractal ) 722 | ); 723 | 724 | var scope = fractal.createData( 725 | resource = resource, 726 | excludes = "author.country.id" 727 | ); 728 | 729 | var expectedData = { 730 | "data" = { 731 | "year" = 1960, 732 | "title" = "To Kill a Mockingbird", 733 | "id" = 1, 734 | "bookCount" = 4, 735 | "author" = { 736 | "data" = { 737 | "name" = "Harper Lee", 738 | "country" = { 739 | "data" = { 740 | "name" = "United States" 741 | } 742 | } 743 | } 744 | } 745 | } 746 | }; 747 | expect( scope.convert() ).toBe( expectedData ); 748 | } ); 749 | 750 | it( "makes the scoped excludes available in a closure transformer", function() { 751 | var book = new tests.resources.Book( { 752 | id = 1, 753 | title = "To Kill a Mockingbird", 754 | year = "1960", 755 | author = new tests.resources.Author( { 756 | id = 1, 757 | name = "Harper Lee", 758 | birthdate = createDate( 1926, 04, 28 ) 759 | } ) 760 | } ); 761 | 762 | var resource = fractal.item( book, function( book, includes, excludes ) { 763 | expect( isNull( excludes ) ).toBeFalse( "excludes should not be null" ); 764 | expect( excludes ).toBeArray(); 765 | expect( excludes ).toHaveLength( 1 ); 766 | expect( excludes ).toBe( [ "year" ] ); 767 | return {}; 768 | } ); 769 | 770 | fractal.createData( 771 | resource = resource, 772 | excludes = "year,author.name" 773 | ).convert(); 774 | } ); 775 | 776 | it( "passes an empty array for scoped excludes and all excludes if there are no excludes", function() { 777 | var book = new tests.resources.Book( { 778 | id = 1, 779 | title = "To Kill a Mockingbird", 780 | year = "1960", 781 | author = new tests.resources.Author( { 782 | id = 1, 783 | name = "Harper Lee", 784 | birthdate = createDate( 1926, 04, 28 ) 785 | } ) 786 | } ); 787 | 788 | var resource = fractal.item( book, function( book, includes, excludes ) { 789 | expect( isNull( excludes ) ).toBeFalse( "excludes should not be null" ); 790 | expect( excludes ).toBeArray(); 791 | expect( excludes ).toBeEmpty(); 792 | return {}; 793 | } ); 794 | 795 | fractal.createData( 796 | resource = resource, 797 | excludes = "" 798 | ).convert(); 799 | } ); 800 | 801 | it( "makes the full excludes available in a closure transformer", function() { 802 | var book = new tests.resources.Book( { 803 | id = 1, 804 | title = "To Kill a Mockingbird", 805 | year = "1960", 806 | author = new tests.resources.Author( { 807 | id = 1, 808 | name = "Harper Lee", 809 | birthdate = createDate( 1926, 04, 28 ) 810 | } ) 811 | } ); 812 | 813 | var resource = fractal.item( book, function( book, includes, excludes, allIncludes, allExcludes ) { 814 | expect( isNull( allExcludes ) ).toBeFalse( "allExcludes should not be null" ); 815 | expect( allExcludes ).toBeArray(); 816 | expect( allExcludes ).toHaveLength( 2 ); 817 | expect( allExcludes ).toBe( [ "year", "author.name" ] ); 818 | return {}; 819 | } ); 820 | 821 | fractal.createData( 822 | resource = resource, 823 | excludes = "year,author.name" 824 | ).convert(); 825 | } ); 826 | 827 | it( "makes the scoped excludes available in a component transformer", function() { 828 | var book = new tests.resources.Book( { 829 | id = 1, 830 | title = "To Kill a Mockingbird", 831 | year = "1960", 832 | author = new tests.resources.Author( { 833 | id = 1, 834 | name = "Harper Lee", 835 | birthdate = createDate( 1926, 04, 28 ) 836 | } ) 837 | } ); 838 | 839 | var bookTransformer = createMock( "tests.resources.BookTransformer" ).setManager( fractal ); 840 | bookTransformer.$( "transform", {} ); 841 | var authorTransformer = createMock( "tests.resources.AuthorTransformer" ).setManager( fractal ); 842 | authorTransformer.$( "transform", {} ); 843 | bookTransformer.$property( propertyName = "authorTransformer", mock = authorTransformer ); 844 | var resource = fractal.item( book, bookTransformer ); 845 | fractal.createData( 846 | resource = resource, 847 | includes = "author", 848 | excludes = "year,author.name" 849 | ).convert(); 850 | 851 | // Book Transformer 852 | var callLog = bookTransformer.$callLog(); 853 | expect( callLog ).toBeStruct(); 854 | expect( callLog ).toHaveKey( "transform" ); 855 | var transformCallLog = callLog.transform; 856 | expect( transformCallLog ).toBeArray(); 857 | var firstCall = transformCallLog[ 1 ]; 858 | expect( arrayLen( firstCall ) ).toBeGTE( 3, "At least three arguments should be passed to the transform function" ); 859 | var scopedExcludes = firstCall[ 3 ]; 860 | expect( scopedExcludes ).toBeArray(); 861 | expect( scopedExcludes ).toBe( [ "year" ] ); 862 | 863 | // Author Transformer 864 | var callLog = authorTransformer.$callLog(); 865 | expect( callLog ).toBeStruct(); 866 | expect( callLog ).toHaveKey( "transform" ); 867 | var transformCallLog = callLog.transform; 868 | expect( transformCallLog ).toBeArray(); 869 | var firstCall = transformCallLog[ 1 ]; 870 | expect( arrayLen( firstCall ) ).toBeGTE( 3, "At least three arguments should be passed to the transform function" ); 871 | var scopedExcludes = firstCall[ 3 ]; 872 | expect( scopedExcludes ).toBeArray(); 873 | expect( scopedExcludes ).toBe( [ "name" ] ); 874 | } ); 875 | 876 | it( "makes the full excludes available in a component transformer", function() { 877 | var book = new tests.resources.Book( { 878 | id = 1, 879 | title = "To Kill a Mockingbird", 880 | year = "1960", 881 | author = new tests.resources.Author( { 882 | id = 1, 883 | name = "Harper Lee", 884 | birthdate = createDate( 1926, 04, 28 ) 885 | } ) 886 | } ); 887 | 888 | var bookTransformer = createMock( "tests.resources.BookTransformer" ).setManager( fractal ); 889 | bookTransformer.$( "transform", {} ); 890 | var authorTransformer = createMock( "tests.resources.AuthorTransformer" ).setManager( fractal ); 891 | authorTransformer.$( "transform", {} ); 892 | bookTransformer.$property( propertyName = "authorTransformer", mock = authorTransformer ); 893 | var resource = fractal.item( book, bookTransformer ); 894 | fractal.createData( 895 | resource = resource, 896 | includes = "author", 897 | excludes = "year,author.name" 898 | ).convert(); 899 | 900 | // Book Transformer 901 | var callLog = bookTransformer.$callLog(); 902 | expect( callLog ).toBeStruct(); 903 | expect( callLog ).toHaveKey( "transform" ); 904 | var transformCallLog = callLog.transform; 905 | expect( transformCallLog ).toBeArray(); 906 | var firstCall = transformCallLog[ 1 ]; 907 | expect( arrayLen( firstCall ) ).toBeGTE( 5, "At least five arguments should be passed to the transform function" ); 908 | var allIncludes = firstCall[ 5 ]; 909 | expect( allIncludes ).toBeArray(); 910 | expect( allIncludes ).toBe( [ "year", "author.name" ] ); 911 | 912 | // Author Transformer 913 | var callLog = authorTransformer.$callLog(); 914 | expect( callLog ).toBeStruct(); 915 | expect( callLog ).toHaveKey( "transform" ); 916 | var transformCallLog = callLog.transform; 917 | expect( transformCallLog ).toBeArray(); 918 | var firstCall = transformCallLog[ 1 ]; 919 | expect( arrayLen( firstCall ) ).toBeGTE( 5, "At least five arguments should be passed to the transform function" ); 920 | var allIncludes = firstCall[ 5 ]; 921 | expect( allIncludes ).toBeArray(); 922 | expect( allIncludes ).toBe( [ "year", "author.name" ] ); 923 | } ); 924 | } ); 925 | } ); 926 | 927 | describe( "converting collections", function() { 928 | it( "with a callback transformer", function() { 929 | var books = [ 930 | new tests.resources.Book( { 931 | id = 1, 932 | title = "To Kill a Mockingbird", 933 | year = "1960" 934 | } ), 935 | new tests.resources.Book( { 936 | id = 2, 937 | title = "A Tale of Two Cities", 938 | year = "1859" 939 | } ) 940 | ]; 941 | var resource = fractal.collection( books, function( book ) { 942 | return { 943 | "id" = book.getId(), 944 | "title" = book.getTitle(), 945 | "year" = book.getYear() 946 | }; 947 | } ); 948 | 949 | var scope = fractal.createData( resource ); 950 | expect( scope.convert() ).toBe( {"data":[{"year":1960,"title":"To Kill a Mockingbird","id":1},{"year":1859,"title":"A Tale of Two Cities","id":2}]} ); 951 | } ); 952 | 953 | it( "with a custom transformer", function() { 954 | var books = [ 955 | new tests.resources.Book( { 956 | id = 1, 957 | title = "To Kill a Mockingbird", 958 | year = "1960" 959 | } ), 960 | new tests.resources.Book( { 961 | id = 2, 962 | title = "A Tale of Two Cities", 963 | year = "1859" 964 | } ) 965 | ]; 966 | 967 | var resource = fractal.collection( books, new tests.resources.BookTransformer().setManager( fractal ) ); 968 | 969 | var scope = fractal.createData( resource ); 970 | expect( scope.convert() ).toBe( {"data":[{"year":1960,"title":"To Kill a Mockingbird","id":1},{"year":1859,"title":"A Tale of Two Cities","id":2}]} ); 971 | } ); 972 | 973 | describe( "pagination", function() { 974 | it( "returns pagination data in a meta field", function() { 975 | var books = [ 976 | new tests.resources.Book( { 977 | id = 1, 978 | title = "To Kill a Mockingbird", 979 | year = "1960" 980 | } ), 981 | new tests.resources.Book( { 982 | id = 2, 983 | title = "A Tale of Two Cities", 984 | year = "1859" 985 | } ) 986 | ]; 987 | 988 | var resource = fractal.collection( books, new tests.resources.BookTransformer().setManager( fractal ) ); 989 | resource.setPagingData( { "maxrows" = 50, "page" = 2, "pages" = 3, "totalRecords" = 112 } ); 990 | 991 | var scope = fractal.createData( resource ); 992 | expect( scope.convert() ).toBe( { 993 | "data": [ 994 | { 995 | "id": 1, 996 | "title": "To Kill a Mockingbird", 997 | "year": 1960 998 | }, 999 | { 1000 | "id": 2, 1001 | "title": "A Tale of Two Cities", 1002 | "year": 1859 1003 | } 1004 | ], 1005 | "meta": { 1006 | "pagination": { 1007 | "maxrows": 50, 1008 | "page": 2, 1009 | "pages": 3, 1010 | "totalRecords": 112 1011 | } 1012 | } 1013 | } ); 1014 | } ); 1015 | } ); 1016 | } ); 1017 | } ); 1018 | } ); 1019 | } 1020 | 1021 | } 1022 | --------------------------------------------------------------------------------