diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..c9f2a90 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,24 @@ +version: 2 +jobs: + build: + working_directory: ~/maprules + docker: + - image: circleci/node:10.7.0 + steps: + - checkout + - run: + name: update-npm + command: 'sudo npm install -g npm@latest' + - run: + name: install dependencies + command: 'npm install --save sqlite3 && npm install' + - run: + name: build + command: 'npm run build' + - run: + name: fixture + command: 'NODE_ENV=testing npm run fixture' + - run: + name: test + command: 'npm test' + diff --git a/.gitignore b/.gitignore index e95dd59..0626659 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ package-lock.json doc spec rules/ +!adapters/rules +!schemas/rules maprule/ db/*.sqlite node_modules/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8282e09..62917c5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -35,7 +35,7 @@ The 'API' refers to the API and database that stores the tagging rules and forma #### Technical details -- the API is written using [hapijs](https://hapijs.com/). For full documentation regarding the API, check out its own [documentation](link.to.api.docs) +- the API is written using [hapijs](https://hapijs.com/). For full documentation regarding the API, check out its own [documentation](https://github.com/radiant-maxar/maprules/blob/master/maprules.apidocs.md) - each editor/tool integration is made possible by an `adapter` modules and a schema module. These generate files that make presets/validations usable in integrations. - the schema modules are written using [joi](https://github.com/hapijs/joi). These modules define what is a valid output for a given integration file as well as inputs for the config file. @@ -49,12 +49,12 @@ Currently supported integrations and integrations in development are below... ### JOSM -- the JOSM [MapRules Plugin]() provides support for MapRules in JOSM +- the JOSM [MapRules Plugin](https://github.com/radiant-maxar/maprules-josm) provides support for MapRules in JOSM -STATUS: Developed and awaiting integration into the [JOSM Plugins]() repo +STATUS: Developed and awaiting integration into the [JOSM Plugins](https://github.com/openstreetmap/josm-plugins) repo ### iD - a feature branch brings MapRules support to iD -STATUS: Still just a fork in the MapRules organization +STATUS: Still just a [fork](https://github.com/radiant-maxar/iD/tree/remote-presets) diff --git a/CHANGELOG.md b/CHANGELOG.md index f745b6a..19349a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # 0.0.1-alpha -##### September 30th, 2018 +##### October 9th, 2018 - :tada: first release! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdbbfb7..fcecdc9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to MapRules -First off, thanks for considering to contribute! Before heading to file a ticket for a bug you found or an awesome idea, please read the [Code of Conduct](link.to.code.of.conduct). +First off, thanks for considering to contribute! Before heading to file a ticket for a bug you found or an awesome idea, please read the [Code of Conduct](https://github.com/radiant-maxar/maprules/blob/master/CODE_OF_CONDUCT.md). # File an issue @@ -28,18 +28,18 @@ These labels are for issues needing fixing... | bug-maprules | bug that applies directly to the MapRules service code | | bug-integration | bug that applies to using MapRules in one of the tools it is integrated with | -## Actionables +## Actionables :hammer: -These labels are for issues to be handled +These labels are applied issues to describe who aught to hammer them out | type | description | |------------------|-----------------------------------------------------------------------------------------------------------| | good first issue | issue for first time contributors | | help wanted | a heftier task that requires some prior knowledge of or willingness to get to know the MapRules code base | -## features +## Features :squirrel: -These labels are for features to be added +These labels are for features to be added and shipped in the future | type | description | |------------------|-----------------------------------------------------------| @@ -48,7 +48,7 @@ These labels are for features to be added | rule | feature that adds a new rule type for MapRules to support | | performance | feature that makes MapRules more performant | -## discussion +## discussion :telephone_receiver: These labels should be applied to issues that encapsulate some general discussion @@ -57,7 +57,7 @@ These labels should be applied to issues that encapsulate some general discussio | question | if it's not clear if a new feature should be in MapRules, or perhaps there lacks' clarity on how best to solve a problem, this label applies | | considering | if an ask is interesting but more consensus is needed before it's all systems go, this label applies | -## housekeeping +## housekeeping :house_with_garden: These labels should be applied to indicate what developers are doing to address or not address an issues diff --git a/README.md b/README.md index 9db7805..a3b1049 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,7 @@ nvm use ##### install node dependencies ``` -yarn install --save sqlite3 -yarn install +yarn install -G sqlite3 && yarn install ``` ### Development diff --git a/adapters/rules/config/index.js b/adapters/rules/config/index.js new file mode 100644 index 0000000..ce63311 --- /dev/null +++ b/adapters/rules/config/index.js @@ -0,0 +1,38 @@ +'use strict'; + +const buildTagChecks = require('./tagChecks'); +const buildDisabledFeatureChecks = require('../../disabledFeatures'); + +const translateOsmType = require('../../helpers').translateOsmType; +const flattenElements = require('../../helpers').flattenElements; +const inferJosmGeometries = require('../../josmPresets/helpers').inferJosmGeometries; + +/** + * Provided presetConfig, replies rules used to build MapCSS + * replies a single mapcss rule config. + * @param {Object} config presetConfig + * @return {Object} single mapcss rule config. + */ +module.exports = (config) => { + const configRules = config.presets.map((preset) => { + const rules = preset.fields.map((field) => { + return buildTagChecks(field, preset.primary, preset.name, config.name); + }); + + return inferJosmGeometries(preset.geometry).map((geometry) => { + return { + osmType: geometry, + rules: flattenElements(rules) + }; + }); + }); + + if (config.hasOwnProperty('disabledFeatures')) { + const disabledFeatures = ['node', 'way'].map((geom) => { + return buildDisabledFeatureChecks(config.disabledFeatures, geom, config.name); + }); + configRules.concat(disabledFeatures); + } + + return flattenElements(configRules); +}; \ No newline at end of file diff --git a/adapters/rules/config/tagChecks/fieldConditional/index.js b/adapters/rules/config/tagChecks/fieldConditional/index.js new file mode 100644 index 0000000..e152ecc --- /dev/null +++ b/adapters/rules/config/tagChecks/fieldConditional/index.js @@ -0,0 +1,34 @@ +'use strict'; + +const adaptEqualityToConditional = require('../../../../helpers').adaptEqualityToConditional; +const escaped = require('../../../../helpers').escaped; + +/** + * Provided parameters for rule's truthiness and severity, + * returns config used to build mapcss selector and validation error/warning + * @param {String} key key part of selector's key, value pair + * @param {Boolean} valCondition defines truthiness of rule when equalityValues are present + * @param {Array} equalityValues values, that if present, define the possible values that if coupled / not coupled with key, cause an validation error/warning + * @return {Object} config object used to build mapcss selector and validation error/warning + */ +const _alpha = (key, value) => { + const values = value.values.map(v => `^${escaped(v.split(' - ')[0])}$`).join('|'); + const equality = value.valCondition ? '!~' : '=~'; + return [`[${key}][${key}${equality}/${values}/]`]; +}; + +/** + * Provided key and numericEquality object, + * replies config used to build mapcss selector and validation error/warning + * @param {String} key key part of the selector's key/val pair + * @param {Object} numericEquality subset of numeric values object + * @return {string} string component of mapcss config + */ +const _numeric = (key, value) => { + return [adaptEqualityToConditional(key, value)]; +}; + +module.exports = { + numeric: _numeric, + alpha: _alpha +}; \ No newline at end of file diff --git a/adapters/rules/config/tagChecks/index.js b/adapters/rules/config/tagChecks/index.js new file mode 100644 index 0000000..9ec485b --- /dev/null +++ b/adapters/rules/config/tagChecks/index.js @@ -0,0 +1,50 @@ +'use strict'; + +const THROW_ERROR = require('../../../constants').THROW_ERROR; +const THROW_WARNING = require('../../../constants').THROW_WARNING; + +const message = require('./message'); +const fieldConditional = require('./fieldConditional'); +const getToThrow = require('../../../helpers').getToThrow; +const impliesEqual = require('../../../helpers').impliesEqual; + +/** + * Provided field and primary key, + * replies array of configs defining mapcss selectors + * @param {object} field field config from preset within presetConfig + * @param {object} primary primary tag config from field config + * @return {array} array of configs defining mapcss selectors + */ +module.exports = (field, primary, presetName,) => { + const keyCondition = Number(field.keyCondition); + const values = field.values; + const tagChecks = []; + const tagCheckBase = primary.map(p => `[${p.key}=${p.val}]`).join(''); + const conditionalVerb = keyCondition === 0 ? 'must not' : keyCondition === 1 ? 'must' : 'may'; + + tagChecks.push({ + base: tagCheckBase, + fieldConditionals: [keyCondition !== 0 ? `[!${field.key}]` : `[${field.key}]`], + toThrow: keyCondition !== 2 ? THROW_ERROR : THROW_WARNING, + message: `'${presetName}' preset ${conditionalVerb} include ${field.key}` + }); + + if (keyCondition !== 0) { + values.forEach((value) => { + const checkType = value.valCondition > 2 || impliesEqual(values) ? 'numeric' : 'alpha'; + + const messageBuilder = message[checkType]; + const fieldConditionBuilder = fieldConditional[checkType]; + + tagChecks.push({ + base: tagCheckBase, + toThrow: getToThrow(value), + fieldConditionals: fieldConditionBuilder(field.key, value), + message: messageBuilder(field.key, value) + }); + }); + + } + + return tagChecks; +}; diff --git a/adapters/rules/config/tagChecks/message/index.js b/adapters/rules/config/tagChecks/message/index.js new file mode 100644 index 0000000..a13cb7a --- /dev/null +++ b/adapters/rules/config/tagChecks/message/index.js @@ -0,0 +1,33 @@ +'use strict'; + +const adaptNumericToMessage = require('../../../../helpers').adaptNumericToMessage; + +/** + * builds a error message for alpha (so string based) rules + * @param {Array} equalityValues list of equality values to be adapted to error messages + * @param {Number} messageMatch numeric encoding of must/must not/may + * @return {String} error message + */ +const _alpha = (key, value) => { + const valuesString = value.values.map(v => `'${v}'`).join(','); + let conditionalMessage = ''; + if (value.valCondition === 0) conditionalMessage += 'must not'; + if (value.valCondition === 1) conditionalMessage += 'must'; + if (value.valCondition === 2) conditionalMessage += 'may'; + return `${key} ${conditionalMessage} be ${valuesString}`; +}; + +/** + * builds a error message for numeric rules + * @param {Array} equalityValues list of equality values to be adapted to error messages + * @return {String} error message + */ +const _numeric = (key, value) => { + const valueString = adaptNumericToMessage(value.values[0], value.valCondition); + return `${key} must be ${valueString}`; +}; + +module.exports = { + alpha: _alpha, + numeric: _numeric +}; diff --git a/adapters/rules/index.js b/adapters/rules/index.js new file mode 100644 index 0000000..bed3b25 --- /dev/null +++ b/adapters/rules/index.js @@ -0,0 +1,19 @@ +'use strict'; + +const buildRulesConfig = require('./config'); +const buildMapCSS = require('./mapCSS'); + +/** + * Provided a preset config, adapts/generates + * mapcss string reflecting rules written inside config + * @param {Object} config presetConfig + * @return {String} mapcss string + */ +module.exports = (config) => { + try { + const rulesConfig = buildRulesConfig(config); + return buildMapCSS(rulesConfig); + } catch (error) { + throw error; + } +}; \ No newline at end of file diff --git a/adapters/rules/mapCSS/index.js b/adapters/rules/mapCSS/index.js new file mode 100644 index 0000000..c8f3759 --- /dev/null +++ b/adapters/rules/mapCSS/index.js @@ -0,0 +1,17 @@ +'use strict'; + +const buildRule = require('./rule'); +const flattenElements = require('../../helpers').flattenElements; + +/** + * Provided mapcss config, replies mapcss string + * @param {Object} config the mapcss config + * @return {String} mapcss string + */ +module.exports = (config) => { + return flattenElements(config.map((configRule) => { + return configRule.rules.map((tagCheck) => { + return buildRule(tagCheck, configRule.osmType); + }); + })).join(''); +}; diff --git a/adapters/rules/mapCSS/rule.js b/adapters/rules/mapCSS/rule.js new file mode 100644 index 0000000..f61cdf3 --- /dev/null +++ b/adapters/rules/mapCSS/rule.js @@ -0,0 +1,34 @@ +'use strict'; + +/** + * + * @param {Object} rule config for mapcss tag selector + * @param {String} osmType osmType that prepends tag selector + * @return {String} mapcss selector + */ +module.exports = (rule, osmType) => { + const isClosed = osmType === 'closedway'; + const fieldConditionals = rule.fieldConditionals; + const toThrow = rule.toThrow; + + let selectors = ''; + if (fieldConditionals !== undefined) { + selectors += fieldConditionals.map(fieldConditional => { + + let selector = `${isClosed ? 'way': osmType}${rule.base}${fieldConditional}`; + if (isClosed) { + selector += ':closed'; + } + + return selector; + + }).join(fieldConditionals.length > 1 ? ',\n' : ''); + + } else { + selectors += `${isClosed ? 'way' : osmType}${rule.base}`; + + } + return `${selectors}{ + ${toThrow}: "${rule.message}"; + }\n`; +}; diff --git a/build.js b/build.js index e29ccd8..82534bb 100644 --- a/build.js +++ b/build.js @@ -1,41 +1,14 @@ 'use strict'; const shell = require('shelljs'); -const path = require('path'); const colors = require('colors/safe'); const wd = process.cwd(); function build() { // build docs - shell.exec('npm run makeapidocs'); - - console.log(colors.rainbow('\n\nDOCS BUILT!!\n\n')); - - // build ui - const uiSource = path.join(wd, 'node_modules/maprules-ui'); - shell.exec('npm install --prefix ' + uiSource); - shell.exec('npm run build --prefix ' + uiSource); - - // make the maprule dir - const uiDir = path.join(wd + '/maprule'); - - if (shell.test('-d', uiDir)) shell.exec('rm -rf ' + uiDir); - shell.mkdir(uiDir); - const uiSourceDist = path.join(uiSource, 'dist/maprules-ui'); - - shell.cp(uiSourceDist + '/*', uiDir); - - // take care of relative paths :( - const index = path.join(uiDir, 'index.html'); - const styles = path.join(uiDir, 'styles.js'); - - shell.sed('-i', /src=\"/g, 'src="maprule/', index); - /* eslint-disable */ - shell.sed('-i', /fontawesome-webfont/g, 'maprule/fontawesome-webfont', styles); - - console.log(colors.rainbow('\n\nUI BUILT!!\n\n')) - console.log(colors.rainbow('FINISHED!')) + shell.exec('npm run makedocs'); + console.log(colors.rainbow('\nDOCS BUILT!!\n\n')); }; build(); \ No newline at end of file diff --git a/handlers/maprule.js b/handlers/maprule.js deleted file mode 100644 index 09f51e1..0000000 --- a/handlers/maprule.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const path = require('path'); - -module.exports = { - directory: { - path: path.join(process.cwd(), 'maprule'), - listing: false, - index: [ 'index.html'], - redirectToSlash: false - } -}; \ No newline at end of file diff --git a/package.json b/package.json index 5a2bdc4..ce4f93f 100644 --- a/package.json +++ b/package.json @@ -26,14 +26,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/maprules/maprules.git" + "url": "git+https://github.com/radiant-maxar/maprules.git" }, "author": "MapRules Contributors", "license": "GPL-3.0-only", "bugs": { - "url": "https://github.com/maprules/maprules/issues" + "url": "https://github.com/radiant-maxar/maprules/issues" }, - "homepage": "https://github.com/maprules/maprules#readme", + "homepage": "https://github.com/radiant-maxar/maprules#readme", "nyc": { "include": [ "adapters/**/*", @@ -53,7 +53,6 @@ "joi": "^13.1.2", "knex": "^0.14.4", "mapcss-parse": "github:radiant-maxar/mapcss-parse#develop", - "maprules-ui": "github:radiant-maxar /maprules-ui#develop", "path": "^0.12.7", "pm2": "^2.10.3", "sqlite3": "^4.0.2", diff --git a/routes/fontawesome.js b/routes/fontawesome.js deleted file mode 100644 index ae8c652..0000000 --- a/routes/fontawesome.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - method: 'GET', - path: '/{file*}', - handler: require('../handlers/fontawesome') -}; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index f2574fd..711cbd1 100644 --- a/routes/index.js +++ b/routes/index.js @@ -10,6 +10,5 @@ module.exports = [ require('./presetConfig').post, require('./docs'), require('./spec'), - require('./rules'), - require('./maprule') + require('./rules') ]; diff --git a/routes/maprule.js b/routes/maprule.js deleted file mode 100644 index 0143ee9..0000000 --- a/routes/maprule.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - method: 'GET', - path: '/maprule/{file*}', - handler: require('../handlers/maprule') -}; \ No newline at end of file diff --git a/schemas/rules/baseMapCSSRule.js b/schemas/rules/baseMapCSSRule.js new file mode 100644 index 0000000..a46f451 --- /dev/null +++ b/schemas/rules/baseMapCSSRule.js @@ -0,0 +1,5 @@ +'use strict'; + +const Joi = require('joi'); + +module.exports = Joi.string(); \ No newline at end of file diff --git a/schemas/rules/configRules.js b/schemas/rules/configRules.js new file mode 100644 index 0000000..75416ee --- /dev/null +++ b/schemas/rules/configRules.js @@ -0,0 +1,44 @@ +'use strict'; + +const Joi = require('joi'); +const tagChecksSchema = require('./tagChecks'); +const josmGeometries = require('../components').josmGeometries; +const wayType = require('../components').wayType; + +const keys = { + osmType: josmGeometries, + rules: Joi.array().items(tagChecksSchema), + wayType: wayType +}; + +const requiredKeys = ['osmType','rules']; + +module.exports = Joi + .object() + .keys(keys) + .requiredKeys(requiredKeys); + +/** + * Potenial Rule Combinations + * + * keyCondition = 1 && no specified values: + * - [base][!fieldKey] + * + * keyCondition = 1 && 1 specified values + * - [base][fieldKey!=value] + * - [base][!fieldKey] + * + * keyCondition = 1 && > 1 specified value + * - [base][fieldKey!~/a|b/] + * - [base][!fieldKey] + * + * keyCondition = 0 && no specified values + * - [base][fieldKey] + * + * keyCondition = 0 && 1 specified value + * - [base][fieldKey=value] + * + * keyCondition = 0 && > 1 specified value + * - [base][fieldKey=~/a|b/] + * + */ \ No newline at end of file diff --git a/schemas/rules/fieldConditionals/index.js b/schemas/rules/fieldConditionals/index.js new file mode 100644 index 0000000..52093dd --- /dev/null +++ b/schemas/rules/fieldConditionals/index.js @@ -0,0 +1,9 @@ +'use strict'; + +const Joi = require('joi'); +const singleFieldConditionalSchema = require('./single'); +const multipleFieldConditionalSchema = require('./multiple'); + +module.exports = Joi + .array() + .items([singleFieldConditionalSchema,multipleFieldConditionalSchema]); \ No newline at end of file diff --git a/schemas/rules/fieldConditionals/multiple.js b/schemas/rules/fieldConditionals/multiple.js new file mode 100644 index 0000000..330a80f --- /dev/null +++ b/schemas/rules/fieldConditionals/multiple.js @@ -0,0 +1,10 @@ +'use strict'; + +const Joi = require('joi'); + +const multipleAlternatives = [ + Joi.string().regex(/^\[([A-Z0-9]{3}|[A-Z0-9]{4,5}_[A-Z0-9]{3,4})\]\[([A-Z0-9]{3}|[A-Z0-9]{4,5}_[A-Z0-9]{3,4})(=~|!~)\/([A-Za-z0-9]*\|?)*\/\]$/), + Joi.string().regex(/^\[([A-Z0-9]{3}|[A-Z0-9]{4,5}_[A-Z0-9]{3,4})\]\[([A-Z0-9]{3}|[A-Z0-9]{4,5}_[A-Z0-9]{3,4})(=~|!~)\/([A-Za-z0-9]*\|?)*\/\]$/) +]; + +module.exports = multipleAlternatives; diff --git a/schemas/rules/fieldConditionals/single.js b/schemas/rules/fieldConditionals/single.js new file mode 100644 index 0000000..696b898 --- /dev/null +++ b/schemas/rules/fieldConditionals/single.js @@ -0,0 +1,5 @@ +'use strict'; + +const Joi = require('joi'); + +module.exports = Joi.string().regex(/^\[!?.*\]$/); \ No newline at end of file diff --git a/schemas/rules/index.js b/schemas/rules/index.js new file mode 100644 index 0000000..5e2c571 --- /dev/null +++ b/schemas/rules/index.js @@ -0,0 +1,6 @@ +'use strict'; + +const Joi = require('joi'); +const configRulesSchema = require('./configRules'); + +module.exports = Joi.array().items(configRulesSchema); diff --git a/schemas/rules/tagChecks.js b/schemas/rules/tagChecks.js new file mode 100644 index 0000000..1d720dc --- /dev/null +++ b/schemas/rules/tagChecks.js @@ -0,0 +1,22 @@ +'use strict'; + +const Joi = require('joi'); +const tagChecksSchema = require('./tagChecks'); + +const baseMapCSSRuleSchema = require('./baseMapCSSRule'); +const fieldConditionalsSchema = require('./fieldConditionals'); + +const keys = { + base: baseMapCSSRuleSchema, + fieldConditionals: fieldConditionalsSchema, + toThrow: Joi.string().regex(/^throwWarning$|^throwError$/), + message: Joi.string() +}; + +const requiredKeys = ['base','toThrow','message']; + +module.exports = Joi + .object() + .keys(keys) + .requiredKeys(requiredKeys); +