Skip to content

Commit

Permalink
Merge pull request #459 from drehelis/regex_text
Browse files Browse the repository at this point in the history
[WiP] Textfield to support RegExp validation
  • Loading branch information
bugy authored Nov 29, 2022
2 parents 472b480 + 8b7993b commit 081b20f
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 14 deletions.
1 change: 1 addition & 0 deletions src/model/external_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def parameter_to_external(parameter):
'min': parameter.min,
'max': parameter.max,
'max_length': parameter.max_length,
'regex': parameter.regex,
'values': parameter.values,
'secure': parameter.secure,
'fileRecursive': parameter.file_recursive,
Expand Down
12 changes: 12 additions & 0 deletions src/model/parameter_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import re
from collections import OrderedDict
from ipaddress import ip_address, IPv4Address, IPv6Address

Expand Down Expand Up @@ -31,6 +32,7 @@
'min',
'max',
'max_length',
'regex',
'constant',
'_values_provider',
'values',
Expand Down Expand Up @@ -77,6 +79,7 @@ def _reload(self):
self.min = config.get('min')
self.max = config.get('max')
self.max_length = config.get('max_length')
self.regex = config.get('regex')
self.secure = read_bool_from_config('secure', config, default=False)
self.separator = config.get('separator', ',')
self.multiselect_argument_type = read_str_from_config(
Expand Down Expand Up @@ -299,6 +302,13 @@ def validate_value(self, value, *, ignore_required=False):
return 'should be boolean, but has value ' + value_string

if self.type == 'text' or self.type == 'multiline_text':
if self.regex is not None:
regex_pattern = self.regex.get('pattern', None)
if not is_empty(regex_pattern):
regex_matched = re.fullmatch(regex_pattern, value)
if not regex_matched:
description = self.regex.get('description') or regex_pattern
return 'does not match regex pattern: ' + description
if (not is_empty(self.max_length)) and (len(value) > int(self.max_length)):
return 'is longer than allowed char length (' \
+ str(len(value)) + ' > ' + str(self.max_length) + ')'
Expand Down Expand Up @@ -526,6 +536,8 @@ def get_sorted_config(param_config):
'values',
'min',
'max',
'max_length',
'regex',
'multiselect_argument_type',
'separator',
'file_dir',
Expand Down
27 changes: 27 additions & 0 deletions src/tests/parameter_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,33 @@ def test_list_with_script_when_matches_and_win_newline(self):
error = parameter.validate_value('123')
self.assertIsNone(error)

@parameterized.expand([
('a\d', 'ab', 'some desc', 'some desc'),
('a\d', '12', 'desc 2', 'desc 2'),
('a\d', 'a12', 'some long description', 'some long description'),
('\d+\wa+', 'aaaa', 'some desc', 'some desc'),
('\d+\wa+', 'aaaa', None, '\d+\wa+'),
])
def test_regex_validation_when_fail_with_description(self, regex, value, description, expected_description):
parameter = create_parameter_model('param', regex={'pattern': regex, 'description': description})

error = parameter.validate_value(value)
self.assert_error(error)
self.assertEqual(error, "does not match regex pattern: " + expected_description)

@parameterized.expand([
('a\d', 'a1',),
('\da', '2a',),
('a\d+', 'a12',),
('\d+\wa+', '1Xaaaa'),
(None, '1Xaaaa'),
])
def test_regex_validation_when_success(self, regex, value):
parameter = create_parameter_model('param', regex={'pattern': regex})

error = parameter.validate_value(value)
self.assertIsNone(error)

@parameterized.expand([(False,), (True,), (None,)])
def test_list_with_dependency_when_matches(self, shell):
parameters = []
Expand Down
12 changes: 9 additions & 3 deletions src/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ def create_script_param_config(
excluded_files=None,
same_arg_param=None,
values_script_shell=None,
max_length=None):
max_length=None,
regex=None):
conf = {'name': param_name}

if type is not None:
Expand Down Expand Up @@ -214,6 +215,9 @@ def create_script_param_config(
if same_arg_param is not None:
conf['same_arg_param'] = same_arg_param

if regex is not None:
conf['regex'] = regex

if max_length is not None:
conf['max_length'] = max_length

Expand Down Expand Up @@ -285,7 +289,8 @@ def create_parameter_model(name=None,
file_recursive=None,
other_param_values: ObservableDict = None,
values_script_shell=None,
max_length=None):
max_length=None,
regex=None):
config = create_script_param_config(
name,
type=type,
Expand All @@ -305,7 +310,8 @@ def create_parameter_model(name=None,
file_dir=file_dir,
file_recursive=file_recursive,
values_script_shell=values_script_shell,
max_length=max_length)
max_length=max_length,
regex=regex)

if all_parameters is None:
all_parameters = []
Expand Down
16 changes: 10 additions & 6 deletions src/tests/web/script_config_socket_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,35 +256,39 @@ def _text1():
return {'name': 'text 1', 'description': None, 'withoutValue': False, 'required': True, 'default': None,
'type': 'text', 'min': None, 'max': None, 'max_length': None, 'values': None, 'secure': False,
'fileRecursive': False, 'fileType': None,
'requiredParameters': []
}
'requiredParameters': [],
'regex': None}


def _list1():
return {'name': 'list 1', 'description': None, 'withoutValue': False, 'required': False, 'default': None,
'type': 'list', 'min': None, 'max': None, 'max_length': None, 'values': ['A', 'B', 'C'],
'secure': False, 'fileRecursive': False, 'fileType': None,
'requiredParameters': []}
'requiredParameters': [],
'regex': None}


def _file1():
return {'name': 'file 1', 'description': None, 'withoutValue': False, 'required': False, 'default': None,
'type': 'server_file', 'min': None, 'max': None, 'max_length': None, 'values': ['x', 'y', 'z'],
'secure': False, 'fileRecursive': False, 'fileType': None,
'requiredParameters': []}
'requiredParameters': [],
'regex': None}


def _list2(list2_values):
return {'name': 'list 2', 'description': None, 'withoutValue': False, 'required': False, 'default': None,
'type': 'list', 'min': None, 'max': None, 'max_length': None, 'values': list2_values,
'secure': False,
'fileRecursive': False, 'fileType': None,
'requiredParameters': ['file 1']}
'requiredParameters': ['file 1'],
'regex': None}


def _included_text2():
return {'name': 'included text 2', 'description': None, 'withoutValue': False, 'required': False, 'default': None,
'type': 'text', 'min': None, 'max': None, 'max_length': None, 'values': None, 'secure': False,
'fileRecursive': False, 'fileType': None,
'requiredParameters': []
'requiredParameters': [],
'regex': None
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
@error="handleError('Excluded files', $event)"/>
</div>
<div v-if="selectedType === 'text' || selectedType === undefined || selectedType === 'multiline_text'" class="row">
<Textfield v-model="regexConfigPattern" :config="regexPatternField" class="col s4"
@error="handleError(regexPatternField, $event)"/>
<Textfield v-model="regexConfigDescription" :config="regexDescriptionField" class="col s4"
@error="handleError(regexDescriptionField, $event)"/>
<Textfield v-model="max_length" :config="maxLengthField" class="col s4"
@error="handleError(maxLengthField, $event)"/>
</div>
Expand Down Expand Up @@ -101,6 +105,8 @@ import {
fileTypeField,
maxField,
maxLengthField,
regexPatternField,
regexDescriptionField,
minField,
multiselectArgumentTypeField,
nameField,
Expand Down Expand Up @@ -181,6 +187,8 @@ export default {
min: null,
max: null,
max_length: null,
regexConfigPattern: null,
regexConfigDescription: null,
allowedValues: null,
allowedValuesScript: null,
allowedValuesFromScript: null,
Expand All @@ -206,6 +214,8 @@ export default {
minField,
maxField: Object.assign({}, maxField),
maxLengthField,
regexPatternField,
regexDescriptionField,
allowedValuesScriptField,
allowedValuesFromScriptField,
defaultValueField: Object.assign({}, defaultValueField),
Expand Down Expand Up @@ -235,6 +245,8 @@ export default {
this.min = config['min'];
this.max = config['max'];
this.max_length = config['max_length'];
this.regexConfigPattern = get(config, 'regex.pattern', '');
this.regexConfigDescription = get(config, 'regex.description', '');
this.constant = !!get(config, 'constant', false);
this.secure = !!get(config, 'secure', false);
this.multiselectArgumentType = get(config, 'multiselect_argument_type', 'single_argument');
Expand Down Expand Up @@ -320,6 +332,12 @@ export default {
allowedValuesScriptShellEnabled() {
this.updateAllowedValues();
},
regexConfigPattern() {
this.updateRegexConfig();
},
regexConfigDescription() {
this.updateRegexConfig();
},
defaultValue() {
if (this.selectedType === 'multiselect') {
updateValue(this.value, 'default', this.defaultValue.split(',').filter(s => !isEmptyString(s)));
Expand Down Expand Up @@ -350,6 +368,12 @@ export default {
methods: {
updateRegexConfig() {
updateValue(this.value, 'regex', {
pattern: this.regexConfigPattern,
description: this.regexConfigDescription
});
},
updateAllowedValues() {
if (this.allowedValuesFromScript) {
updateValue(this.value, 'values', {
Expand Down
10 changes: 10 additions & 0 deletions web-src/src/admin/components/scripts-config/parameter-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ export const allowedValuesScriptField = {
required: true
};

export const regexPatternField = {
name: 'RegExp pattern',
required: false
};

export const regexDescriptionField = {
name: 'RegExp description (optional)',
required: false
};

export const maxLengthField = {
name: 'Max characters length',
type: 'int'
Expand Down
21 changes: 16 additions & 5 deletions web-src/src/common/components/textfield.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
</template>

<script>
import '@/common/materializecss/imports/input-fields';
import '@/common/materializecss/imports/autocomplete';
import {isBlankString, isEmptyString, isNull} from '@/common/utils/common';
import '@/common/materializecss/imports/input-fields';
import {isBlankString, isEmptyString, isFullRegexMatch, isNull} from '@/common/utils/common';
export default {
name: 'Textfield',
Expand Down Expand Up @@ -153,7 +153,8 @@ export default {
}
if (!empty) {
var typeError = getValidByTypeError(value, this.config.type, this.config.min, this.config.max, this.config.max_length);
var typeError = getValidByTypeError(value, this.config.type, this.config.min, this.config.max,
this.config.max_length, this.config.regex);
if (!isEmptyString(typeError)) {
return typeError;
}
Expand Down Expand Up @@ -191,8 +192,18 @@ export default {
}
}
function getValidByTypeError(value, type, min, max, max_length) {
if (type === 'text') {
function getValidByTypeError(value, type, min, max, max_length, regex) {
if (!type || (type === 'text')) {
if (regex) {
let matches = isFullRegexMatch(regex.pattern, value);
if (!matches) {
if (regex.description) {
return regex.description
} else {
return 'pattern mismatch'
}
}
}
if (max_length) {
if (value.length > max_length) {
return 'Max chars allowed: ' + max_length
Expand Down
15 changes: 15 additions & 0 deletions web-src/src/common/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,19 @@ export function getFileInputValue(fileField) {
}

return value
}

export function isFullRegexMatch(regex, value) {
let fullStringPattern = regex

if (!fullStringPattern.startsWith('^')) {
fullStringPattern = '^' + fullStringPattern
}

if (!fullStringPattern.endsWith('$')) {
fullStringPattern += '$'
}

const regexPattern = new RegExp(fullStringPattern)
return regexPattern.test(value)
}
36 changes: 36 additions & 0 deletions web-src/tests/unit/textfield_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,42 @@ describe('Test TextField', function () {

expect(this.textfield.currentError).toBe('integer expected')
});

it('Test set invalid external value when regex', async function () {
setDeepProp(this.textfield, 'config.regex', {pattern: 'a\\d\\db', description: 'test desc'});
this.textfield.setProps({value: 'a123'});
await vueTicks();

expect(this.textfield.currentError).toBe('test desc')
});

it('Test set invalid external value when regex fullstring match', async function () {
setDeepProp(this.textfield, 'config.regex', {pattern: 'a', description: 'test desc'});
this.textfield.setProps({value: 'wat'});
await vueTicks();

expect(this.textfield.currentError).toBe('test desc')
});

it('Test set invalid external value when regex fullstring match and no desc', async function () {
setDeepProp(this.textfield, 'config.regex', {pattern: 'a', description: ''});
this.textfield.setProps({value: 'a1a'});
await vueTicks();

expect(this.textfield.currentError).toBe('pattern mismatch')
});

it('Test user set invalid value when regex', async function () {
setDeepProp(this.textfield, 'config.regex', {pattern: 'a\\d\\db', description: 'test desc'});
await vueTicks();

const inputField = this.textfield.find('input').element;
setInputValue(inputField, 'a12XXX', true);

await vueTicks();

expect(this.textfield.currentError).toBe('test desc')
});
});

describe('Test IP validaton', function () {
Expand Down

0 comments on commit 081b20f

Please sign in to comment.