initial commit
This commit is contained in:
@ -0,0 +1,3 @@
|
||||
export const ACTION_TYPES = {
|
||||
RECEIVE_MODEL_ROUTES: 'RECEIVE_MODEL_ROUTES',
|
||||
};
|
22
packages/woocommerce-blocks/assets/js/data/schema/actions.js
Normal file
22
packages/woocommerce-blocks/assets/js/data/schema/actions.js
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES as types } from './action-types.js';
|
||||
import { API_BLOCK_NAMESPACE } from '../constants';
|
||||
|
||||
/**
|
||||
* Returns an action object used to update the store with the provided list
|
||||
* of model routes.
|
||||
*
|
||||
* @param {Object} routes An array of routes to add to the store state.
|
||||
* @param {string} namespace
|
||||
*
|
||||
* @return {Object} The action object.
|
||||
*/
|
||||
export function receiveRoutes( routes, namespace = API_BLOCK_NAMESPACE ) {
|
||||
return {
|
||||
type: types.RECEIVE_MODEL_ROUTES,
|
||||
routes,
|
||||
namespace,
|
||||
};
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Identifier key for this store reducer.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const STORE_KEY = 'wc/store/schema';
|
24
packages/woocommerce-blocks/assets/js/data/schema/index.js
Normal file
24
packages/woocommerce-blocks/assets/js/data/schema/index.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerStore } from '@wordpress/data';
|
||||
import { controls } from '@wordpress/data-controls';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from './constants';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
import * as resolvers from './resolvers';
|
||||
import reducer from './reducers';
|
||||
|
||||
registerStore( STORE_KEY, {
|
||||
reducer,
|
||||
actions,
|
||||
controls,
|
||||
selectors,
|
||||
resolvers,
|
||||
} );
|
||||
|
||||
export const SCHEMA_STORE_KEY = STORE_KEY;
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { combineReducers } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES as types } from './action-types';
|
||||
import {
|
||||
extractResourceNameFromRoute,
|
||||
getRouteIds,
|
||||
simplifyRouteWithId,
|
||||
} from './utils';
|
||||
import { hasInState, updateState } from '../utils';
|
||||
|
||||
/**
|
||||
* Reducer for routes
|
||||
*
|
||||
* @param {Object} state The current state.
|
||||
* @param {Object} action The action object for parsing.
|
||||
*
|
||||
* @return {Object} The new (or original) state.
|
||||
*/
|
||||
export const receiveRoutes = ( state = {}, action ) => {
|
||||
const { type, routes, namespace } = action;
|
||||
if ( type === types.RECEIVE_MODEL_ROUTES ) {
|
||||
routes.forEach( ( route ) => {
|
||||
const resourceName = extractResourceNameFromRoute(
|
||||
namespace,
|
||||
route
|
||||
);
|
||||
if ( resourceName && resourceName !== namespace ) {
|
||||
const routeIdNames = getRouteIds( route );
|
||||
const savedRoute = simplifyRouteWithId( route, routeIdNames );
|
||||
if (
|
||||
! hasInState( state, [
|
||||
namespace,
|
||||
resourceName,
|
||||
savedRoute,
|
||||
] )
|
||||
) {
|
||||
state = updateState(
|
||||
state,
|
||||
[ namespace, resourceName, savedRoute ],
|
||||
routeIdNames
|
||||
);
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers( {
|
||||
routes: receiveRoutes,
|
||||
} );
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { select, apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { receiveRoutes } from './actions';
|
||||
import { STORE_KEY } from './constants';
|
||||
|
||||
/**
|
||||
* Resolver for the getRoute selector.
|
||||
*
|
||||
* Note: All this essentially does is ensure the routes for the given namespace
|
||||
* have been resolved.
|
||||
*
|
||||
* @param {string} namespace The namespace of the route being resolved.
|
||||
*/
|
||||
export function* getRoute( namespace ) {
|
||||
// we call this simply to do any resolution of all endpoints if necessary.
|
||||
// allows for jit population of routes for a given namespace.
|
||||
yield select( STORE_KEY, 'getRoutes', namespace );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolver for the getRoutes selector.
|
||||
*
|
||||
* @param {string} namespace The namespace of the routes being resolved.
|
||||
*/
|
||||
export function* getRoutes( namespace ) {
|
||||
const routeResponse = yield apiFetch( { path: namespace } );
|
||||
const routes =
|
||||
routeResponse && routeResponse.routes
|
||||
? Object.keys( routeResponse.routes )
|
||||
: [];
|
||||
yield receiveRoutes( routes, namespace );
|
||||
}
|
158
packages/woocommerce-blocks/assets/js/data/schema/selectors.js
Normal file
158
packages/woocommerce-blocks/assets/js/data/schema/selectors.js
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { sprintf } from '@wordpress/i18n';
|
||||
import { createRegistrySelector } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from './constants';
|
||||
|
||||
/**
|
||||
* Returns the requested route for the given arguments.
|
||||
*
|
||||
* @param {Object} state The original state.
|
||||
* @param {string} namespace The namespace for the route.
|
||||
* @param {string} resourceName The resource being requested
|
||||
* (eg. products/attributes)
|
||||
* @param {Array} [ids] This is for any ids that might be implemented in
|
||||
* the route request. It is not for any query
|
||||
* parameters.
|
||||
*
|
||||
* Ids example:
|
||||
* If you are looking for the route for a single product on the `wc/blocks`
|
||||
* namespace, then you'd have `[ 20 ]` as the ids. This would produce something
|
||||
* like `/wc/blocks/products/20`
|
||||
*
|
||||
*
|
||||
* @throws {Error} If there is no route for the given arguments, then this will
|
||||
* throw
|
||||
*
|
||||
* @return {string} The route if it is available.
|
||||
*/
|
||||
export const getRoute = createRegistrySelector(
|
||||
( select ) => ( state, namespace, resourceName, ids = [] ) => {
|
||||
const hasResolved = select(
|
||||
STORE_KEY
|
||||
).hasFinishedResolution( 'getRoutes', [ namespace ] );
|
||||
state = state.routes;
|
||||
let error = '';
|
||||
if ( ! state[ namespace ] ) {
|
||||
error = sprintf(
|
||||
'There is no route for the given namespace (%s) in the store',
|
||||
namespace
|
||||
);
|
||||
} else if ( ! state[ namespace ][ resourceName ] ) {
|
||||
error = sprintf(
|
||||
'There is no route for the given resource name (%s) in the store',
|
||||
resourceName
|
||||
);
|
||||
}
|
||||
if ( error !== '' ) {
|
||||
if ( hasResolved ) {
|
||||
throw new Error( error );
|
||||
}
|
||||
return '';
|
||||
}
|
||||
const route = getRouteFromResourceEntries(
|
||||
state[ namespace ][ resourceName ],
|
||||
ids
|
||||
);
|
||||
if ( route === '' ) {
|
||||
if ( hasResolved ) {
|
||||
throw new Error(
|
||||
sprintf(
|
||||
'While there is a route for the given namespace (%1$s) and resource name (%2$s), there is no route utilizing the number of ids you included in the select arguments. The available routes are: (%3$s)',
|
||||
namespace,
|
||||
resourceName,
|
||||
JSON.stringify( state[ namespace ][ resourceName ] )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return route;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Return all the routes for a given namespace.
|
||||
*
|
||||
* @param {Object} state The current state.
|
||||
* @param {string} namespace The namespace to return routes for.
|
||||
*
|
||||
* @return {Array} An array of all routes for the given namespace.
|
||||
*/
|
||||
export const getRoutes = createRegistrySelector(
|
||||
( select ) => ( state, namespace ) => {
|
||||
const hasResolved = select(
|
||||
STORE_KEY
|
||||
).hasFinishedResolution( 'getRoutes', [ namespace ] );
|
||||
const routes = state.routes[ namespace ];
|
||||
if ( ! routes ) {
|
||||
if ( hasResolved ) {
|
||||
throw new Error(
|
||||
sprintf(
|
||||
'There is no route for the given namespace (%s) in the store',
|
||||
namespace
|
||||
)
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
let namespaceRoutes = [];
|
||||
for ( const resourceName in routes ) {
|
||||
namespaceRoutes = [
|
||||
...namespaceRoutes,
|
||||
...Object.keys( routes[ resourceName ] ),
|
||||
];
|
||||
}
|
||||
return namespaceRoutes;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the route from the given slice of the route state.
|
||||
*
|
||||
* @param {Object} stateSlice This will be a slice of the route state from a
|
||||
* given namespace and resource name.
|
||||
* @param {Array} [ids=[]] Any id references that are to be replaced in
|
||||
* route placeholders.
|
||||
*
|
||||
* @return {string} The route or an empty string if nothing found.
|
||||
*/
|
||||
const getRouteFromResourceEntries = ( stateSlice, ids = [] ) => {
|
||||
// convert to array for easier discovery
|
||||
stateSlice = Object.entries( stateSlice );
|
||||
const match = stateSlice.find( ( [ , idNames ] ) => {
|
||||
return ids.length === idNames.length;
|
||||
} );
|
||||
const [ matchingRoute, routePlaceholders ] = match || [];
|
||||
// if we have a matching route, let's return it.
|
||||
if ( matchingRoute ) {
|
||||
return ids.length === 0
|
||||
? matchingRoute
|
||||
: assembleRouteWithPlaceholders(
|
||||
matchingRoute,
|
||||
routePlaceholders,
|
||||
ids
|
||||
);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* For a given route, route parts and ids,
|
||||
*
|
||||
* @param {string} route
|
||||
* @param {Array} routePlaceholders
|
||||
* @param {Array} ids
|
||||
*
|
||||
* @return {string} Assembled route.
|
||||
*/
|
||||
const assembleRouteWithPlaceholders = ( route, routePlaceholders, ids ) => {
|
||||
routePlaceholders.forEach( ( part, index ) => {
|
||||
route = route.replace( `{${ part }}`, ids[ index ] );
|
||||
} );
|
||||
return route;
|
||||
};
|
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import deepFreeze from 'deep-freeze';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { receiveRoutes } from '../reducers';
|
||||
import { ACTION_TYPES as types } from '../action-types';
|
||||
|
||||
describe( 'receiveRoutes', () => {
|
||||
it( 'returns original state when action type is not a match', () => {
|
||||
expect( receiveRoutes( undefined, { type: 'invalid' } ) ).toEqual( {} );
|
||||
} );
|
||||
it( 'returns original state when the given endpoints already exists', () => {
|
||||
const routes = [
|
||||
'wc/blocks/products/attributes',
|
||||
'wc/blocks/products/attributes/(?P<attribute_id>[d]+)/terms/(?P<id>[d]+)',
|
||||
];
|
||||
const originalState = deepFreeze( {
|
||||
'wc/blocks': {
|
||||
'products/attributes': {
|
||||
'wc/blocks/products/attributes': [],
|
||||
},
|
||||
'products/attributes/terms': {
|
||||
'wc/blocks/products/attributes/{attribute_id}/terms/{id}': [
|
||||
'attribute_id',
|
||||
'id',
|
||||
],
|
||||
},
|
||||
},
|
||||
} );
|
||||
const newState = receiveRoutes( originalState, {
|
||||
type: types.RECEIVE_MODEL_ROUTES,
|
||||
namespace: 'wc/blocks',
|
||||
routes,
|
||||
} );
|
||||
expect( newState ).toBe( originalState );
|
||||
} );
|
||||
it( 'returns expected state when new route added', () => {
|
||||
const action = {
|
||||
type: types.RECEIVE_MODEL_ROUTES,
|
||||
namespace: 'wc/blocks',
|
||||
routes: [ 'wc/blocks/products/attributes' ],
|
||||
};
|
||||
const originalState = deepFreeze( {
|
||||
'wc/blocks': {
|
||||
'products/attributes/terms': {
|
||||
'wc/blocks/products/attributes/{attribute_id}/terms/{id}': [
|
||||
'attribute_id',
|
||||
'id',
|
||||
],
|
||||
},
|
||||
},
|
||||
} );
|
||||
const newState = receiveRoutes( originalState, action );
|
||||
expect( newState ).not.toBe( originalState );
|
||||
expect( newState ).toEqual( {
|
||||
'wc/blocks': {
|
||||
'products/attributes': {
|
||||
'wc/blocks/products/attributes': [],
|
||||
},
|
||||
'products/attributes/terms': {
|
||||
'wc/blocks/products/attributes/{attribute_id}/terms/{id}': [
|
||||
'attribute_id',
|
||||
'id',
|
||||
],
|
||||
},
|
||||
},
|
||||
} );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { select, apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getRoute, getRoutes } from '../resolvers';
|
||||
import { receiveRoutes } from '../actions';
|
||||
import { STORE_KEY } from '../constants';
|
||||
|
||||
jest.mock( '@wordpress/data-controls' );
|
||||
|
||||
describe( 'getRoute', () => {
|
||||
it( 'yields select control response', () => {
|
||||
const fulfillment = getRoute( 'wc/blocks' );
|
||||
fulfillment.next();
|
||||
expect( select ).toHaveBeenCalledWith(
|
||||
STORE_KEY,
|
||||
'getRoutes',
|
||||
'wc/blocks'
|
||||
);
|
||||
const { done } = fulfillment.next();
|
||||
expect( done ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
describe( 'getRoutes', () => {
|
||||
describe( 'yields with expected responses', () => {
|
||||
let fulfillment;
|
||||
const rewind = () => ( fulfillment = getRoutes( 'wc/blocks' ) );
|
||||
test( 'with apiFetch control invoked', () => {
|
||||
rewind();
|
||||
fulfillment.next();
|
||||
expect( apiFetch ).toHaveBeenCalledWith( { path: 'wc/blocks' } );
|
||||
} );
|
||||
test( 'with receiveRoutes action with valid response', () => {
|
||||
const testResponse = {
|
||||
routes: {
|
||||
'/wc/blocks/products/attributes': [],
|
||||
},
|
||||
};
|
||||
const { value } = fulfillment.next( testResponse );
|
||||
expect( value ).toEqual(
|
||||
receiveRoutes( Object.keys( testResponse.routes ), 'wc/blocks' )
|
||||
);
|
||||
} );
|
||||
test( 'with receiveRoutesAction with invalid response', () => {
|
||||
rewind();
|
||||
fulfillment.next();
|
||||
const { value } = fulfillment.next( {} );
|
||||
expect( value ).toEqual( receiveRoutes( [], 'wc/blocks' ) );
|
||||
} );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import deepFreeze from 'deep-freeze';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getRoute, getRoutes } from '../selectors';
|
||||
|
||||
const mockHasFinishedResolution = jest.fn().mockReturnValue( false );
|
||||
jest.mock( '@wordpress/data', () => ( {
|
||||
__esModule: true,
|
||||
createRegistrySelector: ( callback ) =>
|
||||
callback( () => ( {
|
||||
hasFinishedResolution: mockHasFinishedResolution,
|
||||
} ) ),
|
||||
} ) );
|
||||
|
||||
const testState = deepFreeze( {
|
||||
routes: {
|
||||
'wc/blocks': {
|
||||
'products/attributes': {
|
||||
'wc/blocks/products/attributes': [],
|
||||
},
|
||||
'products/attributes/terms': {
|
||||
'wc/blocks/products/attributes/{attribute_id}/terms/{id}': [
|
||||
'attribute_id',
|
||||
'id',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} );
|
||||
|
||||
describe( 'getRoute', () => {
|
||||
const invokeTest = ( namespace, resourceName, ids = [] ) => () => {
|
||||
return getRoute( testState, namespace, resourceName, ids );
|
||||
};
|
||||
describe( 'with throwing errors', () => {
|
||||
beforeEach( () => mockHasFinishedResolution.mockReturnValue( true ) );
|
||||
it( 'throws an error if there is no route for the given namespace', () => {
|
||||
expect( invokeTest( 'invalid' ) ).toThrowError( /given namespace/ );
|
||||
} );
|
||||
it(
|
||||
'throws an error if there are routes for the given namespace, but no ' +
|
||||
'route for the given resource',
|
||||
() => {
|
||||
expect( invokeTest( 'wc/blocks', 'invalid' ) ).toThrowError();
|
||||
}
|
||||
);
|
||||
it(
|
||||
'throws an error if there are routes for the given namespace and ' +
|
||||
'resource name, but no routes for the given ids',
|
||||
() => {
|
||||
expect(
|
||||
invokeTest( 'wc/blocks', 'products/attributes', [ 10 ] )
|
||||
).toThrowError( /number of ids you included/ );
|
||||
}
|
||||
);
|
||||
} );
|
||||
describe( 'with no throwing of errors if resolution has not finished', () => {
|
||||
beforeEach( () => mockHasFinishedResolution.mockReturnValue( false ) );
|
||||
it.each`
|
||||
description | args
|
||||
${ 'is no route for the given namespace' } | ${ [ 'invalid' ] }
|
||||
${ 'are no routes for the given namespace, but no route for the given resource' } | ${ [ 'wc/blocks', 'invalid' ] }
|
||||
${ 'are routes for the given namespace and resource name, but no routes for the given ids' } | ${ [ 'wc/blocks', 'products/attributes', [ 10 ] ] }
|
||||
`( 'does not throw an error if there $description', ( { args } ) => {
|
||||
expect( invokeTest( ...args ) ).not.toThrowError();
|
||||
} );
|
||||
} );
|
||||
describe( 'returns expected value for given valid arguments', () => {
|
||||
test( 'when there is a route with no placeholders', () => {
|
||||
expect( invokeTest( 'wc/blocks', 'products/attributes' )() ).toBe(
|
||||
'wc/blocks/products/attributes'
|
||||
);
|
||||
} );
|
||||
test( 'when there is a route with placeholders', () => {
|
||||
expect(
|
||||
invokeTest( 'wc/blocks', 'products/attributes/terms', [
|
||||
10,
|
||||
20,
|
||||
] )()
|
||||
).toBe( 'wc/blocks/products/attributes/10/terms/20' );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getRoutes', () => {
|
||||
const invokeTest = ( namespace ) => () => {
|
||||
return getRoutes( testState, namespace );
|
||||
};
|
||||
it( 'throws an error if there is no route for the given namespace', () => {
|
||||
mockHasFinishedResolution.mockReturnValue( true );
|
||||
expect( invokeTest( 'invalid' ) ).toThrowError( /given namespace/ );
|
||||
} );
|
||||
it( 'returns expected routes for given namespace', () => {
|
||||
expect( invokeTest( 'wc/blocks' )() ).toEqual( [
|
||||
'wc/blocks/products/attributes',
|
||||
'wc/blocks/products/attributes/{attribute_id}/terms/{id}',
|
||||
] );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
extractResourceNameFromRoute,
|
||||
getRouteIds,
|
||||
simplifyRouteWithId,
|
||||
} from '../utils';
|
||||
|
||||
describe( 'extractResourceNameFromRoute', () => {
|
||||
it.each`
|
||||
namespace | route | expected
|
||||
${ 'wc/blocks' } | ${ 'wc/blocks/products' } | ${ 'products' }
|
||||
${ 'wc/other' } | ${ 'wc/blocks/product' } | ${ 'wc/blocks/product' }
|
||||
${ 'wc/blocks' } | ${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)' } | ${ 'products/attributes' }
|
||||
${ 'wc/blocks' } | ${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)/terms' } | ${ 'products/attributes/terms' }
|
||||
${ 'wc/blocks' } | ${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)/terms/(?P<id>[d]+)' } | ${ 'products/attributes/terms' }
|
||||
`(
|
||||
'returns "$expected" when namespace is "$namespace" and route is "$route"',
|
||||
( { namespace, route, expected } ) => {
|
||||
expect( extractResourceNameFromRoute( namespace, route ) ).toBe(
|
||||
expected
|
||||
);
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
describe( 'getRouteIds', () => {
|
||||
it.each`
|
||||
route | expected
|
||||
${ 'wc/blocks/products' } | ${ [] }
|
||||
${ 'wc/blocks/products/(?P<id>[\\d]+)' } | ${ [ 'id' ] }
|
||||
${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)/terms/(?P<id>[\\d]+)' } | ${ [ 'attribute_id', 'id' ] }
|
||||
`(
|
||||
'returns "$expected" when route is "$route"',
|
||||
( { route, expected } ) => {
|
||||
expect( getRouteIds( route ) ).toEqual( expected );
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
describe( 'simplifyRouteWithId', () => {
|
||||
it.each`
|
||||
route | matchIds | expected
|
||||
${ 'wc/blocks/products' } | ${ [] } | ${ 'wc/blocks/products' }
|
||||
${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)' } | ${ [ 'attribute_id' ] } | ${ 'wc/blocks/products/attributes/{attribute_id}' }
|
||||
${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)/terms' } | ${ [ 'attribute_id' ] } | ${ 'wc/blocks/products/attributes/{attribute_id}/terms' }
|
||||
${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)/terms/(?P<id>[\\d]+)' } | ${ [ 'attribute_id', 'id' ] } | ${ 'wc/blocks/products/attributes/{attribute_id}/terms/{id}' }
|
||||
${ 'wc/blocks/products/attributes/(?P<attribute_id>[\\d]+)/terms/(?P<id>[\\d]+)' } | ${ [ 'id', 'attribute_id' ] } | ${ 'wc/blocks/products/attributes/{attribute_id}/terms/{id}' }
|
||||
`(
|
||||
'returns "$expected" when route is "$route" and matchIds is "$matchIds"',
|
||||
( { route, matchIds, expected } ) => {
|
||||
expect( simplifyRouteWithId( route, matchIds ) ).toBe( expected );
|
||||
}
|
||||
);
|
||||
} );
|
65
packages/woocommerce-blocks/assets/js/data/schema/utils.js
Normal file
65
packages/woocommerce-blocks/assets/js/data/schema/utils.js
Normal file
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* This returns a resource name string as an index for a given route.
|
||||
*
|
||||
* For example:
|
||||
* /wc/blocks/products/attributes/(?P<id>[\d]+)/terms
|
||||
* returns
|
||||
* /products/attributes/terms
|
||||
*
|
||||
* @param {string} namespace
|
||||
* @param {string} route
|
||||
*
|
||||
* @return {string} The resource name extracted from the route.
|
||||
*/
|
||||
export const extractResourceNameFromRoute = ( namespace, route ) => {
|
||||
route = route.replace( `${ namespace }/`, '' );
|
||||
return route.replace( /\/\(\?P\<[a-z_]*\>\[\\*[a-z]\]\+\)/g, '' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of the identifier for the named capture groups in a given
|
||||
* route.
|
||||
*
|
||||
* For example, if the route was this:
|
||||
* /wc/blocks/products/attributes/(?P<attribute_id>[\d]+)/terms/(?P<id>[\d]+)
|
||||
*
|
||||
* ...then the following would get returned
|
||||
* [ 'attribute_id', 'id' ]
|
||||
*
|
||||
* @param {string} route - The route to extract identifier names from.
|
||||
*
|
||||
* @return {Array} An array of named route identifier names.
|
||||
*/
|
||||
export const getRouteIds = ( route ) => {
|
||||
const matches = route.match( /\<[a-z_]*\>/g );
|
||||
if ( ! Array.isArray( matches ) || matches.length === 0 ) {
|
||||
return [];
|
||||
}
|
||||
return matches.map( ( match ) => match.replace( /<|>/g, '' ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* This replaces regex placeholders in routes with the relevant named string
|
||||
* found in the matchIds.
|
||||
*
|
||||
* Something like:
|
||||
* /wc/blocks/products/attributes/(?P<attribute_id>[\d]+)/terms/(?P<id>[\d]+)
|
||||
*
|
||||
* ..ends up as:
|
||||
* /wc/blocks/products/attributes/{attribute_id}/terms/{id}
|
||||
*
|
||||
* @param {string} route The route to manipulate
|
||||
* @param {Array} matchIds An array of named ids ( [ attribute_id, id ] )
|
||||
*
|
||||
* @return {string} The route with new id placeholders
|
||||
*/
|
||||
export const simplifyRouteWithId = ( route, matchIds ) => {
|
||||
if ( ! Array.isArray( matchIds ) || matchIds.length === 0 ) {
|
||||
return route;
|
||||
}
|
||||
matchIds.forEach( ( matchId ) => {
|
||||
const expression = `\\(\\?P<${ matchId }>.*?\\)`;
|
||||
route = route.replace( new RegExp( expression ), `{${ matchId }}` );
|
||||
} );
|
||||
return route;
|
||||
};
|
Reference in New Issue
Block a user