blob: c74af66bb699a5b6789126121a7c86fc42f7ca95 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import re
import sys
from typing import Optional
from enum import Enum
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
# The repo's root directory.
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))
# Add the repo's root directory for clearer imports.
sys.path.insert(0, _ROOT_DIR)
import metadata.fields.util as util
import metadata.validation_result as vr
# Pattern used to check if the entire string is either "yes" or "no",
# case-insensitive.
_PATTERN_YES_OR_NO = re.compile(r"^(yes|no)$", re.IGNORECASE)
# Pattern used to check if the string starts with "yes" or "no",
# case-insensitive. e.g. "No (test only)", "Yes?"
_PATTERN_STARTS_WITH_YES_OR_NO = re.compile(r"^(yes|no)", re.IGNORECASE)
class MetadataField:
"""Base class for all metadata fields."""
# The delimiter used to separate multiple values.
VALUE_DELIMITER = ","
def __init__(self, name: str, structured: bool = True):
self._name = name
self._structured = structured
def __eq__(self, other):
if not isinstance(other, MetadataField):
return False
return self._name.lower() == other._name.lower()
def __hash__(self):
return hash(self._name.lower())
def get_name(self):
return self._name
def should_terminate_field(self, field_value) -> bool:
"""Whether this field should be terminated based on the given `field_value`.
"""
return False
def is_structured(self):
"""Whether the field represents structured data, such as a list of
URLs.
If true, parser will treat `Field Name Like Pattern:` in subsequent
lines as a new field, in addition to all known field names.
If false, parser will only recognize known field names, and unknown
fields will be merged into the preceding field value.
"""
return self._structured
def validate(self, value: str) -> Optional[vr.ValidationResult]:
"""Checks the given value is acceptable for the field.
Raises: NotImplementedError if called. This method must be
overridden with the actual validation of the field.
"""
raise NotImplementedError(f"{self._name} field validation not defined.")
def narrow_type(self, value):
"""Returns a narrowly typed (e.g. bool) value for this field for
downstream consumption.
The alternative being the downstream parses the string again.
"""
raise NotImplementedError(
f"{self._name} field value coersion not defined.")
class FreeformTextField(MetadataField):
"""Field where the value is freeform text."""
def validate(self, value: str) -> Optional[vr.ValidationResult]:
"""Checks the given value has at least one non-whitespace
character.
"""
if util.is_empty(value):
return vr.ValidationError(reason=f"{self._name} is empty.")
return None
def narrow_type(self, value):
assert value is not None
return value
class SingleLineTextField(FreeformTextField):
"""Field where the field as a whole is a single line of text."""
def __init__(self, name):
super().__init__(name=name)
def should_terminate_field(self, field_value) -> bool:
# Look for line breaks.
#
# We don't use `os.linesep`` here because Chromium uses line feed
# (`\n`) for git checkouts on all platforms (see README.* entries
# in chromium.src/.gitattributes).
return field_value.endswith("\n")
class YesNoField(SingleLineTextField):
"""Field where the value must be yes or no."""
def __init__(self, name: str):
super().__init__(name=name)
def validate(self, value: str) -> Optional[vr.ValidationResult]:
"""Checks the given value is either yes or no."""
if util.matches(_PATTERN_YES_OR_NO, value):
return None
if util.matches(_PATTERN_STARTS_WITH_YES_OR_NO, value):
return vr.ValidationWarning(
reason=f"{self._name} is invalid.",
additional=[
f"This field should be only {util.YES} or {util.NO}.",
f"Current value is '{value}'.",
])
return vr.ValidationError(
reason=f"{self._name} is invalid.",
additional=[
f"This field must be {util.YES} or {util.NO}.",
f"Current value is '{value}'.",
])
def narrow_type(self, value) -> Optional[bool]:
return util.infer_as_boolean(super().narrow_type(value))