From 875aa625e05ff45f00559570b97bec92b1d1efed Mon Sep 17 00:00:00 2001 From: Boaz Leskes Date: Mon, 17 Feb 2014 11:44:18 +0100 Subject: [PATCH] Added autocomplete support for url query strings parameters Add params for search, single doc APIs & _cat Closes #119 --- sense/app/autocomplete.js | 135 ++++++++- .../{url_path_autocomplete.js => engine.js} | 163 ++++++++++- sense/app/autocomplete/url_params.js | 62 ++++ sense/app/autocomplete/url_pattern_matcher.js | 124 ++++++++ sense/app/kb.js | 16 +- sense/app/kb/api.js | 6 +- sense/app/kb/api_0_90/document.js | 63 +++- sense/app/kb/api_0_90/search.js | 20 ++ sense/app/kb/api_1_0/cat.js | 37 ++- sense/app/kb/api_1_0/document.js | 63 +++- sense/app/kb/api_1_0/search.js | 23 ++ sense/app/kb/url_pattern_matcher.js | 276 ------------------ sense/tests/index.html | 1 + sense/tests/src/integration_tests.js | 137 +++++++++ sense/tests/src/kb_tests.js | 6 +- sense/tests/src/url_autocomplete_tests.js | 23 +- sense/tests/src/url_params_tests.js | 107 +++++++ 17 files changed, 926 insertions(+), 336 deletions(-) rename sense/app/autocomplete/{url_path_autocomplete.js => engine.js} (51%) create mode 100644 sense/app/autocomplete/url_params.js create mode 100644 sense/app/autocomplete/url_pattern_matcher.js delete mode 100644 sense/app/kb/url_pattern_matcher.js create mode 100644 sense/tests/src/url_params_tests.js diff --git a/sense/app/autocomplete.js b/sense/app/autocomplete.js index 159d883ce1e4..f99c9f6b83e6 100644 --- a/sense/app/autocomplete.js +++ b/sense/app/autocomplete.js @@ -6,12 +6,12 @@ define([ 'jquery', 'utils', 'autocomplete/json_body_autocomplete', - 'autocomplete/url_path_autocomplete', - 'kb/url_pattern_matcher', + 'autocomplete/engine', + 'autocomplete/url_pattern_matcher', '_', 'jquery-ui', 'ace_ext_language_tools' -], function (history, kb, mappings, ace, $, utils, json_body_autocomplete, url_path_autocomplete, url_pattern_matcher, _) { +], function (history, kb, mappings, ace, $, utils, json_body_autocomplete, autocomplete_engine, url_pattern_matcher, _) { 'use strict'; var AceRange = ace.require('ace/range').Range; @@ -48,6 +48,19 @@ define([ } } + function isUrlParamsToken(token) { + switch ((token || {}).type) { + case "url.param": + case "url.equal": + case "url.value": + case "url.questionmark": + case "url.amp": + return true; + default: + return false; + } + } + function getAutoCompleteValueFromToken(token) { switch ((token || {}).type) { case "variable": @@ -204,6 +217,9 @@ define([ case "path": addPathAutoCompleteSetToContext(context, pos); break; + case "url_params": + addUrlParamsAutoCompleteSetToContext(context, pos); + break; case "method": addMethodAutoCompleteSetToContext(context, pos); break; @@ -254,11 +270,23 @@ define([ return "path"; break; default: - return isUrlPathToken(t) ? "path" : null; + if (isUrlPathToken(t)) { + return "path"; + } + if (isUrlParamsToken(t)) { + return "url_params"; + } + return null; } break; default: - return isUrlPathToken(t) ? "path" : null; + if (isUrlPathToken(t)) { + return "path"; + } + if (isUrlParamsToken(t)) { + return "url_params"; + } + return null; } } @@ -315,6 +343,8 @@ define([ case "url.method": case "url.endpoint": case "url.part": + case "url.param": + case "url.value": insertingRelativeToToken = 0; context.rangeToReplace = new AceRange( pos.row, context.updatedForToken.start, pos.row, @@ -347,6 +377,9 @@ define([ case "path": addPathPrefixSuffixToContext(context); break; + case "url_params": + addUrlParamsPrefixSuffixToContext(context); + break; case "method": addMethodPrefixSuffixToContext(context); break; @@ -453,6 +486,11 @@ define([ return context; } + function addUrlParamsPrefixSuffixToContext(context) { + context.prefixToAdd = ""; + context.suffixToAdd = ""; + } + function addMethodPrefixSuffixToContext(context) { context.prefixToAdd = ""; context.suffixToAdd = ""; @@ -482,10 +520,42 @@ define([ context.method = ret.method; context.token = ret.token; context.urlTokenPath = ret.urlTokenPath; - url_path_autocomplete.populateContext(ret.urlTokenPath, context, editor, true, kb.getTopLevelUrlCompleteComponents()); + autocomplete_engine.populateContext(ret.urlTokenPath, context, editor, true, kb.getTopLevelUrlCompleteComponents()); context.autoCompleteSet = addMetaToTermsList(context.autoCompleteSet, "endpoint"); } + function addUrlParamsAutoCompleteSetToContext(context, pos) { + var ret = getCurrentMethodAndTokenPaths(pos); + context.method = ret.method; + context.otherTokenValues = ret.otherTokenValues; + context.urlTokenPath = ret.urlTokenPath; + if (!ret.urlTokenPath) { // zero length tokenPath is true + + console.log("Can't extract a valid url token path."); + return context; + } + + autocomplete_engine.populateContext(ret.urlTokenPath, context, editor, false, kb.getTopLevelUrlCompleteComponents()); + + if (!context.endpoint) { + console.log("couldn't resolve an endpoint."); + return context; + } + + if (!ret.urlParamsTokenPath) { // zero length tokenPath is true + console.log("Can't extract a valid urlParams token path."); + return context; + } + var tokenPath = [], currentParam = ret.urlParamsTokenPath.pop(); + if (currentParam) { + tokenPath = Object.keys(currentParam); // single key object + context.otherTokenValues = currentParam[tokenPath[0]]; + } + + autocomplete_engine.populateContext(tokenPath, context, editor, true, + context.endpoint.paramsAutocomplete.getTopLevelComponents()); + return context; + } function addBodyAutoCompleteSetToContext(context, pos) { @@ -499,7 +569,7 @@ define([ return context; } - url_path_autocomplete.populateContext(ret.urlTokenPath, context, editor, false, kb.getTopLevelUrlCompleteComponents()); + autocomplete_engine.populateContext(ret.urlTokenPath, context, editor, false, kb.getTopLevelUrlCompleteComponents()); context.bodyTokenPath = ret.bodyTokenPath; if (!ret.bodyTokenPath) { // zero length tokenPath is true @@ -621,8 +691,8 @@ define([ } if (tokenIter.getCurrentTokenRow() == startPos.row) { - if (t.type == "url.part") { - // we are on the same line as cursor and dealing with url on. Current token is not part of the context + if (t.type === "url.part" || t.type === "url.param" || t.type === "url.value") { + // we are on the same line as cursor and dealing with a url. Current token is not part of the context t = tokenIter.stepBackward(); } bodyTokenPath = null; // no not on a body line. @@ -630,8 +700,49 @@ define([ ret.bodyTokenPath = bodyTokenPath; ret.urlTokenPath = []; - + ret.urlParamsTokenPath = null; var curUrlPart; + + while (t && isUrlParamsToken(t)) { + switch (t.type) { + case "url.value": + if (_.isArray(curUrlPart)) { + curUrlPart.unshift(t.value); + } + else if (curUrlPart) { + curUrlPart = [ t.value, curUrlPart ]; + } + else { + curUrlPart = t.value; + } + break; + case "url.comma": + if (!curUrlPart) { + curUrlPart = []; + } + else if (!_.isArray(curUrlPart)) { + curUrlPart = [ curUrlPart ]; + } + break; + case "url.param": + var v = curUrlPart; + curUrlPart = {}; + curUrlPart[t.value] = v; + break; + case "url.amp": + case "url.questionmark": + if (!ret.urlParamsTokenPath) { + ret.urlParamsTokenPath = []; + } + ret.urlParamsTokenPath.unshift(curUrlPart || {}); + curUrlPart = null; + break; + } + t = tokenIter.stepBackward(); + } + + + curUrlPart = null; while (t && t.type.indexOf("url") != -1) { switch (t.type) { case "url.part": @@ -665,7 +776,7 @@ define([ ret.urlTokenPath.unshift(curUrlPart); } - if (!ret.bodyTokenPath) { + if (!ret.bodyTokenPath && !ret.urlParamsTokenPath) { if (ret.urlTokenPath.length > 0) { // started on the url, first token is current token @@ -803,7 +914,7 @@ define([ }); callback(null, _.map(terms, function (t, i) { - t.insert_value = t.value; + t.insert_value = t.insert_value || t.value; t.value = '' + t.value; // normalize to strings t.score = -i; return t; diff --git a/sense/app/autocomplete/url_path_autocomplete.js b/sense/app/autocomplete/engine.js similarity index 51% rename from sense/app/autocomplete/url_path_autocomplete.js rename to sense/app/autocomplete/engine.js index e8f88a802a13..fbfbc1eea709 100644 --- a/sense/app/autocomplete/url_path_autocomplete.js +++ b/sense/app/autocomplete/engine.js @@ -9,11 +9,6 @@ define([ this.name = name; }; - exports.Matcher = function (name, next) { - this.name = name; - this.next = next; - }; - exports.AutocompleteComponent.prototype.getTerms = function (context, editor) { return []; }; @@ -36,6 +31,164 @@ define([ }; }; + function SharedComponent(name, parent) { + exports.AutocompleteComponent.call(this, name); + this._nextDict = {}; + if (parent) { + parent.addComponent(this); + } + // for debugging purposes + this._parent = parent; + } + + SharedComponent.prototype = _.create( + exports.AutocompleteComponent.prototype, + { 'constructor': SharedComponent }); + + exports.SharedComponent = SharedComponent; + + (function (cls) { + cls.getComponent = function (name) { + return this._nextDict[name]; + }; + + cls.addComponent = function (c) { + this._nextDict[c.name] = c; + this.next = _.values(this._nextDict); + }; + + })(SharedComponent.prototype); + + /** A component that suggests one of the give options, but accepts anything */ + function ListComponent(name, list, parent, multi_valued) { + SharedComponent.call(this, name, parent); + this.listGenerator = _.isArray(list) ? function () { + return list + } : list; + this.multi_valued = _.isUndefined(multi_valued) ? true : multi_valued; + } + + ListComponent.prototype = _.create(SharedComponent.prototype, { "constructor": ListComponent }); + exports.ListComponent = ListComponent; + + + (function (cls) { + cls.getTerms = function (context, editor) { + if (!this.multi_valued && context.otherTokenValues) { + // already have a value -> no suggestions + return [] + } + var already_set = context.otherTokenValues || []; + if (_.isString(already_set)) { + already_set = [already_set]; + } + var ret = _.difference(this.listGenerator(context, editor), already_set); + + if (this.getDefaultTermMeta()) { + var meta = this.getDefaultTermMeta(); + ret = _.map(ret, function (t) { + if (_.isString(t)) { + t = { "name": t}; + } + return _.defaults(t, { meta: meta }); + }); + } + + return ret; + }; + + cls.validateToken = function (token, context, editor) { + if (!this.multi_valued && token.length > 1) { + return false; + } + + // verify we have all tokens + var list = this.listGenerator(); + var not_found = _.any(token, function (p) { + return list.indexOf(p) == -1; + }); + + if (not_found) { + return false; + } + return true; + }; + + cls.getContextKey = function (context, editor) { + return this.name; + }; + + cls.getDefaultTermMeta = function (context, editor) { + return this.name; + }; + + cls.match = function (token, context, editor) { + if (!_.isArray(token)) { + token = [ token ] + } + if (!this.validateToken(token, context, editor)) { + return null + } + + var r = Object.getPrototypeOf(cls).match.call(this, token, context, editor); + r.context_values = r.context_values || {}; + r.context_values[this.getContextKey()] = token; + return r; + } + })(ListComponent.prototype); + + function SimpleParamComponent(name, parent) { + SharedComponent.call(this, name, parent); + } + + SimpleParamComponent.prototype = _.create(SharedComponent.prototype, { "constructor": SimpleParamComponent }); + exports.SimpleParamComponent = SimpleParamComponent; + + (function (cls) { + cls.match = function (token, context, editor) { + var r = Object.getPrototypeOf(cls).match.call(this, token, context, editor); + r.context_values = r.context_values || {}; + r.context_values[this.name] = token; + return r; + } + + })(SimpleParamComponent.prototype); + + function ConstantComponent(name, parent, options) { + SharedComponent.call(this, name, parent); + if (_.isString(options)) { + options = [options]; + } + this.options = options || [name]; + } + + ConstantComponent.prototype = _.create(SharedComponent.prototype, { "constructor": ConstantComponent }); + exports.ConstantComponent = ConstantComponent; + + (function (cls) { + cls.getTerms = function () { + return this.options; + }; + + cls.addOption = function (options) { + if (!_.isArray(options)) { + options = [options]; + } + + [].push.apply(this.options, options); + this.options = _.uniq(this.options); + }; + cls.match = function (token, context, editor) { + if (token !== this.name) { + return null; + } + + return Object.getPrototypeOf(cls).match.call(this, token, context, editor); + + } + })(ConstantComponent.prototype); + + function passThroughContext(context, extensionList) { function PTC() { diff --git a/sense/app/autocomplete/url_params.js b/sense/app/autocomplete/url_params.js new file mode 100644 index 000000000000..07cdab5787e0 --- /dev/null +++ b/sense/app/autocomplete/url_params.js @@ -0,0 +1,62 @@ +define([ + "_", "exports", "./engine" +], function (_, exports, engine) { + "use strict"; + + function ParamComponent(name, parent, description) { + engine.ConstantComponent.call(this, name, parent); + this.description = description; + } + + ParamComponent.prototype = _.create(engine.ConstantComponent.prototype, { "constructor": ParamComponent }); + exports.ParamComponent = ParamComponent; + + (function (cls) { + cls.getTerms = function () { + var t = { name: this.name }; + if (this.description === "__flag__") { + t.meta = "flag" + } + else { + t.meta = "param"; + t.insert_value = this.name + "="; + } + return [t]; + }; + + })(ParamComponent.prototype); + + function UrlParams(description, defaults) { + // This is not really a component, just a handy container to make iteration logic simpler + this.rootComponent = new engine.SharedComponent("ROOT"); + if (_.isUndefined(defaults)) { + defaults = { + "pretty": "__flag__", + "format": ["json", "yaml"] + }; + } + description = _.clone(description || {}); + _.defaults(description, defaults); + _.each(description, function (p_description, param) { + var values, component; + component = new ParamComponent(param, this.rootComponent, p_description); + if (_.isArray(p_description)) { + values = new engine.ListComponent(param, p_description, component); + } + else if (p_description === "__flag__") { + values = new engine.ListComponent(param, ["true", "false"], component); + } + }, this); + + } + + (function (cls) { + + cls.getTopLevelComponents = function () { + return this.rootComponent.next; + } + + })(UrlParams.prototype); + + exports.UrlParams = UrlParams; +}); \ No newline at end of file diff --git a/sense/app/autocomplete/url_pattern_matcher.js b/sense/app/autocomplete/url_pattern_matcher.js new file mode 100644 index 000000000000..2409f24c2ce3 --- /dev/null +++ b/sense/app/autocomplete/url_pattern_matcher.js @@ -0,0 +1,124 @@ +define([ + "_", "exports", "./engine" +], function (_, exports, engine) { + "use strict"; + + exports.URL_PATH_END_MARKER = "__url_path_end__"; + + + function AcceptEndpointComponent(endpoint, parent) { + engine.SharedComponent.call(this, endpoint.id, parent); + this.endpoint = endpoint + } + + AcceptEndpointComponent.prototype = _.create(engine.SharedComponent.prototype, { "constructor": AcceptEndpointComponent }); + + (function (cls) { + + cls.match = function (token, context, editor) { + if (token !== exports.URL_PATH_END_MARKER) { + return null; + } + if (this.endpoint.methods && -1 === _.indexOf(this.endpoint.methods, context.method)) { + return null; + } + var r = Object.getPrototypeOf(cls).match.call(this, token, context, editor); + r.context_values = r.context_values || {}; + r.context_values['endpoint'] = this.endpoint; + if (_.isNumber(this.endpoint.priority)) { + r.priority = this.endpoint.priority; + } + return r; + } + })(AcceptEndpointComponent.prototype); + + + /** + * @param globalSharedComponentFactories a dict of the following structure + * that will be used as a fall back for pattern parameters (i.e.: {indices}) + * { + * indices: function (part, parent, endpoint) { + * return new SharedComponent(part, parent) + * } + * } + * @constructor + */ + function UrlPatternMatcher(globalSharedComponentFactories) { + // This is not really a component, just a handy container to make iteration logic simpler + this.rootComponent = new engine.SharedComponent("ROOT"); + this.globalSharedComponentFactories = globalSharedComponentFactories || {}; + } + + (function (cls) { + cls.addEndpoint = function (pattern, endpoint) { + var c, + active_component = this.rootComponent, + endpointComponents = endpoint.url_components || {}; + var partList = pattern.split("/"); + _.each(partList, function (part, partIndex) { + if (part.search(/^{.+}$/) >= 0) { + part = part.substr(1, part.length - 2); + if (active_component.getComponent(part)) { + // we already have something for this, reuse + active_component = active_component.getComponent(part); + return; + } + // a new path, resolve. + + if ((c = endpointComponents[part])) { + // endpoint specific. Support list + if (_.isArray(c)) { + c = new engine.ListComponent(part, c, active_component); + } + else { + console.warn("incorrectly configured url component ", part, " in endpoint", endpoint); + c = new engine.SharedComponent(part); + } + } + else if ((c = this.globalSharedComponentFactories[part])) { + // c is a f + c = c(part, active_component, endpoint); + } + else { + // just accept whatever with not suggestions + c = new engine.SimpleParamComponent(part, active_component); + } + + active_component = c; + } + else { + // not pattern + var lookAhead = part, s; + + for (partIndex++; partIndex < partList.length; partIndex++) { + s = partList[partIndex]; + if (s.indexOf("{") >= 0) { + break; + } + lookAhead += "/" + s; + + } + + if (active_component.getComponent(part)) { + // we already have something for this, reuse + active_component = active_component.getComponent(part); + active_component.addOption(lookAhead); + } + else { + c = new engine.ConstantComponent(part, active_component, lookAhead); + active_component = c; + } + } + }, this); + // mark end of endpoint path + new AcceptEndpointComponent(endpoint, active_component); + }; + + cls.getTopLevelComponents = function () { + return this.rootComponent.next; + } + + })(UrlPatternMatcher.prototype); + + exports.UrlPatternMatcher = UrlPatternMatcher; +}); \ No newline at end of file diff --git a/sense/app/kb.js b/sense/app/kb.js index bc3cc5da5ca6..dd9b77fa90e2 100644 --- a/sense/app/kb.js +++ b/sense/app/kb.js @@ -4,10 +4,10 @@ define([ 'mappings', 'es', 'kb/api', - 'kb/url_pattern_matcher', + 'autocomplete/engine', 'require' ], - function (_, exports, mappings, es, api, url_pattern_matcher, require) { + function (_, exports, mappings, es, api, autocomplete_engine, require) { 'use strict'; var ACTIVE_API = new api.Api("empty"); @@ -17,11 +17,11 @@ define([ } function IndexUrlComponent(name, parent, multi_valued) { - url_pattern_matcher.ListComponent.call(this, name, mappings.getIndices, parent, multi_valued); + autocomplete_engine.ListComponent.call(this, name, mappings.getIndices, parent, multi_valued); } IndexUrlComponent.prototype = _.create( - url_pattern_matcher.ListComponent.prototype, + autocomplete_engine.ListComponent.prototype, { 'constructor': IndexUrlComponent }); (function (cls) { @@ -47,11 +47,11 @@ define([ } function TypeUrlComponent(name, parent, multi_valued) { - url_pattern_matcher.ListComponent.call(this, name, TypeGenerator, parent, multi_valued); + autocomplete_engine.ListComponent.call(this, name, TypeGenerator, parent, multi_valued); } TypeUrlComponent.prototype = _.create( - url_pattern_matcher.ListComponent.prototype, + autocomplete_engine.ListComponent.prototype, { 'constructor': TypeUrlComponent }); (function (cls) { @@ -74,11 +74,11 @@ define([ function IdUrlComponent(name, parent) { - url_pattern_matcher.SharedComponent.call(this, name, parent); + autocomplete_engine.SharedComponent.call(this, name, parent); } IdUrlComponent.prototype = _.create( - url_pattern_matcher.SharedComponent.prototype, + autocomplete_engine.SharedComponent.prototype, { 'constructor': IdUrlComponent }); (function (cls) { diff --git a/sense/app/kb/api.js b/sense/app/kb/api.js index 21970745ee32..9fe9adc9a8d2 100644 --- a/sense/app/kb/api.js +++ b/sense/app/kb/api.js @@ -1,5 +1,5 @@ -define([ '_', 'exports', './url_pattern_matcher'], - function (_, exports, url_pattern_matcher) { +define([ '_', 'exports', 'autocomplete/url_pattern_matcher', 'autocomplete/url_params'], + function (_, exports, url_pattern_matcher, url_params) { 'use strict'; /** @@ -38,6 +38,8 @@ define([ '_', 'exports', './url_pattern_matcher'], this.urlPatternMatcher.addEndpoint(p, copiedDescription); }, this); + copiedDescription.paramsAutocomplete = new url_params.UrlParams(copiedDescription.url_params); + this.endpoints[endpoint] = copiedDescription; }; diff --git a/sense/app/kb/api_0_90/document.js b/sense/app/kb/api_0_90/document.js index cc60f148091e..0eac42d504ef 100644 --- a/sense/app/kb/api_0_90/document.js +++ b/sense/app/kb/api_0_90/document.js @@ -6,7 +6,13 @@ define(function () { methods: ['GET'], patterns: [ "{index}/{type}/{id}" - ] + ], + url_params: { + "version": 1, + "routing": "", + "parent": "" + } + }); api.addEndpointDescription('_get_doc_source', { methods: ['GET'], @@ -18,19 +24,68 @@ define(function () { methods: ['DELETE'], patterns: [ "{index}/{type}/{id}/" - ] + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "routing": "", + "parent": "" + } }); api.addEndpointDescription('index_doc', { methods: ['PUT', 'POST'], patterns: [ "{index}/{type}/{id}" - ] + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "op_type": ["create"], + "routing": "", + "parent": "", + "timestamp": "", + "ttl": "5m", + "consistency": ["qurom", "one", "all"], + "replication": ["sync", "async"], + "refresh": "__flag__", + "timeout": "1m" + } + }); + api.addEndpointDescription('create_doc', { + methods: ['PUT', 'POST'], + patterns: [ + "{index}/{type}/{id}/_create" + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "routing": "", + "parent": "", + "timestamp": "", + "ttl": "5m", + "consistency": ["qurom", "one", "all"], + "replication": ["sync", "async"], + "refresh": "__flag__", + "timeout": "1m" + } }); api.addEndpointDescription('index_doc_no_id', { methods: ['POST'], patterns: [ "{index}/{type}" - ] + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "routing": "", + "parent": "", + "timestamp": "", + "ttl": "5m", + "consistency": ["qurom", "one", "all"], + "replication": ["sync", "async"], + "refresh": "__flag__", + "timeout": "1m" + } }); } diff --git a/sense/app/kb/api_0_90/search.js b/sense/app/kb/api_0_90/search.js index e5b8eca29593..a44a57621976 100644 --- a/sense/app/kb/api_0_90/search.js +++ b/sense/app/kb/api_0_90/search.js @@ -10,6 +10,26 @@ define(function () { "{indices}/_search", "_search" ], + url_params: { + q: "", + df: "", + analyzer: "", + default_operator: ["AND", "OR"], + explain: "__flag__", + fields: [], + sort: "", + track_scores: "__flag__", + timeout: 1, + from: 0, + size: 10, + search_type: ["dfs_query_then_fetch", "dfs_query_and_fetch", "query_then_fetch", "query_and_fetch", "count", "scan"], + lowercase_expanded_terms: ["true", "false"], + analyze_wildcard: "__flag__", + preference: ["_primary", "_primary_first", "_local", "_only_node:xyz", "_prefer_node:xyz", "_shards:2,3"], + scroll: "5m", + scroll_id: "", + routing: "" + }, data_autocomplete_rules: { query: { // populated by a global rule diff --git a/sense/app/kb/api_1_0/cat.js b/sense/app/kb/api_1_0/cat.js index 3a61b8287aed..c02bafa9dcb2 100644 --- a/sense/app/kb/api_1_0/cat.js +++ b/sense/app/kb/api_1_0/cat.js @@ -1,28 +1,37 @@ -define(function () { +define(["_"], function (_) { 'use strict'; - function addSimpleCat(endpoint, api) { + function addSimpleCat(endpoint, api, params, patterns) { + var url_params = { "help": "__flag__", "v": "__flag__", "bytes": ["b"]}; + _.each(params || [], function (p) { + if (_.isString(p)) { + url_params[p] = "__flag__"; + } + else { + var k = Object.keys(p)[0]; + url_params[k] = p[k]; + } + }); api.addEndpointDescription(endpoint, { match: endpoint, - def_method: 'GET', - methods: ['GET' ], - endpoint_autocomplete: [ - endpoint - ], - indices_mode: 'none', - types_mode: 'none', - doc_id_mode: 'none', - data_autocomplete_rules: {} + url_params: url_params, + patterns: patterns || [endpoint] }); } return function init(api) { addSimpleCat('_cat/aliases', api); - addSimpleCat('_cat/allocation', api); + addSimpleCat('_cat/allocation', api, null, ['_cat/allocation', '_cat/allocation/{nodes}']); addSimpleCat('_cat/count', api); - addSimpleCat('_cat/health', api); - addSimpleCat('_cat/indices', api); + addSimpleCat('_cat/health', api, [ + {"ts": ["false", "true"]} + ]); + addSimpleCat('_cat/indices', api, [ + {h: []}, + "pri", + ], + ['_cat/indices', '_cat/indices/{indices}']); addSimpleCat('_cat/master', api); addSimpleCat('_cat/nodes', api); addSimpleCat('_cat/pending_tasks', api); diff --git a/sense/app/kb/api_1_0/document.js b/sense/app/kb/api_1_0/document.js index cc60f148091e..0eac42d504ef 100644 --- a/sense/app/kb/api_1_0/document.js +++ b/sense/app/kb/api_1_0/document.js @@ -6,7 +6,13 @@ define(function () { methods: ['GET'], patterns: [ "{index}/{type}/{id}" - ] + ], + url_params: { + "version": 1, + "routing": "", + "parent": "" + } + }); api.addEndpointDescription('_get_doc_source', { methods: ['GET'], @@ -18,19 +24,68 @@ define(function () { methods: ['DELETE'], patterns: [ "{index}/{type}/{id}/" - ] + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "routing": "", + "parent": "" + } }); api.addEndpointDescription('index_doc', { methods: ['PUT', 'POST'], patterns: [ "{index}/{type}/{id}" - ] + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "op_type": ["create"], + "routing": "", + "parent": "", + "timestamp": "", + "ttl": "5m", + "consistency": ["qurom", "one", "all"], + "replication": ["sync", "async"], + "refresh": "__flag__", + "timeout": "1m" + } + }); + api.addEndpointDescription('create_doc', { + methods: ['PUT', 'POST'], + patterns: [ + "{index}/{type}/{id}/_create" + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "routing": "", + "parent": "", + "timestamp": "", + "ttl": "5m", + "consistency": ["qurom", "one", "all"], + "replication": ["sync", "async"], + "refresh": "__flag__", + "timeout": "1m" + } }); api.addEndpointDescription('index_doc_no_id', { methods: ['POST'], patterns: [ "{index}/{type}" - ] + ], + url_params: { + "version": 1, + "version_type": ["external", "internal"], + "routing": "", + "parent": "", + "timestamp": "", + "ttl": "5m", + "consistency": ["qurom", "one", "all"], + "replication": ["sync", "async"], + "refresh": "__flag__", + "timeout": "1m" + } }); } diff --git a/sense/app/kb/api_1_0/search.js b/sense/app/kb/api_1_0/search.js index b087f42641a5..65560f86ed27 100644 --- a/sense/app/kb/api_1_0/search.js +++ b/sense/app/kb/api_1_0/search.js @@ -10,6 +10,29 @@ define(function () { "{indices}/_search", "_search" ], + url_params: { + q: "", + df: "", + analyzer: "", + default_operator: ["AND", "OR"], + explain: "__flag__", + _source: "", + _source_include: "", + _source_exclude: "", + fields: [], + sort: "", + track_scores: "__flag__", + timeout: 1, + from: 0, + size: 10, + search_type: ["dfs_query_then_fetch", "dfs_query_and_fetch", "query_then_fetch", "query_and_fetch", "count", "scan"], + lowercase_expanded_terms: ["true", "false"], + analyze_wildcard: "__flag__", + preference: ["_primary", "_primary_first", "_local", "_only_node:xyz", "_prefer_node:xyz", "_shards:2,3"], + scroll: "5m", + scroll_id: "", + routing: "" + }, data_autocomplete_rules: { query: { // populated by a global rule diff --git a/sense/app/kb/url_pattern_matcher.js b/sense/app/kb/url_pattern_matcher.js deleted file mode 100644 index 453a8edd8167..000000000000 --- a/sense/app/kb/url_pattern_matcher.js +++ /dev/null @@ -1,276 +0,0 @@ -define([ - "_", "exports", "autocomplete/url_path_autocomplete" -], function (_, exports, url_path_autocomplete) { - "use strict"; - - exports.URL_PATH_END_MARKER = "__url_path_end__"; - - function SharedComponent(name, parent) { - url_path_autocomplete.AutocompleteComponent.call(this, name); - this._nextDict = {}; - if (parent) { - parent.addComponent(this); - } - // for debugging purposes - this._parent = parent; - } - - SharedComponent.prototype = _.create( - url_path_autocomplete.AutocompleteComponent.prototype, - { 'constructor': SharedComponent }); - - (function (cls) { - cls.getComponent = function (name) { - return this._nextDict[name]; - }; - - cls.addComponent = function (c) { - this._nextDict[c.name] = c; - this.next = _.values(this._nextDict); - }; - - })(SharedComponent.prototype); - - /** A component that suggests one of the give options, but accepts anything */ - function ListComponent(name, list, parent, multi_valued) { - SharedComponent.call(this, name, parent); - this.listGenerator = _.isArray(list) ? function () { - return list - } : list; - this.multi_valued = _.isUndefined(multi_valued) ? true : multi_valued; - } - - ListComponent.prototype = _.create(SharedComponent.prototype, { "constructor": ListComponent }); - - (function (cls) { - cls.getTerms = function (context, editor) { - if (!this.multi_valued && context.otherTokenValues) { - // already have a value -> no suggestions - return [] - } - var already_set = context.otherTokenValues || []; - if (_.isString(already_set)) { - already_set = [already_set]; - } - var ret = _.difference(this.listGenerator(context, editor), already_set); - - if (this.getDefaultTermMeta()) { - var meta = this.getDefaultTermMeta(); - ret = _.map(ret, function (t) { - if (_.isString(t)) { - t = { "name": t}; - } - return _.defaults(t, { meta: meta }); - }); - } - - return ret; - }; - - cls.validateToken = function (token, context, editor) { - if (!this.multi_valued && token.length > 1) { - return false; - } - - // verify we have all tokens - var list = this.listGenerator(); - var not_found = _.any(token, function (p) { - return list.indexOf(p) == -1; - }); - - if (not_found) { - return false; - } - return true; - }; - - cls.getContextKey = function (context, editor) { - return this.name; - }; - - cls.getDefaultTermMeta = function (context, editor) { - return null; - }; - - cls.match = function (token, context, editor) { - if (!_.isArray(token)) { - token = [ token ] - } - if (!this.validateToken(token, context, editor)) { - return null - } - - var r = Object.getPrototypeOf(cls).match.call(this, token, context, editor); - r.context_values = r.context_values || {}; - r.context_values[this.getContextKey()] = token; - return r; - } - })(ListComponent.prototype); - - function SimpleParamComponent(name, parent) { - SharedComponent.call(this, name, parent); - } - - SimpleParamComponent.prototype = _.create(SharedComponent.prototype, { "constructor": SimpleParamComponent }); - - (function (cls) { - cls.match = function (token, context, editor) { - var r = Object.getPrototypeOf(cls).match.call(this, token, context, editor); - r.context_values = r.context_values || {}; - r.context_values[this.name] = token; - return r; - } - - })(SimpleParamComponent.prototype); - - function SimplePartComponent(name, parent, options) { - SharedComponent.call(this, name, parent); - if (_.isString(options)) { - options = [options]; - } - this.options = options || [name]; - } - - SimplePartComponent.prototype = _.create(SharedComponent.prototype, { "constructor": SimplePartComponent }); - - (function (cls) { - cls.getTerms = function () { - return this.options; - }; - - cls.addOption = function (options) { - if (!_.isArray(options)) { - options = [options]; - } - - [].push.apply(this.options, options); - this.options = _.uniq(this.options); - }; - cls.match = function (token, context, editor) { - if (token !== this.name) { - return null; - } - - return Object.getPrototypeOf(cls).match.call(this, token, context, editor); - - } - })(SimplePartComponent.prototype); - - function AcceptEndpointComponent(endpoint, parent) { - SharedComponent.call(this, endpoint.id, parent); - this.endpoint = endpoint - } - - AcceptEndpointComponent.prototype = _.create(SharedComponent.prototype, { "constructor": AcceptEndpointComponent }); - - (function (cls) { - - cls.match = function (token, context, editor) { - if (token !== exports.URL_PATH_END_MARKER) { - return null; - } - if (this.endpoint.methods && -1 === _.indexOf(this.endpoint.methods, context.method)) { - return null; - } - var r = Object.getPrototypeOf(cls).match.call(this, token, context, editor); - r.context_values = r.context_values || {}; - r.context_values['endpoint'] = this.endpoint; - if (_.isNumber(this.endpoint.priority)) { - r.priority = this.endpoint.priority; - } - return r; - } - })(AcceptEndpointComponent.prototype); - - - /** - * @param globalSharedComponentFactories a dict of the following structure - * that will be used as a fall back for pattern parameters (i.e.: {indices}) - * { - * indices: function (part, parent, endpoint) { - * return new SharedComponent(part, parent) - * } - * } - * @constructor - */ - function UrlPatternMatcher(globalSharedComponentFactories) { - // This is not really a component, just a handy container to make iteration logic simpler - this.rootComponent = new SharedComponent("ROOT"); - this.globalSharedComponentFactories = globalSharedComponentFactories || {}; - } - - (function (cls) { - cls.addEndpoint = function (pattern, endpoint) { - var c, - active_component = this.rootComponent, - endpointComponents = endpoint.url_components || {}; - var partList = pattern.split("/"); - _.each(partList, function (part, partIndex) { - if (part.search(/^{.+}$/) >= 0) { - part = part.substr(1, part.length - 2); - if (active_component.getComponent(part)) { - // we already have something for this, reuse - active_component = active_component.getComponent(part); - return; - } - // a new path, resolve. - - if ((c = endpointComponents[part])) { - // endpoint specific. Support list - if (_.isArray(c)) { - c = new ListComponent(part, c, active_component); - } - else { - console.warn("incorrectly configured url component ", part, " in endpoint", endpoint); - c = new SharedComponent(part); - } - } - else if ((c = this.globalSharedComponentFactories[part])) { - // c is a f - c = c(part, active_component, endpoint); - } - else { - // just accept whatever with not suggestions - c = new SimpleParamComponent(part, active_component); - } - - active_component = c; - } - else { - // not pattern - var lookAhead = part, s; - - for (partIndex++; partIndex < partList.length; partIndex++) { - s = partList[partIndex]; - if (s.indexOf("{") >= 0) { - break; - } - lookAhead += "/" + s; - - } - - if (active_component.getComponent(part)) { - // we already have something for this, reuse - active_component = active_component.getComponent(part); - active_component.addOption(lookAhead); - } - else { - c = new SimplePartComponent(part, active_component, lookAhead); - active_component = c; - } - } - }, this); - // mark end of endpoint path - new AcceptEndpointComponent(endpoint, active_component); - }; - - cls.getTopLevelComponents = function () { - return this.rootComponent.next; - } - - })(UrlPatternMatcher.prototype); - - exports.UrlPatternMatcher = UrlPatternMatcher; - exports.SharedComponent = SharedComponent; - exports.ListComponent = ListComponent; -}); \ No newline at end of file diff --git a/sense/tests/index.html b/sense/tests/index.html index 1d9d2786ef2f..4ef58f6d611a 100644 --- a/sense/tests/index.html +++ b/sense/tests/index.html @@ -59,6 +59,7 @@ var tests = [ '../tests/src/url_autocomplete_tests.js', + '../tests/src/url_params_tests.js', '../tests/src/curl_tests.js', '../tests/src/kb_tests.js', '../tests/src/mapping_tests.js', diff --git a/sense/tests/src/integration_tests.js b/sense/tests/src/integration_tests.js index 330bdb251e76..8fe7828ae90a 100644 --- a/sense/tests/src/integration_tests.js +++ b/sense/tests/src/integration_tests.js @@ -814,6 +814,10 @@ define([ endpoints: { "_search": { patterns: ["_search", "{indices}/{types}/_search", "{indices}/_search"], + url_params: { + "search_type": ["count", "query_then_fetch" ], + "scroll": "10m" + }, data_autocomplete_rules: { } }, @@ -959,4 +963,137 @@ define([ } ] ); + + + context_tests( + null, + MAPPING, + CLUSTER_KB, + "GET _search?", + [ + { + name: "Params just after ?", + cursor: { row: 0, column: 12}, + autoCompleteSet: [ + { name: "format", meta: "param", "insert_value": "format=" }, + { name: "pretty", meta: "flag" }, + { name: "scroll", meta: "param", "insert_value": "scroll=" }, + { name: "search_type", meta: "param", "insert_value": "search_type=" }, + ], + prefixToAdd: "", + suffixToAdd: "" + } + ] + ); + + context_tests( + null, + MAPPING, + CLUSTER_KB, + "GET _search?format=", + [ + { + name: "Params values", + cursor: { row: 0, column: 19}, + autoCompleteSet: [ + { name: "json", meta: "format" }, + { name: "yaml", meta: "format" } + ], + prefixToAdd: "", + suffixToAdd: "" + } + ] + ); + + context_tests( + null, + MAPPING, + CLUSTER_KB, + "GET _search?format=yaml&", + [ + { + name: "Params after amp", + cursor: { row: 0, column: 24}, + autoCompleteSet: [ + { name: "format", meta: "param", "insert_value": "format=" }, + { name: "pretty", meta: "flag" }, + { name: "scroll", meta: "param", "insert_value": "scroll=" }, + { name: "search_type", meta: "param", "insert_value": "search_type=" }, + ], + prefixToAdd: "", + suffixToAdd: "" + } + ] + ); + + context_tests( + null, + MAPPING, + CLUSTER_KB, + "GET _search?format=yaml&search", + [ + { + name: "Params on existing param", + cursor: { row: 0, column: 26}, + rangeToReplace: { + start: { row: 0, column: 24}, + end: { row: 0, column: 30} + }, + autoCompleteSet: [ + { name: "format", meta: "param", "insert_value": "format=" }, + { name: "pretty", meta: "flag" }, + { name: "scroll", meta: "param", "insert_value": "scroll=" }, + { name: "search_type", meta: "param", "insert_value": "search_type=" }, + ], + prefixToAdd: "", + suffixToAdd: "" + } + ] + ); + + context_tests( + null, + MAPPING, + CLUSTER_KB, + "GET _search?format=yaml&search_type=cou", + [ + { + name: "Params on existing value", + cursor: { row: 0, column: 37}, + rangeToReplace: { + start: { row: 0, column: 36}, + end: { row: 0, column: 39} + }, + autoCompleteSet: [ + { name: "count", meta: "search_type" }, + { name: "query_then_fetch", meta: "search_type" }, + ], + prefixToAdd: "", + suffixToAdd: "" + } + ] + ); + context_tests( + null, + MAPPING, + CLUSTER_KB, + "GET _search?format=yaml&search_type=cou", + [ + { + name: "Params on just after = with existing value", + cursor: { row: 0, column: 36}, + rangeToReplace: { + start: { row: 0, column: 36}, + end: { row: 0, column: 36} + }, + autoCompleteSet: [ + { name: "count", meta: "search_type" }, + { name: "query_then_fetch", meta: "search_type" }, + ], + prefixToAdd: "", + suffixToAdd: "" + } + ] + ); + }); \ No newline at end of file diff --git a/sense/tests/src/kb_tests.js b/sense/tests/src/kb_tests.js index dcd71d9d4bf4..a92ab77c4a0d 100644 --- a/sense/tests/src/kb_tests.js +++ b/sense/tests/src/kb_tests.js @@ -2,8 +2,8 @@ define([ 'kb', 'mappings', 'kb/api', - 'autocomplete/url_path_autocomplete' -], function (kb, mappings, api, url_path_autocomplete) { + 'autocomplete/engine' +], function (kb, mappings, api, autocomplete_engine) { 'use strict'; module("Knowledge base", { @@ -52,7 +52,7 @@ define([ } var context = { otherTokenValues: otherTokenValues}; - url_path_autocomplete.populateContext(tokenPath, context, null, + autocomplete_engine.populateContext(tokenPath, context, null, expectedContext.autoCompleteSet, kb.getTopLevelUrlCompleteComponents() ); diff --git a/sense/tests/src/url_autocomplete_tests.js b/sense/tests/src/url_autocomplete_tests.js index 13e042defa37..8e4419d30f12 100644 --- a/sense/tests/src/url_autocomplete_tests.js +++ b/sense/tests/src/url_autocomplete_tests.js @@ -1,8 +1,8 @@ define([ '_', - 'kb/url_pattern_matcher', - 'autocomplete/url_path_autocomplete' -], function (_, url_pattern_matcher, url_path_autocomplete) { + 'autocomplete/url_pattern_matcher', + 'autocomplete/engine' +], function (_, url_pattern_matcher, autocomplete_engine) { 'use strict'; module("Url autocomplete"); @@ -44,7 +44,7 @@ define([ if (expectedContext.method) { context.method = expectedContext.method; } - url_path_autocomplete.populateContext(tokenPath, context, null, + autocomplete_engine.populateContext(tokenPath, context, null, expectedContext.autoCompleteSet, patternMatcher.getTopLevelComponents() ); @@ -62,6 +62,13 @@ define([ } + function t(name, meta) { + if (meta) { + return {name: name, meta: meta}; + } + return name; + } + (function () { var endpoints = { "1": { @@ -222,7 +229,7 @@ define([ patterns_test("option testing - partial, with auto complete", endpoints, "a", - { autoCompleteSet: ["a", "b", "c"] } + { autoCompleteSet: [t("a", "p"), t("b", "p"), "c"] } ); patterns_test("option testing - partial, without auto complete", @@ -259,7 +266,7 @@ define([ }; var globalFactories = { "p": function (name, parent) { - return new url_pattern_matcher.ListComponent(name, ["g1", "g2"], parent); + return new autocomplete_engine.ListComponent(name, ["g1", "g2"], parent); } }; @@ -281,14 +288,14 @@ define([ patterns_test("global parameters testing - partial, with auto complete", endpoints, "a", - { autoCompleteSet: ["a", "b"] }, + { autoCompleteSet: [t("a", "p"), t("b", "p")] }, globalFactories ); patterns_test("global parameters testing - partial, with auto complete 2", endpoints, "b", - { autoCompleteSet: ["g1", "g2"] }, + { autoCompleteSet: [t("g1", "p"), t("g2", "p")] }, globalFactories ); diff --git a/sense/tests/src/url_params_tests.js b/sense/tests/src/url_params_tests.js new file mode 100644 index 000000000000..8a69ec77e7a7 --- /dev/null +++ b/sense/tests/src/url_params_tests.js @@ -0,0 +1,107 @@ +define([ + '_', + 'autocomplete/url_params', + 'autocomplete/engine' +], function (_, url_params, autocomplete_engine) { + 'use strict'; + + module("Url params"); + + function param_test(name, description, tokenPath, expectedContext, globalParams) { + + test(name, function () { + var urlParams = new url_params.UrlParams(description, globalParams || {}); + if (typeof tokenPath === "string") { + tokenPath = _.map(tokenPath.split("/"), function (p) { + p = p.split(","); + if (p.length === 1) { + return p[0]; + } + return p; + }); + } + + if (expectedContext.autoCompleteSet) { + expectedContext.autoCompleteSet = _.map(expectedContext.autoCompleteSet, function (t) { + if (_.isString(t)) { + t = { name: t} + } + return t; + }); + expectedContext.autoCompleteSet = _.sortBy(expectedContext.autoCompleteSet, 'name'); + } + + var context = {}; + + autocomplete_engine.populateContext(tokenPath, context, null, + expectedContext.autoCompleteSet, urlParams.getTopLevelComponents() + ); + + + if (context.autoCompleteSet) { + context.autoCompleteSet = _.sortBy(context.autoCompleteSet, 'name'); + } + + deepEqual(context, expectedContext); + }); + + } + + function t(name, meta, insert_value) { + var r = name; + if (meta) { + r = {name: name, meta: meta}; + if (meta === "param" && !insert_value) { + insert_value = name + "="; + } + } + if (insert_value) { + if (_.isString(r)) { + r = {name: name} + } + r.insert_value = insert_value; + } + return r; + } + + (function () { + var params = { + "a": ["1", "2"], + "b": "__flag__" + }; + param_test("settings params", + params, + "a/1", + { "a": ["1"]} + ); + + param_test("autocomplete top level", + params, + [], + { autoCompleteSet: [ t("a", "param"), t("b", "flag")]} + ); + + param_test("autocomplete top level, with defaults", + params, + [], + { autoCompleteSet: [ t("a", "param"), t("b", "flag"), t("c", "param")]}, + { + "c": [2] + } + ); + + param_test("autocomplete values", + params, + "a", + { autoCompleteSet: [ t("1", "a"), t("2", "a")]} + ); + + param_test("autocomplete values flag", + params, + "b", + { autoCompleteSet: [ t("true", "b"), t("false", "b")]} + ); + + + })(); +}); \ No newline at end of file