diff --git a/aws_doc_sdk_examples_tools/categories.py b/aws_doc_sdk_examples_tools/categories.py new file mode 100644 index 0000000..b292389 --- /dev/null +++ b/aws_doc_sdk_examples_tools/categories.py @@ -0,0 +1,115 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict, List, Optional +from dataclasses import dataclass, field + +from aws_doc_sdk_examples_tools import metadata_errors +from .metadata_errors import ( + MetadataErrors, +) + + +@dataclass +class TitleInfo: + title: Optional[str] = field(default=None) + title_abbrev: Optional[str] = field(default=None) + synopsis: Optional[str] = field(default=None) + title_suffixes: str | Dict[str, str] = field(default_factory=dict) + + @classmethod + def from_yaml(cls, yaml: Dict[str, str] | None) -> Optional[TitleInfo]: + if yaml is None: + return None + + title = yaml.get("title") + title_suffixes: str | Dict[str, str] = yaml.get("title_suffixes", {}) + title_abbrev = yaml.get("title_abbrev") + synopsis = yaml.get("synopsis") + + return cls( + title=title, + title_suffixes=title_suffixes, + title_abbrev=title_abbrev, + synopsis=synopsis, + ) + + +@dataclass +class CategoryWithNoDisplayError(metadata_errors.MetadataError): + def message(self): + return "Category has no display value" + + +@dataclass +class Category: + key: str + display: str + defaults: Optional[TitleInfo] = field(default=None) + overrides: Optional[TitleInfo] = field(default=None) + description: Optional[str] = field(default=None) + + def validate(self, errors: MetadataErrors): + if not self.display: + errors.append(CategoryWithNoDisplayError(id=self.key)) + + @classmethod + def from_yaml( + cls, key: str, yaml: Dict[str, Any] + ) -> tuple[Category, MetadataErrors]: + errors = MetadataErrors() + display = str(yaml.get("display")) + defaults = TitleInfo.from_yaml(yaml.get("defaults")) + overrides = TitleInfo.from_yaml(yaml.get("overrides")) + description = yaml.get("description") + + return ( + cls( + key=key, + display=display, + defaults=defaults, + overrides=overrides, + description=description, + ), + errors, + ) + + +def parse( + file: Path, yaml: Dict[str, Any] +) -> tuple[List[str], Dict[str, Category], MetadataErrors]: + categories: Dict[str, Category] = {} + errors = MetadataErrors() + + standard_cats = yaml.get("standard_categories", []) + # Work around inconsistency where some tools use 'Actions' and DocGen uses 'Api' to refer to single-action examples. + for i in range(len(standard_cats)): + if standard_cats[i] == "Actions": + standard_cats[i] = "Api" + for key, yaml_cat in yaml.get("categories", {}).items(): + if yaml_cat is None: + errors.append(metadata_errors.MissingCategoryBody(id=key, file=file)) + else: + category, cat_errs = Category.from_yaml(key, yaml_cat) + categories[key] = category + for error in cat_errs: + error.file = file + error.id = key + errors.extend(cat_errs) + + return standard_cats, categories, errors + + +if __name__ == "__main__": + from pprint import pp + import yaml + + path = Path(__file__).parent / "config" / "categories.yaml" + with open(path) as file: + meta = yaml.safe_load(file) + standard_cats, cats, errs = parse(path, meta) + pp(standard_cats) + pp(cats) diff --git a/aws_doc_sdk_examples_tools/categories_test.py b/aws_doc_sdk_examples_tools/categories_test.py new file mode 100644 index 0000000..603b222 --- /dev/null +++ b/aws_doc_sdk_examples_tools/categories_test.py @@ -0,0 +1,73 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path +from typing import Dict, List, Tuple +import pytest +import yaml + +from aws_doc_sdk_examples_tools import metadata_errors +from .categories import ( + parse, + Category, + TitleInfo, +) + + +def load( + path: str, +) -> Tuple[List[str], Dict[str, Category], metadata_errors.MetadataErrors]: + root = Path(__file__).parent + filename = root / "test_resources" / path + with open(filename) as file: + meta = yaml.safe_load(file) + return parse(filename, meta) + + +def test_empty_categories(): + _, _, errs = load("empty_categories.yaml") + assert [*errs] == [ + metadata_errors.MissingCategoryBody( + file=Path(__file__).parent / "test_resources/empty_categories.yaml", + id="EmptyCat", + ) + ] + + +def test_categories(): + _, categories, _ = load("categories.yaml") + assert categories == { + "Actions": Category( + key="Actions", + display="Actions test", + overrides=TitleInfo( + title="Title override", + title_suffixes={ + "cli": " with a CLI", + "sdk": " with an &AWS; SDK", + "sdk_cli": " with an &AWS; SDK or CLI", + }, + title_abbrev="Title abbrev override", + synopsis="synopsis test.", + ), + description="test description.", + ), + "Basics": Category( + key="Basics", + display="Basics", + defaults=TitleInfo( + title="Title default", + title_abbrev="Title abbrev default", + ), + description="default description.", + ), + "TributaryLite": Category( + key="TributaryLite", + display="Tea light", + description="light your way.", + ), + } + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/aws_doc_sdk_examples_tools/config/categories.yaml b/aws_doc_sdk_examples_tools/config/categories.yaml new file mode 100644 index 0000000..8a9fd4d --- /dev/null +++ b/aws_doc_sdk_examples_tools/config/categories.yaml @@ -0,0 +1,34 @@ +standard_categories: ["Hello", "Basics", "Actions", "Scenarios"] +categories: + Hello: + display: "Hello" + overrides: + title: "Hello {{.ServiceEntity.Short}}" + title_abbrev: "Hello {{.ServiceEntity.Short}}" + synopsis: "get started using {{.ServiceEntity.Short}}." + Actions: + display: "Actions" + overrides: + title: "Use {{.Action}}" + title_suffixes: + cli: " with a CLI" + sdk: " with an &AWS; SDK" + sdk_cli: " with an &AWS; SDK or CLI" + title_abbrev: "{{.Action}}" + synopsis: "use {{.Action}}." + description: "are code excerpts from larger programs and must be run in context. While actions + show you how to call individual service functions, you can see actions in context in their related scenarios." + Basics: + display: "Basics" + defaults: + title: "Learn the basics of {{.ServiceEntity.Short}} with an &AWS; SDK" + title_abbrev: "Learn the basics" + description: "are code examples that show you how to perform the essential operations within a service." + Scenarios: + display: "Scenarios" + description: "are code examples that show you how to accomplish specific tasks by + calling multiple functions within a service or combined with other &AWS-services;." + TributaryLite: + display: "&AWS; community contributions" + description: "are examples that were created and are maintained by multiple teams across &AWS;. + To provide feedback, use the mechanism provided in the linked repositories." diff --git a/aws_doc_sdk_examples_tools/doc_gen.py b/aws_doc_sdk_examples_tools/doc_gen.py index 0ce6a41..52ee8eb 100644 --- a/aws_doc_sdk_examples_tools/doc_gen.py +++ b/aws_doc_sdk_examples_tools/doc_gen.py @@ -12,6 +12,7 @@ # from os import glob +from .categories import Category, parse as parse_categories from .metadata import ( Example, DocFilenames, @@ -55,6 +56,8 @@ class DocGen: validation: ValidationConfig = field(default_factory=ValidationConfig) sdks: Dict[str, Sdk] = field(default_factory=dict) services: Dict[str, Service] = field(default_factory=dict) + standard_categories: List[str] = field(default_factory=list) + categories: Dict[str, Category] = field(default_factory=dict) snippets: Dict[str, Snippet] = field(default_factory=dict) snippet_files: Set[str] = field(default_factory=set) examples: Dict[str, Example] = field(default_factory=dict) @@ -201,6 +204,19 @@ def for_root( except Exception: pass + try: + categories_path = config / "categories.yaml" + with categories_path.open(encoding="utf-8") as file: + meta = yaml.safe_load(file) + standard_categories, categories, errs = parse_categories( + categories_path, meta + ) + self.standard_categories = standard_categories + self.categories = categories + self.errors.extend(errs) + except Exception: + pass + try: entities_config_path = config / "entities.yaml" with entities_config_path.open(encoding="utf-8") as file: @@ -236,6 +252,7 @@ def process_metadata(self, path: Path) -> "DocGen": yaml.safe_load(file), self.sdks, self.services, + self.standard_categories, self.cross_blocks, self.validation, self.root, @@ -268,6 +285,8 @@ def validate(self): sdk.validate(self.errors) for service in self.services.values(): service.validate(self.errors) + for category in self.categories.values(): + category.validate(self.errors) for example in self.examples.values(): example.validate(self.errors, self.root) validate_metadata(self.root, self.validation.strict_titles, self.errors) @@ -339,6 +358,7 @@ def parse_examples( yaml: Dict[str, Any], sdks: Dict[str, Sdk], services: Dict[str, Service], + standard_categories: List[str], blocks: Set[str], validation: Optional[ValidationConfig], root: Optional[Path] = None, @@ -350,12 +370,13 @@ def parse_examples( example, example_errors = example_from_yaml( yaml[id], sdks, services, blocks, validation, root or file.parent ) - check_id_format( - id, - example.services, - validation.strict_titles and example.category == "Api", - example_errors, - ) + if example.category in standard_categories: + check_id_format( + id, + example.services, + validation.strict_titles and example.category == "Api", + example_errors, + ) for error in example_errors: error.file = file error.id = id @@ -411,7 +432,7 @@ def get_doc_filenames(example_id: str, example: Example) -> Optional[DocFilename ) ) else: - anchor = "actions" if example.category == "Actions" else "scenarios" + anchor = "actions" if example.category == "Api" else "scenarios" sdk_pages[language.property][version.sdk_version] = SDKPageVersion( actions_scenarios={ service_id: f"{base_url}/{language.property}_{version.sdk_version}_{service_id}_code_examples.html#{anchor}" diff --git a/aws_doc_sdk_examples_tools/metadata_errors.py b/aws_doc_sdk_examples_tools/metadata_errors.py index 0859a2f..08dc4cb 100644 --- a/aws_doc_sdk_examples_tools/metadata_errors.py +++ b/aws_doc_sdk_examples_tools/metadata_errors.py @@ -370,6 +370,12 @@ def message(self): return f"URL {self.url} is missing a title" +@dataclass +class MissingCategoryBody(MetadataParseError): + def message(self): + return "category definition missing body" + + @dataclass class ExampleMergeMismatchedId(MetadataError): other_id: str = "" diff --git a/aws_doc_sdk_examples_tools/metadata_test.py b/aws_doc_sdk_examples_tools/metadata_test.py index 6d7ceb4..c7f705b 100644 --- a/aws_doc_sdk_examples_tools/metadata_test.py +++ b/aws_doc_sdk_examples_tools/metadata_test.py @@ -33,7 +33,13 @@ def load( with path.open() as file: meta = yaml.safe_load(file) return parse_examples( - path, meta, doc_gen.sdks, doc_gen.services, blocks, doc_gen.validation + path, + meta, + doc_gen.sdks, + doc_gen.services, + doc_gen.standard_categories, + blocks, + doc_gen.validation, ) @@ -88,12 +94,14 @@ def load( "JavaScript": Sdk(name="JavaScript", versions=[], guide="", property="javascript"), "PHP": Sdk(name="PHP", versions=[], guide="", property="php"), } +STANDARD_CATS = ["Api"] DOC_GEN = DocGen( root=Path(), errors=metadata_errors.MetadataErrors(), validation=ValidationConfig(), services=SERVICES, sdks=SDKS, + standard_categories=STANDARD_CATS, ) GOOD_SINGLE_CPP = """ @@ -118,7 +126,13 @@ def load( def test_parse(): meta = yaml.safe_load(GOOD_SINGLE_CPP) parsed, errors = parse_examples( - Path("test_cpp.yaml"), meta, SDKS, SERVICES, set(), DOC_GEN.validation + Path("test_cpp.yaml"), + meta, + SDKS, + SERVICES, + STANDARD_CATS, + set(), + DOC_GEN.validation, ) assert len(errors) == 0 assert len(parsed) == 1 @@ -140,7 +154,7 @@ def test_parse(): example = Example( file=Path("test_cpp.yaml"), id="medical-imaging_CreateDatastore", - category="Cross", + category="Scenarios", services={ "medical-imaging": set(["Operation1", "Operation2"]), "api-gateway": set(["Operation1", "Operation2"]), @@ -220,6 +234,7 @@ def test_parse_strict_titles(): meta, SDKS, SERVICES, + STANDARD_CATS, set(), ValidationConfig(strict_titles=True), ) @@ -259,7 +274,7 @@ def test_parse_strict_titles(): actions_scenarios={ "medical-imaging": make_doc_link( stub="cpp_1_medical-imaging_code_examples", - anchor="scenarios", + anchor="actions", ), } ) @@ -354,6 +369,7 @@ def test_parse_strict_title_errors(): meta, SDKS, SERVICES, + STANDARD_CATS, set(), ValidationConfig(strict_titles=True), ) @@ -401,6 +417,7 @@ def test_parse_cross(): meta, SDKS, SERVICES, + STANDARD_CATS, set(["cross_DeleteTopic_block.xml"]), DOC_GEN.validation, ) @@ -600,12 +617,6 @@ def test_verify_load_successful(): file=EMPTY_METADATA_PATH, id="medical-imaging_EmptyExample", ), - metadata_errors.ServiceNameFormat( - file=EMPTY_METADATA_PATH, - id="medical-imaging_EmptyExample", - svc="medical-imaging", - svcs=[], - ), ], [], ), diff --git a/aws_doc_sdk_examples_tools/test_resources/categories.yaml b/aws_doc_sdk_examples_tools/test_resources/categories.yaml new file mode 100644 index 0000000..c71fbe0 --- /dev/null +++ b/aws_doc_sdk_examples_tools/test_resources/categories.yaml @@ -0,0 +1,21 @@ +categories: + Actions: + display: "Actions test" + overrides: + title: "Title override" + title_suffixes: + cli: " with a CLI" + sdk: " with an &AWS; SDK" + sdk_cli: " with an &AWS; SDK or CLI" + title_abbrev: "Title abbrev override" + synopsis: "synopsis test." + description: "test description." + Basics: + display: "Basics" + defaults: + title: "Title default" + title_abbrev: "Title abbrev default" + description: "default description." + TributaryLite: + display: "Tea light" + description: "light your way." diff --git a/aws_doc_sdk_examples_tools/test_resources/empty_categories.yaml b/aws_doc_sdk_examples_tools/test_resources/empty_categories.yaml new file mode 100644 index 0000000..d171f0d --- /dev/null +++ b/aws_doc_sdk_examples_tools/test_resources/empty_categories.yaml @@ -0,0 +1,2 @@ +categories: + EmptyCat: diff --git a/aws_doc_sdk_examples_tools/test_resources/formaterror_metadata.yaml b/aws_doc_sdk_examples_tools/test_resources/formaterror_metadata.yaml index 9f6d95c..569daa2 100644 --- a/aws_doc_sdk_examples_tools/test_resources/formaterror_metadata.yaml +++ b/aws_doc_sdk_examples_tools/test_resources/formaterror_metadata.yaml @@ -2,7 +2,6 @@ WrongNameFormat: title: Test title title_abbrev: Test title abbrev synopsis: Test synopsis - category: Test languages: Java: versions: diff --git a/aws_doc_sdk_examples_tools/yaml_mapper.py b/aws_doc_sdk_examples_tools/yaml_mapper.py index 5e205dc..5c08351 100644 --- a/aws_doc_sdk_examples_tools/yaml_mapper.py +++ b/aws_doc_sdk_examples_tools/yaml_mapper.py @@ -42,7 +42,7 @@ def example_from_yaml( parsed_services = parse_services(yaml.get("services", {}), errors, services) category = yaml.get("category", "") if category == "": - category = "Api" if len(parsed_services) == 1 else "Cross" + category = "Api" if len(parsed_services) == 1 else "Scenarios" is_action = category == "Api" is_basics = category == "Basics"