// *****************************************************************************
// Copyright 2013-2023 Aerospike, Inc.
//
// Licensed under the Apache License, Version 2.0 (the 'License')
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// *****************************************************************************
'use strict'
const minimatch = require('minimatch')
/**
* @module aerospike/info
*
* @description The info protocol provides access to configuration and
* statistics for the Aerospike server. This module provides the {@link
* module:aerospike/info.parse|parse} utility function for parsing the info
* data returned by the Aerospike server.
*
* @see {@link Client#info}
* @see <a href="http://www.aerospike.com/docs/reference/info" title="Info Command Reference">⇑Info Command Reference</a>
*
* @example
*
* const Aerospike = require('aerospike')
*
* // INSERT HOSTNAME AND PORT NUMBER OF AEROSPIKE SERVER NODE HERE!
* var config = {
* hosts: '192.168.33.10:3000',
* }
* Aerospike.connect(config, (error, client) => {
* if (error) throw error
*
* var cmd = 'build\nfeatures'
* client.infoAny(cmd, (err, infoStr) => {
* if (err) {
* console.error('error retrieving info for cmd "%s": %s [%d]',
* cmd, err.message, err.code)
* } else {
* var info = Aerospike.info.parse(infoStr)
* console.log(info) // => { build: '3.12.0',
* // features: [
* // 'cdt-list',
* // 'pipelining',
* // 'geo',
* // ...,
* // 'udf' ] }
* }
* client.close()
* })
* })
*/
/**
* @function module:aerospike/info.parse
*
* @summary Parses the info string returned from a cluster node into key-value pairs.
*
* @param {string} info - The info string returned by the cluster node.
* @returns {Object} key-value pairs
*
* @since v2.6
*/
function parse (info) {
if (!info) return {}
const infoHash = parseKeyValue(info, '\n', '\t')
Object.keys(infoHash).forEach(key => {
const separators = getSeparators(key)
const value = infoHash[key]
infoHash[key] = deepSplitString(value, separators)
})
return infoHash
}
/**
* Parses a string value into a primitive type (string or number).
*
* Ex.:
* - parseValue('foo') => 'foo'
* - parseValue('42') => 42
*
* @private
*/
function parseValue (value) {
if (Number(value).toString() === value) {
return Number(value)
}
return value
}
/**
* Parses a string value representing a key-value-map separated by sep1 and
* sep2 into an Object.
*
* Ex.:
* - parseKeyValue('a=1;b=2', ';', '=') => { a: 1, b: 2 }
*
* @private
*/
function parseKeyValue (str, sep1, sep2) {
const result = {}
str.split(sep1).forEach(function (kv) {
if (kv.length > 0) {
kv = kv.split(sep2, 2)
result[kv[0]] = parseValue(kv[1])
}
})
return result
}
/**
* Split string into list or key-value-pairs depending on whether
* the given separator chars appear in the string. This is the logic used by
* the old parseInfo function and the default logic used by the new parse
* function unless a specific format is defined for an info key.
*
* Ex.:
* - smartParse('foo') => 'foo'
* - smartParse('foo;bar') => ['foo', 'bar']
* - smartParse('foo=1;bar=2') => {foo: 1, bar: 2}
*
* @private
*/
function smartParse (str, sep1, sep2) {
sep1 = sep1 || ';'
sep2 = sep2 || '='
if ((typeof str === 'string') && str.indexOf(sep1) >= 0) {
if (str.indexOf(sep2) >= 0) {
return parseKeyValue(str, sep1, sep2)
} else {
return str.split(sep1)
}
}
return str
}
/**
* Returns separators to use for the given info key.
*
* @private
*/
function getSeparators (key) {
const pattern = Object.keys(separators).find(p => minimatch(key, p))
const seps = separators[pattern] || defaultSeparators
return seps.slice() // return a copy of the array
}
/**
* Splits a string into a, possibly nested, array or object using the given
* separators.
*
* @private
*/
function deepSplitString (input, separators) {
if (input === null || typeof input === 'undefined') {
return input
}
if (separators.length === 0) {
return input
}
const sep = separators.shift()
let output = input
if (typeof input === 'string') {
output = splitString(input, sep)
} else if (Array.isArray(input)) {
output = input.map(i => splitString(i, sep))
} else if (typeof input === 'object') {
output = {}
Object.keys(input).forEach(key => {
output[key] = splitString(input[key], sep)
})
}
if (separators.length > 0) {
return deepSplitString(output, separators)
} else {
return output
}
}
function splitString (input, sep) {
switch (typeof sep) {
case 'function':
return sep(input)
case 'string':
if (sep.length === 2) {
return parseKeyValue(input, sep[0], sep[1])
} else {
const list = input.split(sep)
if (list[list.length - 1].length === 0) {
list.pop()
}
return list
}
}
}
/**
* Separators to use for specific info keys. A single-char separator splits the
* info value into a list of strings; a separator consisting of two characters
* splits the info value into a list of key-value-pairs. For keys containing
* nested key-value-lists, multiple separators can be specified. For info keys
* that require more complex parsing logic, a split function can
* be specified instead of a character separator.
*
* @private
*/
const separators = {
bins: [';:', splitBins],
'bins/*': [splitBins],
'namespace/*': [';='],
service: [';'],
sindex: [';', ':='],
'sindex/*': [';', ':='],
'sindex/*/**': [';='],
'udf-list': [';', ',='],
'get-dc-config': [';', ':='],
sets: [';', ':='],
'sets/*': [';', ':='],
'sets/*/**': [chop, ':='] // remove trailing ';' to return single object rather than list of objects
}
const defaultSeparators = [smartParse]
/**********************************************************
* Functions for dealing with specific info command results
**********************************************************/
/** Returns a new string with the last character removed
* @private
* */
function chop (str) {
return str.substring(0, str.length - 1)
}
function splitBins (str) {
const stats = {}
const names = []
str.split(',').forEach(function (elem) {
const parts = elem.split('=', 2)
if (parts.length === 2) {
stats[parts[0]] = parseValue(parts[1])
} else {
names.push(parts[0])
}
})
return {
stats,
names
}
}
module.exports = {
parse,
separators
}