blob: 7db603301d4c63dbf5321e869254351f96ca620f [file] [log] [blame]
Alex Klein2b236722019-06-19 15:44:26 -06001# -*- coding: utf-8 -*-
2# Copyright 2019 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Alex Klein2008aee2019-08-20 16:25:27 -06006"""Validation helpers for simple input validation in the API.
7
8Note: Every validator MUST respect config.do_validation. This is an internally
9set config option that allows the mock call decorators to be placed before or
10after the validation decorators, rather than forcing an ordering that could then
11produce incorrect outputs if missed.
12"""
Alex Klein2b236722019-06-19 15:44:26 -060013
14from __future__ import print_function
15
Alex Klein4de25e82019-08-05 15:58:39 -060016import functools
Alex Klein2b236722019-06-19 15:44:26 -060017import os
Mike Frysingeref94e4c2020-02-10 23:59:54 -050018import sys
Alex Kleinbebccd52021-01-22 13:37:35 -070019from typing import Callable, Iterable, List, Optional, Union
Alex Klein2b236722019-06-19 15:44:26 -060020
Mike Frysinger849d6402019-10-17 00:14:16 -040021from google.protobuf import message as protobuf_message
22
Alex Klein2b236722019-06-19 15:44:26 -060023from chromite.lib import cros_build_lib
24from chromite.lib import cros_logging as logging
25
Mike Frysingeref94e4c2020-02-10 23:59:54 -050026assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
27
28
Alex Kleinbebccd52021-01-22 13:37:35 -070029def _value(
30 field: str, message: protobuf_message.Message
31) -> Union[bool, int, str, None, List, protobuf_message.Message]:
Alex Klein2b236722019-06-19 15:44:26 -060032 """Helper function to fetch the value of the field.
33
34 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -070035 field: The field name. Can be nested via . separation.
36 message: The protobuf message it is being fetched from.
Alex Klein2b236722019-06-19 15:44:26 -060037
38 Returns:
Alex Kleinbebccd52021-01-22 13:37:35 -070039 The value of the field.
Alex Klein2b236722019-06-19 15:44:26 -060040 """
Alex Kleinbdace302020-12-03 14:40:23 -070041 if not field:
42 return message
43
Alex Klein2b236722019-06-19 15:44:26 -060044 value = message
45 for part in field.split('.'):
46 if not isinstance(value, protobuf_message.Message):
47 value = None
48 break
49
50 try:
51 value = getattr(value, part)
52 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -040053 cros_build_lib.Die('Invalid field: %s', e)
Alex Klein2b236722019-06-19 15:44:26 -060054
55 return value
56
Alex Klein69339cc2019-07-22 14:08:35 -060057
Mike Frysinger88e02c12019-10-01 15:05:36 -040058# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -070059def exists(*fields: str):
Alex Klein2b236722019-06-19 15:44:26 -060060 """Validate that the paths in |fields| exist.
61
62 Args:
63 fields (str): The fields being checked. Can be . separated nested
64 fields.
65 """
66 assert fields
67
68 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060069 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060070 def _exists(input_proto, output_proto, config, *args, **kwargs):
71 if config.do_validation:
72 for field in fields:
73 logging.debug('Validating %s exists.', field)
Alex Klein2b236722019-06-19 15:44:26 -060074
Alex Klein2008aee2019-08-20 16:25:27 -060075 value = _value(field, input_proto)
76 if not value or not os.path.exists(value):
77 cros_build_lib.Die('%s path does not exist: %s' % (field, value))
Alex Klein2b236722019-06-19 15:44:26 -060078
Alex Klein2008aee2019-08-20 16:25:27 -060079 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -060080
81 return _exists
82
83 return decorator
84
85
Alex Kleinbebccd52021-01-22 13:37:35 -070086def is_in(field: str, values: Iterable):
Alex Kleinbdace302020-12-03 14:40:23 -070087 """Validate |field| is an element of |values|.
Alex Klein231d2da2019-07-22 16:44:45 -060088
89 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -070090 field: The field being checked. May be . separated nested fields.
91 values: The possible values field may take.
Alex Klein231d2da2019-07-22 16:44:45 -060092 """
93 assert field
94 assert values
95
96 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060097 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060098 def _is_in(input_proto, output_proto, config, *args, **kwargs):
99 if config.do_validation:
100 logging.debug('Validating %s is in %r', field, values)
101 value = _value(field, input_proto)
Alex Klein231d2da2019-07-22 16:44:45 -0600102
Alex Klein2008aee2019-08-20 16:25:27 -0600103 if value not in values:
104 cros_build_lib.Die('%s (%r) must be in %r', field, value, values)
Alex Klein231d2da2019-07-22 16:44:45 -0600105
Alex Klein2008aee2019-08-20 16:25:27 -0600106 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein231d2da2019-07-22 16:44:45 -0600107
108 return _is_in
109
110 return decorator
111
112
Alex Kleinbebccd52021-01-22 13:37:35 -0700113def each_in(field: str,
114 subfield: Optional[str],
115 values: Iterable,
116 optional: bool = False):
Alex Kleinbdace302020-12-03 14:40:23 -0700117 """Validate each |subfield| of the repeated |field| is in |values|.
118
119 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700120 field: The field being checked. May be . separated nested fields.
121 subfield: The field in the repeated |field| to validate, or None
Alex Kleinbdace302020-12-03 14:40:23 -0700122 when |field| is not a repeated message, e.g. enum, scalars.
Alex Kleinbebccd52021-01-22 13:37:35 -0700123 values: The possible values field may take.
124 optional: Also allow the field to be empty when True.
Alex Kleinbdace302020-12-03 14:40:23 -0700125 """
126 assert field
127 assert values
128
129 def decorator(func):
130 @functools.wraps(func)
131 def _is_in(input_proto, output_proto, config, *args, **kwargs):
132 if config.do_validation:
133 members = _value(field, input_proto) or []
134 if not optional and not members:
135 cros_build_lib.Die('The %s field is empty.', field)
136 for member in members:
137 logging.debug('Validating %s.[each].%s is in %r.', field, subfield,
138 values)
139 value = _value(subfield, member)
140 if value not in values:
141 cros_build_lib.Die('%s.[each].%s (%r) must be in %r is required.',
142 field, subfield, value, values)
143
144 return func(input_proto, output_proto, config, *args, **kwargs)
145
146 return _is_in
147
148 return decorator
149
150
Mike Frysinger88e02c12019-10-01 15:05:36 -0400151# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700152def require(*fields: str):
Alex Klein2b236722019-06-19 15:44:26 -0600153 """Verify |fields| have all been set.
154
155 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700156 fields: The fields being checked. May be . separated nested fields.
Alex Klein2b236722019-06-19 15:44:26 -0600157 """
158 assert fields
159
160 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -0600161 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -0600162 def _require(input_proto, output_proto, config, *args, **kwargs):
163 if config.do_validation:
164 for field in fields:
165 logging.debug('Validating %s is set.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600166
Alex Klein2008aee2019-08-20 16:25:27 -0600167 value = _value(field, input_proto)
168 if not value:
169 cros_build_lib.Die('%s is required.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600170
Alex Klein2008aee2019-08-20 16:25:27 -0600171 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -0600172
173 return _require
174
175 return decorator
Alex Klein69339cc2019-07-22 14:08:35 -0600176
177
Alex Klein60c80522020-10-13 18:05:38 -0600178# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700179def require_any(*fields: str):
Alex Klein60c80522020-10-13 18:05:38 -0600180 """Verify at least one of |fields| have been set.
181
182 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700183 fields: The fields being checked. May be . separated nested fields.
Alex Klein60c80522020-10-13 18:05:38 -0600184 """
185 assert fields
186
187 def decorator(func):
188 @functools.wraps(func)
189 def _require(input_proto, output_proto, config, *args, **kwargs):
190 if config.do_validation:
191 for field in fields:
192 logging.debug('Validating %s is set.', field)
193 value = _value(field, input_proto)
194 if value:
195 break
196 else:
197 cros_build_lib.Die('At least one of the following must be set: %s',
198 ', '.join(fields))
199
200 return func(input_proto, output_proto, config, *args, **kwargs)
201
202 return _require
203
204 return decorator
205
206
Alex Kleinbebccd52021-01-22 13:37:35 -0700207def require_each(field: str,
208 subfields: Iterable[str],
209 allow_empty: bool = True):
Alex Klein86242bf2020-09-22 15:23:23 -0600210 """Verify |field| each have all of the |subfields| set.
211
212 When |allow_empty| is True, |field| may be empty, and |subfields| are only
213 validated when it is not empty. When |allow_empty| is False, |field| must
214 also have at least one entry.
215
216 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700217 field: The repeated field being checked. May be . separated nested
Alex Klein86242bf2020-09-22 15:23:23 -0600218 fields.
Alex Kleinbebccd52021-01-22 13:37:35 -0700219 subfields: The fields of the repeated message to validate.
220 allow_empty: Also require at least one entry in the repeated field.
Alex Klein86242bf2020-09-22 15:23:23 -0600221 """
222 assert field
223 assert subfields
224 assert not isinstance(subfields, str)
225
226 def decorator(func):
227 @functools.wraps(func)
228 def _require_each(input_proto, output_proto, config, *args, **kwargs):
229 if config.do_validation:
230 members = _value(field, input_proto) or []
231 if not allow_empty and not members:
232 cros_build_lib.Die('The %s field is empty.', field)
233 for member in members:
234 for subfield in subfields:
235 logging.debug('Validating %s.[each].%s is set.', field, subfield)
236 value = _value(subfield, member)
237 if not value:
238 cros_build_lib.Die('%s is required.', field)
239
240 return func(input_proto, output_proto, config, *args, **kwargs)
241
242 return _require_each
243
244 return decorator
245
246
Alex Kleinbebccd52021-01-22 13:37:35 -0700247def validation_complete(func: Callable):
Alex Klein69339cc2019-07-22 14:08:35 -0600248 """Automatically skip the endpoint when called after all other validators.
249
250 This decorator MUST be applied after all other validate decorators.
251 The config can be checked manually if there is non-decorator validation, but
252 this is much cleaner if it is all done in decorators.
253 """
Alex Klein4de25e82019-08-05 15:58:39 -0600254
255 @functools.wraps(func)
Alex Klein69339cc2019-07-22 14:08:35 -0600256 def _validate_only(request, response, configs, *args, **kwargs):
257 if configs.validate_only:
258 # Avoid calling the endpoint.
259 return 0
260 else:
261 return func(request, response, configs, *args, **kwargs)
262
263 return _validate_only