blob: 42c413a4d9ecee16caee6230a708f24e57d43e47 [file] [log] [blame]
Alex Klein2b236722019-06-19 15:44:26 -06001# Copyright 2019 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Klein2008aee2019-08-20 16:25:27 -06005"""Validation helpers for simple input validation in the API.
6
7Note: Every validator MUST respect config.do_validation. This is an internally
8set config option that allows the mock call decorators to be placed before or
9after the validation decorators, rather than forcing an ordering that could then
10produce incorrect outputs if missed.
11"""
Alex Klein2b236722019-06-19 15:44:26 -060012
Alex Klein4de25e82019-08-05 15:58:39 -060013import functools
Alex Klein2b236722019-06-19 15:44:26 -060014import os
Alex Kleinbebccd52021-01-22 13:37:35 -070015from typing import Callable, Iterable, List, Optional, Union
Alex Klein2b236722019-06-19 15:44:26 -060016
Mike Frysinger2c024062021-05-22 15:43:22 -040017from chromite.third_party.google.protobuf import message as protobuf_message
Mike Frysinger849d6402019-10-17 00:14:16 -040018
Alex Klein2b236722019-06-19 15:44:26 -060019from chromite.lib import cros_build_lib
20from chromite.lib import cros_logging as logging
21
Mike Frysingeref94e4c2020-02-10 23:59:54 -050022
Alex Kleinbebccd52021-01-22 13:37:35 -070023def _value(
24 field: str, message: protobuf_message.Message
25) -> Union[bool, int, str, None, List, protobuf_message.Message]:
Alex Klein2b236722019-06-19 15:44:26 -060026 """Helper function to fetch the value of the field.
27
28 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -070029 field: The field name. Can be nested via . separation.
30 message: The protobuf message it is being fetched from.
Alex Klein2b236722019-06-19 15:44:26 -060031
32 Returns:
Alex Kleinbebccd52021-01-22 13:37:35 -070033 The value of the field.
Alex Klein2b236722019-06-19 15:44:26 -060034 """
Alex Kleinbdace302020-12-03 14:40:23 -070035 if not field:
36 return message
37
Alex Klein2b236722019-06-19 15:44:26 -060038 value = message
39 for part in field.split('.'):
40 if not isinstance(value, protobuf_message.Message):
41 value = None
42 break
43
44 try:
45 value = getattr(value, part)
46 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -040047 cros_build_lib.Die('Invalid field: %s', e)
Alex Klein2b236722019-06-19 15:44:26 -060048
49 return value
50
Alex Klein69339cc2019-07-22 14:08:35 -060051
Mike Frysinger88e02c12019-10-01 15:05:36 -040052# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -070053def exists(*fields: str):
Alex Klein2b236722019-06-19 15:44:26 -060054 """Validate that the paths in |fields| exist.
55
56 Args:
57 fields (str): The fields being checked. Can be . separated nested
58 fields.
59 """
60 assert fields
61
62 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060063 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060064 def _exists(input_proto, output_proto, config, *args, **kwargs):
65 if config.do_validation:
66 for field in fields:
67 logging.debug('Validating %s exists.', field)
Alex Klein2b236722019-06-19 15:44:26 -060068
Alex Klein2008aee2019-08-20 16:25:27 -060069 value = _value(field, input_proto)
70 if not value or not os.path.exists(value):
71 cros_build_lib.Die('%s path does not exist: %s' % (field, value))
Alex Klein2b236722019-06-19 15:44:26 -060072
Alex Klein2008aee2019-08-20 16:25:27 -060073 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -060074
75 return _exists
76
77 return decorator
78
79
Alex Kleinbebccd52021-01-22 13:37:35 -070080def is_in(field: str, values: Iterable):
Alex Kleinbdace302020-12-03 14:40:23 -070081 """Validate |field| is an element of |values|.
Alex Klein231d2da2019-07-22 16:44:45 -060082
83 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -070084 field: The field being checked. May be . separated nested fields.
85 values: The possible values field may take.
Alex Klein231d2da2019-07-22 16:44:45 -060086 """
87 assert field
88 assert values
89
90 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060091 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060092 def _is_in(input_proto, output_proto, config, *args, **kwargs):
93 if config.do_validation:
94 logging.debug('Validating %s is in %r', field, values)
95 value = _value(field, input_proto)
Alex Klein231d2da2019-07-22 16:44:45 -060096
Alex Klein2008aee2019-08-20 16:25:27 -060097 if value not in values:
98 cros_build_lib.Die('%s (%r) must be in %r', field, value, values)
Alex Klein231d2da2019-07-22 16:44:45 -060099
Alex Klein2008aee2019-08-20 16:25:27 -0600100 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein231d2da2019-07-22 16:44:45 -0600101
102 return _is_in
103
104 return decorator
105
106
Alex Kleinbebccd52021-01-22 13:37:35 -0700107def each_in(field: str,
108 subfield: Optional[str],
109 values: Iterable,
110 optional: bool = False):
Alex Kleinbdace302020-12-03 14:40:23 -0700111 """Validate each |subfield| of the repeated |field| is in |values|.
112
113 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700114 field: The field being checked. May be . separated nested fields.
115 subfield: The field in the repeated |field| to validate, or None
Alex Kleinbdace302020-12-03 14:40:23 -0700116 when |field| is not a repeated message, e.g. enum, scalars.
Alex Kleinbebccd52021-01-22 13:37:35 -0700117 values: The possible values field may take.
118 optional: Also allow the field to be empty when True.
Alex Kleinbdace302020-12-03 14:40:23 -0700119 """
120 assert field
121 assert values
122
123 def decorator(func):
124 @functools.wraps(func)
125 def _is_in(input_proto, output_proto, config, *args, **kwargs):
126 if config.do_validation:
127 members = _value(field, input_proto) or []
128 if not optional and not members:
129 cros_build_lib.Die('The %s field is empty.', field)
130 for member in members:
131 logging.debug('Validating %s.[each].%s is in %r.', field, subfield,
132 values)
133 value = _value(subfield, member)
134 if value not in values:
135 cros_build_lib.Die('%s.[each].%s (%r) must be in %r is required.',
136 field, subfield, value, values)
137
138 return func(input_proto, output_proto, config, *args, **kwargs)
139
140 return _is_in
141
142 return decorator
143
144
Mike Frysinger88e02c12019-10-01 15:05:36 -0400145# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700146def require(*fields: str):
Alex Klein2b236722019-06-19 15:44:26 -0600147 """Verify |fields| have all been set.
148
149 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700150 fields: The fields being checked. May be . separated nested fields.
Alex Klein2b236722019-06-19 15:44:26 -0600151 """
152 assert fields
153
154 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -0600155 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -0600156 def _require(input_proto, output_proto, config, *args, **kwargs):
157 if config.do_validation:
158 for field in fields:
159 logging.debug('Validating %s is set.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600160
Alex Klein2008aee2019-08-20 16:25:27 -0600161 value = _value(field, input_proto)
162 if not value:
163 cros_build_lib.Die('%s is required.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600164
Alex Klein2008aee2019-08-20 16:25:27 -0600165 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -0600166
167 return _require
168
169 return decorator
Alex Klein69339cc2019-07-22 14:08:35 -0600170
171
Alex Klein60c80522020-10-13 18:05:38 -0600172# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700173def require_any(*fields: str):
Alex Klein60c80522020-10-13 18:05:38 -0600174 """Verify at least one of |fields| have been set.
175
176 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700177 fields: The fields being checked. May be . separated nested fields.
Alex Klein60c80522020-10-13 18:05:38 -0600178 """
179 assert fields
180
181 def decorator(func):
182 @functools.wraps(func)
183 def _require(input_proto, output_proto, config, *args, **kwargs):
184 if config.do_validation:
185 for field in fields:
186 logging.debug('Validating %s is set.', field)
187 value = _value(field, input_proto)
188 if value:
189 break
190 else:
191 cros_build_lib.Die('At least one of the following must be set: %s',
192 ', '.join(fields))
193
194 return func(input_proto, output_proto, config, *args, **kwargs)
195
196 return _require
197
198 return decorator
199
200
Alex Kleinbebccd52021-01-22 13:37:35 -0700201def require_each(field: str,
202 subfields: Iterable[str],
203 allow_empty: bool = True):
Alex Klein86242bf2020-09-22 15:23:23 -0600204 """Verify |field| each have all of the |subfields| set.
205
206 When |allow_empty| is True, |field| may be empty, and |subfields| are only
207 validated when it is not empty. When |allow_empty| is False, |field| must
208 also have at least one entry.
209
210 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700211 field: The repeated field being checked. May be . separated nested
Alex Klein86242bf2020-09-22 15:23:23 -0600212 fields.
Alex Kleinbebccd52021-01-22 13:37:35 -0700213 subfields: The fields of the repeated message to validate.
214 allow_empty: Also require at least one entry in the repeated field.
Alex Klein86242bf2020-09-22 15:23:23 -0600215 """
216 assert field
217 assert subfields
218 assert not isinstance(subfields, str)
219
220 def decorator(func):
221 @functools.wraps(func)
222 def _require_each(input_proto, output_proto, config, *args, **kwargs):
223 if config.do_validation:
224 members = _value(field, input_proto) or []
225 if not allow_empty and not members:
226 cros_build_lib.Die('The %s field is empty.', field)
227 for member in members:
228 for subfield in subfields:
229 logging.debug('Validating %s.[each].%s is set.', field, subfield)
230 value = _value(subfield, member)
231 if not value:
232 cros_build_lib.Die('%s is required.', field)
233
234 return func(input_proto, output_proto, config, *args, **kwargs)
235
236 return _require_each
237
238 return decorator
239
240
Alex Kleinbebccd52021-01-22 13:37:35 -0700241def validation_complete(func: Callable):
Alex Klein69339cc2019-07-22 14:08:35 -0600242 """Automatically skip the endpoint when called after all other validators.
243
244 This decorator MUST be applied after all other validate decorators.
245 The config can be checked manually if there is non-decorator validation, but
246 this is much cleaner if it is all done in decorators.
247 """
Alex Klein4de25e82019-08-05 15:58:39 -0600248
249 @functools.wraps(func)
Alex Klein69339cc2019-07-22 14:08:35 -0600250 def _validate_only(request, response, configs, *args, **kwargs):
251 if configs.validate_only:
252 # Avoid calling the endpoint.
253 return 0
254 else:
255 return func(request, response, configs, *args, **kwargs)
256
257 return _validate_only