phantomIdFactory/phantomIdFactoryService.js

/**
 * Angular ResourcePhantomIdFactoryService
 * Copyright 2016 Andreas Stocker
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
 * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */


(function () {
    'use strict';

    var
        module = angular.module('ngResourceFactory');

    /**
     * Factory service to generate new resource phantom id generators.
     *
     * @name ResourcePhantomIdFactoryService
     * @ngdoc service
     * @see ResourcePhantomIdNegativeInt
     * @see ResourcePhantomIdUuid4
     * @class
     *
     * @example
     * // ResourcePhantomIdFactory that creates negative IDs as phantom IDs (NOTE: this is already a build-in
     * // phantom ID factory, so you do not have to implement this - @see ResourcePhantomIdNegativeInt)
     * inject(function (ResourceFactoryService, ResourcePhantomIdFactoryService) {
     *     var
     *         lastPkValue = 0,
     *         generator = ResourcePhantomIdFactoryService.createPhantomIdFactory({
     *             generate: function () {
     *                 return --lastPkValue;
     *             },
     *             is: function (pkValue) {
     *                 return pkValue < 0;
     *             }
     *         }),
     *         service = ResourceFactoryService('TestResourceService', 'http://test/:pk/', {
     *             phantomIdGenerator: generator
     *         }),
     *         phantomInstance1 = service.new(),
     *         phantomInstance2 = service.new();
     *
     *     expect(phantomInstance1.$isPhantom()).toBe(true);
     *     expect(phantomInstance2.$isPhantom()).toBe(true);
     *
     *     // Change IDs to non-negative numbers
     *     phantomInstance1.pk = 1;
     *     phantomInstance2.pk = 2;
     *
     *     expect(phantomInstance1.$isPhantom()).toBe(false);
     *     expect(phantomInstance2.$isPhantom()).toBe(false);
     * });
     */
    module.service('ResourcePhantomIdFactoryService',
        function () {
            'ngInject';

            var
                self = this;

            /**
             * Creates a new phantom id generator with the given configuration.
             *
             * @memberOf ResourcePhantomIdFactoryService
             * @function createPhantomIdFactory
             * @param {{generate: Function, is: Function}} config Configuration object for new phantom ID factory
             * @return {ResourcePhantomIdFactory} New phantom ID factory instance
             * @static
             */
            self.createPhantomIdFactory = function (config) {
                config = angular.extend({
                    generate: function () { },
                    is: function () { }
                }, config);

                return new ResourcePhantomIdFactory(config.generate, config.is);
            };

            /**
             * Constructor function for a phantom id generate. Takes a function that generates the PK, and a
             * functions that checks if the given PK is a phantom PK.
             *
             * @name ResourcePhantomIdFactory
             * @param {Function} generateFn Function that returns a new, phantom ID. Gets the instance as the first parameter
             * @param {Function} isPhantomFn Function that checks if the first given parameter is a phantom ID. Gets the instance as the second parameter
             * @see ResourcePhantomIdNegativeInt
             * @see ResourcePhantomIdUuid4
             * @class
             *
             * @example
             * // ResourcePhantomIdFactory that creates negative IDs as phantom IDs (NOTE: this is already a build-in
             * // phantom ID factory, so you do not have to implement this)
             * inject(function (ResourceFactoryService, ResourcePhantomIdFactoryService) {
             *     var
             *         lastPkValue = 0,
             *         generator = ResourcePhantomIdFactoryService.createPhantomIdFactory({
             *             generate: function () {
             *                 return --lastPkValue;
             *             },
             *             is: function (pkValue) {
             *                 return pkValue < 0;
             *             }
             *         }),
             *         service = ResourceFactoryService('TestResourceService', 'http://test/:pk/', {
             *             phantomIdGenerator: generator
             *         }),
             *         phantomInstance1 = service.new(),
             *         phantomInstance2 = service.new();
             *
             *     expect(phantomInstance1.$isPhantom()).toBe(true);
             *     expect(phantomInstance2.$isPhantom()).toBe(true);
             *
             *     // Change IDs to non-negative numbers
             *     phantomInstance1.pk = 1;
             *     phantomInstance2.pk = 2;
             *
             *     expect(phantomInstance1.$isPhantom()).toBe(false);
             *     expect(phantomInstance2.$isPhantom()).toBe(false);
             * });
             */
            function ResourcePhantomIdFactory (generateFn, isPhantomFn) {
                var
                    self = this;

                /**
                 * Generates a new phantom PK value
                 *
                 * @memberof ResourcePhantomIdFactory
                 * @function generate
                 * @param {ResourceInstance} instance Instance to generate the phantom ID for
                 * @return {String|int}
                 * @instance
                 */
                self.generate = function (instance) {
                    return generateFn(instance);
                };

                /**
                 * Checks if the given PK value on the given instance is a phantom PK value
                 *
                 * @memberof ResourcePhantomIdFactory
                 * @function isPhantom
                 * @param {String|int} pkValue ID value to check
                 * @param {ResourceInstance} instance Instance to work on
                 * @return {*}
                 * @instance
                 */
                self.isPhantom = function (pkValue, instance) {
                    return isPhantomFn(pkValue, instance);
                };
            }
        }
    );

    /**
     * Resource phantom id generator that generates negative integer IDs
     *
     * @name ResourcePhantomIdNegativeInt
     * @ngdoc object
     * @param {ResourcePhantomIdFactoryService} ResourcePhantomIdFactoryService Phantom ID factory service
     *
     * @example
     * // Using `ResourcePhantomIdNegativeInt`
     * inject(function (ResourceFactoryService, ResourcePhantomIdNegativeInt) {
     *     var
     *         service = ResourceFactoryService('TestResourceService', 'http://test/:pk/', {
     *             phantomIdGenerator: ResourcePhantomIdNegativeInt
     *         }),
     *         phantomInstance1 = service.new(),
     *         phantomInstance2 = service.new();
     *
     *     // Test if IDs are negative
     *     expect(phantomInstance1.pk).toBe(-1);
     *     expect(phantomInstance2.pk).toBe(-2);
     *
     *     // Test if instances are marked as phantom
     *     expect(phantomInstance1.$isPhantom()).toBe(true);
     *     expect(phantomInstance2.$isPhantom()).toBe(true);
     *
     *     // Change IDs
     *     phantomInstance1.pk = 1;
     *     phantomInstance2.pk = 2;
     *
     *     // Test if instances are marked as concrete for positive IDs
     *     expect(phantomInstance1.$isPhantom()).toBe(false);
     *     expect(phantomInstance2.$isPhantom()).toBe(false);
     * });
     */
    module.factory('ResourcePhantomIdNegativeInt',
        function (ResourcePhantomIdFactoryService) {
            'ngInject';

            var
                lastPkValue = 0;

            return ResourcePhantomIdFactoryService.createPhantomIdFactory({
                generate: function () {
                    return --lastPkValue;
                },
                is: function (pkValue) {
                    return pkValue < 0;
                }
            });
        }
    );

    /**
     * Resource phantom id generator that generates UUID4 IDs
     *
     * @name ResourcePhantomIdUuid4
     * @ngdoc object
     * @param {ResourcePhantomIdFactoryService} ResourcePhantomIdFactoryService Phantom ID factory service
     *
     * @example
     * // Using `ResourcePhantomIdUuid4`
     * inject(function (ResourceFactoryService, ResourcePhantomIdUuid4) {
     *     var
     *         service = ResourceFactoryService('TestResourceService', 'http://test/:pk/', {
     *             phantomIdGenerator: ResourcePhantomIdUuid4
     *         }),
     *         phantomInstance1 = service.new(),
     *         phantomInstance2 = service.new();
     *
     *     // Test if IDs are valid UUID4s
     *     expect(phantomInstance1.pk).toMatch(/[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/i);
     *     expect(phantomInstance2.pk).toMatch(/[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/i);
     *
     *     // Test if instances are marked as phantom as long as UUID4s match one of the generated
     *     expect(phantomInstance1.$isPhantom()).toBe(true);
     *     expect(phantomInstance2.$isPhantom()).toBe(true);
     *
     *     // Change IDs
     *     phantomInstance1.pk = '00000000-0000-0000-0000-000000000000';
     *     phantomInstance2.pk = '11111111-1111-1111-1111-111111111111';
     *
     *     // Test if instances are marked as concrete for not generated UUID4s
     *     expect(phantomInstance1.$isPhantom()).toBe(false);
     *     expect(phantomInstance2.$isPhantom()).toBe(false);
     * });
     */
    module.factory('ResourcePhantomIdUuid4',
        function (ResourcePhantomIdFactoryService) {
            'ngInject';

            var
                generatedIds = [];

            return ResourcePhantomIdFactoryService.createPhantomIdFactory({
                generate: function () {
                    var
                        pkValue = uuid4();

                    generatedIds.push(pkValue);
                    return pkValue;
                },
                is: function (pkValue) {
                    return generatedIds.indexOf(pkValue) !== -1;
                }
            });

            function uuid4 () {
                return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                    var
                        r = Math.random() * 16|0,
                        v = c === 'x' ? r : (r&0x3|0x8);

                    return v.toString(16);
                });
            }
        }
    );
})();