Skip to content

Commit

Permalink
Merge pull request #1 from jmespath-community/jep/string-functions
Browse files Browse the repository at this point in the history
JEP-14 String Functions
  • Loading branch information
springcomp authored Dec 8, 2022
2 parents eca3c16 + f5888c6 commit fcc3513
Show file tree
Hide file tree
Showing 5 changed files with 408 additions and 5 deletions.
3 changes: 3 additions & 0 deletions bin/jp.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def main():
except exceptions.JMESPathTypeError as e:
sys.stderr.write("invalid-type: %s\n" % e)
return 1
except exceptions.JMESPathValueError as e:
sys.stderr.write("invalid-value: %s\n" % e)
return 1
except exceptions.UnknownFunctionError as e:
sys.stderr.write("unknown-function: %s\n" % e)
return 1
Expand Down
13 changes: 13 additions & 0 deletions jmespath/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ def __str__(self):
self.expected_types, self.actual_type))


@with_str_method
class JMESPathValueError(JMESPathError):
def __init__(self, function_name, current_value, expected_types):
self.function_name = function_name
self.current_value = current_value
self.expected_types = expected_types

def __str__(self):
return ('In function %s(), invalid value: "%s", '
'expected: %s"%s"' % (
self.function_name, self.current_value,
self.expected_types))

class EmptyExpressionError(JMESPathError):
def __init__(self):
super(EmptyExpressionError, self).__init__(
Expand Down
183 changes: 178 additions & 5 deletions jmespath/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,28 @@ def call_function(self, function_name, resolved_args):
return function(self, *resolved_args)

def _validate_arguments(self, args, signature, function_name):
if signature and signature[-1].get('variadic'):
required_arguments_count = len([param for param in signature if not param.get('optional') or not param['optional']])
optional_arguments_count = len([param for param in signature if param.get('optional') and param['optional']])
has_variadic = signature[-1].get('variadic') if signature != None else False
if has_variadic:
if len(args) < len(signature):
raise exceptions.VariadictArityError(
len(signature), len(args), function_name)
elif len(args) != len(signature):
elif optional_arguments_count > 0:
if len(args) < required_arguments_count or len(args) > (required_arguments_count + optional_arguments_count):
raise exceptions.ArityError(
len(signature), len(args), function_name)
elif len(args) != required_arguments_count:
raise exceptions.ArityError(
len(signature), len(args), function_name)
return self._type_check(args, signature, function_name)

def _type_check(self, actual, signature, function_name):
for i in range(len(signature)):
allowed_types = signature[i]['types']
for i in range(min(len(signature), len(actual))):
allowed_types = self._get_allowed_types_from_signature(signature[i])
if allowed_types:
self._type_check_single(actual[i], allowed_types,
function_name)

def _type_check_single(self, current, types, function_name):
# Type checking involves checking the top level type,
# and in the case of arrays, potentially checking the types
Expand All @@ -120,6 +126,13 @@ def _type_check_single(self, current, types, function_name):
self._subtype_check(current, allowed_subtypes,
types, function_name)

## signature supports monotype {'type': 'type-name'}
## or multiple types {'types': ['type1-name', 'type2-name']}
def _get_allowed_types_from_signature(self, spec):
if spec.get('type'):
spec.update({'types': [spec.get('type')]})
return spec.get('types')

def _get_allowed_pytypes(self, types):
allowed_types = []
allowed_subtypes = []
Expand Down Expand Up @@ -164,6 +177,14 @@ def _subtype_check(self, current, allowed_subtypes, types, function_name):
@signature({'types': ['number']})
def _func_abs(self, arg):
return abs(arg)

@signature({'types': ['string']})
def _func_lower(self, arg):
return arg.lower()

@signature({'types': ['string']})
def _func_upper(self, arg):
return arg.upper()

@signature({'types': ['array-number']})
def _func_avg(self, arg):
Expand Down Expand Up @@ -287,6 +308,158 @@ def _func_keys(self, arg):
# should we also return the indices of a list?
return list(arg.keys())

@signature(
{'type': 'string'},
{'type': 'string'},
{'type': 'number', 'optional': True},
{'type': 'number', 'optional': True})
def _func_find_first(self, text, search, start = 0, end = None):
self._ensure_integer('find_first', 'start', start)
self._ensure_integer('find_first', 'end', end)
return self._find_impl(
text,
search,
lambda t, s: t.find(s),
start,
end
)

@signature(
{'type': 'string'},
{'type': 'string'},
{'type': 'number', 'optional': True},
{'type': 'number', 'optional': True})
def _func_find_last(self, text, search, start = 0, end = None):
self._ensure_integer('find_last', 'start', start)
self._ensure_integer('find_last', 'end', end)
return self._find_impl(
text,
search,
lambda t, s: t.rfind(s),
start,
end
)

def _find_impl(self, text, search, func, start, end):
if len(search) == 0:
return None
if end == None:
end = len(text)

pos = func(text[start:end], search)
if start < 0:
start = start + len(text)

# restrict resulting range to valid indices
start = min(max(start, 0), len(text))
return start + pos if pos != -1 else None

@signature(
{'type': 'string'},
{'type': 'number'},
{'type': 'string', 'optional': True})
def _func_pad_left(self, text, width, padding = ' '):
self._ensure_non_negative_integer('pad_left', 'width', width)
return self._pad_impl(lambda : text.rjust(width, padding), padding)

@signature(
{'type': 'string'},
{'type': 'number'},
{'type': 'string', 'optional': True})
def _func_pad_right(self, text, width, padding = ' '):
self._ensure_non_negative_integer('pad_right', 'width', width)
return self._pad_impl(lambda : text.ljust(width, padding), padding)

def _pad_impl(self, func, padding):
if len(padding) != 1:
raise exceptions.JMESPathError(
'syntax-error: pad_right() expects $padding to have a '
'single character, but received `{}` instead.'
.format(padding))
return func()

@signature(
{'type': 'string'},
{'type': 'string'},
{'type': 'string'},
{'type': 'number', 'optional': True})
def _func_replace(self, text, search, replacement, count = None):
self._ensure_non_negative_integer(
'replace',
'count',
count)

if count != None:
return text.replace(search, replacement, int(count))
return text.replace(search, replacement)

@signature(
{'type': 'string'},
{'type': 'string'},
{'type': 'number', 'optional': True})
def _func_split(self, text, search, count = None):
self._ensure_non_negative_integer(
'split',
'count',
count)

if len(search) == 0:
chars = list(text)
if count == None:
return chars

head = [c for c in chars[:count]]
tail = [''.join(chars[count:])]
return head + tail

if count != None:
return text.split(search, count)
return text.split(search)

def _ensure_integer(
self,
func_name,
param_name,
param_value):

if param_value != None:
if int(param_value) != param_value:
raise exceptions.JMESPathValueError(
func_name,
param_value,
"integer")

def _ensure_non_negative_integer(
self,
func_name,
param_name,
param_value):

if param_value != None:
if int(param_value) != param_value or int(param_value) < 0:
raise exceptions.JMESPathValueError(
func_name,
param_name,
"non-negative integer")

@signature({'type': 'string'}, {'type': 'string', 'optional': True})
def _func_trim(self, text, chars = None):
if chars == None or len(chars) == 0:
return text.strip()
return text.strip(chars)

@signature({'type': 'string'}, {'type': 'string', 'optional': True})
def _func_trim_left(self, text, chars = None):
if chars == None or len(chars) == 0:
return text.lstrip()
return text.lstrip(chars)

@signature({'type': 'string'}, {'type': 'string', 'optional': True})
def _func_trim_right(self, text, chars = None):
if chars == None or len(chars) == 0:
return text.rstrip()
return text.rstrip(chars)

@signature({"types": ['object']})
def _func_values(self, arg):
return list(arg.values())
Expand Down
145 changes: 145 additions & 0 deletions tests/compliance/string_functions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
[
{
"given": {
"abab": "aabaaabaaaab",
"string": "subject string",
"split": "avg|-|min|-|max|-|mean|-|mode|-|median"
},
"cases": [
{
"expression": "find_first(string, 'string', `1`, `2`, `3`)",
"error": "invalid-arity"
},
{
"expression": "find_first(@, 'string', `1`, `2`)",
"error": "invalid-type"
},
{
"expression": "find_first(string, 'string', '1')",
"error": "invalid-type"
},
{
"expression": "find_first(string, 'string', `1`, '2')",
"error": "invalid-type"
},
{
"expression": "find_first(string, 'string', `1.3`, '2')",
"error": "invalid-type"
},
{
"expression": "find_first(string, 'string', `1`, '2.4')",
"error": "invalid-value"
},

{ "expression": "find_first(string, 'string')", "result": 8 },
{ "expression": "find_first(string, 'string', `0`)", "result": 8 },
{ "expression": "find_first(string, 'string', `0`, `14`)", "result": 8 },
{ "expression": "find_first(string, 'string', `-6`)", "result": 8 },
{ "expression": "find_first(string, 'string', `-99`, `100`)", "result": 8 },
{ "expression": "find_first(string, 'string', `0`, `13`)", "result": null },
{ "expression": "find_first(string, 'string', `8`)", "result": 8 },
{ "expression": "find_first(string, 'string', `8`, `11`)", "result": null },
{ "expression": "find_first(string, 'string', `9`)", "result": null },
{ "expression": "find_first(string, 's')", "result": 0 },
{ "expression": "find_first(string, 's', `1`)", "result": 8 },
{ "expression": "find_first(string, '')", "result": null },
{ "expression": "find_first('', '')", "result": null },

{ "expression": "find_last(string, 'string')", "result": 8 },
{ "expression": "find_last(string, 'string', `8`)", "result": 8 },
{ "expression": "find_last(string, 'string', `8`, `9`)", "result": null },
{ "expression": "find_last(string, 'string', `9`)", "result": null },
{ "expression": "find_last(string, 's', `1`)", "result": 8 },
{ "expression": "find_last(string, 's', `-6`)", "result": 8 },
{ "expression": "find_last(string, 's', `0`, `7`)", "result": 0 },
{ "expression": "find_last(string, '')", "result": null },
{ "expression": "find_last('', '')", "result": null },

{ "expression": "lower('STRING')", "result": "string" },
{ "expression": "upper('string')", "result": "STRING" },

{
"expression": "replace(abab, 'aa', '-', `0.333333`)",
"error": "invalid-value"
},

{
"expression": "replace(abab, 'aa', '-', `0.001`)",
"error": "invalid-value"
},

{ "expression": "replace(abab, 'aa', '-', `0`)", "result": "aabaaabaaaab" },
{ "expression": "replace(abab, 'aa', '-', `1`)", "result": "-baaabaaaab" },
{ "expression": "replace(abab, 'aa', '-', `2`)", "result": "-b-abaaaab" },
{ "expression": "replace(abab, 'aa', '-', `3`)", "result": "-b-ab-aab" },
{ "expression": "replace(abab, 'aa', '-')", "result": "-b-ab--b" },

{ "expression": "trim(' subject string ')", "result": "subject string" },
{ "expression": "trim(' subject string ', '')", "result": "subject string" },
{ "expression": "trim(' subject string ', ' ')", "result": "subject string" },
{ "expression": "trim(' subject string ', 's')", "result": " subject string " },
{ "expression": "trim(' subject string ', 'su')", "result": " subject string " },
{ "expression": "trim(' subject string ', 'su ')", "result": "bject string" },
{ "expression": "trim(' subject string ', 'gsu ')", "result": "bject strin" },

{
"expression": "trim('\u0009\u000A\u000B\u000C\u000D\u0020\u0085\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000')",
"result": ""
},

{ "expression": "trim_left(' subject string ')", "result": "subject string " },
{ "expression": "trim_left(' subject string ', 's')", "result": " subject string " },
{ "expression": "trim_left(' subject string ', 'su')", "result": " subject string " },
{ "expression": "trim_left(' subject string ', 'su ')", "result": "bject string " },
{ "expression": "trim_left(' subject string ', 'gsu ')", "result": "bject string " },

{ "expression": "trim_right(' subject string ')", "result": " subject string" },
{ "expression": "trim_right(' subject string ', 's')", "result": " subject string " },
{ "expression": "trim_right(' subject string ', 'su')", "result": " subject string " },
{ "expression": "trim_right(' subject string ', 'su ')", "result": " subject string" },
{ "expression": "trim_right(' subject string ', 'gsu ')", "result": " subject strin" },

{
"expression": "pad_left('string', '1')",
"error": "syntax"

},
{
"expression": "pad_left('string', `1`, '--')",
"error": "syntax"

},
{
"expression": "pad_left('string', `1.4`)",
"error": "invalid-value"

},

{ "expression": "pad_left('string', `0`)", "result": "string" },
{ "expression": "pad_left('string', `5`)", "result": "string" },
{ "expression": "pad_left('string', `10`)", "result": " string" },
{ "expression": "pad_left('string', `10`, '-')", "result": "----string" },

{ "expression": "pad_right('string', `0`)", "result": "string" },
{ "expression": "pad_right('string', `5`)", "result": "string" },
{ "expression": "pad_right('string', `10`)", "result": "string " },
{ "expression": "pad_right('string', `10`, '-')", "result": "string----" },

{
"expression": "split('/', '/', `3.7`)",
"error": "invalid-value"
},

{ "expression": "split('/', '/')", "result": [ "", "" ] },
{ "expression": "split('', '')", "result": [ ] },
{ "expression": "split('all chars', '')", "result": [ "a", "l", "l", " ", "c", "h", "a", "r", "s" ] },
{ "expression": "split('all chars', '', `3`)", "result": [ "a", "l", "l", " chars" ] },

{ "expression": "split(split, '|-|')", "result": [ "avg", "min", "max", "mean", "mode", "median" ] },
{ "expression": "split(split, '|-|', `3`)", "result": [ "avg", "min", "max", "mean|-|mode|-|median" ] },
{ "expression": "split(split, '|-|', `2`)", "result": [ "avg", "min", "max|-|mean|-|mode|-|median" ] },
{ "expression": "split(split, '|-|', `1`)", "result": [ "avg", "min|-|max|-|mean|-|mode|-|median" ] },
{ "expression": "split(split, '|-|', `0`)", "result": [ "avg|-|min|-|max|-|mean|-|mode|-|median" ] }
]
}
]
Loading

0 comments on commit fcc3513

Please sign in to comment.