blob: 0f61a393244e498038a59469f23ccd993d574700 [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 """
39 value = message
40 for part in field.split('.'):
41 if not isinstance(value, protobuf_message.Message):
42 value = None
43 break
44
45 try:
46 value = getattr(value, part)
47 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -040048 cros_build_lib.Die('Invalid field: %s', e)
Alex Klein2b236722019-06-19 15:44:26 -060049
50 return value
51
Alex Klein69339cc2019-07-22 14:08:35 -060052
Mike Frysinger88e02c12019-10-01 15:05:36 -040053# pylint: disable=docstring-misnamed-args
Alex Klein2b236722019-06-19 15:44:26 -060054def exists(*fields):
55 """Validate that the paths in |fields| exist.
56
57 Args:
58 fields (str): The fields being checked. Can be . separated nested
59 fields.
60 """
61 assert fields
62
63 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060064 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060065 def _exists(input_proto, output_proto, config, *args, **kwargs):
66 if config.do_validation:
67 for field in fields:
68 logging.debug('Validating %s exists.', field)
Alex Klein2b236722019-06-19 15:44:26 -060069
Alex Klein2008aee2019-08-20 16:25:27 -060070 value = _value(field, input_proto)
71 if not value or not os.path.exists(value):
72 cros_build_lib.Die('%s path does not exist: %s' % (field, value))
Alex Klein2b236722019-06-19 15:44:26 -060073
Alex Klein2008aee2019-08-20 16:25:27 -060074 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -060075
76 return _exists
77
78 return decorator
79
80
Alex Klein231d2da2019-07-22 16:44:45 -060081def is_in(field, values):
LaMont Jonesb20b3d92019-11-23 11:47:48 -070082 """Validate |field| contains |value|.
Alex Klein231d2da2019-07-22 16:44:45 -060083
84 Args:
85 field (str): The field being checked. May be . separated nested fields.
86 values (list): The possible values field may take.
87 """
88 assert field
89 assert values
90
91 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060092 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060093 def _is_in(input_proto, output_proto, config, *args, **kwargs):
94 if config.do_validation:
95 logging.debug('Validating %s is in %r', field, values)
96 value = _value(field, input_proto)
Alex Klein231d2da2019-07-22 16:44:45 -060097
Alex Klein2008aee2019-08-20 16:25:27 -060098 if value not in values:
99 cros_build_lib.Die('%s (%r) must be in %r', field, value, values)
Alex Klein231d2da2019-07-22 16:44:45 -0600100
Alex Klein2008aee2019-08-20 16:25:27 -0600101 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein231d2da2019-07-22 16:44:45 -0600102
103 return _is_in
104
105 return decorator
106
107
Mike Frysinger88e02c12019-10-01 15:05:36 -0400108# pylint: disable=docstring-misnamed-args
Alex Klein2b236722019-06-19 15:44:26 -0600109def require(*fields):
110 """Verify |fields| have all been set.
111
112 Args:
Alex Klein231d2da2019-07-22 16:44:45 -0600113 fields (str): The fields being checked. May be . separated nested fields.
Alex Klein2b236722019-06-19 15:44:26 -0600114 """
115 assert fields
116
117 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -0600118 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -0600119 def _require(input_proto, output_proto, config, *args, **kwargs):
120 if config.do_validation:
121 for field in fields:
122 logging.debug('Validating %s is set.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600123
Alex Klein2008aee2019-08-20 16:25:27 -0600124 value = _value(field, input_proto)
125 if not value:
126 cros_build_lib.Die('%s is required.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600127
Alex Klein2008aee2019-08-20 16:25:27 -0600128 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -0600129
130 return _require
131
132 return decorator
Alex Klein69339cc2019-07-22 14:08:35 -0600133
134
Alex Klein60c80522020-10-13 18:05:38 -0600135# pylint: disable=docstring-misnamed-args
136def require_any(*fields):
137 """Verify at least one of |fields| have been set.
138
139 Args:
140 fields (str): The fields being checked. May be . separated nested fields.
141 """
142 assert fields
143
144 def decorator(func):
145 @functools.wraps(func)
146 def _require(input_proto, output_proto, config, *args, **kwargs):
147 if config.do_validation:
148 for field in fields:
149 logging.debug('Validating %s is set.', field)
150 value = _value(field, input_proto)
151 if value:
152 break
153 else:
154 cros_build_lib.Die('At least one of the following must be set: %s',
155 ', '.join(fields))
156
157 return func(input_proto, output_proto, config, *args, **kwargs)
158
159 return _require
160
161 return decorator
162
163
Alex Klein86242bf2020-09-22 15:23:23 -0600164def require_each(field, subfields, allow_empty=True):
165 """Verify |field| each have all of the |subfields| set.
166
167 When |allow_empty| is True, |field| may be empty, and |subfields| are only
168 validated when it is not empty. When |allow_empty| is False, |field| must
169 also have at least one entry.
170
171 Args:
172 field (str): The repeated field being checked. May be . separated nested
173 fields.
174 subfields (list[str]): The fields of the repeated message to validate.
175 allow_empty (bool): Also require at least one entry in the repeated field.
176 """
177 assert field
178 assert subfields
179 assert not isinstance(subfields, str)
180
181 def decorator(func):
182 @functools.wraps(func)
183 def _require_each(input_proto, output_proto, config, *args, **kwargs):
184 if config.do_validation:
185 members = _value(field, input_proto) or []
186 if not allow_empty and not members:
187 cros_build_lib.Die('The %s field is empty.', field)
188 for member in members:
189 for subfield in subfields:
190 logging.debug('Validating %s.[each].%s is set.', field, subfield)
191 value = _value(subfield, member)
192 if not value:
193 cros_build_lib.Die('%s is required.', field)
194
195 return func(input_proto, output_proto, config, *args, **kwargs)
196
197 return _require_each
198
199 return decorator
200
201
Alex Klein69339cc2019-07-22 14:08:35 -0600202def validation_complete(func):
203 """Automatically skip the endpoint when called after all other validators.
204
205 This decorator MUST be applied after all other validate decorators.
206 The config can be checked manually if there is non-decorator validation, but
207 this is much cleaner if it is all done in decorators.
208 """
Alex Klein4de25e82019-08-05 15:58:39 -0600209
210 @functools.wraps(func)
Alex Klein69339cc2019-07-22 14:08:35 -0600211 def _validate_only(request, response, configs, *args, **kwargs):
212 if configs.validate_only:
213 # Avoid calling the endpoint.
214 return 0
215 else:
216 return func(request, response, configs, *args, **kwargs)
217
218 return _validate_only