blob: 7bb7b23b492ca4f62d5878ded6eac029e9834b82 [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 Klein2b236722019-06-19 15:44:26 -060019
Mike Frysinger849d6402019-10-17 00:14:16 -040020from google.protobuf import message as protobuf_message
21
Alex Klein2b236722019-06-19 15:44:26 -060022from chromite.lib import cros_build_lib
23from chromite.lib import cros_logging as logging
24
Alex Klein2b236722019-06-19 15:44:26 -060025
Mike Frysingeref94e4c2020-02-10 23:59:54 -050026assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
27
28
Alex Klein2b236722019-06-19 15:44:26 -060029def _value(field, message):
30 """Helper function to fetch the value of the field.
31
32 Args:
33 field (str): The field name. Can be nested via . separation.
34 message (Message): The protobuf message it is being fetched from.
35
36 Returns:
37 str|None|int|list|Message|bool - The value of the field.
38 """
Alex Kleinbdace302020-12-03 14:40:23 -070039 if not field:
40 return message
41
Alex Klein2b236722019-06-19 15:44:26 -060042 value = message
43 for part in field.split('.'):
44 if not isinstance(value, protobuf_message.Message):
45 value = None
46 break
47
48 try:
49 value = getattr(value, part)
50 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -040051 cros_build_lib.Die('Invalid field: %s', e)
Alex Klein2b236722019-06-19 15:44:26 -060052
53 return value
54
Alex Klein69339cc2019-07-22 14:08:35 -060055
Mike Frysinger88e02c12019-10-01 15:05:36 -040056# pylint: disable=docstring-misnamed-args
Alex Klein2b236722019-06-19 15:44:26 -060057def exists(*fields):
58 """Validate that the paths in |fields| exist.
59
60 Args:
61 fields (str): The fields being checked. Can be . separated nested
62 fields.
63 """
64 assert fields
65
66 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060067 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060068 def _exists(input_proto, output_proto, config, *args, **kwargs):
69 if config.do_validation:
70 for field in fields:
71 logging.debug('Validating %s exists.', field)
Alex Klein2b236722019-06-19 15:44:26 -060072
Alex Klein2008aee2019-08-20 16:25:27 -060073 value = _value(field, input_proto)
74 if not value or not os.path.exists(value):
75 cros_build_lib.Die('%s path does not exist: %s' % (field, value))
Alex Klein2b236722019-06-19 15:44:26 -060076
Alex Klein2008aee2019-08-20 16:25:27 -060077 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -060078
79 return _exists
80
81 return decorator
82
83
Alex Klein231d2da2019-07-22 16:44:45 -060084def is_in(field, values):
Alex Kleinbdace302020-12-03 14:40:23 -070085 """Validate |field| is an element of |values|.
Alex Klein231d2da2019-07-22 16:44:45 -060086
87 Args:
88 field (str): The field being checked. May be . separated nested fields.
89 values (list): The possible values field may take.
90 """
91 assert field
92 assert values
93
94 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060095 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060096 def _is_in(input_proto, output_proto, config, *args, **kwargs):
97 if config.do_validation:
98 logging.debug('Validating %s is in %r', field, values)
99 value = _value(field, input_proto)
Alex Klein231d2da2019-07-22 16:44:45 -0600100
Alex Klein2008aee2019-08-20 16:25:27 -0600101 if value not in values:
102 cros_build_lib.Die('%s (%r) must be in %r', field, value, values)
Alex Klein231d2da2019-07-22 16:44:45 -0600103
Alex Klein2008aee2019-08-20 16:25:27 -0600104 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein231d2da2019-07-22 16:44:45 -0600105
106 return _is_in
107
108 return decorator
109
110
Alex Kleinbdace302020-12-03 14:40:23 -0700111def each_in(field, subfield, values, optional=False):
112 """Validate each |subfield| of the repeated |field| is in |values|.
113
114 Args:
115 field (str): The field being checked. May be . separated nested fields.
116 subfield (str|None): The field in the repeated |field| to validate, or None
117 when |field| is not a repeated message, e.g. enum, scalars.
118 values (list): The possible values field may take.
119 optional (bool): Also allow the field to be empty when True.
120 """
121 assert field
122 assert values
123
124 def decorator(func):
125 @functools.wraps(func)
126 def _is_in(input_proto, output_proto, config, *args, **kwargs):
127 if config.do_validation:
128 members = _value(field, input_proto) or []
129 if not optional and not members:
130 cros_build_lib.Die('The %s field is empty.', field)
131 for member in members:
132 logging.debug('Validating %s.[each].%s is in %r.', field, subfield,
133 values)
134 value = _value(subfield, member)
135 if value not in values:
136 cros_build_lib.Die('%s.[each].%s (%r) must be in %r is required.',
137 field, subfield, value, values)
138
139 return func(input_proto, output_proto, config, *args, **kwargs)
140
141 return _is_in
142
143 return decorator
144
145
Mike Frysinger88e02c12019-10-01 15:05:36 -0400146# pylint: disable=docstring-misnamed-args
Alex Klein2b236722019-06-19 15:44:26 -0600147def require(*fields):
148 """Verify |fields| have all been set.
149
150 Args:
Alex Klein231d2da2019-07-22 16:44:45 -0600151 fields (str): The fields being checked. May be . separated nested fields.
Alex Klein2b236722019-06-19 15:44:26 -0600152 """
153 assert fields
154
155 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -0600156 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -0600157 def _require(input_proto, output_proto, config, *args, **kwargs):
158 if config.do_validation:
159 for field in fields:
160 logging.debug('Validating %s is set.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600161
Alex Klein2008aee2019-08-20 16:25:27 -0600162 value = _value(field, input_proto)
163 if not value:
164 cros_build_lib.Die('%s is required.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600165
Alex Klein2008aee2019-08-20 16:25:27 -0600166 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -0600167
168 return _require
169
170 return decorator
Alex Klein69339cc2019-07-22 14:08:35 -0600171
172
Alex Klein60c80522020-10-13 18:05:38 -0600173# pylint: disable=docstring-misnamed-args
174def require_any(*fields):
175 """Verify at least one of |fields| have been set.
176
177 Args:
178 fields (str): The fields being checked. May be . separated nested fields.
179 """
180 assert fields
181
182 def decorator(func):
183 @functools.wraps(func)
184 def _require(input_proto, output_proto, config, *args, **kwargs):
185 if config.do_validation:
186 for field in fields:
187 logging.debug('Validating %s is set.', field)
188 value = _value(field, input_proto)
189 if value:
190 break
191 else:
192 cros_build_lib.Die('At least one of the following must be set: %s',
193 ', '.join(fields))
194
195 return func(input_proto, output_proto, config, *args, **kwargs)
196
197 return _require
198
199 return decorator
200
201
Alex Klein86242bf2020-09-22 15:23:23 -0600202def require_each(field, subfields, allow_empty=True):
203 """Verify |field| each have all of the |subfields| set.
204
205 When |allow_empty| is True, |field| may be empty, and |subfields| are only
206 validated when it is not empty. When |allow_empty| is False, |field| must
207 also have at least one entry.
208
209 Args:
210 field (str): The repeated field being checked. May be . separated nested
211 fields.
212 subfields (list[str]): The fields of the repeated message to validate.
213 allow_empty (bool): Also require at least one entry in the repeated field.
214 """
215 assert field
216 assert subfields
217 assert not isinstance(subfields, str)
218
219 def decorator(func):
220 @functools.wraps(func)
221 def _require_each(input_proto, output_proto, config, *args, **kwargs):
222 if config.do_validation:
223 members = _value(field, input_proto) or []
224 if not allow_empty and not members:
225 cros_build_lib.Die('The %s field is empty.', field)
226 for member in members:
227 for subfield in subfields:
228 logging.debug('Validating %s.[each].%s is set.', field, subfield)
229 value = _value(subfield, member)
230 if not value:
231 cros_build_lib.Die('%s is required.', field)
232
233 return func(input_proto, output_proto, config, *args, **kwargs)
234
235 return _require_each
236
237 return decorator
238
239
Alex Klein69339cc2019-07-22 14:08:35 -0600240def validation_complete(func):
241 """Automatically skip the endpoint when called after all other validators.
242
243 This decorator MUST be applied after all other validate decorators.
244 The config can be checked manually if there is non-decorator validation, but
245 this is much cleaner if it is all done in decorators.
246 """
Alex Klein4de25e82019-08-05 15:58:39 -0600247
248 @functools.wraps(func)
Alex Klein69339cc2019-07-22 14:08:35 -0600249 def _validate_only(request, response, configs, *args, **kwargs):
250 if configs.validate_only:
251 # Avoid calling the endpoint.
252 return 0
253 else:
254 return func(request, response, configs, *args, **kwargs)
255
256 return _validate_only