Skip to content

Commit 2477c96

Browse files
authored
Added per-category field validation and improved some validation messages (#174)
* Improved error messages likely to occur when authoring IAM policies. * Additional required fields checked per category.
1 parent 585e7ac commit 2477c96

File tree

7 files changed

+130
-27
lines changed

7 files changed

+130
-27
lines changed

aws_doc_sdk_examples_tools/doc_gen.py

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from functools import reduce
1010
from pathlib import Path
1111
from typing import Dict, Iterable, Optional, Set, Tuple, List, Any
12+
from yaml.parser import ParserError
13+
14+
from yaml import YAMLError
1215

1316
# from os import glob
1417

@@ -28,6 +31,7 @@
2831
NameFormat,
2932
ActionNameFormat,
3033
ServiceNameFormat,
34+
YamlParseError,
3135
)
3236
from .metadata_validator import validate_metadata
3337
from .project_validator import ValidationConfig
@@ -211,25 +215,28 @@ def find_and_process_metadata(self, metadata_path: Path):
211215
def process_metadata(self, path: Path) -> "DocGen":
212216
if path in self._loaded:
213217
return self
214-
with open(path) as file:
215-
examples, errs = parse_examples(
216-
path,
217-
yaml.safe_load(file),
218-
self.sdks,
219-
self.services,
220-
self.standard_categories,
221-
self.cross_blocks,
222-
self.validation,
223-
)
224-
self.extend_examples(examples, self.errors)
225-
self.errors.extend(errs)
226-
for example in examples:
227-
for lang in example.languages:
228-
language = example.languages[lang]
229-
for version in language.versions:
230-
for excerpt in version.excerpts:
231-
self.snippet_files.update(excerpt.snippet_files)
232-
self._loaded.add(path)
218+
try:
219+
with open(path) as file:
220+
examples, errs = parse_examples(
221+
path,
222+
yaml.safe_load(file),
223+
self.sdks,
224+
self.services,
225+
self.standard_categories,
226+
self.cross_blocks,
227+
self.validation,
228+
)
229+
self.extend_examples(examples, self.errors)
230+
self.errors.extend(errs)
231+
for example in examples:
232+
for lang in example.languages:
233+
language = example.languages[lang]
234+
for version in language.versions:
235+
for excerpt in version.excerpts:
236+
self.snippet_files.update(excerpt.snippet_files)
237+
self._loaded.add(path)
238+
except ParserError as e:
239+
self.errors.append(YamlParseError(file=path, parser_error=str(e)))
233240
return self
234241

235242
@classmethod

aws_doc_sdk_examples_tools/metadata_errors.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ class MetadataErrors(ErrorsList[MetadataError]):
9393
pass
9494

9595

96+
@dataclass
97+
class YamlParseError(MetadataError):
98+
parser_error: Optional[str] = None
99+
100+
def prefix(self):
101+
prefix = f"In {self.file},"
102+
return prefix
103+
104+
def message(self) -> str:
105+
return (
106+
f"incorrect YAML format {self.parser_error}.\n"
107+
f"\tThis indicates a problem with the YAML itself. Use a YAML authoring tool to help diagnose further."
108+
)
109+
110+
96111
@dataclass
97112
class MetadataParseError(MetadataError):
98113
id: Optional[str] = None
@@ -172,7 +187,7 @@ class FieldError(MetadataParseError):
172187
@dataclass
173188
class MissingField(FieldError):
174189
def message(self):
175-
return f"missing field {self.field}"
190+
return f"missing field '{self.field}'"
176191

177192

178193
@dataclass
@@ -213,7 +228,8 @@ def message(self) -> str:
213228
class UnknownLanguage(LanguageError):
214229
def message(self):
215230
return (
216-
f"contains {self.language} as a language, which is not listed in sdks.yaml."
231+
f"contains '{self.language}' as a language, which is not listed in sdks.yaml. Typically, this indicates a "
232+
f"typo in the name of the language."
217233
)
218234

219235

@@ -333,7 +349,10 @@ class UnknownService(MetadataParseError):
333349
service: str = ""
334350

335351
def message(self):
336-
return f"has unknown service {self.service}"
352+
return (
353+
f"has unknown service '{self.service}'. Typically this indicates a typo in the name of the service, "
354+
f"which must match a key used in services.yaml."
355+
)
337356

338357

339358
@dataclass
@@ -384,7 +403,10 @@ class PersonMissingField(SdkVersionError):
384403
alias: str = ""
385404

386405
def message(self):
387-
return f"person is missing a field: name: {self.name}, alias: {self.alias}"
406+
return (
407+
f"person is missing a field: name: {self.name}, alias: {self.alias}. A person must have both a name "
408+
f"and an alias."
409+
)
388410

389411

390412
@dataclass

aws_doc_sdk_examples_tools/metadata_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,62 @@ def test_parse_cross():
500500
assert actual[0] == example
501501

502502

503+
CATEGORY_FIELD_ERRORS = """
504+
medical-imaging_TestRequiredCategoryFields:
505+
title: Delete Topic
506+
title_abbrev: Delete topic
507+
synopsis: this is a synopsis.
508+
category: IAMPolicy
509+
languages:
510+
PHP:
511+
versions:
512+
- sdk_version: 1
513+
excerpts:
514+
- snippet_files:
515+
- snippet_files/AccountControlApiDoc/iam-policies.AccountControlApiDoc.src.manage-acct-update-contact-alternate.xml.1.xml
516+
services:
517+
medical-imaging:
518+
"""
519+
520+
521+
def test_parse_required_category_fields_errors():
522+
meta = yaml.safe_load(CATEGORY_FIELD_ERRORS)
523+
_, errors = parse_examples(
524+
Path("test_meta.yaml"),
525+
meta,
526+
SDKS,
527+
SERVICES,
528+
STANDARD_CATS,
529+
set(),
530+
ValidationConfig(strict_titles=True),
531+
)
532+
expected = [
533+
metadata_errors.MissingField(
534+
file=Path("test_meta.yaml"),
535+
id="medical-imaging_TestRequiredCategoryFields",
536+
language="PHP",
537+
sdk_version=1,
538+
field="owner",
539+
),
540+
metadata_errors.MissingField(
541+
file=Path("test_meta.yaml"),
542+
id="medical-imaging_TestRequiredCategoryFields",
543+
language="PHP",
544+
sdk_version=1,
545+
field="source",
546+
),
547+
metadata_errors.MissingField(
548+
file=Path("test_meta.yaml"),
549+
id="medical-imaging_TestRequiredCategoryFields",
550+
language="PHP",
551+
sdk_version=1,
552+
field="authors",
553+
),
554+
]
555+
for e in expected:
556+
assert e in errors
557+
558+
503559
def test_verify_load_successful():
504560
actual, errors = load(
505561
TEST_RESOURCES_PATH / "valid_metadata.yaml",

aws_doc_sdk_examples_tools/metadata_validator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import re
1515
import xml.etree.ElementTree as xml_tree
1616
import yaml
17+
import yaml.parser
1718
from dataclasses import dataclass, field
1819
from pathlib import Path
1920
from typing import Any, Dict, Iterable, List, Optional, Set
@@ -200,6 +201,8 @@ def validate_files(
200201
data = yamale.make_data(meta_name)
201202
yamale.validate(schema, data, strict=strict)
202203
print(f"{meta_name.resolve()} validation success! 👍")
204+
except yaml.parser.ParserError as e:
205+
pass # YAML parse errors are found and reported by the DocGen validator so we won't report them here.
203206
except YamaleError as e:
204207
errors.append(ValidateYamaleError(file=meta_name, yamale_error=e))
205208
return errors

aws_doc_sdk_examples_tools/snippets.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,10 @@ class MissingSnippetFile(MetadataError):
261261
snippet_file: Optional[str] = None
262262

263263
def message(self):
264-
return f"missing snippet_file {self.snippet_file}"
264+
return (
265+
f"missing snippet_file {self.snippet_file}. The relative path to the snippet_file listed in the "
266+
f"metadata for this example must map to an actual file in the file system."
267+
)
265268

266269

267270
@dataclass

aws_doc_sdk_examples_tools/yaml_mapper.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
from .metadata_validator import StringExtension
1919

2020

21+
CATEGORY_REQUIRED_FIELDS = {"IAMPolicy": {"version": {"authors", "owner", "source"}}}
22+
23+
2124
def example_from_yaml(
2225
yaml: Any,
2326
sdks: Dict[str, Sdk],
@@ -83,7 +86,7 @@ def example_from_yaml(
8386
else:
8487
for name in yaml_languages:
8588
language, errs = language_from_yaml(
86-
name, yaml_languages[name], sdks, blocks, is_action
89+
name, yaml_languages[name], sdks, blocks, is_action, category
8790
)
8891
languages[language.name] = language
8992
errors.extend(errs)
@@ -146,6 +149,7 @@ def language_from_yaml(
146149
sdks: Dict[str, Sdk],
147150
blocks: Set[str],
148151
is_action: bool,
152+
category: str,
149153
) -> Tuple[Language, MetadataErrors]:
150154
errors = MetadataErrors()
151155
if name not in sdks:
@@ -161,7 +165,7 @@ def language_from_yaml(
161165

162166
versions: List[Version] = []
163167
for version in yaml_versions:
164-
vers, version_errors = version_from_yaml(version, blocks, is_action)
168+
vers, version_errors = version_from_yaml(version, blocks, is_action, category)
165169
errors.extend(version_errors)
166170
versions.append(vers)
167171

@@ -222,9 +226,14 @@ def version_from_yaml(
222226
yaml: Dict[str, Any],
223227
cross_content_blocks: Set[str],
224228
is_action: bool,
229+
category: str,
225230
) -> Tuple["Version", MetadataErrors]:
226231
errors = MetadataErrors()
227232

233+
for field in CATEGORY_REQUIRED_FIELDS.get(category, {}).get("version", {}):
234+
if not yaml.get(field):
235+
errors.append(metadata_errors.MissingField(field=field))
236+
228237
sdk_version = int(yaml.get("sdk_version", 0))
229238
if sdk_version == 0:
230239
errors.append(metadata_errors.MissingField(field="sdk_version"))

aws_doc_sdk_examples_tools/yaml_writer_test.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ def test_doc_gen(sample_doc_gen: DocGen):
1818
writes = prepare_write(sample_doc_gen.examples)
1919
assert writes
2020

21-
writes = {k.replace(str(ROOT) + "/", ""): v for k, v in writes.items()}
21+
writes = {
22+
Path(k.replace(str(ROOT), "")).as_posix().lstrip("/"): v
23+
for k, v in writes.items()
24+
}
2225

2326
expected_writes = {
2427
".doc_gen/metadata/aws_entity_metadata.yaml": """sns_EntitySuccesses:

0 commit comments

Comments
 (0)