From 102a74b9147faada66ab3924d72da69889254785 Mon Sep 17 00:00:00 2001 From: jonathan343 <43360731+jonathan343@users.noreply.github.com> Date: Tue, 28 Feb 2023 07:46:27 -0800 Subject: [PATCH] Implemented Nested Docs. (#3597) * Implemented Nested Docs. * Manually included js/custom.js since Sphinx < 1.8 doesnt support custom js. * Added a new Resource section; Improved redirecting from old links. * Updated Resources notes & redirects; Updated copyright year. --- boto3/docs/__init__.py | 4 +- boto3/docs/action.py | 20 +- boto3/docs/base.py | 9 + boto3/docs/collection.py | 23 ++- boto3/docs/resource.py | 125 +++++++++--- boto3/docs/service.py | 80 ++++++-- boto3/docs/subresource.py | 27 ++- boto3/docs/utils.py | 5 +- boto3/docs/waiter.py | 24 ++- docs/source/_static/js/custom.js | 73 +++++++ docs/source/_templates/layout.html | 1 + docs/source/conf.py | 4 +- tests/functional/docs/__init__.py | 23 +++ tests/functional/docs/test_dynamodb.py | 118 +++++------- tests/functional/docs/test_ec2.py | 28 ++- tests/functional/docs/test_s3.py | 35 +++- tests/functional/docs/test_smoke.py | 116 +++++++---- tests/unit/docs/__init__.py | 14 +- tests/unit/docs/test_action.py | 60 +++++- tests/unit/docs/test_client.py | 101 ++++++---- tests/unit/docs/test_collection.py | 18 +- tests/unit/docs/test_resource.py | 164 +++++++++++----- tests/unit/docs/test_service.py | 254 +++++++++++++++++++------ tests/unit/docs/test_subresource.py | 19 +- tests/unit/docs/test_waiter.py | 16 +- 25 files changed, 1032 insertions(+), 329 deletions(-) create mode 100644 docs/source/_static/js/custom.js diff --git a/boto3/docs/__init__.py b/boto3/docs/__init__.py index d1cd8b52f4..261747b42f 100644 --- a/boto3/docs/__init__.py +++ b/boto3/docs/__init__.py @@ -32,7 +32,9 @@ def generate_docs(root_dir, session): os.makedirs(services_doc_path) for service_name in session.get_available_services(): - docs = ServiceDocumenter(service_name, session).document_service() + docs = ServiceDocumenter( + service_name, session, services_doc_path + ).document_service() service_doc_path = os.path.join( services_doc_path, service_name + '.rst' ) diff --git a/boto3/docs/action.py b/boto3/docs/action.py index 907f428663..acfcebf1b3 100644 --- a/boto3/docs/action.py +++ b/boto3/docs/action.py @@ -10,7 +10,10 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import os + from botocore import xform_name +from botocore.docs.bcdoc.restdoc import DocumentStructure from botocore.docs.method import ( document_custom_method, document_model_driven_method, @@ -18,7 +21,7 @@ from botocore.model import OperationModel from botocore.utils import get_service_module_name -from boto3.docs.base import BaseDocumenter +from boto3.docs.base import NestedDocumenter from boto3.docs.method import document_model_driven_resource_method from boto3.docs.utils import ( add_resource_type_overview, @@ -27,7 +30,7 @@ ) -class ActionDocumenter(BaseDocumenter): +class ActionDocumenter(NestedDocumenter): def document_actions(self, section): modeled_actions_list = self._resource_model.actions modeled_actions = {} @@ -49,7 +52,10 @@ def document_actions(self, section): ) for action_name in sorted(resource_actions): - action_section = section.add_new_section(action_name) + # Create a new DocumentStructure for each action and add contents. + action_doc = DocumentStructure(action_name, target='html') + action_doc.add_title_section(action_name) + action_section = action_doc.add_new_section(action_name) if action_name in ['load', 'reload'] and self._resource_model.load: document_load_reload_action( section=action_section, @@ -71,6 +77,14 @@ def document_actions(self, section): document_custom_method( action_section, action_name, resource_actions[action_name] ) + # Write actions in individual/nested files. + # Path: /reference/services///.rst + actions_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{self._resource_sub_path}', + ) + action_doc.write_to_file(actions_dir_path, action_name) def document_action( diff --git a/boto3/docs/base.py b/boto3/docs/base.py index a12d94543f..7945d0757d 100644 --- a/boto3/docs/base.py +++ b/boto3/docs/base.py @@ -30,3 +30,12 @@ def __init__(self, resource): @property def class_name(self): return f'{self._service_docs_name}.{self._resource_name}' + + +class NestedDocumenter(BaseDocumenter): + def __init__(self, resource, root_docs_path): + super().__init__(resource) + self._root_docs_path = root_docs_path + self._resource_sub_path = self._resource_name.lower() + if self._resource_name == self._service_name: + self._resource_sub_path = 'service-resource' diff --git a/boto3/docs/collection.py b/boto3/docs/collection.py index a1f9ca7faa..9de772cc6f 100644 --- a/boto3/docs/collection.py +++ b/boto3/docs/collection.py @@ -10,11 +10,14 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import os + from botocore import xform_name +from botocore.docs.bcdoc.restdoc import DocumentStructure from botocore.docs.method import get_instance_public_methods from botocore.docs.utils import DocumentedShape -from boto3.docs.base import BaseDocumenter +from boto3.docs.base import NestedDocumenter from boto3.docs.method import document_model_driven_resource_method from boto3.docs.utils import ( add_resource_type_overview, @@ -22,7 +25,7 @@ ) -class CollectionDocumenter(BaseDocumenter): +class CollectionDocumenter(NestedDocumenter): def document_collections(self, section): collections = self._resource.meta.resource_model.collections collections_list = [] @@ -37,10 +40,24 @@ def document_collections(self, section): ) self.member_map['collections'] = collections_list for collection in collections: - collection_section = section.add_new_section(collection.name) collections_list.append(collection.name) + # Create a new DocumentStructure for each collection and add contents. + collection_doc = DocumentStructure(collection.name, target='html') + collection_doc.add_title_section(collection.name) + collection_section = collection_doc.add_new_section( + collection.name + ) self._document_collection(collection_section, collection) + # Write collections in individual/nested files. + # Path: /reference/services///.rst + collections_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{self._resource_sub_path}', + ) + collection_doc.write_to_file(collections_dir_path, collection.name) + def _document_collection(self, section, collection): methods = get_instance_public_methods( getattr(self._resource, collection.name) diff --git a/boto3/docs/resource.py b/boto3/docs/resource.py index 7d5516e9c1..b4eef3c544 100644 --- a/boto3/docs/resource.py +++ b/boto3/docs/resource.py @@ -10,7 +10,10 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import os + from botocore import xform_name +from botocore.docs.bcdoc.restdoc import DocumentStructure from botocore.docs.utils import get_official_service_name from boto3.docs.action import ActionDocumenter @@ -32,14 +35,18 @@ class ResourceDocumenter(BaseDocumenter): - def __init__(self, resource, botocore_session): + def __init__(self, resource, botocore_session, root_docs_path): super().__init__(resource) self._botocore_session = botocore_session + self._root_docs_path = root_docs_path + self._resource_sub_path = self._resource_name.lower() + if self._resource_name == self._service_name: + self._resource_sub_path = 'service-resource' def document_resource(self, section): self._add_title(section) + self._add_resource_note(section) self._add_intro(section) - overview_section = section.add_new_section('member-overview') self._add_identifiers(section) self._add_attributes(section) self._add_references(section) @@ -47,7 +54,6 @@ def document_resource(self, section): self._add_sub_resources(section) self._add_collections(section) self._add_waiters(section) - self._add_overview_of_members(overview_section) def _add_title(self, section): section.style.h2(self._resource_name) @@ -60,23 +66,27 @@ def _add_intro(self, section): # Write out the class signature. class_args = get_identifier_args_for_signature(identifier_names) - section.style.start_sphinx_py_class( + start_class = section.add_new_section('start_class') + start_class.style.start_sphinx_py_class( class_name=f'{self.class_name}({class_args})' ) # Add as short description about the resource - description_section = section.add_new_section('description') + description_section = start_class.add_new_section('description') self._add_description(description_section) # Add an example of how to instantiate the resource - example_section = section.add_new_section('example') + example_section = start_class.add_new_section('example') self._add_example(example_section, identifier_names) # Add the description for the parameters to instantiate the # resource. - param_section = section.add_new_section('params') + param_section = start_class.add_new_section('params') self._add_params_description(param_section, identifier_names) + end_class = section.add_new_section('end_class') + end_class.style.end_sphinx_py_class() + def _add_description(self, section): official_service_name = get_official_service_name(self._service_model) section.write( @@ -118,23 +128,15 @@ def _add_params_description(self, section, identifier_names): section.write(f':param {identifier_name}: {description}') section.style.new_line() - def _add_overview_of_members(self, section): - for resource_member_type in self.member_map: - section.style.new_line() - section.write( - f'These are the resource\'s available {resource_member_type}:' - ) - section.style.new_line() - for member in self.member_map[resource_member_type]: - if resource_member_type in ( - 'attributes', - 'collections', - 'identifiers', - 'references', - ): - section.style.li(f':py:attr:`{member}`') - else: - section.style.li(f':py:meth:`{member}()`') + def _add_overview_of_member_type(self, section, resource_member_type): + section.style.new_line() + section.write( + f'These are the resource\'s available {resource_member_type}:' + ) + section.style.new_line() + section.style.toctree() + for member in self.member_map[resource_member_type]: + section.style.tocitem(f'{member}') def _add_identifiers(self, section): identifiers = self._resource.meta.resource_model.identifiers @@ -152,13 +154,29 @@ def _add_identifiers(self, section): intro_link='identifiers_attributes_intro', ) for identifier in identifiers: - identifier_section = section.add_new_section(identifier.name) member_list.append(identifier.name) + # Create a new DocumentStructure for each identifier and add contents. + identifier_doc = DocumentStructure(identifier.name, target='html') + identifier_doc.add_title_section(identifier.name) + identifier_section = identifier_doc.add_new_section( + identifier.name + ) document_identifier( section=identifier_section, resource_name=self._resource_name, identifier_model=identifier, ) + # Write identifiers in individual/nested files. + # Path: /reference/services///.rst + identifiers_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{self._resource_sub_path}', + ) + identifier_doc.write_to_file(identifiers_dir_path, identifier.name) + + if identifiers: + self._add_overview_of_member_type(section, 'identifiers') def _add_attributes(self, section): service_model = self._resource.meta.client.meta.service_model @@ -187,8 +205,11 @@ def _add_attributes(self, section): self.member_map['attributes'] = attribute_list for attr_name in sorted(attributes): _, attr_shape = attributes[attr_name] - attribute_section = section.add_new_section(attr_name) attribute_list.append(attr_name) + # Create a new DocumentStructure for each attribute and add contents. + attribute_doc = DocumentStructure(attr_name, target='html') + attribute_doc.add_title_section(attr_name) + attribute_section = attribute_doc.add_new_section(attr_name) document_attribute( section=attribute_section, service_name=self._service_name, @@ -197,6 +218,16 @@ def _add_attributes(self, section): event_emitter=self._resource.meta.client.meta.events, attr_model=attr_shape, ) + # Write attributes in individual/nested files. + # Path: /reference/services///.rst + attributes_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{self._resource_sub_path}', + ) + attribute_doc.write_to_file(attributes_dir_path, attr_name) + if attributes: + self._add_overview_of_member_type(section, 'attributes') def _add_references(self, section): section = section.add_new_section('references') @@ -213,36 +244,57 @@ def _add_references(self, section): intro_link='references_intro', ) self.member_map['references'] = reference_list + self._add_overview_of_member_type(section, 'references') for reference in references: - reference_section = section.add_new_section(reference.name) reference_list.append(reference.name) + # Create a new DocumentStructure for each reference and add contents. + reference_doc = DocumentStructure(reference.name, target='html') + reference_doc.add_title_section(reference.name) + reference_section = reference_doc.add_new_section(reference.name) document_reference( section=reference_section, reference_model=reference ) + # Write references in individual/nested files. + # Path: /reference/services///.rst + references_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{self._resource_sub_path}', + ) + reference_doc.write_to_file(references_dir_path, reference.name) + if references: + self._add_overview_of_member_type(section, 'references') def _add_actions(self, section): section = section.add_new_section('actions') actions = self._resource.meta.resource_model.actions if actions: - documenter = ActionDocumenter(self._resource) + documenter = ActionDocumenter(self._resource, self._root_docs_path) documenter.member_map = self.member_map documenter.document_actions(section) + self._add_overview_of_member_type(section, 'actions') def _add_sub_resources(self, section): section = section.add_new_section('sub-resources') sub_resources = self._resource.meta.resource_model.subresources if sub_resources: - documenter = SubResourceDocumenter(self._resource) + documenter = SubResourceDocumenter( + self._resource, self._root_docs_path + ) documenter.member_map = self.member_map documenter.document_sub_resources(section) + self._add_overview_of_member_type(section, 'sub-resources') def _add_collections(self, section): section = section.add_new_section('collections') collections = self._resource.meta.resource_model.collections if collections: - documenter = CollectionDocumenter(self._resource) + documenter = CollectionDocumenter( + self._resource, self._root_docs_path + ) documenter.member_map = self.member_map documenter.document_collections(section) + self._add_overview_of_member_type(section, 'collections') def _add_waiters(self, section): section = section.add_new_section('waiters') @@ -252,10 +304,21 @@ def _add_waiters(self, section): self._service_name ) documenter = WaiterResourceDocumenter( - self._resource, service_waiter_model + self._resource, service_waiter_model, self._root_docs_path ) documenter.member_map = self.member_map documenter.document_resource_waiters(section) + self._add_overview_of_member_type(section, 'waiters') + + def _add_resource_note(self, section): + section = section.add_new_section('feature-freeze') + section.style.start_note() + section.write( + "Before using anything on this page, please refer to the resources " + ":doc:`user guide <../../../../guide/resources>` for the most recent " + "guidance on using resources." + ) + section.style.end_note() class ServiceResourceDocumenter(ResourceDocumenter): diff --git a/boto3/docs/service.py b/boto3/docs/service.py index 56a4e6360d..6d3b31d5d6 100644 --- a/boto3/docs/service.py +++ b/boto3/docs/service.py @@ -26,12 +26,13 @@ class ServiceDocumenter(BaseServiceDocumenter): # The path used to find examples EXAMPLE_PATH = os.path.join(os.path.dirname(boto3.__file__), 'examples') - def __init__(self, service_name, session): + def __init__(self, service_name, session, root_docs_path): super().__init__( service_name=service_name, # I know that this is an internal attribute, but the botocore session # is needed to load the paginator and waiter models. session=session._session, + root_docs_path=root_docs_path, ) self._boto3_session = session self._client = self._boto3_session.client(service_name) @@ -44,10 +45,14 @@ def __init__(self, service_name, session): 'client', 'paginators', 'waiters', - 'service-resource', 'resources', 'examples', ] + self._root_docs_path = root_docs_path + self._USER_GUIDE_LINK = ( + 'https://boto3.amazonaws.com/' + 'v1/documentation/api/latest/guide/resources.html' + ) def document_service(self): """Documents an entire service. @@ -64,10 +69,7 @@ def document_service(self): self.paginator_api(doc_structure.get_section('paginators')) self.waiter_api(doc_structure.get_section('waiters')) if self._service_resource: - self._document_service_resource( - doc_structure.get_section('service-resource') - ) - self._document_resources(doc_structure.get_section('resources')) + self.resource_section(doc_structure.get_section('resources')) self._document_examples(doc_structure.get_section('examples')) return doc_structure.flush_structure() @@ -78,12 +80,52 @@ def client_api(self, section): except DataNotFoundError: pass - Boto3ClientDocumenter(self._client, examples).document_client(section) + Boto3ClientDocumenter( + self._client, self._root_docs_path, examples + ).document_client(section) + + def resource_section(self, section): + section.style.h2('Resources') + section.style.new_line() + section.write( + 'Resources are available in boto3 via the ' + '``resource`` method. For more detailed instructions ' + 'and examples on the usage of resources, see the ' + 'resources ' + ) + section.style.external_link( + title='user guide', + link=self._USER_GUIDE_LINK, + ) + section.write('.') + section.style.new_line() + section.style.new_line() + section.write('The available resources are:') + section.style.new_line() + section.style.toctree() + self._document_service_resource(section) + self._document_resources(section) def _document_service_resource(self, section): + # Create a new DocumentStructure for each Service Resource and add contents. + service_resource_doc = DocumentStructure( + 'service-resource', target='html' + ) ServiceResourceDocumenter( - self._service_resource, self._session - ).document_resource(section) + self._service_resource, self._session, self._root_docs_path + ).document_resource(service_resource_doc) + # Write collections in individual/nested files. + # Path: /reference/services///.rst + resource_name = self._service_resource.meta.resource_model.name + if resource_name == self._service_name: + resource_name = 'service-resource' + service_resource_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{resource_name.lower()}', + ) + service_resource_doc.write_to_file(service_resource_dir_path, 'index') + section.style.tocitem(f'{self._service_name}/{resource_name}/index') def _document_resources(self, section): temp_identifier_value = 'foo' @@ -113,8 +155,24 @@ def _document_resources(self, section): for _ in identifiers: args.append(temp_identifier_value) resource = resource_cls(*args, client=self._client) - ResourceDocumenter(resource, self._session).document_resource( - section.add_new_section(resource.meta.resource_model.name) + # Create a new DocumentStructure for each Resource and add contents. + resource_name = resource.meta.resource_model.name.lower() + resource_doc = DocumentStructure(resource_name, target='html') + ResourceDocumenter( + resource, self._session, self._root_docs_path + ).document_resource( + resource_doc.add_new_section(resource.meta.resource_model.name) + ) + # Write collections in individual/nested files. + # Path: /reference/services///.rst + service_resource_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{resource_name}', + ) + resource_doc.write_to_file(service_resource_dir_path, 'index') + section.style.tocitem( + f'{self._service_name}/{resource_name}/index' ) def _get_example_file(self): diff --git a/boto3/docs/subresource.py b/boto3/docs/subresource.py index e22311c814..b0c4ff4a69 100644 --- a/boto3/docs/subresource.py +++ b/boto3/docs/subresource.py @@ -10,10 +10,13 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import os + from botocore import xform_name +from botocore.docs.bcdoc.restdoc import DocumentStructure from botocore.utils import get_service_module_name -from boto3.docs.base import BaseDocumenter +from boto3.docs.base import NestedDocumenter from boto3.docs.utils import ( add_resource_type_overview, get_identifier_args_for_signature, @@ -22,7 +25,7 @@ ) -class SubResourceDocumenter(BaseDocumenter): +class SubResourceDocumenter(NestedDocumenter): def document_sub_resources(self, section): add_resource_type_overview( section=section, @@ -41,8 +44,15 @@ def document_sub_resources(self, section): sub_resources_list = [] self.member_map['sub-resources'] = sub_resources_list for sub_resource in sub_resources: - sub_resource_section = section.add_new_section(sub_resource.name) sub_resources_list.append(sub_resource.name) + # Create a new DocumentStructure for each sub_resource and add contents. + sub_resource_doc = DocumentStructure( + sub_resource.name, target='html' + ) + sub_resource_doc.add_title_section(sub_resource.name) + sub_resource_section = sub_resource_doc.add_new_section( + sub_resource.name + ) document_sub_resource( section=sub_resource_section, resource_name=self._resource_name, @@ -50,6 +60,17 @@ def document_sub_resources(self, section): service_model=self._service_model, ) + # Write sub_resources in individual/nested files. + # Path: /reference/services///.rst + sub_resources_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{self._resource_sub_path}', + ) + sub_resource_doc.write_to_file( + sub_resources_dir_path, sub_resource.name + ) + def document_sub_resource( section, diff --git a/boto3/docs/utils.py b/boto3/docs/utils.py index 13a098dd61..0830af5052 100644 --- a/boto3/docs/utils.py +++ b/boto3/docs/utils.py @@ -71,10 +71,7 @@ def add_resource_type_overview( section, resource_type, description, intro_link=None ): section.style.new_line() - section.write('.. rst-class:: admonition-title') - section.style.new_line() - section.style.new_line() - section.write(resource_type) + section.style.h3(resource_type) section.style.new_line() section.style.new_line() section.write(description) diff --git a/boto3/docs/waiter.py b/boto3/docs/waiter.py index 255d850bc3..559447cab5 100644 --- a/boto3/docs/waiter.py +++ b/boto3/docs/waiter.py @@ -10,20 +10,23 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import os + from botocore import xform_name +from botocore.docs.bcdoc.restdoc import DocumentStructure from botocore.docs.method import document_model_driven_method from botocore.utils import get_service_module_name -from boto3.docs.base import BaseDocumenter +from boto3.docs.base import NestedDocumenter from boto3.docs.utils import ( add_resource_type_overview, get_resource_ignore_params, ) -class WaiterResourceDocumenter(BaseDocumenter): - def __init__(self, resource, service_waiter_model): - super().__init__(resource) +class WaiterResourceDocumenter(NestedDocumenter): + def __init__(self, resource, service_waiter_model, root_docs_path): + super().__init__(resource, root_docs_path) self._service_waiter_model = service_waiter_model def document_resource_waiters(self, section): @@ -40,8 +43,11 @@ def document_resource_waiters(self, section): waiter_list = [] self.member_map['waiters'] = waiter_list for waiter in waiters: - waiter_section = section.add_new_section(waiter.name) waiter_list.append(waiter.name) + # Create a new DocumentStructure for each waiter and add contents. + waiter_doc = DocumentStructure(waiter.name, target='html') + waiter_doc.add_title_section(waiter.name) + waiter_section = waiter_doc.add_new_section(waiter.name) document_resource_waiter( section=waiter_section, resource_name=self._resource_name, @@ -50,6 +56,14 @@ def document_resource_waiters(self, section): resource_waiter_model=waiter, service_waiter_model=self._service_waiter_model, ) + # Write waiters in individual/nested files. + # Path: /reference/services///.rst + waiters_dir_path = os.path.join( + self._root_docs_path, + f'{self._service_name}', + f'{self._resource_sub_path}', + ) + waiter_doc.write_to_file(waiters_dir_path, waiter.name) def document_resource_waiter( diff --git a/docs/source/_static/js/custom.js b/docs/source/_static/js/custom.js new file mode 100644 index 0000000000..3a5b1be1fe --- /dev/null +++ b/docs/source/_static/js/custom.js @@ -0,0 +1,73 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const nonResourceSubHeadings = [ + 'client', + 'waiters', + 'paginators', + 'resources', + 'examples' +]; +// Checks if an html doc name matches a service class name. +function isValidServiceName(docName, serviceClassName) { + const newDocName = docName.replaceAll('-', '').toLowerCase(); + return newDocName === serviceClassName; +} +// Checks if all elements of the split fragment are valid. +// Fragment items should only contain alphanumerics, hyphens, & underscores. +// A fragment should also only be redirected if it contain 3-5 items. +function isValidFragment(splitFragment) { + const regex = /^[a-z0-9-_]+$/i; + for (index in splitFragment) { + if (!regex.test(splitFragment[index])) { + return false; + } + } + return splitFragment.length >= 1 && splitFragment.length < 5; +} +// Checks if a name is a possible resource name. +function isValidResource(name, serviceDocName) { + return name.replaceAll('-', '') !== serviceDocName && !nonResourceSubHeadings.includes(name); +} +// Reroutes previously existing links to the new path. +// Old: /reference/services/s3.html#S3.Client.delete_bucket +// New: /reference/services/s3/client/delete_bucket.html +// This must be done client side since the fragment (#S3.Client.delete_bucket) is never +// passed to the server. +(function () { + const currentPath = window.location.pathname.split('/'); + const fragment = window.location.hash.substring(1); + const splitFragment = fragment.split('.').map(part => part.replace(/serviceresource/i, 'service-resource')); + // Only redirect when viewing a top-level service page. + if (isValidFragment(splitFragment) && currentPath[currentPath.length - 2] === 'services') { + const serviceDocName = currentPath[currentPath.length - 1].replace('.html', ''); + if (splitFragment.length > 1) { + splitFragment[0] = splitFragment[0].toLowerCase(); + splitFragment[1] = splitFragment[1].toLowerCase(); + } + let newPath; + if (splitFragment.length >= 3 && isValidServiceName(serviceDocName, splitFragment[0])) { + splitFragment[0] = serviceDocName; + newPath = `${ splitFragment.slice(0, 3).join('/') }.html#${ splitFragment.length > 3 ? fragment : '' }`; + } else if (splitFragment.length == 2 && isValidResource(splitFragment[1].toLowerCase(), serviceDocName)) { + newPath = `${ splitFragment.join('/') }/index.html#${ fragment }`; + } else if (splitFragment.length == 1 && isValidResource(splitFragment[0], serviceDocName)) { + newPath = `${ serviceDocName }/${ splitFragment.join('/') }/index.html`; + } else { + return; + } + window.location.assign(newPath); + } +}()); diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html index f7503fe37d..2241114db3 100644 --- a/docs/source/_templates/layout.html +++ b/docs/source/_templates/layout.html @@ -7,6 +7,7 @@ {%- block footer %}