Skip to content

Instantly share code, notes, and snippets.

@lifeart
Last active December 6, 2017 20:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lifeart/a8ad46c6dddf442e3c699e31666115c2 to your computer and use it in GitHub Desktop.
Save lifeart/a8ad46c6dddf442e3c699e31666115c2 to your computer and use it in GitHub Desktop.
angular-can
  • New Service: can

Methods:

can.addAction // accept: abilityName, actionName, actionFunction | return: can
/*

	can.addAction('profile','email',()=>false);
	
	if can be used like can('email to profile'); // --> false
	
*/

can.addActionAlias // accept: actionName, aliasName | return: can
/*
	for case if action fn "read" equals "access", we can do it:
	can.addActionAlias('read','access');
	can('read profile') === can('access profile');
*/


can.addAbility // accept: abilityName, object -> list of actions | return: can
/*
	we need to add some ability for profile, with restrictions to "read", "write", "print"
	
	can.addAbility('profile',{
		read: ()=>(return false),
		write: ()=>(return false),
		print: ()=>(return true)
	});
	
	if can be used like can('read profile');

*/
can.addAbilityAlias // accept: abilityName, aliasName | return: can
/*
	aliasing for abilities
	can.addAbilityAlias('wake up','come back');
	can.addAbilityAlias('wake up','wake');
	
*/
can.setRestrictionAliases // accept: object -> list of non-semantic aliases | return: can
/*
	use if for non-semantic restrictions, like
	
	can('WakeUpAt7AM') -> we do this:
	
	can.addAbility('work',{
		wakeUp: ()=>false
	});
	
	can.setRestrictionAliases({
		'WakeUpAt7AM': 'wake up for work'
	});
	
	can('WakeUpAt7AM') ==== can('wake up for work')
	
*/
can.getState // accept: actionName, aliasName | return: abilities, abilityAliases, actionAliases, restrictionsAliases
/*
	notable for debugging
*/
can.can // accept: action key, params | return: true/false
/*
	if we need to check some restriction, we can do it:
	can('write interesting posts');
	
	
	can('write interesting posts') 
	=== can('writeInteresting posts')
	=== can('writeInteresting to posts')
	=== can('write-interesting posts')
	
	stopwords:  "of", "in", "for", "to", "from", "on", if parser find one of whem, left part of string will be key
*/
can.cannot // accept: action key, params | return: true/false
/*
	logical "NOT"
	can.cannot('some expression') === !els.can('some expression');
*/
can.enableDebug // accept: - | return: can
/*
	enable / disable logging
*/
can.disableDebug // accept: - | return: can
/*
	enable / disable logging
*/

  • New template filters: can, cannot

*Inspired by Ruby can-can and Ember ember-can / https://github.com/minutebase/ember-can (http://ember-can.com/)

Usage inside html templates:

<section-loader data-is-loaded="isLoaded" ng-if="'play jazz' | cannot">
   <h2>You not allowed to play jazz</h2>
</section-loader>

<section-loader data-is-loaded="isLoaded" ng-if="'play jazz' | can">
   <h2>You allowed to play jazz!</h2>
</section-loader>

<section-loader data-is-loaded="isLoaded" ng-if="['edit user', userModel] | can">
   <h2>You can manage this user</h2>
</section-loader>

<section-loader data-is-loaded="isLoaded" ng-if="userModel | can:'play jazz'">
   <h2>You can play Jazz!</h2>
</section-loader>

<section-loader data-is-loaded="isLoaded" ng-if="userModel | can:'play jazz':instrument">
   <h2>You can play Jazz! (Based on userModel  and instrument)</h2>
</section-loader>

inside js:

       can
                .addAbility('jazz', { play: () => true })
                .addAbility('jazz', { dance: () => true })
                .addAbility('jazz', { play: () => true })
                .addAbility('jazz', { playHard: () => true , danceHard:()=>true})
                .addAbilityAlias('jazz', 'rock')
                .addActionAlias('dance', 'slam');

  can.can('play jazz') // -> true
  can('play jazz') // -> true
  can.cannot('play jazz') // -> false,
'use strict';
angular.module('common')
.provider('can', [
'lodash',
function (
lodash
) {
const abilities = {};
const abilityAliases = {};
const actionAliases = {};
const restrictionsAliases = {};
let debug = false;
let elsLoggingService = null;
const { camelCase, isArray, lowerCase } = lodash;
const _this = this;
const stopwords = [
"of", "in", "for", "to", "from", "on"
];
this.getState = () => {
return {
abilities,
abilityAliases,
actionAliases,
restrictionsAliases
};
};
function stringReducer(inputStr, matchFn) {
const results = [];
inputStr.split('').reverse().reduce((summ, element) => {
const newElement = element + summ;
results.push([matchFn(newElement), newElement])
return newElement;
}, '');
const existingResults = results.filter(([match])=>!false);
return existingResults.map(([exists,el])=>el).reverse();
}
function getRealAbility(rawName) {
const name = normalize(rawName);
return abilityAliases[name] || name;
}
function normalize(str) {
return camelCase(prepareString(str));
}
function extractAbilityFromString(rawString) {
if (rawString.indexOf(' ') === -1) {
rawString = lowerCase(rawString);
}
return stringReducer(rawString, (stringPart)=>{
return getAbility(stringPart);
});
}
function prepareString(str) {
return String(str).replace(/\s{2,}/g, ' ').trim();
}
function getLastWord(abilityKey) {
return prepareString(abilityKey).split(' ').filter(e => e.trim().length).pop();
}
function getFirstWord(abilityKey) {
const firstWordCandidate = prepareString(abilityKey)
.split(getLastWord(abilityKey))[0].split(' ')
.filter(str => !stopwords.includes(str)).join(' ').trim();
if (!firstWordCandidate) {
console.error(`Unable to get action by key "${abilityKey}"`, _this.getState());
return '';
}
return firstWordCandidate.replace('can ','').replace('Can ','');
}
function log(method, params) {
if (debug) {
elsLoggingService.info.apply(null, [method, params]);
}
}
function getAbility(rawKey) {
return abilities[getRealAbility(rawKey)] || false;
}
this.addAbility = (rawName, actions = {}) => {
const rawActions = Object.keys(actions);
const trueActions = {};
rawActions.forEach(actionName=>{
trueActions[normalize(actionName)] = actions[actionName];
});
const name = getRealAbility(rawName);
if (typeof abilities[name] !== 'object') {
abilities[name] = {};
}
Object.assign(abilities[name], trueActions);
return this;
}
this.addAbilityAlias = (rawName, rawAlias) => {
const name = getRealAbility(rawName);
const alias = normalize(rawAlias);
abilityAliases[alias] = name;
return this;
}
this.addAction = (rawAbility, rawActionName, fn) => {
const ability = getRealAbility(rawAbility);
const actionName = normalize(rawActionName);
this.addAbility(realAbility, { [actionName]: fn});
return this;
}
this.addActionAlias = (rawActionName, rawAlias) => {
const actionName = normalize(rawActionName);
const alias = normalize(rawAlias);
actionAliases[alias] = actionName;
return this;
}
this.setRestrictionAliases = (aliasesObj) => {
Object.assign(restrictionsAliases, aliasesObj);
return this;
}
this.enableDebug = () => {
debug = true;
log('can', 'debug enabled');
return this;
}
this.disableDebug = () => {
debug = false;
log('can', 'debug disabled');
return this;
}
this._check = (checkType, abilityKey, ...args) => {
const originalKey = abilityKey;
if (restrictionsAliases[abilityKey]) {
abilityKey = restrictionsAliases[abilityKey];
}
if (isArray(abilityKey)) {
log(checkType, 'passed ability is an Array');
let realAbilityKey = abilityKey.shift();
args = abilityKey;
abilityKey = realAbilityKey;
}
const abilityName = getLastWord(abilityKey);
const abilityAction = normalize(getFirstWord(abilityKey));
log(`${checkType} - abilityName`, abilityName);
log(`${checkType} - abilityAction`, abilityAction);
const ability = getAbility(abilityName);
if (!ability) {
console.error(`Unable to get ability by key "${abilityKey}"`, this.getState());
return false;
}
const trueActionName = actionAliases[abilityAction] || abilityAction;
let result = false;
if (typeof ability[trueActionName] === 'function') {
result = ability[trueActionName].apply(this, args);
} else if (typeof ability[trueActionName] !== 'undefined') {
result = ability[trueActionName];
} else {
console.error(`Resolved action name "${trueActionName}"`);
console.error(`Resolved ability name "${abilityName}"`);
console.error(`Unable to find ability by key "${abilityKey}"`, this.getState());
return false;
}
const checkResult = checkType === 'can' ? result : !result
log(checkType, `key: ${originalKey} / resolved key: ${abilityKey}/ ability: ${abilityName} / action: ${trueActionName} / result: ${result} / check result: ${checkResult}`);
return checkResult;
}
this.can = (abilityKey, ...args) => {
return this._check('can', abilityKey, args);
}
this.cannot = (abilityKey, ...args) => {
return this._check('cannot', abilityKey, args);
};
const getter = this.can;
getter.addActionAlias = this.addActionAlias;
getter.addAction = this.addAction;
getter.addAbility = this.addAbility;
getter.addAbilityAlias = this.addAbilityAlias;
getter.setRestrictionAliases = this.setRestrictionAliases;
getter.getState = this.getState;
getter.can = this.can;
getter.cannot = this.cannot;
getter.enableDebug = this.enableDebug;
getter.disableDebug = this.disableDebug;
return {
$get: ['$injector',($injector) => {
if (!elsLoggingService) {
elsLoggingService = $injector.get('elsLoggingService');
}
return getter;
}]
}
}
]);
'use strict';
angular.module('common').filter('can', ['can', function (can) {
return function (inputString, param = false, ...args) {
if (inputString) {
// if param it means that filter called like this:
// firstParam | can:"param", and we should swap inputString and param to pass it into can
if (param) {
return can.can.apply(can, [param, inputString].concat(args));
} else {
return can.can.apply(can, [inputString].concat(args));
}
} else {
if (param) {
return can.can.apply(can, [param].concat(args));
}
console.error('Unable to find params for can service, default value returned', inputString,param,args);
console.error('Check param value, passed to "can" filter, it may be undefined or empty');
return false;
}
}
}]);
angular.module('common').filter('cannot', ['can', function (can) {
return function (inputString, param = false, ...args) {
if (inputString) {
// if param it means that filter called like this:
// firstParam | can:"param", and we should swap inputString and param to pass it into can
if (param) {
return can.cannot.apply(can, [param, inputString].concat(args));
} else {
return can.cannot.apply(can, [inputString].concat(args));
}
} else {
if (param) {
return can.cannot.apply(can, [param].concat(args));
}
console.error('Unable to find params for can service, default value returned', inputString, param, args);
console.error('Check param value, passed to "cannot" filter, it may be undefined or empty');
return !false;
}
}
}]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment