From 0aa04ab5add0af11103a56e24ff71e49b43717df Mon Sep 17 00:00:00 2001 From: Daron Spektor Date: Fri, 8 Jun 2018 22:05:06 +0000 Subject: [PATCH] transition to apidoctor - rename to apidoctor - support full-fidelity edmx generation - support warning/error suppression - support link validation in xml files --- ApiDocs.Publishing/CSDL/CsdlWriter.cs | 826 --------- ApiDocs.Validation/OData/IODataNavigable.cs | 115 -- ApiDocs.Validation/ParameterDefinition.cs | 136 -- .../ApiDoctor.ConsoleApp.csproj | 18 +- .../ApiDoctor.nuspec | 13 +- .../App.config | 0 .../AppConfigFile.cs | 10 +- .../AppVeyor/BuildWorkerApi.cs | 4 +- .../Auth/BasicAccount.cs | 6 +- .../Auth/OAuthAccount.cs | 4 +- .../Auth/OAuthAccountException.cs | 2 +- .../Auth/OAuthTokenGenerator.cs | 4 +- .../CheckResults.cs | 8 +- .../CommandLineOptions.cs | 84 +- .../FancyConsole.cs | 4 +- .../GitHelper.cs | 4 +- .../Program.cs | 684 +++++-- .../Properties/AssemblyInfo.cs | 2 +- .../TestReport.cs | 10 +- .../WildcardExtensions.cs | 4 +- .../packages.config | 0 ...r.DocumentationGeneration.UnitTests.csproj | 16 +- .../DescriptionTests.cs | 6 +- .../DocumentationTestBase.cs | 6 +- .../NavigationPropertyTests.cs | 8 +- .../Properties/AssemblyInfo.cs | 4 +- .../PropertyTests.cs | 8 +- .../ResourceRoundTripTests.cs | 58 +- .../packages.config | 0 .../ApiDoctor.DocumentationGeneration.csproj | 12 +- .../DocumentationGenerator.cs | 10 +- .../Extensions/DocumentationExtensions.cs | 8 +- .../Model/DocumentationComplexType.cs | 8 +- .../Model/DocumentationEntityType.cs | 8 +- .../Model/DocumentationNavigationProperty.cs | 6 +- .../Model/DocumentationProperty.cs | 13 +- .../Properties/AssemblyInfo.cs | 6 +- .../Properties/Templates.Designer.cs | 4 +- .../Properties/Templates.resx | 0 .../Templates/resource.md.mustache | 0 .../packages.config | 0 .../ApiDoctor.Publishing.csproj | 9 +- .../ApiDoctor.Publishing.nuspec | 14 +- .../CSDL/ObjectGraphMerger.cs | 0 .../CSDL/csdlextensionmethods.cs | 85 +- ApiDoctor.Publishing/CSDL/csdlwriter.cs | 1590 +++++++++++++++++ .../CSDL/methodcollection.cs | 15 +- ApiDoctor.Publishing/CSDL/xmlsorter.cs | 168 ++ .../Html/ApiDocsConditionalTag.cs | 2 +- .../Html/DocumentPublisherHtml.cs | 10 +- .../Html/ExtendedElseTag.cs | 4 +- .../Html/FileTagDefinition.cs | 6 +- .../Html/HtmlMustacheWriter.cs | 8 +- .../Html/IfMatchTagDefinition.cs | 4 +- .../PathExtensionMethods.cs | 4 +- .../Properties/AssemblyInfo.cs | 0 .../Swagger/SwaggerClasses.cs | 4 +- .../Swagger/SwaggerExtensionMethods.cs | 12 +- .../Swagger/swaggerwriter.cs | 143 +- .../packages.config | 0 .../ApiDoctor.Validation.UnitTests.csproj | 8 +- .../BrokenLinkTests.cs | 56 +- .../DocFileForTesting.cs | 4 +- .../ExtensionMethods.cs | 6 +- .../JsonPathTest.cs | 6 +- .../JsonRewriteTests.cs | 8 +- .../MultipartMimeTests.cs | 4 +- .../NullableTests.cs | 22 +- .../ObjectGraphMergerTests.cs | 6 +- .../Properties/AssemblyInfo.cs | 4 +- .../Properties/Resources.Designer.cs | 4 +- .../Properties/Resources.resx | 0 .../ResourceStringValidationTests.cs | 46 +- .../SchemaBuilderTests.cs | 6 +- .../SchemaValidatorTests.cs | 87 +- .../packages.config | 0 .../test-docs/ExampleResources.md | 20 + .../test-docs/ExampleValidateResponse.md | 0 .../ExampleValidationSelectStatement.md | 0 ...ExampleValidationSelectStatementFailure.md | 0 .../IndentedCodeBlock/ComplexExample.md | 0 .../IndentedCodeBlock/IndentedCodeBlock.md | 0 .../IndentedCodeBlock/NonIndentedExample.md | 0 .../IndentedCodeBlock/SimpleBuLists.md | 0 .../ApiDoctor.Validation.csproj | 8 +- .../ApiDoctor.Validation.nuspec | 12 +- .../AuthScopeDefinition.cs | 4 +- .../AuthenicationCredentials.cs | 6 +- .../BackoffHelper.cs | 4 +- .../CodeBlockAnnotation.cs | 316 +++- .../Config/ApiRequirementsFile.cs | 4 +- .../Config/ConfigFile.cs | 9 +- .../Config/DocumentOutlineFile.cs | 4 +- .../DocFile.cs | 522 ++++-- .../DocSet.cs | 382 ++-- ApiDoctor.Validation/EnumerationDefinition.cs | 50 + ApiDoctor.Validation/Error/IssueLogger.cs | 284 +++ .../Error/ValidationMessage.cs | 4 +- .../Error/ValidationResult.cs | 4 +- .../Error/ValidationWarning.cs | 4 +- .../Error/validationerror.cs | 66 +- .../ErrorDefinition.cs | 4 +- .../ExampleDefinition.cs | 6 +- .../ExtensionMethods.cs | 291 ++- .../Http/HttpParser.cs | 4 +- .../Http/HttpParserRequestException.cs | 2 +- .../Http/HttpValidationExtensionMethods.cs | 32 +- .../Http/httprequest.cs | 13 +- .../Http/httpresponse.cs | 33 +- .../HttpLog/HttpLogGenerator.cs | 6 +- .../IServiceAccount.cs | 4 +- .../ItemDefinition.cs | 4 +- .../Json/JsonExample.cs | 4 +- .../Json/JsonPath.cs | 4 +- .../Json/JsonSchema.cs | 278 +-- .../Json/ValidationOptions.cs | 4 +- .../Json/jsonresourcecollection.cs | 86 +- .../Json/jsonrewriter.cs | 28 +- .../MetadataTransforms.cs | 4 +- .../MethodDefinition.cs | 155 +- .../MultipartMime/MimeContentType.cs | 4 +- .../MultipartMime/MultipartMimeContent.cs | 4 +- .../OData/Action.cs | 84 +- .../OData/Annotations.cs | 4 +- .../OData/ComplexType.cs | 45 +- .../OData/DataServices.cs | 4 +- .../OData/EntityContainer.cs | 10 +- .../OData/EntityFramework.cs | 4 +- .../OData/EntitySet.cs | 11 +- .../OData/EntityType.cs | 27 +- .../OData/ExtensionMethods.cs | 124 +- .../OData/Function.cs | 7 +- .../OData/IODataNamedElement.cs | 4 +- ApiDoctor.Validation/OData/IODataNavigable.cs | 188 ++ .../OData/IOdataAnnotatable.cs | 4 +- .../OData/Key.cs | 4 +- .../OData/NavigationProperty.cs | 4 +- .../OData/ODataParser.cs | 21 +- .../OData/Parameter.cs | 26 +- .../OData/Property.cs | 15 +- .../OData/PropertyRef.cs | 4 +- .../OData/ReturnType.cs | 25 +- .../OData/Schema.cs | 4 +- .../OData/Singleton.cs | 12 +- .../OData/Term.cs | 4 +- .../OData/Transformation/ITransformable.cs | 4 +- .../PublishSchemaChangesConfig.cs | 29 +- .../OData/Transformation/SchemaConfigFile.cs | 82 + .../Transformation/TransformationHelper.cs | 2 +- .../OData/XmlBackedObject.cs | 6 +- .../OData/XmlParseHelper.cs | 2 +- .../OData/annotation.cs | 39 +- .../OData/enumtype.cs | 12 +- .../OData/propertyvalue.cs | 46 +- .../OData/record.cs | 7 +- .../OData/schemavalidation.cs | 4 +- .../OData/sortcollectionshelper.cs | 2 +- .../ObjectGraph/ExtensionMethods.cs | 2 +- .../ObjectGraph/objectgraphmerger.cs | 2 +- .../PageAnnotation.cs | 6 +- .../ParameterDataType.cs | 133 +- ApiDoctor.Validation/ParameterDefinition.cs | 217 +++ .../Params/BasicRequestDefinition.cs | 6 +- .../Params/CSharpEval.cs | 8 +- .../Params/PlaceholderValue.cs | 4 +- .../PlaceholderValueNotFoundException.cs | 2 +- .../Params/ScenarioDefinition.cs | 4 +- .../Params/requestdefinitionextensions.cs | 23 +- .../Params/testsetuprequestdefinition.cs | 21 +- .../Properties/AssemblyInfo.cs | 4 +- .../Properties/Resources.Designer.cs | 4 +- .../Properties/Resources.resx | 0 .../ResourceDefinition.cs | 112 +- .../SamplesDefinition.cs | 21 +- .../Scenarios.cs | 8 +- .../SchemaBuildException.cs | 4 +- .../SingleOrArrayConverter.cs | 2 +- .../StringSuggestions.cs | 2 +- ApiDoctor.Validation/SupplementalFile.cs | 120 ++ .../TableSpec/TableAndHeaderConfig.json | 0 .../TableSpec/TableDefinition.cs | 6 +- .../TableSpec/TableParserConfig.cs | 31 +- .../TableSpec/tablespecconverter.cs | 165 +- .../Tags/TagProcessor.cs | 195 +- .../ValidationConfig.cs | 4 +- .../Writers/DocumentPublisher.cs | 8 +- .../Writers/MarkdownPublisher.cs.cs | 6 +- .../Writers/ipublishoptions.cs | 4 +- .../Writers/outlinepublisher.cs | 7 +- .../logging.cs | 6 +- .../methodvalidationextensionmethods.cs | 25 +- .../packages.config | 0 .../scenarioextensionmethods.cs | 36 +- MarkdownScanner.sln => ApiDoctor.sln | 19 +- CONTRIBUTING.md | 8 +- Deploy/DeployToAppVeyor.bat | 4 - Deploy/nuget.exe | Bin 1664000 -> 0 bytes TableAndHeaderConfig.json | 2 +- apidoc.sh | 3 + apidocs.sh | 3 - docs/markdown-customizations.md | 4 +- docs/markdown-requirements.md | 8 +- docs/publishing.md | 12 +- license.txt | 2 +- readme.md | 22 +- 205 files changed, 6431 insertions(+), 2861 deletions(-) delete mode 100644 ApiDocs.Publishing/CSDL/CsdlWriter.cs delete mode 100644 ApiDocs.Validation/OData/IODataNavigable.cs delete mode 100644 ApiDocs.Validation/ParameterDefinition.cs rename ApiDocs.Console/ApiDocs.ConsoleApp.csproj => ApiDoctor.Console/ApiDoctor.ConsoleApp.csproj (90%) rename ApiDocs.Console/ApiDocs.ConsoleApp.nuspec => ApiDoctor.Console/ApiDoctor.nuspec (56%) rename {ApiDocs.Console => ApiDoctor.Console}/App.config (100%) rename {ApiDocs.Console => ApiDoctor.Console}/AppConfigFile.cs (92%) rename {ApiDocs.Console => ApiDoctor.Console}/AppVeyor/BuildWorkerApi.cs (99%) rename {ApiDocs.Console => ApiDoctor.Console}/Auth/BasicAccount.cs (96%) rename {ApiDocs.Console => ApiDoctor.Console}/Auth/OAuthAccount.cs (99%) rename {ApiDocs.Console => ApiDoctor.Console}/Auth/OAuthAccountException.cs (98%) rename {ApiDocs.Console => ApiDoctor.Console}/Auth/OAuthTokenGenerator.cs (98%) rename {ApiDocs.Console => ApiDoctor.Console}/CheckResults.cs (97%) rename {ApiDocs.Console => ApiDoctor.Console}/CommandLineOptions.cs (89%) rename {ApiDocs.Console => ApiDoctor.Console}/FancyConsole.cs (99%) rename {ApiDocs.Console => ApiDoctor.Console}/GitHelper.cs (98%) rename {ApiDocs.Console => ApiDoctor.Console}/Program.cs (66%) rename {ApiDocs.Console => ApiDoctor.Console}/Properties/AssemblyInfo.cs (96%) rename {ApiDocs.Console => ApiDoctor.Console}/TestReport.cs (97%) rename {ApiDocs.Console => ApiDoctor.Console}/WildcardExtensions.cs (97%) rename {ApiDocs.Console => ApiDoctor.Console}/packages.config (100%) rename ApiDocs.DocumentationGeneration.UnitTests/ApiDocs.DocumentationGeneration.UnitTests.csproj => ApiDoctor.DocumentationGeneration.UnitTests/ApiDoctor.DocumentationGeneration.UnitTests.csproj (87%) rename {ApiDocs.DocumentationGeneration.UnitTests => ApiDoctor.DocumentationGeneration.UnitTests}/DescriptionTests.cs (97%) rename {ApiDocs.DocumentationGeneration.UnitTests => ApiDoctor.DocumentationGeneration.UnitTests}/DocumentationTestBase.cs (98%) rename {ApiDocs.DocumentationGeneration.UnitTests => ApiDoctor.DocumentationGeneration.UnitTests}/NavigationPropertyTests.cs (95%) rename {ApiDocs.DocumentationGeneration.UnitTests => ApiDoctor.DocumentationGeneration.UnitTests}/Properties/AssemblyInfo.cs (90%) rename {ApiDocs.DocumentationGeneration.UnitTests => ApiDoctor.DocumentationGeneration.UnitTests}/PropertyTests.cs (96%) rename {ApiDocs.DocumentationGeneration.UnitTests => ApiDoctor.DocumentationGeneration.UnitTests}/ResourceRoundTripTests.cs (78%) rename {ApiDocs.DocumentationGeneration.UnitTests => ApiDoctor.DocumentationGeneration.UnitTests}/packages.config (100%) rename ApiDocs.DocumentationGeneration/ApiDocs.DocumentationGeneration.csproj => ApiDoctor.DocumentationGeneration/ApiDoctor.DocumentationGeneration.csproj (91%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/DocumentationGenerator.cs (95%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Extensions/DocumentationExtensions.cs (97%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Model/DocumentationComplexType.cs (94%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Model/DocumentationEntityType.cs (92%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Model/DocumentationNavigationProperty.cs (93%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Model/DocumentationProperty.cs (88%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Properties/AssemblyInfo.cs (88%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Properties/Templates.Designer.cs (93%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Properties/Templates.resx (100%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/Templates/resource.md.mustache (100%) rename {ApiDocs.DocumentationGeneration => ApiDoctor.DocumentationGeneration}/packages.config (100%) rename ApiDocs.Publishing/ApiDocs.Publishing.csproj => ApiDoctor.Publishing/ApiDoctor.Publishing.csproj (94%) rename ApiDocs.Publishing/ApiDocs.Publishing.nuspec => ApiDoctor.Publishing/ApiDoctor.Publishing.nuspec (63%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/CSDL/ObjectGraphMerger.cs (100%) rename ApiDocs.Publishing/CSDL/CsdlExtensionMethods.cs => ApiDoctor.Publishing/CSDL/csdlextensionmethods.cs (58%) create mode 100644 ApiDoctor.Publishing/CSDL/csdlwriter.cs rename ApiDocs.Publishing/CSDL/MethodCollection.cs => ApiDoctor.Publishing/CSDL/methodcollection.cs (94%) create mode 100644 ApiDoctor.Publishing/CSDL/xmlsorter.cs rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Html/ApiDocsConditionalTag.cs (99%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Html/DocumentPublisherHtml.cs (98%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Html/ExtendedElseTag.cs (97%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Html/FileTagDefinition.cs (96%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Html/HtmlMustacheWriter.cs (99%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Html/IfMatchTagDefinition.cs (97%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/PathExtensionMethods.cs (97%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Properties/AssemblyInfo.cs (100%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Swagger/SwaggerClasses.cs (98%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/Swagger/SwaggerExtensionMethods.cs (97%) rename ApiDocs.Publishing/Swagger/SwaggerWriter.cs => ApiDoctor.Publishing/Swagger/swaggerwriter.cs (67%) rename {ApiDocs.Publishing => ApiDoctor.Publishing}/packages.config (100%) rename ApiDocs.Validation.UnitTests/ApiDocs.Validation.UnitTests.csproj => ApiDoctor.Validation.UnitTests/ApiDoctor.Validation.UnitTests.csproj (95%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/BrokenLinkTests.cs (70%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/DocFileForTesting.cs (96%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/ExtensionMethods.cs (94%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/JsonPathTest.cs (98%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/JsonRewriteTests.cs (94%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/MultipartMimeTests.cs (97%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/NullableTests.cs (84%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/ObjectGraphMergerTests.cs (97%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/Properties/AssemblyInfo.cs (91%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/Properties/Resources.Designer.cs (98%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/Properties/Resources.resx (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/ResourceStringValidationTests.cs (75%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/SchemaBuilderTests.cs (98%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/SchemaValidatorTests.cs (76%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/packages.config (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/ExampleResources.md (80%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/ExampleValidateResponse.md (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/ExampleValidationSelectStatement.md (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/ExampleValidationSelectStatementFailure.md (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/IndentedCodeBlock/ComplexExample.md (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/IndentedCodeBlock/IndentedCodeBlock.md (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/IndentedCodeBlock/NonIndentedExample.md (100%) rename {ApiDocs.Validation.UnitTests => ApiDoctor.Validation.UnitTests}/test-docs/IndentedCodeBlock/SimpleBuLists.md (100%) rename ApiDocs.Validation/ApiDocs.Validation.csproj => ApiDoctor.Validation/ApiDoctor.Validation.csproj (96%) rename ApiDocs.Validation/ApiDocs.Validation.nuspec => ApiDoctor.Validation/ApiDoctor.Validation.nuspec (69%) rename {ApiDocs.Validation => ApiDoctor.Validation}/AuthScopeDefinition.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/AuthenicationCredentials.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/BackoffHelper.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/CodeBlockAnnotation.cs (52%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Config/ApiRequirementsFile.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Config/ConfigFile.cs (90%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Config/DocumentOutlineFile.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/DocFile.cs (69%) rename {ApiDocs.Validation => ApiDoctor.Validation}/DocSet.cs (59%) create mode 100644 ApiDoctor.Validation/EnumerationDefinition.cs create mode 100644 ApiDoctor.Validation/Error/IssueLogger.cs rename {ApiDocs.Validation => ApiDoctor.Validation}/Error/ValidationMessage.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Error/ValidationResult.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Error/ValidationWarning.cs (97%) rename ApiDocs.Validation/Error/ValidationError.cs => ApiDoctor.Validation/Error/validationerror.cs (80%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ErrorDefinition.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ExampleDefinition.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ExtensionMethods.cs (70%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Http/HttpParser.cs (99%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Http/HttpParserRequestException.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Http/HttpValidationExtensionMethods.cs (70%) rename ApiDocs.Validation/Http/HttpRequest.cs => ApiDoctor.Validation/Http/httprequest.cs (97%) rename ApiDocs.Validation/Http/HttpResponse.cs => ApiDoctor.Validation/Http/httpresponse.cs (86%) rename {ApiDocs.Validation => ApiDoctor.Validation}/HttpLog/HttpLogGenerator.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/IServiceAccount.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ItemDefinition.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Json/JsonExample.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Json/JsonPath.cs (99%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Json/JsonSchema.cs (75%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Json/ValidationOptions.cs (98%) rename ApiDocs.Validation/Json/JsonResourceCollection.cs => ApiDoctor.Validation/Json/jsonresourcecollection.cs (70%) rename ApiDocs.Validation/Json/JsonRewriter.cs => ApiDoctor.Validation/Json/jsonrewriter.cs (82%) rename {ApiDocs.Validation => ApiDoctor.Validation}/MetadataTransforms.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/MethodDefinition.cs (82%) rename {ApiDocs.Validation => ApiDoctor.Validation}/MultipartMime/MimeContentType.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/MultipartMime/MultipartMimeContent.cs (99%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Action.cs (58%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Annotations.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/ComplexType.cs (77%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/DataServices.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/EntityContainer.cs (95%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/EntityFramework.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/EntitySet.cs (87%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/EntityType.cs (85%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/ExtensionMethods.cs (79%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Function.cs (92%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/IODataNamedElement.cs (96%) create mode 100644 ApiDoctor.Validation/OData/IODataNavigable.cs rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/IOdataAnnotatable.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Key.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/NavigationProperty.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/ODataParser.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Parameter.cs (86%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Property.cs (92%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/PropertyRef.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/ReturnType.cs (74%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Schema.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Singleton.cs (85%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Term.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Transformation/ITransformable.cs (93%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Transformation/PublishSchemaChangesConfig.cs (87%) create mode 100644 ApiDoctor.Validation/OData/Transformation/SchemaConfigFile.cs rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/Transformation/TransformationHelper.cs (99%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/XmlBackedObject.cs (84%) rename {ApiDocs.Validation => ApiDoctor.Validation}/OData/XmlParseHelper.cs (96%) rename ApiDocs.Validation/OData/Annotation.cs => ApiDoctor.Validation/OData/annotation.cs (76%) rename ApiDocs.Validation/OData/EnumType.cs => ApiDoctor.Validation/OData/enumtype.cs (92%) rename ApiDocs.Validation/OData/PropertyValue.cs => ApiDoctor.Validation/OData/propertyvalue.cs (60%) rename ApiDocs.Validation/OData/Record.cs => ApiDoctor.Validation/OData/record.cs (88%) rename ApiDocs.Validation/OData/SchemaValidation.cs => ApiDoctor.Validation/OData/schemavalidation.cs (97%) rename ApiDocs.Validation/OData/SortCollectionsHelper.cs => ApiDoctor.Validation/OData/sortcollectionshelper.cs (99%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ObjectGraph/ExtensionMethods.cs (95%) rename ApiDocs.Validation/ObjectGraph/ObjectGraphMerger.cs => ApiDoctor.Validation/ObjectGraph/objectgraphmerger.cs (99%) rename {ApiDocs.Validation => ApiDoctor.Validation}/PageAnnotation.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ParameterDataType.cs (75%) create mode 100644 ApiDoctor.Validation/ParameterDefinition.cs rename {ApiDocs.Validation => ApiDoctor.Validation}/Params/BasicRequestDefinition.cs (98%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Params/CSharpEval.cs (95%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Params/PlaceholderValue.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Params/PlaceholderValueNotFoundException.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Params/ScenarioDefinition.cs (98%) rename ApiDocs.Validation/Params/RequestDefinitionExtensions.cs => ApiDoctor.Validation/Params/requestdefinitionextensions.cs (95%) rename ApiDocs.Validation/Params/TestSetupRequestDefinition.cs => ApiDoctor.Validation/Params/testsetuprequestdefinition.cs (95%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Properties/AssemblyInfo.cs (92%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Properties/Resources.Designer.cs (95%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Properties/Resources.resx (100%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ResourceDefinition.cs (61%) rename ApiDocs.Validation/EnumerationDefinition.cs => ApiDoctor.Validation/SamplesDefinition.cs (72%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Scenarios.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/SchemaBuildException.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/SingleOrArrayConverter.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/StringSuggestions.cs (99%) create mode 100644 ApiDoctor.Validation/SupplementalFile.cs rename {ApiDocs.Validation => ApiDoctor.Validation}/TableSpec/TableAndHeaderConfig.json (100%) rename {ApiDocs.Validation => ApiDoctor.Validation}/TableSpec/TableDefinition.cs (92%) rename {ApiDocs.Validation => ApiDoctor.Validation}/TableSpec/TableParserConfig.cs (84%) rename ApiDocs.Validation/TableSpec/TableSpecConverter.cs => ApiDoctor.Validation/TableSpec/tablespecconverter.cs (60%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Tags/TagProcessor.cs (69%) rename {ApiDocs.Validation => ApiDoctor.Validation}/ValidationConfig.cs (97%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Writers/DocumentPublisher.cs (99%) rename {ApiDocs.Validation => ApiDoctor.Validation}/Writers/MarkdownPublisher.cs.cs (96%) rename ApiDocs.Validation/Writers/IPublishOptions.cs => ApiDoctor.Validation/Writers/ipublishoptions.cs (98%) rename ApiDocs.Validation/Writers/OutlinePublisher.cs => ApiDoctor.Validation/Writers/outlinepublisher.cs (94%) rename ApiDocs.Validation/Logging.cs => ApiDoctor.Validation/logging.cs (96%) rename ApiDocs.Validation/MethodValidationExtensionMethods.cs => ApiDoctor.Validation/methodvalidationextensionmethods.cs (96%) rename {ApiDocs.Validation => ApiDoctor.Validation}/packages.config (100%) rename ApiDocs.Validation/ScenarioExtensionMethods.cs => ApiDoctor.Validation/scenarioextensionmethods.cs (81%) rename MarkdownScanner.sln => ApiDoctor.sln (87%) delete mode 100644 Deploy/DeployToAppVeyor.bat delete mode 100644 Deploy/nuget.exe create mode 100644 apidoc.sh delete mode 100755 apidocs.sh diff --git a/ApiDocs.Publishing/CSDL/CsdlWriter.cs b/ApiDocs.Publishing/CSDL/CsdlWriter.cs deleted file mode 100644 index 69c354fc..00000000 --- a/ApiDocs.Publishing/CSDL/CsdlWriter.cs +++ /dev/null @@ -1,826 +0,0 @@ -/* - * Markdown Scanner - * Copyright (c) Microsoft Corporation - * All rights reserved. - * - * MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the ""Software""), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -namespace ApiDocs.Publishing.CSDL -{ - using ApiDocs.Validation; - using ApiDocs.Validation.Writers; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - using ApiDocs.Validation.OData; - using Validation.Config; - using Validation.OData.Transformation; - - public class CsdlWriter : DocumentPublisher - { - private readonly CsdlWriterOptions options; - - public CsdlWriter(DocSet docs, string[] namespacesToExport, string baseUrl, string preexistingEdmxFile, MetadataFormat format = MetadataFormat.Default, bool sortOutput = false) - : base(docs) - { - this.options = new CsdlWriterOptions() - { - Namespaces = namespacesToExport, - BaseUrl = baseUrl, - SourceMetadataPath = preexistingEdmxFile, - Formats = format, - Sort = sortOutput - }; - } - - public CsdlWriter(DocSet docs, CsdlWriterOptions options) - : base(docs) - { - this.options = options; - } - - public override async Task PublishToFolderAsync(string outputFolder) - { - string outputFilenameSuffix = ""; - - // Step 1: Generate an EntityFramework OM from the documentation and/or template file - EntityFramework framework = CreateEntityFrameworkFromDocs(); - if (null == framework) - return; - - if (!string.IsNullOrEmpty(options.MergeWithMetadataPath)) - { - EntityFramework secondFramework = CreateEntityFrameworkFromDocs(options.MergeWithMetadataPath, generateFromDocs: false); - framework = framework.MergeWith(secondFramework); - outputFilenameSuffix += "-merged"; - } - - // Step 1a: Apply an transformations that may be defined in the documentation - if (!string.IsNullOrEmpty(options.TransformOutput)) - { - PublishSchemaChangesConfigFile transformations = DocSet.TryLoadConfigurationFiles(options.DocumentationSetPath).Where(x => x.SchemaChanges.TransformationName == options.TransformOutput).FirstOrDefault(); - if (null == transformations) - { - throw new KeyNotFoundException($"Unable to locate a transformation set named {options.TransformOutput}. Aborting."); - } - - string[] versionsToPublish = options.Version?.Split(new char[] { ',', ' '}); - framework.ApplyTransformation(transformations.SchemaChanges, versionsToPublish); - if (!string.IsNullOrEmpty(options.Version)) - { - outputFilenameSuffix += $"-{options.Version}"; - } - } - - if (options.Sort) - { - // Sorts the objects in collections, so that we have consistent output regardless of input - framework.SortObjectGraph(); - } - - if (options.ValidateSchema) - { - framework.ValidateSchemaTypes(); - } - - // Step 2: Generate XML representation of EDMX - string xmlData = null; - if (options.Formats.HasFlag(MetadataFormat.EdmxOutput)) - { - xmlData = ODataParser.Serialize(framework, options.AttributesOnNewLines); - } - else if (options.Formats.HasFlag(MetadataFormat.SchemaOutput)) - { - xmlData = ODataParser.Serialize(framework.DataServices.Schemas.First(), options.AttributesOnNewLines); - } - - // Step 3: Write the XML to disk - - var outputFullName = GenerateOutputFileFullName(options.SourceMetadataPath, outputFolder, outputFilenameSuffix); - Console.WriteLine($"Publishing metadata to {outputFullName}"); - - using (var writer = System.IO.File.CreateText(outputFullName)) - { - await writer.WriteAsync(xmlData); - await writer.FlushAsync(); - writer.Close(); - } - } - - private string GenerateOutputFileFullName(string templateFilename, string outputFolderPath, string filenameSuffix) - { - var outputDir = new System.IO.DirectoryInfo(outputFolderPath); - outputDir.Create(); - - filenameSuffix = filenameSuffix ?? ""; - - string filename = null; - if (!string.IsNullOrEmpty(templateFilename)) - { - filename = $"{System.IO.Path.GetFileNameWithoutExtension(templateFilename)}{filenameSuffix}{System.IO.Path.GetExtension(templateFilename)}"; - } - else - { - filename = $"metadata{filenameSuffix}.xml"; - } - - var outputFullName = System.IO.Path.Combine(outputDir.FullName, filename); - return outputFullName; - } - - private EntityFramework CreateEntityFrameworkFromDocs(string sourcePath = null, bool? generateFromDocs = null) - { - sourcePath = sourcePath ?? options.SourceMetadataPath; - - EntityFramework edmx = new EntityFramework(); - if (!string.IsNullOrEmpty(sourcePath)) - { - try - { - if (!System.IO.File.Exists(sourcePath)) - { - throw new System.IO.FileNotFoundException($"Unable to locate source file: {sourcePath}"); - } - - using (System.IO.FileStream stream = new System.IO.FileStream(sourcePath, System.IO.FileMode.Open, System.IO.FileAccess.Read)) - { - if (options.Formats.HasFlag(MetadataFormat.EdmxInput)) - { - edmx = ODataParser.Deserialize(stream); - } - else if (options.Formats.HasFlag(MetadataFormat.SchemaInput)) - { - var schema = ODataParser.Deserialize(stream); - edmx = new EntityFramework(); - edmx.DataServices.Schemas.Add(schema); - } - else - { - throw new InvalidOperationException("Source file was specified but no format for source file was provided."); - } - } - if (options.Namespaces != null && options.Namespaces.Any()) - { - var schemas = edmx.DataServices.Schemas.ToArray(); - foreach(var s in schemas) - { - if (!options.Namespaces.Contains(s.Namespace)) - { - edmx.DataServices.Schemas.Remove(s); - } - } - } - } - catch (Exception ex) - { - Console.WriteLine($"Unable to deserialize source template file: {ex.Message}"); - if (ex.InnerException != null) - { - Console.WriteLine($"{ex.InnerException.Message}"); - } - return null; - } - } - - bool generateNewElements = (generateFromDocs == null && !options.SkipMetadataGeneration) || (generateFromDocs.HasValue && generateFromDocs.Value) ; - - // Add resources - if (generateNewElements && Documents.Files.Any()) - { - foreach (var resource in Documents.Resources) - { - var targetSchema = FindOrCreateSchemaForNamespace(resource.Name.NamespaceOnly(), edmx, generateNewElements: generateNewElements); - if (targetSchema != null) - { - AddResourceToSchema(targetSchema, resource, edmx, generateNewElements: generateNewElements); - } - } - - // Figure out the EntityCollection - this.BuildEntityContainer(edmx, options.BaseUrl); - - // Add actions to the collection - this.ProcessRestRequestPaths(edmx, options.BaseUrl); - } - - return edmx; - } - - /// - /// Scan the MethodDefintions in the documentation and create actions and functions in the - /// EntityFramework for matching call patterns. - /// - /// - private void ProcessRestRequestPaths(EntityFramework edmx, string baseUrlToRemove) - { - Dictionary uniqueRequestPaths = GetUniqueRequestPaths(baseUrlToRemove); - - foreach (var path in uniqueRequestPaths.Keys) - { - var methodCollection = uniqueRequestPaths[path]; - - ODataTargetInfo requestTarget = null; - try - { - // TODO: If we have an input Edmx, we may already know what this so we don't need to infer anything. - // if that is the case, we should just update it with anything else we know from the documentation. - requestTarget = ParseRequestTargetType(path, methodCollection, edmx); - if (requestTarget.Classification == ODataTargetClassification.Unknown && - !string.IsNullOrEmpty(requestTarget.Name) && - requestTarget.QualifiedType != null) - { - CreateNewActionOrFunction(edmx, methodCollection, requestTarget); - } - else if (requestTarget.Classification == ODataTargetClassification.EntityType) - { - // We've learned more about this entity type, let's add that information to the state - AppendToEntityType(edmx, requestTarget, methodCollection); - } - else if (requestTarget.Classification == ODataTargetClassification.NavigationProperty) - { - // TODO: Record somewhere the operations that are available on this NavigationProperty - AppendToNavigationProperty(edmx, requestTarget, methodCollection); - } - else - { - // TODO: Are there interesting things to learn here? - Console.WriteLine("Found type {0}: {1}", requestTarget.Classification, path); - } - } - catch (Exception ex) - { - // TODO: Log this out better than this. - Console.WriteLine("Couldn't serialize request for path {0} into EDMX: {1}", path, ex.Message); - continue; - } - } - } - - private void AppendToNavigationProperty(EntityFramework edmx, ODataTargetInfo navigationProperty, MethodCollection methods) - { - EntityType parentType = edmx.ResourceWithIdentifier(navigationProperty.QualifiedType); - - NavigationProperty matchingProperty = - parentType.NavigationProperties.FirstOrDefault(np => np.Name == navigationProperty.Name); - if (null != matchingProperty) - { - // TODO: Append information from methods into this navigation property - StringBuilder sb = new StringBuilder(); - const string seperator = ", "; - sb.AppendWithCondition(methods.GetAllowed, "GET", seperator); - sb.AppendWithCondition(methods.PostAllowed, "POST", seperator); - sb.AppendWithCondition(methods.PutAllowed, "PUT", seperator); - sb.AppendWithCondition(methods.DeleteAllowed, "DELETE", seperator); - - Console.WriteLine("Collection '{0}' supports: ({1})", navigationProperty.QualifiedType + "/" + navigationProperty.Name, sb); - } - else - { - Console.WriteLine( - "EntityType '{0}' doesn't have a matching navigationProperty '{1}' but a request exists for this. Sounds like a documentation error.", - navigationProperty.QualifiedType, - navigationProperty.Name); - } - } - - /// - /// Use the properties of methodCollection to augment what we know about this entity type - /// - /// - /// - private void AppendToEntityType(EntityFramework edmx, ODataTargetInfo requestTarget, MethodCollection methodCollection) - { - StringBuilder sb = new StringBuilder(); - const string seperator = ", "; - sb.AppendWithCondition(methodCollection.GetAllowed, "GET", seperator); - sb.AppendWithCondition(methodCollection.PostAllowed, "POST", seperator); - sb.AppendWithCondition(methodCollection.PutAllowed, "PUT", seperator); - sb.AppendWithCondition(methodCollection.DeleteAllowed, "DELETE", seperator); - - Console.WriteLine("EntityType '{0}' supports: ({1})", requestTarget.QualifiedType, sb.ToString()); - } - - private void CreateNewActionOrFunction(EntityFramework edmx, MethodCollection methodCollection, ODataTargetInfo requestTarget) - { - // Create a new action (not idempotent) / function (idempotent) based on this request method! - ActionOrFunctionBase target = null; - if (methodCollection.AllMethodsIdempotent) - { - target = new Validation.OData.Function(); - } - else - { - target = new Validation.OData.Action(); - } - - var schemaName = requestTarget.Name.NamespaceOnly(); - target.Name = requestTarget.Name.TypeOnly(); - target.IsBound = true; - target.Parameters.Add(new Parameter { Name = "bindingParameter", Type = requestTarget.QualifiedType, Nullable = false }); - foreach (var param in methodCollection.RequestBodyParameters) - { - target.Parameters.Add( - new Parameter - { - Name = param.Name, - Type = param.Type.ODataResourceName(), - Nullable = (param.Required.HasValue ? param.Required.Value : false) - }); - } - - if (methodCollection.ResponseType != null) - { - target.ReturnType = new ReturnType { Type = methodCollection.ResponseType.ODataResourceName(), Nullable = false }; - } - - var schema = FindOrCreateSchemaForNamespace(schemaName, edmx, true); - if (target is Function) - schema.Functions.Add((Function)target); - else - schema.Actions.Add((Validation.OData.Action)target); - } - - /// - /// Walks the requestPath through the resources / entities defined in the edmx and resolves - /// the type of request represented by the path - /// - /// - /// - /// - /// - private static ODataTargetInfo ParseRequestTargetType(string requestPath, MethodCollection requestMethodCollection, EntityFramework edmx) - { - string[] requestParts = requestPath.Substring(1).Split(new char[] { '/'}); - - EntityContainer entryPoint = (from s in edmx.DataServices.Schemas - where s.EntityContainers.Count > 0 - select s.EntityContainers.FirstOrDefault()).SingleOrDefault(); - - if (entryPoint == null) throw new InvalidOperationException("Couldn't locate an EntityContainer to begin target resolution"); - - IODataNavigable currentObject = entryPoint; - IODataNavigable previousObject = null; - - for(int i=0; i - /// Parse the URI paths for methods defined in the documentation and construct an entity container that contains these - /// entity sets / singletons in the largest namespace. - /// - /// - private void BuildEntityContainer(EntityFramework edmx, string baseUrlToRemove) - { - // Check to see if an entitycontainer already exists - foreach (var s in edmx.DataServices.Schemas) - { - if (s.EntityContainers.Any()) - return; - } - - Dictionary uniqueRequestPaths = GetUniqueRequestPaths(baseUrlToRemove); - var resourcePaths = uniqueRequestPaths.Keys.OrderBy(x => x).ToArray(); - - EntityContainer container = new EntityContainer(); - foreach (var path in resourcePaths) - { - if (EntitySetPathRegEx.IsMatch(path)) - { - var name = EntitySetPathRegEx.Match(path).Groups[1].Value; - container.EntitySets.Add(new EntitySet { Name = name, EntityType = uniqueRequestPaths[path].ResponseType.ODataResourceName() }); - } - else if (SingletonPathRegEx.IsMatch(path)) - { - // Before we declare this a singleton, see if any other paths that have the same root match the entity set regex - var query = (from p in resourcePaths where p.StartsWith(path + "/") && EntitySetPathRegEx.IsMatch(p) select p); - if (query.Any()) - { - // If there's a similar resource path that matches the entity, we don't declare a singleton. - continue; - } - - var name = SingletonPathRegEx.Match(path).Groups[1].Value; - container.Singletons.Add(new Singleton { Name = name, Type = uniqueRequestPaths[path].ResponseType.ODataResourceName() }); - } - } - - // TODO: Allow the default schema name to be specified instead of inferred - var largestSchema = (from x in edmx.DataServices.Schemas - orderby x.EntityTypes.Count descending - select x).First(); - container.Name = largestSchema.Namespace; - largestSchema.EntityContainers.Add(container); - } - - private Dictionary cachedUniqueRequestPaths { get; set; } - - /// - /// Return a dictionary of the unique request paths in the - /// documentation and the method definitions that defined them. - /// - /// - private Dictionary GetUniqueRequestPaths(string baseUrlToRemove) - { - if (cachedUniqueRequestPaths == null) - { - Dictionary uniqueRequestPaths = new Dictionary(); - foreach (var m in Documents.Methods) - { - if (m.ExpectedResponseMetadata != null && m.ExpectedResponseMetadata.ExpectError) - { - // Ignore thigns that are expected to error - continue; - } - - var path = m.RequestUriPathOnly(baseUrlToRemove); - if (!path.StartsWith("/")) - { - // Ignore aboslute URI paths - continue; - } - - Console.WriteLine("Converted '{0}' into generic form '{1}'", m.Request.FirstLineOnly(), path); - - if (!uniqueRequestPaths.ContainsKey(path)) - { - uniqueRequestPaths.Add(path, new MethodCollection()); - } - uniqueRequestPaths[path].Add(m); - - Console.WriteLine("{0} :: {1} --> {2}", path, m.RequestMetadata.ResourceType, m.ExpectedResponseMetadata?.ResourceType); - } - cachedUniqueRequestPaths = uniqueRequestPaths; - } - return cachedUniqueRequestPaths; - } - - /// - /// Find an existing schema definiton or create a new one in an entity framework for a given namespace. - /// - /// - /// - /// - private Schema FindOrCreateSchemaForNamespace(string ns, EntityFramework edmx, bool overrideNamespaceFilter = false, bool generateNewElements = true) - { - // Check to see if this is a namespace that should be exported. - if (!overrideNamespaceFilter && options.Namespaces != null && !options.Namespaces.Contains(ns)) - { - return null; - } - - if (ns.Equals("odata", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var matchingSchema = (from s in edmx.DataServices.Schemas - where s.Namespace == ns - select s).FirstOrDefault(); - - if (null != matchingSchema) - return matchingSchema; - - if (generateNewElements) - { - var newSchema = new Schema() { Namespace = ns }; - edmx.DataServices.Schemas.Add(newSchema); - return newSchema; - } - - return null; - } - - - private void AddResourceToSchema(Schema schema, ResourceDefinition resource, EntityFramework edmx, bool generateNewElements = true) - { - ComplexType type = null; - var typeName = resource.Name.TypeOnly(); - - // First check to see if there is an existing resource that matches this resource in the framework already - var existingEntity = (from e in schema.EntityTypes where e.Name == typeName select e).SingleOrDefault(); - if (existingEntity != null) - { - type = existingEntity; - } - - // If that didn't work, look for a complex type that matches - if (type == null) - { - var existingComplexType = (from e in schema.ComplexTypes where e.Name == typeName select e).SingleOrDefault(); - if (existingComplexType != null) - { - type = existingComplexType; - } - } - - // Finally go ahead and create a new resource in the schema if we didn't find a matching one - if (null == type && generateNewElements) - { - type = CreateResourceInSchema(schema, resource); - } - else if (null == type) - { - Console.WriteLine($"Type {resource.Name} was in the documentation but not found in the schema."); - } - - if (null != type) - { - LogIfDifferent(type.Name, resource.Name.TypeOnly(), $"Schema type {type.Name} is different than the documentation name {resource.Name.TypeOnly()}."); - LogIfDifferent(type.OpenType, resource.OriginalMetadata.IsOpenType, $"Schema type {type.Name} has a different OpenType value {type.OpenType} than the documentation {resource.OriginalMetadata.IsOpenType}."); - LogIfDifferent(type.BaseType, resource.OriginalMetadata.BaseType, $"Schema type {type.Name} has a different BaseType value {type.BaseType} than the documentation {resource.OriginalMetadata.BaseType}."); - - AddDocPropertiesToSchemaResource(type, resource, edmx, generateNewElements); - } - } - - private void AddDocPropertiesToSchemaResource(ComplexType schemaType, ResourceDefinition docResource, EntityFramework edmx, bool generateNewElements) - { - var docProps = (from p in docResource.Parameters - where !p.IsNavigatable && !p.Name.StartsWith("@") - select p).ToList(); - MergePropertiesIntoSchema(schemaType.Name, schemaType.Properties, docProps, edmx, generateNewElements); - - var schemaEntity = schemaType as EntityType; - if (null != schemaEntity) - { - var docNavigationProps = (from p in docResource.Parameters where p.IsNavigatable && !p.Name.StartsWith("@") select p); - MergePropertiesIntoSchema(schemaEntity.Name, schemaEntity.NavigationProperties, docNavigationProps, edmx, generateNewElements); - } - - var docInstanceAnnotations = (from p in docResource.Parameters where p.Name != null && p.Name.StartsWith("@") select p); - MergeInstanceAnnotationsAndRecordTerms(schemaType.Name, docInstanceAnnotations, docResource, edmx, generateNewElements); - } - - private static void MergePropertiesIntoSchema(string typeName, List schemaProps, IEnumerable docProps, EntityFramework edmx, bool generateNewElements) - where TProp : Property, new() - { - var documentedProperties = docProps.ToDictionary(x => x.Name, x => x); - foreach(var schemaProp in schemaProps) - { - ParameterDefinition documentedVersion = null; - if (documentedProperties.TryGetValue(schemaProp.Name, out documentedVersion)) - { - // Compare / update schema with data from documentation - var docProp = ConvertParameterToProperty(typeName, documentedVersion); - LogIfDifferent(schemaProp.Nullable, docProp.Nullable, $"Type {typeName}: Property {docProp.Name} has a different nullable value than documentation."); - LogIfDifferent(schemaProp.TargetEntityType, docProp.TargetEntityType, $"Type {typeName}: Property {docProp.Name} has a different target entity type than documentation."); - LogIfDifferent(schemaProp.Type, docProp.Type, $"Type {typeName}: Property {docProp.Name} has a different Type value than documentation ({schemaProp.Type},{docProp.Type})."); - LogIfDifferent(schemaProp.Unicode, docProp.Unicode, $"Type {typeName}: Property {docProp.Name} has a different unicode value than documentation ({schemaProp.Unicode},{docProp.Unicode})."); - documentedProperties.Remove(documentedVersion.Name); - - AddDescriptionAnnotation(typeName, schemaProp, documentedVersion); - } - else - { - // Log out that this property wasn't in the documentation - Console.WriteLine($"UndocumentedProperty: {typeName} defines {schemaProp.Name} in the schema but has no matching documentation."); - } - } - - if (generateNewElements) - { - foreach(var newPropDef in documentedProperties.Values) - { - // Create new properties based on the documentation - var newProp = ConvertParameterToProperty(typeName, newPropDef); - schemaProps.Add(newProp); - } - } - } - - private static void LogIfDifferent(object schemaValue, object documentationValue, string errorString) - { - if ( (schemaValue == null && documentationValue != null) || - (schemaValue != null && documentationValue == null) || - (schemaValue != null && documentationValue != null && !schemaValue.Equals(documentationValue))) - { - Console.WriteLine(errorString); - } - } - - private static ComplexType CreateResourceInSchema(Schema schema, ResourceDefinition resource) - { - ComplexType type; - // Create a new entity or complex type for this resource - if (!string.IsNullOrEmpty(resource.KeyPropertyName)) - { - var entity = new EntityType(); - entity.Key = new Key { PropertyRef = new PropertyRef { Name = resource.KeyPropertyName } }; - entity.NavigationProperties = (from p in resource.Parameters - where p.IsNavigatable - select ConvertParameterToProperty(entity.Name, p)).ToList(); - schema.EntityTypes.Add(entity); - type = entity; - } - else - { - type = new ComplexType(); - schema.ComplexTypes.Add(type); - } - - return type; - } - - private void MergeInstanceAnnotationsAndRecordTerms(string typeName, IEnumerable annotations, ResourceDefinition containedResource, EntityFramework edmx, bool generateNewElements) - { - foreach (var prop in annotations) - { - var qualifiedName = prop.Name.Substring(1); - var ns = qualifiedName.NamespaceOnly(); - var localName = qualifiedName.TypeOnly(); - - Term term = new Term { Name = localName, AppliesTo = containedResource.Name, Type = prop.Type.ODataResourceName() }; - if (!string.IsNullOrEmpty(prop.Description)) - { - term.Annotations.Add(new Annotation { Term = Term.LongDescriptionTerm, String = prop.Description }); - } - - var targetSchema = FindOrCreateSchemaForNamespace(ns, edmx, overrideNamespaceFilter: true, generateNewElements: generateNewElements); - if (null != targetSchema) - { - targetSchema.Terms.Add(term); - } - } - } - - - private static T ConvertParameterToProperty(string typeName, ParameterDefinition param) where T : Property, new() - { - var prop = new T() - { - Name = param.Name, - Nullable = (param.Required.HasValue ? !param.Required.Value : false), - Type = param.Type.ODataResourceName() - }; - - // Add description annotation - AddDescriptionAnnotation(typeName, prop, param); - return prop; - } - - private static void AddDescriptionAnnotation(string typeName, T targetProperty, ParameterDefinition sourceParameter, string termForDescription = Term.LongDescriptionTerm) where T: Property, IODataAnnotatable - { - if (!string.IsNullOrEmpty(sourceParameter.Description)) - { - if (targetProperty.Annotation == null) - { - targetProperty.Annotation = new List(); - } - - // Check to see if there already is a term with Description - var descriptionTerm = targetProperty.Annotation.Where(t => t.Term == termForDescription).FirstOrDefault(); - if (descriptionTerm != null) - { - LogIfDifferent(descriptionTerm.String, sourceParameter.Description, $"Type {typeName} has a different value for term '{termForDescription}' than the documentation."); - } - else - { - targetProperty.Annotation.Add( - new Annotation() - { - Term = termForDescription, - String = sourceParameter.Description - }); - } - } - else - { - Console.WriteLine($"Description was null for property: {typeName}.{sourceParameter.Name}."); - } - } - } - - - public class CsdlWriterOptions - { - public string OutputDirectoryPath { get; set; } - public string SourceMetadataPath { get; set; } - public string MergeWithMetadataPath { get; set; } - public MetadataFormat Formats { get; set; } - public string[] Namespaces { get; set; } - public bool Sort { get; set; } - public string BaseUrl { get; set; } - public string TransformOutput { get; set; } - public string DocumentationSetPath { get; set; } - public string Version { get; set; } - public bool SkipMetadataGeneration { get; set; } - public bool ValidateSchema { get; set; } - public bool AttributesOnNewLines { get; set; } - } - - [Flags] - public enum MetadataFormat - { - Default = EdmxInput | EdmxOutput, - EdmxInput = 1 << 0, - EdmxOutput = 1 << 1, - SchemaInput = 1 << 2, - SchemaOutput = 1 << 3 - } -} diff --git a/ApiDocs.Validation/OData/IODataNavigable.cs b/ApiDocs.Validation/OData/IODataNavigable.cs deleted file mode 100644 index e390b5fe..00000000 --- a/ApiDocs.Validation/OData/IODataNavigable.cs +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Markdown Scanner - * Copyright (c) Microsoft Corporation - * All rights reserved. - * - * MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the ""Software""), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -namespace ApiDocs.Validation.OData -{ - using System; - - public interface IODataNavigable - { - /// - /// Returns the next target pasted on the value of a component of the URI - /// - /// - /// - IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx); - - /// - /// Returns the next component assuming that an entitytype key is provided - /// - /// - IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx); - - string TypeIdentifier { get; } - } - - public class ODataCollection : IODataNavigable - { - public string TypeIdentifier { get; internal set; } - - public ODataCollection(string typeIdentifier) - { - this.TypeIdentifier = typeIdentifier; - } - - public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx) - { - // NavigationByUriComponent for a collection means that we have a hard coded key in the example path. - return this.NavigateByEntityTypeKey(edmx); - } - - public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx) - { - return edmx.ResourceWithIdentifier(this.TypeIdentifier); - } - } - - public class ODataSimpleType : IODataNavigable - { - public SimpleDataType Type { get; internal set; } - - public ODataSimpleType(SimpleDataType type) - { - this.Type = type; - } - - public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx) - { - throw new NotSupportedException(); - } - - public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx) - { - throw new NotSupportedException(); - } - - public string TypeIdentifier - { - get { return Type.ODataResourceName(); } - } - } - - public class ODataTargetInfo - { - public ODataTargetClassification Classification { get; set; } - - public string QualifiedType { get; set; } - - public string Name { get; set; } - } - - - public enum ODataTargetClassification - { - Unknown, - EntityType, - EntitySet, - Action, - Function, - EntityContainer, - SimpleType, - NavigationProperty - } -} diff --git a/ApiDocs.Validation/ParameterDefinition.cs b/ApiDocs.Validation/ParameterDefinition.cs deleted file mode 100644 index 77eb452b..00000000 --- a/ApiDocs.Validation/ParameterDefinition.cs +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Markdown Scanner - * Copyright (c) Microsoft Corporation - * All rights reserved. - * - * MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the ""Software""), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -namespace ApiDocs.Validation -{ - using System; - using System.Collections.Generic; - using ApiDocs.Validation.Json; - - /// - /// Represents a parameter for a request - /// - public class ParameterDefinition : ItemDefinition - { - /// - /// The name of the parameter. - /// - public string Name { get; set; } - - /// - /// The type of object the parameter expects - /// - public ParameterDataType Type { get; set; } - - /// - /// The location of the parameter in the request/response - /// - public ParameterLocation Location { get; set; } - - /// - /// True if the parameter is required to have a value. - /// - public bool? Required { get; set; } - - /// - /// List of enumerated values for a parameter that uses enumeration - /// - public List EnumeratedValues { get; set; } - - /// - /// Text value of the original value of this parameter. - /// - public string OriginalValue { get; set; } - - /// - /// Indicates that the parameter should be represented as a navigation property - /// - public bool IsNavigatable { get; internal set; } - - /// - /// Merge values from the param object into this object. - /// - /// - internal void AddMissingDetails(ParameterDefinition param) - { - if (!this.Required.HasValue && param.Required.HasValue) - this.Required = param.Required; - - this.IsNavigatable = this.IsNavigatable | param.IsNavigatable; - - if (this.Type != param.Type) - { - if (this.Type.IsObject) - { - if (this.Type.CustomTypeName == null && param.Type.CustomTypeName != null) - { - this.Type = param.Type; - } - } - else if (this.Type.IsCollection) - { - if (this.Type.CollectionResourceType == SimpleDataType.Object && this.Type.CustomTypeName == null && - param.Type.CollectionResourceType == SimpleDataType.Object && param.Type.CustomTypeName != null) - { - this.Type = param.Type; - } - } - else if (this.Type.IsLessSpecificThan(param.Type)) - { - // TODO: This should probably be logged out as documentation warnings! - System.Diagnostics.Debug.WriteLine( - "Parameter '{2}' type changed, {0} --> {1}", - this.Type.Type, - param.Type.Type, - param.Name); - this.Type = param.Type; - } - } - - if (string.IsNullOrEmpty(this.Title)) - { - this.Title = param.Title; - } - if (string.IsNullOrEmpty(this.Description)) - { - this.Description = param.Description; - } - if (param.EnumeratedValues != null) - { - this.EnumeratedValues.AddRange(param.EnumeratedValues); - } - } - } - - public enum ParameterLocation - { - Path, - QueryString, - Header, - JsonObject - } - - -} diff --git a/ApiDocs.Console/ApiDocs.ConsoleApp.csproj b/ApiDoctor.Console/ApiDoctor.ConsoleApp.csproj similarity index 90% rename from ApiDocs.Console/ApiDocs.ConsoleApp.csproj rename to ApiDoctor.Console/ApiDoctor.ConsoleApp.csproj index 1f547db2..95a5a18c 100644 --- a/ApiDocs.Console/ApiDocs.ConsoleApp.csproj +++ b/ApiDoctor.Console/ApiDoctor.ConsoleApp.csproj @@ -7,8 +7,8 @@ {A6F3993F-59C6-4985-ACF1-4D837D61E98F} Exe Properties - ApiDocs.ConsoleApp - apidocs + ApiDoctor.ConsoleApp + apidoc v4.5 512 ..\ @@ -38,7 +38,7 @@ DEBUG;TRACE prompt 4 - check-docs --path /Users/ryan/SourceCode/github/onedrive-api-docs-internal --method get-previous-versions + check-docs --path /Users/dspektor/src/graphdocs2 true @@ -92,17 +92,17 @@ - + {cd27998c-4021-4299-970b-91be877fd01b} - ApiDocs.DocumentationGeneration + ApiDoctor.DocumentationGeneration - + {B675CF73-AA42-4A54-B5E7-FF5F155DA4A7} - ApiDocs.Publishing + ApiDoctor.Publishing - + {33B10320-3802-49CF-8965-3510AE66D5EC} - ApiDocs.Validation + ApiDoctor.Validation diff --git a/ApiDocs.Console/ApiDocs.ConsoleApp.nuspec b/ApiDoctor.Console/ApiDoctor.nuspec similarity index 56% rename from ApiDocs.Console/ApiDocs.ConsoleApp.nuspec rename to ApiDoctor.Console/ApiDoctor.nuspec index eff76f01..909aa232 100644 --- a/ApiDocs.Console/ApiDocs.ConsoleApp.nuspec +++ b/ApiDoctor.Console/ApiDoctor.nuspec @@ -1,21 +1,22 @@ - MarkdownScanner.BinaryTools + ApiDoctor $version$ - Ryan Gregg - rgregg@microsoft.com + Microsoft + dspektor@microsoft.com false - Toolkit that enables validation of API documentation - Check http://github.com/onedrive/markdown-scanner for details. + Tool for validating API documentation + Check http://github.com/onedrive/apidoctor for details. Copyright Microsoft http://msdn.microsoft.com/en-US/cc300389 + http://github.com/onedrive/apidoctor - + diff --git a/ApiDocs.Console/App.config b/ApiDoctor.Console/App.config similarity index 100% rename from ApiDocs.Console/App.config rename to ApiDoctor.Console/App.config diff --git a/ApiDocs.Console/AppConfigFile.cs b/ApiDoctor.Console/AppConfigFile.cs similarity index 92% rename from ApiDocs.Console/AppConfigFile.cs rename to ApiDoctor.Console/AppConfigFile.cs index 6531d392..1f834943 100644 --- a/ApiDocs.Console/AppConfigFile.cs +++ b/ApiDoctor.Console/AppConfigFile.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { - using ApiDocs.ConsoleApp.Auth; - using ApiDocs.Validation.Config; + using ApiDoctor.ConsoleApp.Auth; + using ApiDoctor.Validation.Config; using Newtonsoft.Json; - using ApiDocs.Validation; + using ApiDoctor.Validation; public class AppConfigFile : ConfigFile { diff --git a/ApiDocs.Console/AppVeyor/BuildWorkerApi.cs b/ApiDoctor.Console/AppVeyor/BuildWorkerApi.cs similarity index 99% rename from ApiDocs.Console/AppVeyor/BuildWorkerApi.cs rename to ApiDoctor.Console/AppVeyor/BuildWorkerApi.cs index 047e99d0..b6da9966 100644 --- a/ApiDocs.Console/AppVeyor/BuildWorkerApi.cs +++ b/ApiDoctor.Console/AppVeyor/BuildWorkerApi.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp.AppVeyor +namespace ApiDoctor.ConsoleApp.AppVeyor { using System; using System.IO; diff --git a/ApiDocs.Console/Auth/BasicAccount.cs b/ApiDoctor.Console/Auth/BasicAccount.cs similarity index 96% rename from ApiDocs.Console/Auth/BasicAccount.cs rename to ApiDoctor.Console/Auth/BasicAccount.cs index 3e11b82a..494cfd8b 100644 --- a/ApiDocs.Console/Auth/BasicAccount.cs +++ b/ApiDoctor.Console/Auth/BasicAccount.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,11 +23,11 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp.Auth +namespace ApiDoctor.ConsoleApp.Auth { using System.Threading.Tasks; using System.Collections.Generic; - using ApiDocs.Validation; + using ApiDoctor.Validation; public class BasicAccount : IServiceAccount { diff --git a/ApiDocs.Console/Auth/OAuthAccount.cs b/ApiDoctor.Console/Auth/OAuthAccount.cs similarity index 99% rename from ApiDocs.Console/Auth/OAuthAccount.cs rename to ApiDoctor.Console/Auth/OAuthAccount.cs index 5900e235..c33319f3 100644 --- a/ApiDocs.Console/Auth/OAuthAccount.cs +++ b/ApiDoctor.Console/Auth/OAuthAccount.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp.Auth +namespace ApiDoctor.ConsoleApp.Auth { using System; using Validation; diff --git a/ApiDocs.Console/Auth/OAuthAccountException.cs b/ApiDoctor.Console/Auth/OAuthAccountException.cs similarity index 98% rename from ApiDocs.Console/Auth/OAuthAccountException.cs rename to ApiDoctor.Console/Auth/OAuthAccountException.cs index 399a556f..0bfba805 100644 --- a/ApiDocs.Console/Auth/OAuthAccountException.cs +++ b/ApiDoctor.Console/Auth/OAuthAccountException.cs @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp.Auth +namespace ApiDoctor.ConsoleApp.Auth { using System; using System.Runtime.Serialization; diff --git a/ApiDocs.Console/Auth/OAuthTokenGenerator.cs b/ApiDoctor.Console/Auth/OAuthTokenGenerator.cs similarity index 98% rename from ApiDocs.Console/Auth/OAuthTokenGenerator.cs rename to ApiDoctor.Console/Auth/OAuthTokenGenerator.cs index 70ec813e..399da422 100644 --- a/ApiDocs.Console/Auth/OAuthTokenGenerator.cs +++ b/ApiDoctor.Console/Auth/OAuthTokenGenerator.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp.Auth +namespace ApiDoctor.ConsoleApp.Auth { using System; using System.IO; diff --git a/ApiDocs.Console/CheckResults.cs b/ApiDoctor.Console/CheckResults.cs similarity index 97% rename from ApiDocs.Console/CheckResults.cs rename to ApiDoctor.Console/CheckResults.cs index b87f7daa..f4e2129f 100644 --- a/ApiDocs.Console/CheckResults.cs +++ b/ApiDoctor.Console/CheckResults.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation; + using ApiDoctor.Validation.Error; public class CheckResults { diff --git a/ApiDocs.Console/CommandLineOptions.cs b/ApiDoctor.Console/CommandLineOptions.cs similarity index 89% rename from ApiDocs.Console/CommandLineOptions.cs rename to ApiDoctor.Console/CommandLineOptions.cs index 7ab73bd0..9b357616 100644 --- a/ApiDocs.Console/CommandLineOptions.cs +++ b/ApiDoctor.Console/CommandLineOptions.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,14 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { using System; using System.Collections.Generic; using System.Linq; - using ApiDocs.ConsoleApp.Auth; - using ApiDocs.Validation; - using ApiDocs.Validation.Writers; + using ApiDoctor.ConsoleApp.Auth; + using ApiDoctor.Validation; + using ApiDoctor.Validation.Writers; using CommandLine; using CommandLine.Text; using Publishing.CSDL; @@ -41,6 +41,7 @@ class CommandLineOptions public const string VerbCheckLinks = "check-links"; public const string VerbSet = "set"; public const string VerbDocs = "check-docs"; + public const string VerbFix = "fix-docs"; public const string VerbService = "check-service"; public const string VerbPublish = "publish"; public const string VerbMetadata = "check-metadata"; @@ -74,6 +75,9 @@ class CommandLineOptions [VerbOption(VerbMetadata, HelpText = "Check service CSDL metadata against documentation.")] public CheckMetadataOptions CheckMetadataVerb { get; set; } + [VerbOption(VerbFix, HelpText = "Fix documentation based on input CSDL.")] + public FixDocsOptions FixDocsVerb { get; set; } + [VerbOption(VerbGenerateDocs, HelpText = "Generate documentation from an CSDL model")] public GenerateDocsOptions GenerateDocsVerb { get; set; } @@ -132,7 +136,7 @@ public Dictionary PageParameterDict { #if DEBUG [Option("debug", HelpText = "Launch the debugger before doing anything interesting")] - public bool AttachDebugger { get; set; } + public int AttachDebugger { get; set; } #endif public virtual bool HasRequiredProperties(out string[] missingArguments) @@ -181,6 +185,39 @@ class CheckMetadataOptions : DocSetOptions } + class FixDocsOptions : CheckMetadataOptions + { + [Option("fixbasetypes", HelpText = "Set base types")] + public bool FixBaseTypes { get; set; } + + [Option("dryrun", HelpText = "Dry run -- only print proposed fixes")] + public bool DryRun { get; set; } + + [Option("matches", HelpText = "if specified, only apply changes to the specified matches")] + public string RawMatches { get; set; } + + private HashSet matches; + public HashSet Matches + { + get + { + if (this.matches == null) + { + if (string.IsNullOrWhiteSpace(this.RawMatches)) + { + this.matches = new HashSet(); + } + else + { + this.matches = new HashSet(this.RawMatches.Split(',', ';').Select(m => m.Trim()), StringComparer.OrdinalIgnoreCase); + } + } + + return this.matches; + } + } + } + class PrintOptions : DocSetOptions { [Option("files", HelpText = "Print the files discovered as part of the documentation")] @@ -229,7 +266,7 @@ class BasicCheckOptions : DocSetOptions public bool ForceAllScenarios { get; set; } [Option("relax-string-validation", HelpText = "Relax the validation of JSON string properties.")] - public bool RelaxStringTypeValidation { get; set; } + public bool? RelaxStringTypeValidation { get; set; } [Option("changes-since-branch-only", HelpText="Only perform validation on files changed since the specified branch.")] public string FilesChangedFromOriginalBranch { get; set; } @@ -413,6 +450,9 @@ class PublishMetadataOptions : DocSetOptions [Option("merge-with", HelpText= "Specify a second metadata input file to merge with the first.")] public string SecondSourceMetadataPath { get; set; } + [Option("compare-to", HelpText = "Specify a metadata input file to sort and compare with the published output. Both will be output to {name}-sorted.edmx")] + public string CompareToMetadataPath { get; set; } + [Option("format", DefaultValue=MetadataFormat.Default, HelpText="Specify the input and output formats for metadata.")] public MetadataFormat DataFormat { get; set; } @@ -422,9 +462,6 @@ class PublishMetadataOptions : DocSetOptions [Option("sort", HelpText = "Sort the output. This is supported for EDMX publishing currently.")] public bool SortOutput { get; set; } - [Option("base-url", HelpText = "Specify the base service URL included in method examples to be removed when generating metadata")] - public string BaseUrl { get; set; } - [Option("transform", HelpText="Apply a named publishSchemaChanges transformation to the output file.")] public string TransformOutput { get; set; } @@ -434,34 +471,45 @@ class PublishMetadataOptions : DocSetOptions [Option("skip-generation", HelpText = "Skip generating new elements in the metadata, and only augment elements in the metadata. Can only be used with the --source parameter.")] public bool SkipMetadataGeneration { get; set; } + [Option("keep-unrecognized-objects-from-compare-to", HelpText = "By default, we drop any extra elements that the 'compare-to' file has. When this is true, we keep them.")] + public bool KeepUnrecognizedObjects { get; set; } + + [Option("annotations", DefaultValue = AnnotationOptions.None, HelpText = "Specify whether and how to output annotations.")] + public AnnotationOptions Annotations { get; set; } + [Option("validate", HelpText = "Perform validation on the resulting schema to check for errors.")] public bool ValidateSchema { get; set; } [Option("new-line-attributes", HelpText = "Put XML attributes on a new line")] public bool AttributesOnNewLines { get; set; } + [Option("entity-container", HelpText = "Custom entity container name. Defaults to largest namespace")] + public string EntityContainerName { get; set; } + + [Option("show-sources", HelpText = "Debug option to list the source files for each function or action")] + public bool ShowSources { get; set; } + public CsdlWriterOptions GetOptions() { return new CsdlWriterOptions { - BaseUrl = BaseUrl, Formats = DataFormat, Sort = SortOutput, OutputDirectoryPath = OutputDirectory, SourceMetadataPath = SourceMetadataPath, MergeWithMetadataPath = SecondSourceMetadataPath, - Namespaces = Namespaces?.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries), + Namespaces = Namespaces?.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries), TransformOutput = TransformOutput, DocumentationSetPath = DocumentationSetPath, Version = Version, + Annotations = Annotations, + ShowSources = ShowSources, SkipMetadataGeneration = SkipMetadataGeneration, ValidateSchema = ValidateSchema, - AttributesOnNewLines = AttributesOnNewLines + AttributesOnNewLines = AttributesOnNewLines, + EntityContainerName = EntityContainerName, }; } - - - } class PublishOptions : DocSetOptions, IPublishOptions @@ -520,9 +568,6 @@ public string[] FilesToPublish { [Option("swagger-auth-scope", HelpText = "Override the auth scope detection with a default auth scope on every method")] public string AuthScopeDefault { get; set; } - [Option("base-url", HelpText = "Specify the base service URL included in method examples to be removed when generating metadata")] - public string BaseUrl { get; set; } - [Option("template-format", HelpText = "For EDMX publishing, only publish a single schema in CSDL format instead of the full EDMX.", DefaultValue = MetadataFormat.Default)] public MetadataFormat TemplateFormat { get; set; } @@ -555,6 +600,7 @@ public override bool HasRequiredProperties(out string[] missingArguments) } missingArguments = missingArgs.ToArray(); + FancyConsole.WriteLine(ConsoleColor.Red, $"Missing arguments: {string.Join(",", missingArguments)}"); return missingArgs.Count == 0; } diff --git a/ApiDocs.Console/FancyConsole.cs b/ApiDoctor.Console/FancyConsole.cs similarity index 99% rename from ApiDocs.Console/FancyConsole.cs rename to ApiDoctor.Console/FancyConsole.cs index 50874cf2..d7e363ab 100644 --- a/ApiDocs.Console/FancyConsole.cs +++ b/ApiDoctor.Console/FancyConsole.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { using System; using System.IO; diff --git a/ApiDocs.Console/GitHelper.cs b/ApiDoctor.Console/GitHelper.cs similarity index 98% rename from ApiDocs.Console/GitHelper.cs rename to ApiDoctor.Console/GitHelper.cs index e40b3ef2..9d6d9259 100644 --- a/ApiDocs.Console/GitHelper.cs +++ b/ApiDoctor.Console/GitHelper.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { using System; diff --git a/ApiDocs.Console/Program.cs b/ApiDoctor.Console/Program.cs similarity index 66% rename from ApiDocs.Console/Program.cs rename to ApiDoctor.Console/Program.cs index 4aa844cd..7c73e439 100644 --- a/ApiDocs.Console/Program.cs +++ b/ApiDoctor.Console/Program.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,15 +23,20 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { using System; using System.Collections.Generic; using System.Diagnostics; + using System.IO; using System.Linq; + using System.Text; using System.Threading.Tasks; + using ApiDoctor.DocumentationGeneration; + using ApiDoctor.Validation.OData.Transformation; using AppVeyor; - using ApiDocs.DocumentationGeneration; + using CommandLine; + using Newtonsoft.Json; using Publishing.Html; using Publishing.Swagger; using Validation; @@ -41,9 +46,6 @@ namespace ApiDocs.ConsoleApp using Validation.OData; using Validation.Params; using Validation.Writers; - using CommandLine; - using Newtonsoft.Json; - class Program { @@ -63,12 +65,11 @@ static void Main(string[] args) { Logging.ProviderLogger(new ConsoleAppLogger()); - FancyConsole.WriteLine(ConsoleColor.Green, "APIDocs tool, version {0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); + FancyConsole.WriteLine(ConsoleColor.Green, "API Doctor, version {0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); FancyConsole.WriteLine(); if (args.Length > 0) FancyConsole.WriteLine("Command line: " + args.ComponentsJoinedByString(" ")); - string verbName = null; BaseOptions verbOptions = null; @@ -82,12 +83,13 @@ static void Main(string[] args) verbOptions = (BaseOptions)subOptions; })) { + FancyConsole.WriteLine(ConsoleColor.Red, "COMMAND LINE PARSE ERROR"); Exit(failure: true); } IgnoreErrors = verbOptions.IgnoreErrors; #if DEBUG - if (verbOptions.AttachDebugger) + if (verbOptions.AttachDebugger == 1) { Debugger.Launch(); } @@ -162,12 +164,19 @@ public static void LoadCurrentConfiguration(DocSetOptions options) private static async Task RunInvokedMethodAsync(CommandLineOptions origCommandLineOpts, string invokedVerb, BaseOptions options) { + var issues = new IssueLogger() + { +#if DEBUG + DebugLine = options.AttachDebugger, +#endif + }; + string[] missingProps; if (!options.HasRequiredProperties(out missingProps)) { - var error = new ValidationError(ValidationErrorCode.MissingRequiredArguments, null, "Command line is missing required arguments: {0}", missingProps.ComponentsJoinedByString(", ")); + issues.Error(ValidationErrorCode.MissingRequiredArguments, $"Command line is missing required arguments: {missingProps.ComponentsJoinedByString(", ")}"); FancyConsole.WriteLine(origCommandLineOpts.GetUsage(invokedVerb)); - await WriteOutErrorsAndFinishTestAsync(new ValidationError[] { error }, options.SilenceWarnings, printFailuresOnly: options.PrintFailuresOnly); + await WriteOutErrorsAndFinishTestAsync(issues, options.SilenceWarnings, printFailuresOnly: options.PrintFailuresOnly); Exit(failure: true); } @@ -178,38 +187,47 @@ private static async Task RunInvokedMethodAsync(CommandLineOptions origCommandLi switch (invokedVerb) { case CommandLineOptions.VerbPrint: - await PrintDocInformationAsync((PrintOptions)options); + await PrintDocInformationAsync((PrintOptions)options, issues); break; case CommandLineOptions.VerbCheckLinks: - returnSuccess = await CheckLinksAsync((CheckLinkOptions)options); + returnSuccess = await CheckLinksAsync((CheckLinkOptions)options, issues); break; case CommandLineOptions.VerbDocs: - returnSuccess = await CheckDocsAsync((BasicCheckOptions)options); + returnSuccess = await CheckDocsAsync((BasicCheckOptions)options, issues); break; case CommandLineOptions.VerbCheckAll: - returnSuccess = await CheckDocsAllAsync((CheckLinkOptions)options); + returnSuccess = await CheckDocsAllAsync((CheckLinkOptions)options, issues); break; case CommandLineOptions.VerbService: - returnSuccess = await CheckServiceAsync((CheckServiceOptions)options); + returnSuccess = await CheckServiceAsync((CheckServiceOptions)options, issues); break; case CommandLineOptions.VerbPublish: - returnSuccess = await PublishDocumentationAsync((PublishOptions)options); + returnSuccess = await PublishDocumentationAsync((PublishOptions)options, issues); break; case CommandLineOptions.VerbPublishMetadata: - returnSuccess = await PublishMetadataAsync((PublishMetadataOptions)options); + returnSuccess = await PublishMetadataAsync((PublishMetadataOptions)options, issues); break; case CommandLineOptions.VerbMetadata: - await CheckServiceMetadataAsync((CheckMetadataOptions)options); + await CheckServiceMetadataAsync((CheckMetadataOptions)options, issues); break; case CommandLineOptions.VerbGenerateDocs: returnSuccess = await GenerateDocsAsync((GenerateDocsOptions)options); break; + case CommandLineOptions.VerbFix: + returnSuccess = await FixDocsAsync((FixDocsOptions)options, issues); + break; case CommandLineOptions.VerbAbout: PrintAboutMessage(); Exit(failure: false); break; } + WriteMessages( + issues, + options.IgnoreWarnings, + indent: " ", + printUnusedSuppressions: invokedVerb == CommandLineOptions.VerbCheckAll); + Exit(failure: !returnSuccess); } @@ -220,26 +238,29 @@ private static async Task RunInvokedMethodAsync(CommandLineOptions origCommandLi /// /// /// - private static async Task CheckDocsAllAsync(CheckLinkOptions options) + private static async Task CheckDocsAllAsync(CheckLinkOptions options, IssueLogger issues) { - var docset = await GetDocSetAsync(options); + var docset = await GetDocSetAsync(options, issues); if (null == docset) return false; - var checkLinksResult = await CheckLinksAsync(options, docset); - var checkDocsResults = await CheckDocsAsync(options, docset); + var checkLinksResult = await CheckLinksAsync(options, issues, docset); + var checkDocsResults = await CheckDocsAsync(options, issues, docset); + + var publishOptions = new PublishMetadataOptions(); + var publishResult = await PublishMetadataAsync(publishOptions, issues, docset); - return checkLinksResult && checkDocsResults; + return checkLinksResult && checkDocsResults && publishResult; } private static void PrintAboutMessage() { FancyConsole.WriteLine(); - FancyConsole.WriteLine(ConsoleColor.Cyan, "apidocs.exe - API Documentation Test Tool"); - FancyConsole.WriteLine(ConsoleColor.Cyan, "Copyright (c) 2015 Microsoft Corporation"); + FancyConsole.WriteLine(ConsoleColor.Cyan, "apidoc.exe - API Documentation Test Tool"); + FancyConsole.WriteLine(ConsoleColor.Cyan, "Copyright (c) 2018 Microsoft Corporation"); FancyConsole.WriteLine(); - FancyConsole.WriteLine(ConsoleColor.Cyan, "For more information see http://github.com/onedrive/markdown-scanner/"); + FancyConsole.WriteLine(ConsoleColor.Cyan, "For more information see http://github.com/onedrive/apidoctor"); FancyConsole.WriteLine(); } @@ -249,13 +270,13 @@ private static void PrintAboutMessage() /// /// /// - private static Task GetDocSetAsync(DocSetOptions options) + private static Task GetDocSetAsync(DocSetOptions options, IssueLogger issues) { FancyConsole.VerboseWriteLine("Opening documentation from {0}", options.DocumentationSetPath); DocSet set = null; try { - set = new DocSet(options.DocumentationSetPath); + set = new DocSet(options.DocumentationSetPath, writeFixesBackToDisk: options is FixDocsOptions); } catch (System.IO.FileNotFoundException ex) { @@ -265,7 +286,6 @@ private static Task GetDocSetAsync(DocSetOptions options) } FancyConsole.VerboseWriteLine("Scanning documentation files..."); - ValidationError[] loadErrors; string tagsToInclude; if (null == options.PageParameterDict || !options.PageParameterDict.TryGetValue("tags", out tagsToInclude)) @@ -274,19 +294,11 @@ private static Task GetDocSetAsync(DocSetOptions options) } DateTimeOffset start = DateTimeOffset.Now; - set.ScanDocumentation(tagsToInclude, out loadErrors); + set.ScanDocumentation(tagsToInclude, issues); DateTimeOffset end = DateTimeOffset.Now; TimeSpan duration = end.Subtract(start); FancyConsole.WriteLine($"Took {duration.TotalSeconds} to parse {set.Files.Length} source files."); - - if (loadErrors.Any()) - { - FancyConsole.WriteLine(); - FancyConsole.WriteLine("Errors detected while parsing documentation:"); - WriteMessages(loadErrors, false, " ", false); - } - return Task.FromResult(set); } @@ -310,9 +322,9 @@ public static void RecordError(string format, params object[] variables) /// /// /// - private static async Task PrintDocInformationAsync(PrintOptions options, DocSet docset = null) + private static async Task PrintDocInformationAsync(PrintOptions options, IssueLogger issues, DocSet docset = null) { - docset = docset ?? await GetDocSetAsync(options); + docset = docset ?? await GetDocSetAsync(options, issues); if (null == docset) { return; @@ -320,15 +332,15 @@ private static async Task PrintDocInformationAsync(PrintOptions options, DocSet if (options.PrintFiles) { - await PrintFilesAsync(options, docset); + await PrintFilesAsync(options, docset, issues); } if (options.PrintResources) { - await PrintResourcesAsync(options, docset); + await PrintResourcesAsync(options, docset, issues); } if (options.PrintMethods) { - await PrintMethodsAsync(options, docset); + await PrintMethodsAsync(options, docset, issues); } if (options.PrintAccounts) { @@ -343,9 +355,9 @@ private static async Task PrintDocInformationAsync(PrintOptions options, DocSet /// /// /// - private static async Task PrintFilesAsync(DocSetOptions options, DocSet docset) + private static async Task PrintFilesAsync(DocSetOptions options, DocSet docset, IssueLogger issues) { - docset = docset ?? await GetDocSetAsync(options); + docset = docset ?? await GetDocSetAsync(options, issues); if (null == docset) return; @@ -379,12 +391,9 @@ private static async Task PrintFilesAsync(DocSetOptions options, DocSet docset) /// /// Print a list of the resources detected in the documentation set to the console. /// - /// - /// - /// - private static async Task PrintResourcesAsync(DocSetOptions options, DocSet docset) + private static async Task PrintResourcesAsync(DocSetOptions options, DocSet docset, IssueLogger issues) { - docset = docset ?? await GetDocSetAsync(options); + docset = docset ?? await GetDocSetAsync(options, issues); if (null == docset) return; @@ -416,12 +425,9 @@ private static async Task PrintResourcesAsync(DocSetOptions options, DocSet docs /// /// Print a list of the methods (request/responses) discovered in the documentation to the console. /// - /// - /// - /// - private static async Task PrintMethodsAsync(DocSetOptions options, DocSet docset) + private static async Task PrintMethodsAsync(DocSetOptions options, DocSet docset, IssueLogger issues) { - docset = docset ?? await GetDocSetAsync(options); + docset = docset ?? await GetDocSetAsync(options, issues); if (null == docset) return; @@ -468,10 +474,10 @@ private static async Task PrintAccountsAsync(PrintOptions options, DocSet docset /// /// /// - private static async Task CheckLinksAsync(CheckLinkOptions options, DocSet docs = null) + private static async Task CheckLinksAsync(CheckLinkOptions options, IssueLogger issues, DocSet docs = null) { const string testName = "Check-links"; - var docset = docs ?? await GetDocSetAsync(options); + var docset = docs ?? await GetDocSetAsync(options, issues); if (null == docset) return false; @@ -485,11 +491,10 @@ private static async Task CheckLinksAsync(CheckLinkOptions options, DocSet } TestReport.StartTest(testName); - - ValidationError[] errors; - docset.ValidateLinks(options.EnableVerboseOutput, interestingFiles, out errors, options.RequireFilenameCaseMatch, options.IncludeOrphanPageWarning); - foreach (var error in errors) + docset.ValidateLinks(options.EnableVerboseOutput, interestingFiles, issues, options.RequireFilenameCaseMatch, options.IncludeOrphanPageWarning); + + foreach (var error in issues.Issues) { MessageCategory category; if (error.IsWarning) @@ -503,7 +508,7 @@ private static async Task CheckLinksAsync(CheckLinkOptions options, DocSet await TestReport.LogMessageAsync(message, category); } - return await WriteOutErrorsAndFinishTestAsync(errors, options.SilenceWarnings, successMessage: "No link errors detected.", testName: testName, printFailuresOnly: options.PrintFailuresOnly); + return await WriteOutErrorsAndFinishTestAsync(issues, options.SilenceWarnings, successMessage: "No link errors detected.", testName: testName, printFailuresOnly: options.PrintFailuresOnly); } /// @@ -544,21 +549,21 @@ where m.Identifier.IsWildcardMatch(wildcardPattern) /// /// /// - private static async Task CheckDocsAsync(BasicCheckOptions options, DocSet docs = null) + private static async Task CheckDocsAsync(BasicCheckOptions options, IssueLogger issues, DocSet docs = null) { - var docset = docs ?? await GetDocSetAsync(options); + var docset = docs ?? await GetDocSetAsync(options, issues); if (null == docset) return false; FancyConsole.WriteLine(); - var resultStructure = await CheckDocStructure(options, docset); + var resultStructure = await CheckDocStructure(options, docset, issues); - var resultMethods = await CheckMethodsAsync(options, docset); + var resultMethods = await CheckMethodsAsync(options, docset, issues); CheckResults resultExamples = new CheckResults(); if (string.IsNullOrEmpty(options.MethodName)) { - resultExamples = await CheckExamplesAsync(options, docset); + resultExamples = await CheckExamplesAsync(options, docset, issues); } var combinedResults = resultMethods + resultExamples + resultStructure; @@ -573,18 +578,18 @@ private static async Task CheckDocsAsync(BasicCheckOptions options, DocSet return combinedResults.FailureCount == 0; } - private static async Task CheckDocStructure(BasicCheckOptions options, DocSet docset) + private static async Task CheckDocStructure(BasicCheckOptions options, DocSet docset, IssueLogger issues) { var results = new CheckResults(); TestReport.StartTest("Verify document structure"); - List detectedErrors = new List(); foreach (var doc in docset.Files) { - detectedErrors.AddRange(doc.CheckDocumentStructure()); + doc.CheckDocumentStructure(issues.For(doc.DisplayName)); } - await WriteOutErrorsAndFinishTestAsync(detectedErrors, options.SilenceWarnings, " ", "Passed.", false, "Verify document structure", "Warnings detected", "Errors detected", printFailuresOnly: options.PrintFailuresOnly); - results.IncrementResultCount(detectedErrors); + + await WriteOutErrorsAndFinishTestAsync(issues, options.SilenceWarnings, " ", "Passed.", false, "Verify document structure", "Warnings detected", "Errors detected", printFailuresOnly: options.PrintFailuresOnly); + results.IncrementResultCount(issues.Issues); return results; } @@ -596,7 +601,7 @@ private static async Task CheckDocStructure(BasicCheckOptions opti /// /// /// - private static async Task CheckExamplesAsync(BasicCheckOptions options, DocSet docset) + private static async Task CheckExamplesAsync(BasicCheckOptions options, DocSet docset, IssueLogger issues) { var results = new CheckResults(); @@ -606,7 +611,8 @@ private static async Task CheckExamplesAsync(BasicCheckOptions opt { continue; } - + + var exampleIssues = issues.For(doc.DisplayName); FancyConsole.WriteLine(FancyConsole.ConsoleHeaderColor, "Checking examples in \"{0}\"...", doc.DisplayName); foreach (var example in doc.Examples) @@ -619,11 +625,10 @@ private static async Task CheckExamplesAsync(BasicCheckOptions opt var testName = string.Format("check-example: {0}", example.Metadata.MethodName?.FirstOrDefault(), example.Metadata.ResourceType); TestReport.StartTest(testName, doc.DisplayName); - ValidationError[] errors; - docset.ResourceCollection.ValidateJsonExample(example.Metadata, example.SourceExample, out errors, new ValidationOptions { RelaxedStringValidation = options.RelaxStringTypeValidation }); + docset.ResourceCollection.ValidateJsonExample(example.Metadata, example.SourceExample, exampleIssues, new ValidationOptions { RelaxedStringValidation = options.RelaxStringTypeValidation ?? true }); - await WriteOutErrorsAndFinishTestAsync(errors, options.SilenceWarnings, " ", "Passed.", false, testName, "Warnings detected", "Errors detected", printFailuresOnly: options.PrintFailuresOnly); - results.IncrementResultCount(errors); + await WriteOutErrorsAndFinishTestAsync(exampleIssues, options.SilenceWarnings, " ", "Passed.", false, testName, "Warnings detected", "Errors detected", printFailuresOnly: options.PrintFailuresOnly); + results.IncrementResultCount(exampleIssues.Issues); } } @@ -637,13 +642,14 @@ private static async Task CheckExamplesAsync(BasicCheckOptions opt /// /// /// - private static async Task CheckMethodsAsync(BasicCheckOptions options, DocSet docset) + private static async Task CheckMethodsAsync(BasicCheckOptions options, DocSet docset, IssueLogger issues) { MethodDefinition[] methods = FindTestMethods(options, docset); CheckResults results = new CheckResults(); foreach (var method in methods) { + var methodIssues = issues.For(method.Identifier); var testName = "API Request: " + method.Identifier; TestReport.StartTest(testName, method.SourceFile.DisplayName, skipPrintingHeader: options.PrintFailuresOnly); @@ -657,21 +663,19 @@ private static async Task CheckMethodsAsync(BasicCheckOptions opti var parser = new HttpParser(); - - ValidationError[] errors; try { var expectedResponse = parser.ParseHttpResponse(method.ExpectedResponse); - method.ValidateResponse(expectedResponse, null, null, out errors, new ValidationOptions { RelaxedStringValidation = options.RelaxStringTypeValidation }); + method.ValidateResponse(expectedResponse, null, null, methodIssues, new ValidationOptions { RelaxedStringValidation = options.RelaxStringTypeValidation ?? true }); } catch (Exception ex) { - errors = new ValidationError[] { new ValidationError(ValidationErrorCode.ExceptionWhileValidatingMethod, method.SourceFile.DisplayName, ex.Message) }; + methodIssues.Error(ValidationErrorCode.ExceptionWhileValidatingMethod, method.SourceFile.DisplayName, ex); } - await WriteOutErrorsAndFinishTestAsync(errors, options.SilenceWarnings, " ", "Passed.", false, testName, "Warnings detected", "Errors detected", printFailuresOnly: options.PrintFailuresOnly); - results.IncrementResultCount(errors); + await WriteOutErrorsAndFinishTestAsync(methodIssues, options.SilenceWarnings, " ", "Passed.", false, testName, "Warnings detected", "Errors detected", printFailuresOnly: options.PrintFailuresOnly); + results.IncrementResultCount(methodIssues.Issues); } return results; @@ -780,23 +784,21 @@ where f.DisplayName.IsWildcardMatch(filter) /// /// /// - private static async Task WriteOutErrorsAndFinishTestAsync(IEnumerable errors, bool silenceWarnings, string indent = "", string successMessage = null, bool endLineBeforeWriting = false, string testName = null, string warningsMessage = null, string errorsMessage = null, bool printFailuresOnly = false) + private static async Task WriteOutErrorsAndFinishTestAsync(IssueLogger issues, bool silenceWarnings, string indent = "", string successMessage = null, bool endLineBeforeWriting = false, string testName = null, string warningsMessage = null, string errorsMessage = null, bool printFailuresOnly = false) { - var validationErrors = errors as ValidationError[] ?? errors.ToArray(); - string writeMessageHeader = null; if (printFailuresOnly) { writeMessageHeader = $"Test {testName} results:"; } - WriteMessages(validationErrors, silenceWarnings, indent, endLineBeforeWriting, beforeWriteHeader: writeMessageHeader); + WriteMessages(issues, silenceWarnings, indent, endLineBeforeWriting, beforeWriteHeader: writeMessageHeader); TestOutcome outcome = TestOutcome.None; string outputMessage = null; - var errorMessages = validationErrors.Where(x => x.IsError); - var warningMessages = validationErrors.Where(x => x.IsWarning); + var errorMessages = issues.Issues.Where(x => x.IsError); + var warningMessages = issues.Issues.Where(x => x.IsWarning); if (errorMessages.Any()) { @@ -829,7 +831,7 @@ private static async Task WriteOutErrorsAndFinishTestAsync(IEnumerable WriteOutErrorsAndFinishTestAsync(IEnumerable /// Prints ValidationError messages to the console, optionally excluding warnings and messages. /// - /// - /// - /// - /// - private static void WriteMessages(ValidationError[] validationErrors, bool errorsOnly = false, string indent = "", bool endLineBeforeWriting = false, string beforeWriteHeader = null) + private static void WriteMessages( + IssueLogger issues, + bool errorsOnly = false, + string indent = "", + bool endLineBeforeWriting = false, + string beforeWriteHeader = null, + bool printUnusedSuppressions = false) { bool writtenHeader = false; - foreach (var error in validationErrors) + foreach (var error in issues.Issues.OrderBy(err=>err.IsWarningOrError).ThenBy(err=>err.IsError).ThenBy(err=>err.Source)) { RecordUndocumentedProperties(error); @@ -874,6 +878,46 @@ private static void WriteMessages(ValidationError[] validationErrors, bool error } WriteValidationError(indent, error); } + + var usedSuppressions = issues.UsedSuppressions; + if (usedSuppressions.Count>0) + { + FancyConsole.WriteLine(ConsoleColor.DarkYellow, $"{usedSuppressions.Count} issues were suppressed with manual overrides in the docs."); + } + + if (printUnusedSuppressions) + { + var unusedSuppressions = issues.UnusedSuppressions; + if (unusedSuppressions.Count > 0) + { + FancyConsole.WriteLine(ConsoleColor.Cyan, "Validation suppressions found that weren't used. Please remove:"); + foreach (var sup in unusedSuppressions) + { + FancyConsole.WriteLineIndented(" ", ConsoleColor.Cyan, sup); + } + } + } + + int warningCount = issues.Issues.Count(issue => issue.IsWarning); + int errorCount = issues.Issues.Count(iss => iss.IsError); + + var color = ConsoleColor.Green; + var status = "PASSED"; + if (warningCount > 0) + { + color = ConsoleColor.Yellow; + status = "FAILED with warnings"; + FancyConsole.WriteLine(color, $"{warningCount} warnings"); + } + + if (errorCount > 0) + { + color = ConsoleColor.Red; + status = "FAILED with errors"; + FancyConsole.WriteLine(color, $"{errorCount} errors"); + } + + FancyConsole.WriteLine(color, status); } private static void RecordUndocumentedProperties(ValidationError error) @@ -922,7 +966,7 @@ internal static void WriteValidationError(string indent, ValidationError error) /// /// /// - private static async Task CheckServiceAsync(CheckServiceOptions options) + private static async Task CheckServiceAsync(CheckServiceOptions options, IssueLogger issues) { // See if we're supposed to run check-service on this branch (assuming we know what branch we're running on) if (!string.IsNullOrEmpty(options.BranchName)) @@ -947,7 +991,7 @@ private static async Task CheckServiceAsync(CheckServiceOptions options) HttpRequest.HttpLogSession = httpLogging; } - var docset = await GetDocSetAsync(options); + var docset = await GetDocSetAsync(options, issues); if (null == docset) { return false; @@ -1109,7 +1153,7 @@ await ForEachAsync(methods, concurrentTasks, async method => // Test these scenarios and validate responses ValidationResults results = await method.ValidateServiceResponseAsync(scenarios, primaryAccount, secondaryAccount, new ValidationOptions { - RelaxedStringValidation = commandLineOptions.RelaxStringTypeValidation, + RelaxedStringValidation = commandLineOptions.RelaxStringTypeValidation ?? true, IgnoreRequiredScopes = commandLineOptions.IgnoreRequiredScopes }); @@ -1280,11 +1324,13 @@ private static void AddPause(CheckServiceOptions options) } } - public static async Task PublishMetadataAsync(PublishMetadataOptions options) + public static async Task PublishMetadataAsync(PublishMetadataOptions options, IssueLogger issues, DocSet docSet = null) { - DocSet docs = await GetDocSetAsync(options); + var docs = docSet ?? await GetDocSetAsync(options, issues); if (null == docs) + { return false; + } var publisher = new Publishing.CSDL.CsdlWriter(docs, options.GetOptions()); FancyConsole.WriteLine(); @@ -1295,7 +1341,7 @@ public static async Task PublishMetadataAsync(PublishMetadataOptions optio try { var outputPath = options.OutputDirectory; - await publisher.PublishToFolderAsync(outputPath); + await publisher.PublishToFolderAsync(outputPath, issues); FancyConsole.WriteLine(FancyConsole.ConsoleSuccessColor, "Finished publishing metadata."); } catch (Exception ex) @@ -1303,23 +1349,36 @@ public static async Task PublishMetadataAsync(PublishMetadataOptions optio FancyConsole.WriteLine( FancyConsole.ConsoleErrorColor, "An error occured while publishing: {0}", - ex.Message); - FancyConsole.VerboseWriteLine(ex.ToString()); + ex.ToString()); Exit(failure: true, customExitCode: 99); return false; } - return true; + if (!string.IsNullOrEmpty(options.CompareToMetadataPath)) + { + SchemaConfigFile[] schemaConfigs = DocSet.TryLoadConfigurationFiles(options.DocumentationSetPath); + var schemaConfig = schemaConfigs.FirstOrDefault(); + SchemaDiffConfig config = null; + if (schemaConfig?.SchemaDiffConfig != null) + { + Console.WriteLine($"Using schemadiff config file: {schemaConfig.SourcePath}"); + config = schemaConfig.SchemaDiffConfig; + } + var sorter = new Publishing.CSDL.XmlSorter(config) { KeepUnrecognizedObjects = options.KeepUnrecognizedObjects }; + sorter.Sort(Path.Combine(options.OutputDirectory, "metadata.xml"), options.CompareToMetadataPath); + } + + return true; } - private static async Task PublishDocumentationAsync(PublishOptions options) + private static async Task PublishDocumentationAsync(PublishOptions options, IssueLogger issues) { var outputPath = options.OutputDirectory; FancyConsole.WriteLine("Publishing documentation to {0}", outputPath); - DocSet docs = await GetDocSetAsync(options); + DocSet docs = await GetDocSetAsync(options, issues); if (null == docs) return false; @@ -1366,7 +1425,7 @@ private static async Task PublishDocumentationAsync(PublishOptions options try { - await publisher.PublishToFolderAsync(outputPath); + await publisher.PublishToFolderAsync(outputPath, issues); FancyConsole.WriteLine(FancyConsole.ConsoleSuccessColor, "Finished publishing documentation to: {0}", outputPath); } catch (Exception ex) @@ -1432,12 +1491,316 @@ private static async Task> TryGetMetadataSchemasAsync(CheckMetadata return edmx.DataServices.Schemas; } + private static async Task FixDocsAsync(FixDocsOptions options, IssueLogger issues) + { + var originalOut = Console.Out; + Console.SetOut(new System.IO.StringWriter()); + + // first read the reference metadata + var inputSchemas = await TryGetMetadataSchemasAsync(options); + if (inputSchemas == null) + { + return false; + } + + // next read the doc set + var docSetIssues = new IssueLogger() { DebugLine = issues.DebugLine }; + var docs = await GetDocSetAsync(options, docSetIssues); + if (docs == null) + { + return false; + } + + var csdlOptions = new PublishMetadataOptions(); + var publisher = new Publishing.CSDL.CsdlWriter(docs, csdlOptions.GetOptions()); + var edmx = publisher.CreateEntityFrameworkFromDocs(docSetIssues); + + Console.SetOut(originalOut); + Console.WriteLine("checking for issues..."); + foreach (var inputSchema in inputSchemas) + { + var docSchema = edmx.DataServices?.Schemas?.FirstOrDefault(s => s.Namespace == inputSchema.Namespace); + if (docSchema != null) + { + // remove any superfluous 'keyProperty' declarations. + // this can happen when copy/pasting from an entity declaration, where the key property + // is inherited from a base type and not shown in the current type + foreach (var resource in docs.Resources) + { + if (!string.IsNullOrEmpty(resource.OriginalMetadata.KeyPropertyName) && + !resource.Parameters.Any(p => p.Name == resource.KeyPropertyName)) + { + if (resource.ResolvedBaseTypeReference != null && + string.Equals(resource.OriginalMetadata.KeyPropertyName, resource.ResolvedBaseTypeReference.ExplicitOrInheritedKeyPropertyName)) + { + if (!options.DryRun) + { + resource.OriginalMetadata.KeyPropertyName = null; + resource.OriginalMetadata.PatchSourceFile(); + Console.WriteLine($"Removed keyProperty from {resource.Name} because it was defined by an ancestor."); + } + else + { + Console.WriteLine($"Want to remove keyProperty from {resource.Name} because it's defined by an ancestor."); + } + } + + if (resource.BaseType == null) + { + if (!options.DryRun) + { + resource.OriginalMetadata.KeyPropertyName = null; + resource.OriginalMetadata.PatchSourceFile(); + Console.WriteLine($"Removed keyProperty from {resource.Name} because it's probably a complex type."); + } + else + { + Console.WriteLine($"Want to remove keyProperty from {resource.Name} because it's probably a complex type."); + } + } + } + } + + foreach (var inputEnum in inputSchema.Enumerations) + { + if (!docs.Enums.Any(e=>e.TypeName.TypeOnly() == inputEnum.Name.TypeOnly())) + { + // found an enum that wasn't in the docs. + // see if we can find the resource it belongs to and stick a definition table in. + var enumMembers = inputEnum.Members.ToDictionary(m => m.Name, StringComparer.OrdinalIgnoreCase); + + foreach (var resource in docs.Resources) + { + if (options.Matches.Count > 0 && !options.Matches.Contains(resource.Name.TypeOnly())) + { + continue; + } + + bool found = false; + string tableDescriptionToAlter = null; + foreach (var param in resource.Parameters.Where(p => p.Type?.Type == SimpleDataType.String)) + { + if (param.Description != null && + (param.Description.IContains("possible values") || + param.Description.IContains("one of"))) + { + var tokens = param.Description.TokenizedWords(); + if (tokens.Count(t => enumMembers.ContainsKey(t)) >= 3) + { + found = true; + tableDescriptionToAlter = param.Description; + break; + } + } + + if (param.PossibleEnumValues().Count(v=>enumMembers.ContainsKey(v)) >=2) + { + found = true; + break; + } + } + + if (found) + { + // we want to stick this after #properties but before the next one. + int propertiesLineNumber = 0; + int propertiesHNumber = 0; + int nextWhiteSpaceLineNumber = 0; + var lines = File.ReadAllLines(resource.SourceFile.FullPath); + for (int i=0; i< lines.Length; i++) + { + var line = lines[i]; + if (propertiesLineNumber == 0 && + line.StartsWith("#") && + line.Contains("Properties")) + { + propertiesLineNumber = i; + propertiesHNumber = line.Count(c => c == '#'); + continue; + } + + // ideally we wouldn't have to re-tokenize, but the place we did it earlier + // had markdown syntax stripped out, and we don't have that luxury here... + if (line.TokenizedWords().Count(t => enumMembers.ContainsKey(t)) >= 3) + { + // | paramName | string | description text ending with enum values + var split = line.Split('|'); + if (split.Length > 3 && split[2].IContains("string")) + { + split[2] = split[2].IReplace("string", inputEnum.Name); + lines[i] = string.Join("|", split); + } + } + + if (propertiesLineNumber > 0 && string.IsNullOrWhiteSpace(line)) + { + nextWhiteSpaceLineNumber = i; + break; + } + } + + if (propertiesLineNumber > 0) + { + // produce the table + StringBuilder table = new StringBuilder($"{new string('#', propertiesHNumber + 1)} {inputEnum.Name} values\r\n\r\n"); + table.AppendLine("| Value\r\n|:-------------------------"); + foreach (var member in inputEnum.Members) + { + table.AppendLine($"| {member.Name}"); + } + table.AppendLine(); + + if (nextWhiteSpaceLineNumber == 0) + { + nextWhiteSpaceLineNumber = lines.Length - 1; + } + + var final = FileSplicer(lines, nextWhiteSpaceLineNumber, table.ToString()); + + if (options.DryRun) + { + Console.WriteLine($"Want to splice into L{nextWhiteSpaceLineNumber + 1} of {resource.SourceFile.DisplayName}\r\n{table}"); + } + else + { + File.WriteAllLines(resource.SourceFile.FullPath, final); + } + } + else + { + Console.WriteLine($"WARNING: Couldn't find where to insert enum table for {inputEnum.Name}"); + } + } + } + } + } + + foreach (var inputComplex in inputSchema.ComplexTypes.Concat(inputSchema.EntityTypes)) + { + if (options.Matches.Count > 0 && !options.Matches.Contains(inputComplex.Name)) + { + continue; + } + + var docComplex = + docSchema.ComplexTypes?.FirstOrDefault(c => c.Name.IEquals(inputComplex.Name)) ?? + docSchema.EntityTypes?.FirstOrDefault(c => c.Name.IEquals(inputComplex.Name)); + if (docComplex != null) + { + List> fixes = new List>(); + + if (inputComplex.BaseType != docComplex.BaseType) + { + Console.WriteLine($"Mismatched BaseTypes: '{docComplex.Name}:{docComplex.BaseType}' vs '{inputComplex.Name}:{inputComplex.BaseType}'."); + fixes.Add(code => code.BaseType = inputComplex.BaseType); + } + + if (inputComplex.Abstract != docComplex.Abstract) + { + Console.WriteLine($"Mismatched Abstract: '{docComplex.Name}:{docComplex.Abstract}' vs '{inputComplex.Name}:{inputComplex.Abstract}'."); + fixes.Add(code => code.Abstract = inputComplex.Abstract); + } + + if (inputComplex.OpenType != docComplex.OpenType) + { + Console.WriteLine($"Mismatched OpenType: '{docComplex.Name}:{docComplex.OpenType}' vs '{inputComplex.Name}:{inputComplex.OpenType}'."); + fixes.Add(code => code.IsOpenType = inputComplex.OpenType); + } + + var inputEntity = inputComplex as EntityType; + var docEntity = docComplex as EntityType; + if (docEntity != null && inputEntity != null) + { + if (docEntity.Key?.PropertyRef?.Name != inputEntity.Key?.PropertyRef?.Name) + { + Console.WriteLine($"Mismatched KeyProperty: '{docComplex.Name}:{docEntity.Key?.PropertyRef?.Name}' vs '{inputComplex.Name}:{inputEntity.Key?.PropertyRef?.Name}'."); + fixes.Add(code => code.KeyPropertyName = inputEntity.Key?.PropertyRef?.Name); + } + + if (!docEntity.HasStream && inputEntity.HasStream) + { + Console.WriteLine($"Mismatched IsMediaEntity in {docComplex.Name}."); + fixes.Add(code => code.IsMediaEntity = true); + } + } + + if (fixes.Count > 0) + { + foreach (var resource in docComplex.Contributors) + { + foreach (var fix in fixes) + { + fix(resource.OriginalMetadata); + if (!options.DryRun) + { + resource.OriginalMetadata.PatchSourceFile(); + Console.WriteLine("\tFixed!"); + } + } + } + } + } + } + + issues.Message("Looking for composable functions that couldn't be inferred from the docs..."); + foreach (var inputFunction in inputSchema.Functions.Where(fn => fn.IsComposable)) + { + var pc = ParameterComparer.Instance; + var docFunctions = docSchema.Functions. + Where(fn => + fn.Name == inputFunction.Name && + !fn.IsComposable && + fn.Parameters.OrderBy(p => p, pc).SequenceEqual(inputFunction.Parameters.OrderBy(p => p, pc), pc)). + ToList(); + + foreach (var docFunction in docFunctions) + { + var methods = docFunction.SourceMethods as List; + if (methods != null) + { + foreach (var metadata in methods.Select(m => m.RequestMetadata).Where(md => !md.IsComposable)) + { + metadata.IsComposable = true; + + if (options.DryRun) + { + Console.WriteLine("Would fix " + metadata.MethodName.FirstOrDefault()); + } + else + { + metadata.PatchSourceFile(); + Console.WriteLine("Fixed " + metadata.MethodName.FirstOrDefault()); + } + } + } + } + } + } + } + + return true; + } + + + public static IEnumerable FileSplicer(string[] original, int offset, string valueToSplice) + { + for (int i=0; i< original.Length; i++) + { + yield return original[i]; + + if (i == offset) + { + yield return valueToSplice; + } + } + } + /// /// Validate that the CSDL metadata defined for a service matches the documentation. /// /// /// - private static async Task CheckServiceMetadataAsync(CheckMetadataOptions options) + private static async Task CheckServiceMetadataAsync(CheckMetadataOptions options, IssueLogger issues) { List schemas = await TryGetMetadataSchemasAsync(options); if (null == schemas) @@ -1445,20 +1808,19 @@ private static async Task CheckServiceMetadataAsync(CheckMetadataOptions o FancyConsole.WriteLine(FancyConsole.ConsoleSuccessColor, " found {0} schema definitions: {1}", schemas.Count, (from s in schemas select s.Namespace).ComponentsJoinedByString(", ")); - var docSet = await GetDocSetAsync(options); + var docSet = await GetDocSetAsync(options, issues); if (null == docSet) return false; const string testname = "validate-service-metadata"; TestReport.StartTest(testname); - List foundResources = ODataParser.GenerateResourcesFromSchemas(schemas); + List foundResources = ODataParser.GenerateResourcesFromSchemas(schemas, issues); CheckResults results = new CheckResults(); - List collectedErrors = new List(); - foreach (var resource in foundResources) { + var resourceIssues = issues.For(resource.Name); FancyConsole.WriteLine(); FancyConsole.Write(FancyConsole.ConsoleHeaderColor, "Checking metadata resource: {0}...", resource.Name); @@ -1484,29 +1846,22 @@ from r in docSet.Resources } - ValidationError[] errors; if (null == matchingDocumentationResource) { // Couldn't find this resource in the documentation! - errors = new ValidationError[] - { - new ValidationError( - ValidationErrorCode.ResourceTypeNotFound, - null, - "Resource {0} is not in the documentation.", - resource.Name) - }; + resourceIssues.Error( + ValidationErrorCode.ResourceTypeNotFound, + $"Resource {resource.Name} is not in the documentation."); } else { // Verify that this resource matches the documentation - docSet.ResourceCollection.ValidateJsonExample(resource.OriginalMetadata, resource.ExampleText, out errors, new ValidationOptions { RelaxedStringValidation = true }); + docSet.ResourceCollection.ValidateJsonExample(resource.OriginalMetadata, resource.ExampleText, resourceIssues, new ValidationOptions { RelaxedStringValidation = true }); } - results.IncrementResultCount(errors); - collectedErrors.AddRange(errors); + results.IncrementResultCount(resourceIssues.Issues); - await WriteOutErrorsAndFinishTestAsync(errors, options.SilenceWarnings, successMessage: " passed.", printFailuresOnly: options.PrintFailuresOnly); + await WriteOutErrorsAndFinishTestAsync(resourceIssues, options.SilenceWarnings, successMessage: " passed.", printFailuresOnly: options.PrintFailuresOnly); } if (options.IgnoreWarnings) @@ -1514,7 +1869,7 @@ from r in docSet.Resources results.ConvertWarningsToSuccess(); } - var output = (from e in collectedErrors select e.ErrorText).ComponentsJoinedByString("\r\n"); + var output = (from e in issues.Issues select e.ErrorText).ComponentsJoinedByString("\r\n"); await TestReport.FinishTestAsync(testname, results.WereFailures ? TestOutcome.Failed : TestOutcome.Passed, stdOut:output); @@ -1535,6 +1890,75 @@ private static async Task GenerateDocsAsync(GenerateDocsOptions options) return true; } + + private class ParameterComparer : IEqualityComparer, IComparer + { + public static ParameterComparer Instance { get; } = new ParameterComparer(); + + private static HashSet equivalentBindingParameters = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "bindParameter", + "bindingParameter", + "this", + }; + + private ParameterComparer() + { + } + + public bool Equals(Parameter x, Parameter y) + { + if (object.ReferenceEquals(x, y)) + { + return true; + } + + if (x == null || y == null) + { + return false; + } + + return x.Name == y.Name || + (equivalentBindingParameters.Contains(x.Name) && equivalentBindingParameters.Contains(y.Name)); + } + + public int GetHashCode(Parameter obj) + { + return obj?.Name?.GetHashCode() ?? 0; + } + + public int Compare(Parameter x, Parameter y) + { + if (object.ReferenceEquals(x,y)) + { + return 0; + } + + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + var xName = x.Name; + var yName = y.Name; + if (equivalentBindingParameters.Contains(xName)) + { + xName = "bindingParameter"; + } + + if (equivalentBindingParameters.Contains(yName)) + { + yName = "bindingParameter"; + } + + return xName.CompareTo(yName); + } + } } class ConsoleAppLogger : ILogHelper diff --git a/ApiDocs.Console/Properties/AssemblyInfo.cs b/ApiDoctor.Console/Properties/AssemblyInfo.cs similarity index 96% rename from ApiDocs.Console/Properties/AssemblyInfo.cs rename to ApiDoctor.Console/Properties/AssemblyInfo.cs index f7ede8c9..0dade0d2 100644 --- a/ApiDocs.Console/Properties/AssemblyInfo.cs +++ b/ApiDoctor.Console/Properties/AssemblyInfo.cs @@ -4,7 +4,7 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("API Docs Tool")] +[assembly: AssemblyTitle("API Doctor")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] diff --git a/ApiDocs.Console/TestReport.cs b/ApiDoctor.Console/TestReport.cs similarity index 97% rename from ApiDocs.Console/TestReport.cs rename to ApiDoctor.Console/TestReport.cs index 16d39b0d..ba807ea8 100644 --- a/ApiDocs.Console/TestReport.cs +++ b/ApiDoctor.Console/TestReport.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,18 +23,18 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; - using ApiDocs.ConsoleApp.AppVeyor; - using ApiDocs.Validation; + using ApiDoctor.ConsoleApp.AppVeyor; + using ApiDoctor.Validation; public static class TestReport { - private const string TestFrameworkName = "apidocs"; + private const string TestFrameworkName = "apidoctor"; private static BuildWorkerApi BuildWorkerApi { get { return Program.BuildWorker; } } private static readonly Dictionary TestStartTimes = new Dictionary(); private static readonly Dictionary TestStartFilename = new Dictionary(); diff --git a/ApiDocs.Console/WildcardExtensions.cs b/ApiDoctor.Console/WildcardExtensions.cs similarity index 97% rename from ApiDocs.Console/WildcardExtensions.cs rename to ApiDoctor.Console/WildcardExtensions.cs index 75775b5f..627d5a69 100644 --- a/ApiDocs.Console/WildcardExtensions.cs +++ b/ApiDoctor.Console/WildcardExtensions.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -24,7 +24,7 @@ */ -namespace ApiDocs.ConsoleApp +namespace ApiDoctor.ConsoleApp { using System.Text.RegularExpressions; diff --git a/ApiDocs.Console/packages.config b/ApiDoctor.Console/packages.config similarity index 100% rename from ApiDocs.Console/packages.config rename to ApiDoctor.Console/packages.config diff --git a/ApiDocs.DocumentationGeneration.UnitTests/ApiDocs.DocumentationGeneration.UnitTests.csproj b/ApiDoctor.DocumentationGeneration.UnitTests/ApiDoctor.DocumentationGeneration.UnitTests.csproj similarity index 87% rename from ApiDocs.DocumentationGeneration.UnitTests/ApiDocs.DocumentationGeneration.UnitTests.csproj rename to ApiDoctor.DocumentationGeneration.UnitTests/ApiDoctor.DocumentationGeneration.UnitTests.csproj index 3c2c7b88..063b4044 100644 --- a/ApiDocs.DocumentationGeneration.UnitTests/ApiDocs.DocumentationGeneration.UnitTests.csproj +++ b/ApiDoctor.DocumentationGeneration.UnitTests/ApiDoctor.DocumentationGeneration.UnitTests.csproj @@ -6,8 +6,8 @@ {32323786-6B69-4A7A-A5DA-DBBBF1148387} Library Properties - ApiDocs.DocumentationGeneration.UnitTests - ApiDocs.DocumentationGeneration.UnitTests + ApiDoctor.DocumentationGeneration.UnitTests + ApiDoctor.DocumentationGeneration.UnitTests v4.5 512 10.0 @@ -51,17 +51,17 @@ - + {CD27998C-4021-4299-970B-91BE877FD01B} - ApiDocs.DocumentationGeneration + ApiDoctor.DocumentationGeneration - + {EE3453F1-FD69-406C-9BD7-0643D6E999F3} - ApiDocs.Validation.UnitTests + ApiDoctor.Validation.UnitTests - + {33B10320-3802-49CF-8965-3510AE66D5EC} - ApiDocs.Validation + ApiDoctor.Validation {1569ed47-c7c9-4261-b6f4-7445bd0f2c95} diff --git a/ApiDocs.DocumentationGeneration.UnitTests/DescriptionTests.cs b/ApiDoctor.DocumentationGeneration.UnitTests/DescriptionTests.cs similarity index 97% rename from ApiDocs.DocumentationGeneration.UnitTests/DescriptionTests.cs rename to ApiDoctor.DocumentationGeneration.UnitTests/DescriptionTests.cs index efa4be1c..8ebf2226 100644 --- a/ApiDocs.DocumentationGeneration.UnitTests/DescriptionTests.cs +++ b/ApiDoctor.DocumentationGeneration.UnitTests/DescriptionTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,9 +23,9 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.Tests +namespace ApiDoctor.DocumentationGeneration.Tests { - using ApiDocs.Validation.OData; + using ApiDoctor.Validation.OData; using NUnit.Framework; diff --git a/ApiDocs.DocumentationGeneration.UnitTests/DocumentationTestBase.cs b/ApiDoctor.DocumentationGeneration.UnitTests/DocumentationTestBase.cs similarity index 98% rename from ApiDocs.DocumentationGeneration.UnitTests/DocumentationTestBase.cs rename to ApiDoctor.DocumentationGeneration.UnitTests/DocumentationTestBase.cs index 8dd32923..faae4668 100644 --- a/ApiDocs.DocumentationGeneration.UnitTests/DocumentationTestBase.cs +++ b/ApiDoctor.DocumentationGeneration.UnitTests/DocumentationTestBase.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,11 +23,11 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.Tests +namespace ApiDoctor.DocumentationGeneration.Tests { using System.Collections.Generic; - using ApiDocs.Validation.OData; + using ApiDoctor.Validation.OData; using NUnit.Framework; diff --git a/ApiDocs.DocumentationGeneration.UnitTests/NavigationPropertyTests.cs b/ApiDoctor.DocumentationGeneration.UnitTests/NavigationPropertyTests.cs similarity index 95% rename from ApiDocs.DocumentationGeneration.UnitTests/NavigationPropertyTests.cs rename to ApiDoctor.DocumentationGeneration.UnitTests/NavigationPropertyTests.cs index b08a1e26..6de23fd6 100644 --- a/ApiDocs.DocumentationGeneration.UnitTests/NavigationPropertyTests.cs +++ b/ApiDoctor.DocumentationGeneration.UnitTests/NavigationPropertyTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.UnitTests +namespace ApiDoctor.DocumentationGeneration.UnitTests { - using ApiDocs.DocumentationGeneration.Tests; - using ApiDocs.Validation.OData; + using ApiDoctor.DocumentationGeneration.Tests; + using ApiDoctor.Validation.OData; using NUnit.Framework; diff --git a/ApiDocs.DocumentationGeneration.UnitTests/Properties/AssemblyInfo.cs b/ApiDoctor.DocumentationGeneration.UnitTests/Properties/AssemblyInfo.cs similarity index 90% rename from ApiDocs.DocumentationGeneration.UnitTests/Properties/AssemblyInfo.cs rename to ApiDoctor.DocumentationGeneration.UnitTests/Properties/AssemblyInfo.cs index 699d7a5f..8f016870 100644 --- a/ApiDocs.DocumentationGeneration.UnitTests/Properties/AssemblyInfo.cs +++ b/ApiDoctor.DocumentationGeneration.UnitTests/Properties/AssemblyInfo.cs @@ -5,11 +5,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("ApiDocs.DocumentationGeneration.UnitTests")] +[assembly: AssemblyTitle("ApiDoctor.DocumentationGeneration.UnitTests")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ApiDocs.DocumentationGeneration.UnitTests")] +[assembly: AssemblyProduct("ApiDoctor.DocumentationGeneration.UnitTests")] [assembly: AssemblyCopyright("Copyright © 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/ApiDocs.DocumentationGeneration.UnitTests/PropertyTests.cs b/ApiDoctor.DocumentationGeneration.UnitTests/PropertyTests.cs similarity index 96% rename from ApiDocs.DocumentationGeneration.UnitTests/PropertyTests.cs rename to ApiDoctor.DocumentationGeneration.UnitTests/PropertyTests.cs index 7629ddb8..3568994d 100644 --- a/ApiDocs.DocumentationGeneration.UnitTests/PropertyTests.cs +++ b/ApiDoctor.DocumentationGeneration.UnitTests/PropertyTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.UnitTests +namespace ApiDoctor.DocumentationGeneration.UnitTests { - using ApiDocs.DocumentationGeneration.Tests; - using ApiDocs.Validation.OData; + using ApiDoctor.DocumentationGeneration.Tests; + using ApiDoctor.Validation.OData; using NUnit.Framework; diff --git a/ApiDocs.DocumentationGeneration.UnitTests/ResourceRoundTripTests.cs b/ApiDoctor.DocumentationGeneration.UnitTests/ResourceRoundTripTests.cs similarity index 78% rename from ApiDocs.DocumentationGeneration.UnitTests/ResourceRoundTripTests.cs rename to ApiDoctor.DocumentationGeneration.UnitTests/ResourceRoundTripTests.cs index c969d17a..987ed8b2 100644 --- a/ApiDocs.DocumentationGeneration.UnitTests/ResourceRoundTripTests.cs +++ b/ApiDoctor.DocumentationGeneration.UnitTests/ResourceRoundTripTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,18 +23,18 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.UnitTests +namespace ApiDoctor.DocumentationGeneration.UnitTests { using System.IO; using System.Linq; using System.Reflection; - using ApiDocs.DocumentationGeneration.Extensions; - using ApiDocs.DocumentationGeneration.Tests; - using ApiDocs.Validation; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.OData; - using ApiDocs.Validation.UnitTests; + using ApiDoctor.DocumentationGeneration.Extensions; + using ApiDoctor.DocumentationGeneration.Tests; + using ApiDoctor.Validation; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.OData; + using ApiDoctor.Validation.UnitTests; using NUnit.Framework; @@ -48,14 +48,14 @@ public class ResourceRoundTripTests : DocumentationTestBase public void TestRoudTripEntityType() { EntityType otherEntity = this.GetEntityType(this.schema, "otherEntity"); - ComplexType complexPropertyType = this.GetComplexType(this.schema, "TestComplexPropertyType"); - EntityType entity = this.GetEntityType(this.schema, "EntityType", schemaDescription: "Schema Description"); - this.AddProperty(entity, "TestProperty", "Edm.Int32", schemaDescription: "Test int32 property"); - this.AddProperty(entity, "TestCollectionProperty", "Collection(Edm.String)", "Test collection property"); - this.AddProperty(entity, "TestComplexProperty", this.entityFramework.LookupIdentifierForType(complexPropertyType), "Test complex type property"); - this.AddProperty(entity, "TestComplexCollectionProperty", "Collection(" + this.entityFramework.LookupIdentifierForType(complexPropertyType) + ")", "Test complex type property"); - this.AddNavigationProperty(entity, "TestEntityNavigation", this.entityFramework.LookupIdentifierForType(otherEntity), inlineDescription: "Entity Navigation"); - this.AddNavigationProperty(entity, "TestCollectionNavigation", "Collection(" + this.entityFramework.LookupIdentifierForType(otherEntity) + ")", schemaDescription: "Collection Navigation Property"); + ComplexType complexPropertyType = this.GetComplexType(this.schema, "testComplexPropertyType"); + EntityType entity = this.GetEntityType(this.schema, "entityType", schemaDescription: "Schema Description"); + this.AddProperty(entity, "testProperty", "Edm.Int32", schemaDescription: "Test int32 property"); + this.AddProperty(entity, "testCollectionProperty", "Collection(Edm.String)", "Test collection property"); + this.AddProperty(entity, "testComplexProperty", this.entityFramework.LookupIdentifierForType(complexPropertyType), "Test complex type property"); + this.AddProperty(entity, "testComplexCollectionProperty", "Collection(" + this.entityFramework.LookupIdentifierForType(complexPropertyType) + ")", "Test complex type property"); + this.AddNavigationProperty(entity, "testEntityNavigation", this.entityFramework.LookupIdentifierForType(otherEntity), inlineDescription: "Entity Navigation"); + this.AddNavigationProperty(entity, "testCollectionNavigation", "Collection(" + this.entityFramework.LookupIdentifierForType(otherEntity) + ")", schemaDescription: "Collection Navigation Property"); DocFile testFile = this.GetDocFileForEntityType(entity); @@ -67,12 +67,12 @@ public void TestRoudTripEntityType() [Test] public void TestRoundTripComplexType() { - ComplexType complexPropertyType = this.GetComplexType(this.schema, "TestComplexPropertyType"); - ComplexType testType = this.GetComplexType(this.schema, "TestType", "Test Description"); - this.AddProperty(testType, "TestProperty", "Edm.Boolean", "Test bool property"); - this.AddProperty(testType, "TestCollectionProperty", "Collection(Edm.String)", "Test collection property"); - this.AddProperty(testType, "TestComplexProperty", this.entityFramework.LookupIdentifierForType(complexPropertyType), "Test complex type property"); - this.AddProperty(testType, "TestComplexCollectionProperty", "Collection(" + this.entityFramework.LookupIdentifierForType(complexPropertyType) + ")", "Test complex type property"); + ComplexType complexPropertyType = this.GetComplexType(this.schema, "testComplexPropertyType"); + ComplexType testType = this.GetComplexType(this.schema, "testType", "Test Description"); + this.AddProperty(testType, "testProperty", "Edm.Boolean", "Test bool property"); + this.AddProperty(testType, "testCollectionProperty", "Collection(Edm.String)", "Test collection property"); + this.AddProperty(testType, "testComplexProperty", this.entityFramework.LookupIdentifierForType(complexPropertyType), "Test complex type property"); + this.AddProperty(testType, "testComplexCollectionProperty", "Collection(" + this.entityFramework.LookupIdentifierForType(complexPropertyType) + ")", "Test complex type property"); DocFile testFile = this.GetDocFileForComplexType(testType); @@ -86,10 +86,11 @@ private DocFile GetDocFileForComplexType(ComplexType testType) string markDown = this.documentationGenerator.GetMarkDownForType(this.entityFramework, testType); DocSet docSet = new DocSet(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + DocSet.SchemaConfig.DefaultNamespace = "microsoft.graph"; DocFile testFile = new DocFileForTesting(markDown, @"\resources.md", @"\resources.md", docSet); - ValidationError[] errors = null; - testFile.Scan(string.Empty, out errors); - Assert.IsFalse(errors.WereWarningsOrErrors(), "Expected no validation warnings/errors: {0}", errors.ErrorsToString()); + var issues = new IssueLogger(); + testFile.Scan(string.Empty, issues.For("testFile")); + Assert.IsFalse(issues.Issues.WereWarningsOrErrors(), "Expected no validation warnings/errors: {0}", issues.Issues.ErrorsToString()); return testFile; } @@ -98,10 +99,11 @@ private DocFile GetDocFileForEntityType(EntityType testType) string markDown = this.documentationGenerator.GetMarkDownForType(this.entityFramework, testType); DocSet docSet = new DocSet(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + DocSet.SchemaConfig.DefaultNamespace = "microsoft.graph"; DocFile testFile = new DocFileForTesting(markDown, @"\resources.md", @"\resources.md", docSet); - ValidationError[] errors = null; - testFile.Scan(string.Empty, out errors); - Assert.IsFalse(errors.WereWarningsOrErrors(), "Expected no validation warnings/errors: {0}", errors.Where(e => e.IsWarningOrError).ErrorsToString()); + var issues = new IssueLogger(); + testFile.Scan(string.Empty, issues.For("testFile")); + Assert.IsFalse(issues.Issues.WereWarningsOrErrors(), "Expected no validation warnings/errors: {0}", issues.Issues.Where(e => e.IsWarningOrError).ErrorsToString()); return testFile; } diff --git a/ApiDocs.DocumentationGeneration.UnitTests/packages.config b/ApiDoctor.DocumentationGeneration.UnitTests/packages.config similarity index 100% rename from ApiDocs.DocumentationGeneration.UnitTests/packages.config rename to ApiDoctor.DocumentationGeneration.UnitTests/packages.config diff --git a/ApiDocs.DocumentationGeneration/ApiDocs.DocumentationGeneration.csproj b/ApiDoctor.DocumentationGeneration/ApiDoctor.DocumentationGeneration.csproj similarity index 91% rename from ApiDocs.DocumentationGeneration/ApiDocs.DocumentationGeneration.csproj rename to ApiDoctor.DocumentationGeneration/ApiDoctor.DocumentationGeneration.csproj index 59a2aa13..f7ccff76 100644 --- a/ApiDocs.DocumentationGeneration/ApiDocs.DocumentationGeneration.csproj +++ b/ApiDoctor.DocumentationGeneration/ApiDoctor.DocumentationGeneration.csproj @@ -7,8 +7,8 @@ {CD27998C-4021-4299-970B-91BE877FD01B} Library Properties - ApiDocs.DocumentationGeneration - ApiDocs.DocumentationGeneration + ApiDoctor.DocumentationGeneration + ApiDoctor.DocumentationGeneration v4.5 512 @@ -73,13 +73,13 @@ - + {b675cf73-aa42-4a54-b5e7-ff5f155da4a7} - ApiDocs.Publishing + ApiDoctor.Publishing - + {33b10320-3802-49cf-8965-3510ae66d5ec} - ApiDocs.Validation + ApiDoctor.Validation diff --git a/ApiDocs.DocumentationGeneration/DocumentationGenerator.cs b/ApiDoctor.DocumentationGeneration/DocumentationGenerator.cs similarity index 95% rename from ApiDocs.DocumentationGeneration/DocumentationGenerator.cs rename to ApiDoctor.DocumentationGeneration/DocumentationGenerator.cs index 684ae81d..2c0f3980 100644 --- a/ApiDocs.DocumentationGeneration/DocumentationGenerator.cs +++ b/ApiDoctor.DocumentationGeneration/DocumentationGenerator.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration +namespace ApiDoctor.DocumentationGeneration { using System; using System.Collections.Generic; @@ -32,9 +32,9 @@ namespace ApiDocs.DocumentationGeneration using System.Text; using System.Threading.Tasks; - using ApiDocs.DocumentationGeneration.Extensions; - using ApiDocs.DocumentationGeneration.Properties; - using ApiDocs.Validation.OData; + using ApiDoctor.DocumentationGeneration.Extensions; + using ApiDoctor.DocumentationGeneration.Properties; + using ApiDoctor.Validation.OData; using Mustache; diff --git a/ApiDocs.DocumentationGeneration/Extensions/DocumentationExtensions.cs b/ApiDoctor.DocumentationGeneration/Extensions/DocumentationExtensions.cs similarity index 97% rename from ApiDocs.DocumentationGeneration/Extensions/DocumentationExtensions.cs rename to ApiDoctor.DocumentationGeneration/Extensions/DocumentationExtensions.cs index dc8683e4..a59db2e4 100644 --- a/ApiDocs.DocumentationGeneration/Extensions/DocumentationExtensions.cs +++ b/ApiDoctor.DocumentationGeneration/Extensions/DocumentationExtensions.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,14 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.Extensions +namespace ApiDoctor.DocumentationGeneration.Extensions { using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; - using ApiDocs.DocumentationGeneration.Model; - using ApiDocs.Validation.OData; + using ApiDoctor.DocumentationGeneration.Model; + using ApiDoctor.Validation.OData; public static class DocumentationExtensions { diff --git a/ApiDocs.DocumentationGeneration/Model/DocumentationComplexType.cs b/ApiDoctor.DocumentationGeneration/Model/DocumentationComplexType.cs similarity index 94% rename from ApiDocs.DocumentationGeneration/Model/DocumentationComplexType.cs rename to ApiDoctor.DocumentationGeneration/Model/DocumentationComplexType.cs index 72fcb727..09cd8f2d 100644 --- a/ApiDocs.DocumentationGeneration/Model/DocumentationComplexType.cs +++ b/ApiDoctor.DocumentationGeneration/Model/DocumentationComplexType.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,13 +23,13 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.Model +namespace ApiDoctor.DocumentationGeneration.Model { using System.Collections.Generic; using System.Linq; - using ApiDocs.DocumentationGeneration.Extensions; - using ApiDocs.Validation.OData; + using ApiDoctor.DocumentationGeneration.Extensions; + using ApiDoctor.Validation.OData; using Newtonsoft.Json; diff --git a/ApiDocs.DocumentationGeneration/Model/DocumentationEntityType.cs b/ApiDoctor.DocumentationGeneration/Model/DocumentationEntityType.cs similarity index 92% rename from ApiDocs.DocumentationGeneration/Model/DocumentationEntityType.cs rename to ApiDoctor.DocumentationGeneration/Model/DocumentationEntityType.cs index 59443842..683915a1 100644 --- a/ApiDocs.DocumentationGeneration/Model/DocumentationEntityType.cs +++ b/ApiDoctor.DocumentationGeneration/Model/DocumentationEntityType.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,13 +23,13 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.Model +namespace ApiDoctor.DocumentationGeneration.Model { using System.Collections.Generic; using System.Linq; - using ApiDocs.DocumentationGeneration.Extensions; - using ApiDocs.Validation.OData; + using ApiDoctor.DocumentationGeneration.Extensions; + using ApiDoctor.Validation.OData; public class DocumentationEntityType : DocumentationComplexType { diff --git a/ApiDocs.DocumentationGeneration/Model/DocumentationNavigationProperty.cs b/ApiDoctor.DocumentationGeneration/Model/DocumentationNavigationProperty.cs similarity index 93% rename from ApiDocs.DocumentationGeneration/Model/DocumentationNavigationProperty.cs rename to ApiDoctor.DocumentationGeneration/Model/DocumentationNavigationProperty.cs index d2ee3549..935120c1 100644 --- a/ApiDocs.DocumentationGeneration/Model/DocumentationNavigationProperty.cs +++ b/ApiDoctor.DocumentationGeneration/Model/DocumentationNavigationProperty.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,11 +23,11 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.DocumentationGeneration.Model +namespace ApiDoctor.DocumentationGeneration.Model { using System.Collections.Generic; - using ApiDocs.Validation.OData; + using ApiDoctor.Validation.OData; public class DocumentationNavigationProperty : DocumentationProperty { diff --git a/ApiDocs.DocumentationGeneration/Model/DocumentationProperty.cs b/ApiDoctor.DocumentationGeneration/Model/DocumentationProperty.cs similarity index 88% rename from ApiDocs.DocumentationGeneration/Model/DocumentationProperty.cs rename to ApiDoctor.DocumentationGeneration/Model/DocumentationProperty.cs index 3fa94cb7..aedf87ce 100644 --- a/ApiDocs.DocumentationGeneration/Model/DocumentationProperty.cs +++ b/ApiDoctor.DocumentationGeneration/Model/DocumentationProperty.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,14 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -using ApiDocs.Validation; +using ApiDoctor.Validation; -namespace ApiDocs.DocumentationGeneration.Model +namespace ApiDoctor.DocumentationGeneration.Model { using System.Collections.Generic; - using ApiDocs.DocumentationGeneration.Extensions; - using ApiDocs.Validation.OData; + using ApiDoctor.DocumentationGeneration.Extensions; + using ApiDoctor.Validation.OData; public class DocumentationProperty { @@ -46,7 +46,8 @@ public DocumentationProperty(EntityFramework entityFramework, ComplexType comple } var simpleType = typeName.ToODataSimpleType(); - if (simpleType != SimpleDataType.None) + if (simpleType != SimpleDataType.None && + simpleType != SimpleDataType.Object) { this.Type = new ParameterDataType(simpleType, isCollection); } diff --git a/ApiDocs.DocumentationGeneration/Properties/AssemblyInfo.cs b/ApiDoctor.DocumentationGeneration/Properties/AssemblyInfo.cs similarity index 88% rename from ApiDocs.DocumentationGeneration/Properties/AssemblyInfo.cs rename to ApiDoctor.DocumentationGeneration/Properties/AssemblyInfo.cs index 8f2c11c7..c14bb1bf 100644 --- a/ApiDocs.DocumentationGeneration/Properties/AssemblyInfo.cs +++ b/ApiDoctor.DocumentationGeneration/Properties/AssemblyInfo.cs @@ -11,11 +11,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("ApiDocs.DocumentationGeneration")] +[assembly: AssemblyTitle("ApiDoctor.DocumentationGeneration")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ApiDocs.DocumentationGeneration")] +[assembly: AssemblyProduct("ApiDoctor.DocumentationGeneration")] [assembly: AssemblyCopyright("Copyright © 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -39,4 +39,4 @@ [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: InternalsVisibleTo("ApiDocs.DocumentationGeneration.UnitTests")] \ No newline at end of file +[assembly: InternalsVisibleTo("ApiDoctor.DocumentationGeneration.UnitTests")] diff --git a/ApiDocs.DocumentationGeneration/Properties/Templates.Designer.cs b/ApiDoctor.DocumentationGeneration/Properties/Templates.Designer.cs similarity index 93% rename from ApiDocs.DocumentationGeneration/Properties/Templates.Designer.cs rename to ApiDoctor.DocumentationGeneration/Properties/Templates.Designer.cs index 38fbbf7d..12e71191 100644 --- a/ApiDocs.DocumentationGeneration/Properties/Templates.Designer.cs +++ b/ApiDoctor.DocumentationGeneration/Properties/Templates.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace ApiDocs.DocumentationGeneration.Properties { +namespace ApiDoctor.DocumentationGeneration.Properties { using System; @@ -39,7 +39,7 @@ internal Templates() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ApiDocs.DocumentationGeneration.Properties.Templates", typeof(Templates).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ApiDoctor.DocumentationGeneration.Properties.Templates", typeof(Templates).Assembly); resourceMan = temp; } return resourceMan; diff --git a/ApiDocs.DocumentationGeneration/Properties/Templates.resx b/ApiDoctor.DocumentationGeneration/Properties/Templates.resx similarity index 100% rename from ApiDocs.DocumentationGeneration/Properties/Templates.resx rename to ApiDoctor.DocumentationGeneration/Properties/Templates.resx diff --git a/ApiDocs.DocumentationGeneration/Templates/resource.md.mustache b/ApiDoctor.DocumentationGeneration/Templates/resource.md.mustache similarity index 100% rename from ApiDocs.DocumentationGeneration/Templates/resource.md.mustache rename to ApiDoctor.DocumentationGeneration/Templates/resource.md.mustache diff --git a/ApiDocs.DocumentationGeneration/packages.config b/ApiDoctor.DocumentationGeneration/packages.config similarity index 100% rename from ApiDocs.DocumentationGeneration/packages.config rename to ApiDoctor.DocumentationGeneration/packages.config diff --git a/ApiDocs.Publishing/ApiDocs.Publishing.csproj b/ApiDoctor.Publishing/ApiDoctor.Publishing.csproj similarity index 94% rename from ApiDocs.Publishing/ApiDocs.Publishing.csproj rename to ApiDoctor.Publishing/ApiDoctor.Publishing.csproj index 0a8e25cf..0abf4244 100644 --- a/ApiDocs.Publishing/ApiDocs.Publishing.csproj +++ b/ApiDoctor.Publishing/ApiDoctor.Publishing.csproj @@ -7,8 +7,8 @@ {B675CF73-AA42-4A54-B5E7-FF5F155DA4A7} Library Properties - ApiDocs.Publishing - ApiDocs.Publishing + ApiDoctor.Publishing + ApiDoctor.Publishing v4.5 512 ..\ @@ -51,6 +51,7 @@ + @@ -64,9 +65,9 @@ - + {33B10320-3802-49CF-8965-3510AE66D5EC} - ApiDocs.Validation + ApiDoctor.Validation {1569ED47-C7C9-4261-B6F4-7445BD0F2C95} diff --git a/ApiDocs.Publishing/ApiDocs.Publishing.nuspec b/ApiDoctor.Publishing/ApiDoctor.Publishing.nuspec similarity index 63% rename from ApiDocs.Publishing/ApiDocs.Publishing.nuspec rename to ApiDoctor.Publishing/ApiDoctor.Publishing.nuspec index d887c375..18fc2926 100644 --- a/ApiDocs.Publishing/ApiDocs.Publishing.nuspec +++ b/ApiDoctor.Publishing/ApiDoctor.Publishing.nuspec @@ -1,20 +1,20 @@ - MarkdownScanner.Publishing + ApiDoctor.Publishing $version$ - Markdown scanner API documentation publishing extension - Ryan Gregg - rgregg@microsoft.com + API Doctor API documentation publishing extension + Microsoft + dspektor@microsoft.com false Toolkit to enable publishing from markdown to other formats. - Check http://github.com/onedrive/markdown-scanner for details. + Check http://github.com/onedrive/apidoctor for details. http://msdn.microsoft.com/en-US/cc300389 Copyright Microsoft - + @@ -22,6 +22,6 @@ - + diff --git a/ApiDocs.Publishing/CSDL/ObjectGraphMerger.cs b/ApiDoctor.Publishing/CSDL/ObjectGraphMerger.cs similarity index 100% rename from ApiDocs.Publishing/CSDL/ObjectGraphMerger.cs rename to ApiDoctor.Publishing/CSDL/ObjectGraphMerger.cs diff --git a/ApiDocs.Publishing/CSDL/CsdlExtensionMethods.cs b/ApiDoctor.Publishing/CSDL/csdlextensionmethods.cs similarity index 58% rename from ApiDocs.Publishing/CSDL/CsdlExtensionMethods.cs rename to ApiDoctor.Publishing/CSDL/csdlextensionmethods.cs index 54f25456..657343f4 100644 --- a/ApiDocs.Publishing/CSDL/CsdlExtensionMethods.cs +++ b/ApiDoctor.Publishing/CSDL/csdlextensionmethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,9 +23,9 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.CSDL +namespace ApiDoctor.Publishing.CSDL { - using ApiDocs.Validation; + using ApiDoctor.Validation; using System; using System.Collections.Generic; using System.Reflection; @@ -35,29 +35,102 @@ namespace ApiDocs.Publishing.CSDL using Validation.OData.Transformation; using Validation.Utility; using System.Linq; + using ApiDoctor.Validation.Error; internal static class CsdlExtensionMethods { - public static string RequestUriPathOnly(this MethodDefinition method, string baseUrlToRemove) + public static string RequestUriPathOnly(this MethodDefinition method, string[] baseUrlsToRemove, IssueLogger issues) { var path = method.Request.FirstLineOnly().TextBetweenCharacters(' ', '?').TrimEnd('/'); - if (null != baseUrlToRemove && path.StartsWith(baseUrlToRemove)) + if (baseUrlsToRemove != null) { - path = path.Substring(baseUrlToRemove.Length); + foreach (var baseUrl in baseUrlsToRemove) + { + if (!string.IsNullOrEmpty(baseUrl) && path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(baseUrl.Length); + } + } + } + + // just in case there's a stray example that doesn't match, at least chop the domain part off. + foreach (var scheme in new[] { "https://", "http://" }) + { + if (path.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)) + { + int pathStartIndex = path.IndexOf('/', scheme.Length); + if (pathStartIndex != -1) + { + path = path.Substring(pathStartIndex); + break; + } + } } // Normalize variables in the request path path = path.ReplaceTextBetweenCharacters('{', '}', "var"); path = path.ReplaceTextBetweenCharacters('<', '>', "{var}", true, true); // Fix Graph docs placeholder + if (method.RequestMetadata.SampleKeys != null) + { + foreach (var key in method.RequestMetadata.SampleKeys) + { + path = path.Replace("/" + key, "/{var}"); + } + } + + // Normalize function params + var substitutions = new Dictionary(); + var parenIndex = path.IndexOf('('); + for (int i=0; i< path.Length; i++) + { + if (path[i]=='(') + { + // this is the start of a function. let's find the closing paren. + var close = path.IndexOf(')', i); + if (close > -1) + { + var inner = path.Substring(i + 1, close - i - 1); + substitutions[inner] = NormalizeFunctionParameters(inner, issues); + i = close; + } + } + } + + foreach (var sub in substitutions) + { + path = path.Replace(sub.Key, sub.Value); + } + // Rewrite path syntax into what it logically means path = path.ReplaceTextBetweenCharacters(':', ':', "/children/{var}", requireSecondChar: false, removeTargetChars: true); return path; } + private static string NormalizeFunctionParameters(string funcParams, IssueLogger issues) + { + // foo=bar, baz ='qux',x= 9 + var normalized = new StringBuilder(); + var allParams = funcParams.Split(','); + for (int i=0; i< allParams.Length; i++) + { + var param = allParams[i].Trim(); + var kvp = param.Split('='); + if (kvp.Length != 2) + { + issues.Error(ValidationErrorCode.ParameterParserError, $"Malformed function params {funcParams}"); + return funcParams; + } + + allParams[i] = kvp[0].Trim() + "={var}"; + } + + return string.Join(",", allParams.OrderBy(p => p)); + } + public static string HttpMethodVerb(this MethodDefinition method) { HttpParser parser = new HttpParser(); diff --git a/ApiDoctor.Publishing/CSDL/csdlwriter.cs b/ApiDoctor.Publishing/CSDL/csdlwriter.cs new file mode 100644 index 00000000..4c1f6830 --- /dev/null +++ b/ApiDoctor.Publishing/CSDL/csdlwriter.cs @@ -0,0 +1,1590 @@ +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +namespace ApiDoctor.Publishing.CSDL +{ + using ApiDoctor.Validation; + using ApiDoctor.Validation.Writers; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using ApiDoctor.Validation.OData; + using Validation.Config; + using Validation.OData.Transformation; + using ApiDoctor.Validation.Http; + using System.IO; + using ApiDoctor.Validation.Error; + + public class CsdlWriter : DocumentPublisher + { + private readonly CsdlWriterOptions options; + + public CsdlWriter(DocSet docs, CsdlWriterOptions options) + : base(docs) + { + this.options = options; + } + + public override async Task PublishToFolderAsync(string outputFolder, IssueLogger issues) + { + string outputFilenameSuffix = ""; + + // Step 1: Generate an EntityFramework OM from the documentation and/or template file + EntityFramework framework = CreateEntityFrameworkFromDocs(issues); + if (null == framework) + return; + + if (!string.IsNullOrEmpty(options.MergeWithMetadataPath)) + { + EntityFramework secondFramework = CreateEntityFrameworkFromDocs(issues, options.MergeWithMetadataPath, generateFromDocs: false); + framework = framework.MergeWith(secondFramework); + outputFilenameSuffix += "-merged"; + } + + // Step 1a: Apply an transformations that may be defined in the documentation + if (!string.IsNullOrEmpty(options.TransformOutput)) + { + PublishSchemaChangesConfigFile transformations = DocSet.TryLoadConfigurationFiles(options.DocumentationSetPath).Where(x => x.SchemaChanges.TransformationName == options.TransformOutput).FirstOrDefault(); + if (null == transformations) + { + throw new KeyNotFoundException($"Unable to locate a transformation set named {options.TransformOutput}. Aborting."); + } + + string[] versionsToPublish = options.Version?.Split(new char[] { ',', ' ' }); + framework.ApplyTransformation(transformations.SchemaChanges, versionsToPublish); + if (!string.IsNullOrEmpty(options.Version)) + { + outputFilenameSuffix += $"-{options.Version}"; + } + } + + if (options.Sort) + { + // Sorts the objects in collections, so that we have consistent output regardless of input + framework.SortObjectGraph(); + } + + if (options.ValidateSchema) + { + framework.ValidateSchemaTypes(); + } + + // Step 2: Generate XML representation of EDMX + string xmlData = null; + if (options.Formats.HasFlag(MetadataFormat.EdmxOutput)) + { + xmlData = ODataParser.Serialize(framework, options.AttributesOnNewLines); + } + else if (options.Formats.HasFlag(MetadataFormat.SchemaOutput)) + { + xmlData = ODataParser.Serialize(framework.DataServices.Schemas.First(), options.AttributesOnNewLines); + } + + // Step 3: Write the XML to disk + + if (!string.IsNullOrEmpty(outputFolder)) + { + var outputFullName = GenerateOutputFileFullName(options.SourceMetadataPath, outputFolder, outputFilenameSuffix); + Console.WriteLine($"Publishing metadata to {outputFullName}"); + + using (var writer = System.IO.File.CreateText(outputFullName)) + { + await writer.WriteAsync(xmlData); + await writer.FlushAsync(); + writer.Close(); + } + } + else + { + issues.Message($"Skipping writing published metadata to disk. No output folder given."); + } + } + + private string GenerateOutputFileFullName(string templateFilename, string outputFolderPath, string filenameSuffix) + { + var outputDir = new System.IO.DirectoryInfo(outputFolderPath); + outputDir.Create(); + + filenameSuffix = filenameSuffix ?? ""; + + string filename = null; + if (!string.IsNullOrEmpty(templateFilename)) + { + filename = $"{System.IO.Path.GetFileNameWithoutExtension(templateFilename)}{filenameSuffix}{System.IO.Path.GetExtension(templateFilename)}"; + } + else + { + filename = $"metadata{filenameSuffix}.xml"; + } + + var outputFullName = System.IO.Path.Combine(outputDir.FullName, filename); + return outputFullName; + } + + public EntityFramework CreateEntityFrameworkFromDocs(IssueLogger issues, string sourcePath = null, bool? generateFromDocs = null) + { + sourcePath = sourcePath ?? options.SourceMetadataPath; + + EntityFramework edmx = new EntityFramework(); + if (!string.IsNullOrEmpty(sourcePath)) + { + try + { + if (!System.IO.File.Exists(sourcePath)) + { + throw new System.IO.FileNotFoundException($"Unable to locate source file: {sourcePath}"); + } + + using (System.IO.FileStream stream = new System.IO.FileStream(sourcePath, System.IO.FileMode.Open, System.IO.FileAccess.Read)) + { + if (options.Formats.HasFlag(MetadataFormat.EdmxInput)) + { + edmx = ODataParser.Deserialize(stream); + } + else if (options.Formats.HasFlag(MetadataFormat.SchemaInput)) + { + var schema = ODataParser.Deserialize(stream); + edmx = new EntityFramework(); + edmx.DataServices.Schemas.Add(schema); + } + else + { + throw new InvalidOperationException("Source file was specified but no format for source file was provided."); + } + } + if (options.Namespaces != null && options.Namespaces.Any()) + { + var schemas = edmx.DataServices.Schemas.ToArray(); + foreach(var s in schemas) + { + if (!options.Namespaces.Contains(s.Namespace)) + { + edmx.DataServices.Schemas.Remove(s); + } + } + } + } + catch (Exception ex) + { + issues.Error(ValidationErrorCode.Unknown, $"Unable to deserialize source template file", ex); + return null; + } + } + + bool generateNewElements = (generateFromDocs == null && !options.SkipMetadataGeneration) || (generateFromDocs.HasValue && generateFromDocs.Value) ; + + // Add resources + if (generateNewElements && Documents.Files.Any()) + { + foreach (var resource in this.Documents.Resources) + { + var targetSchema = FindOrCreateSchemaForNamespace(resource.Name.NamespaceOnly(), edmx, generateNewElements: generateNewElements); + if (targetSchema != null) + { + AddResourceToSchema(targetSchema, resource, edmx, issues, generateNewElements: generateNewElements); + } + } + + // Figure out the EntityCollection + this.BuildEntityContainer(edmx, DocSet.SchemaConfig.BaseUrls, issues); + + // Add actions to the collection + this.ProcessRestRequestPaths(edmx, DocSet.SchemaConfig.BaseUrls, issues); + + // add enums to the default schema + var defaultSchema = edmx.DataServices.Schemas.FirstOrDefault(s => s.Namespace == DocSet.SchemaConfig.DefaultNamespace); + foreach (var enumDefinition in Documents.Enums.GroupBy(e=>e.TypeName)) + { + var enumType = new EnumType + { + Name = enumDefinition.Key, + Members = enumDefinition.Distinct(new EnumComparer()).Select(e => + new EnumMember + { + Name = e.MemberName, + Value = e.NumericValue.HasValue ? e.NumericValue.GetValueOrDefault().ToString() : null + }).ToList(), + IsFlags = enumDefinition.FirstOrDefault().IsFlags, + }; + + if (enumType.Members[0].Value == null) + { + for (int i=0; i an.Target); + foreach (var complex in schema.ComplexTypes.Concat(schema.EntityTypes)) + { + var fullName = complex.Namespace + "." + complex.Name; + MergeAnnotations(fullName, complex, annotationsMap); + + foreach (var property in complex.Properties) + { + MergeAnnotations(fullName + "/" + property.Name, property, annotationsMap); + } + + var entity = complex as EntityType; + if (entity != null && entity.NavigationProperties != null) + { + foreach (var property in entity.NavigationProperties) + { + MergeAnnotations(fullName + "/" + property.Name, property, annotationsMap); + } + } + + Dictionary capabilitiesPerProperty = new Dictionary(); + foreach (var annotation in complex.Contributors.Where(c=>c.OriginalMetadata?.OdataAnnotations != null).SelectMany(c => c.OriginalMetadata.OdataAnnotations)) + { + if (annotation?.Capabilities != null) + { + ODataCapabilities capabilities; + if (!capabilitiesPerProperty.TryGetValue(annotation.Property ?? "", out capabilities)) + { + capabilitiesPerProperty.Add(annotation.Property ?? "", annotation.Capabilities); + continue; + } + + annotation.Capabilities.MergeWith(capabilities, issues.For(complex.Name + "/annotations")); + } + } + + foreach (var kvp in capabilitiesPerProperty) + { + var capabilities = kvp.Value; + var property = new Property { Name = kvp.Key, Annotation = new List() }; + + TryAddAnnotation(property, "ChangeTracking", "Supported", capabilities.ChangeTracking); + TryAddAnnotation(property, "Org.OData.Core.V1.Computed", null, capabilities.Computed); + TryAddAnnotation(property, "CountRestrictions", "Countable", capabilities.Countable); + TryAddAnnotation(property, "DeleteRestrictions", "Deletable", capabilities.Deletable); + TryAddAnnotation(property, "ExpandRestrictions", "Expandable", capabilities.Expandable); + TryAddAnnotation(property, "FilterRestrictions", "Filterable", capabilities.Filterable); + TryAddAnnotation(property, "InsertRestrictions", "Insertable", capabilities.Insertable); + TryAddAnnotation(property, "SearchRestrictions", "Searchable", capabilities.Searchable); + TryAddAnnotation(property, "SelectRestrictions", "Selectable", capabilities.Selectable); + TryAddAnnotation(property, "SkipSupported", null, capabilities.Skippable); + TryAddAnnotation(property, "SortRestrictions", "Sortable", capabilities.Sortable); + TryAddAnnotation(property, "TopSupported", null, capabilities.Toppable); + TryAddAnnotation(property, "UpdateRestrictions", "Updatable", capabilities.Updatable); + + if (capabilities.Navigability != null || capabilities.Referenceable.HasValue) + { + var annotation = new Annotation + { + Term = "Org.OData.Capabilities.V1.NavigationRestrictions", + Records = new List(), + }; + + if (capabilities.Referenceable.HasValue) + { + annotation.Records.Add(new Record + { + PropertyValues = new List + { + new PropertyValue + { + Property ="Referenceable", + Bool = capabilities.Referenceable.GetValueOrDefault(), + }, + } + }); + } + + if (capabilities.Navigability != null) + { + annotation.Records.Add(new Record + { + PropertyValues = new List + { + new PropertyValue + { + Property ="Navigability", + EnumMember = string.IsNullOrEmpty(capabilities.Navigability) + ? string.Empty + : ("Org.OData.Capabilities.V1.NavigationType/" + capabilities.Navigability) + }, + } + }); + } + + property.Annotation.Add(annotation); + } + + if (capabilities.Permissions != null) + { + var annotation = new Annotation + { + Term = "Org.OData.Core.V1.Permissions", + EnumMember = string.IsNullOrEmpty(capabilities.Permissions) + ? string.Empty + : ("Org.OData.Core.V1.Permission/" + capabilities.Permissions) + }; + + property.Annotation.Add(annotation); + } + + MergeAnnotations((fullName + "/" + property.Name).TrimEnd('/'), property, annotationsMap); + } + } + + if (this.options.Annotations.HasFlag(AnnotationOptions.HttpRequests)) + { + foreach (var container in schema.EntityContainers) + { + var prefix = schema.Namespace + "." + container.Name; + foreach (var entitySet in container.EntitySets) + { + var annotationName = prefix + "/" + entitySet.Name; + AddHttpRequestsAnnotations(annotationsMap, entitySet.SourceMethods as MethodCollection, annotationName, issues.For(annotationName)); + } + + foreach (var singleton in container.Singletons) + { + var annotationName = prefix + "/" + singleton.Name; + AddHttpRequestsAnnotations(annotationsMap, singleton.SourceMethods as MethodCollection, annotationName, issues.For(annotationName)); + } + } + + foreach (var operation in schema.Actions.Cast().Concat(schema.Functions)) + { + var target = + (operation.Parameters.FirstOrDefault(p => p.Name.IEquals("bindingParameter")).Type ?? schema.Namespace) + + "/" + + operation.Name; + + AddHttpRequestsAnnotations(annotationsMap, operation.SourceMethods as MethodCollection, target, issues.For(target)); + } + } + + schema.Annotations = annotationsMap.Values.ToList(); + + if (this.options.Annotations.HasFlag(AnnotationOptions.OnlyAnnotations)) + { + schema.ComplexTypes = null; + schema.EntityContainers = null; + schema.EntityTypes = null; + schema.Enumerations = null; + schema.Functions = null; + schema.Actions = null; + schema.Terms = null; + } + } + } + + return edmx; + } + + private static void TryAddAnnotation(Property annotatable, string term, string property, bool? value) + { + if (value.HasValue) + { + var annotation = new Annotation + { + Term = term.Contains(".")? term : $"Org.OData.Capabilities.V1.{term}" + }; + + if (property != null) + { + annotation.Records = new List + { + new Record + { + PropertyValues = new List + { + new PropertyValue { Property=property, Bool = value.GetValueOrDefault()}, + } + } + }; + } + else + { + annotation.Bool = value.GetValueOrDefault(); + } + + annotatable.Annotation.Add(annotation); + } + } + + private static void AddHttpRequestsAnnotations(Dictionary annotationsMap, MethodCollection methods, string target, IssueLogger issues) + { + if (methods != null) + { + var annotatable = new Property(); + AddHttpRequestsAnnotations(annotatable, methods, issues); + MergeAnnotations(target, annotatable, annotationsMap); + } + } + + private static void AddHttpRequestsAnnotations(IODataAnnotatable annotatable, MethodCollection methods, IssueLogger issues) + { + if (methods != null) + { + var annotation = new Annotation + { + Term = "Org.OData.Core.V1.HttpRequests", + Collection = new RecordCollection + { + Records = new List(), + } + }; + + foreach (var method in methods) + { + try + { + var record = GenerateHttpRequestMethodAnnotation(method); + annotation.Collection.Records.Add(record); + } + catch (Exception ex) + { + issues.Error(ValidationErrorCode.Unknown, + $"Exception in {method.SourceFile.DisplayName}!", ex); + } + } + + if (annotatable.Annotation == null) + { + annotatable.Annotation = new List(); + } + + annotatable.Annotation.Add(annotation); + } + } + + private static Record GenerateHttpRequestMethodAnnotation(MethodDefinition method) + { + var parser = new HttpParser(); + var request = parser.ParseHttpRequest(method.Request); + var response = parser.ParseHttpResponse(method.ExpectedResponse); + var record = new Record + { + PropertyValues = new List + { + new PropertyValue + { + Property = "Description", + String = method.Description.ToStringClean(), + }, + new PropertyValue { Property = "MethodDescription", String = method.Title }, + new PropertyValue { Property = "MethodType", String = method.HttpMethodVerb()}, + new PropertyValue + { + Property = "CustomHeaders", + Collection = new RecordCollection + { + Records = (method.Parameters?.Where(p=>p.Location == ParameterLocation.Header)?.Select(p => + new Record + { + PropertyValues = new List + { + new PropertyValue { Property = "Name", String = p.Name }, + new PropertyValue { Property = "Description", String = p.Type.ToStringClean() }, + new PropertyValue { Property = "Required", Bool = p.Required.GetValueOrDefault() }, + } + }))?.ToList() + }, + }, + new PropertyValue + { + Property = "CustomQueryOptions", + Collection = new RecordCollection + { + Records = (method.Parameters?.Where(p=>p.Location == ParameterLocation.QueryString)?.Select(p => + new Record + { + PropertyValues = new List + { + new PropertyValue { Property = "Name", String = p.Name }, + new PropertyValue { Property = "Description", String = p.Type.ToStringClean() }, + new PropertyValue { Property = "Required", Bool = p.Required.GetValueOrDefault() }, + } + }))?.ToList() + }, + }, + new PropertyValue + { + Property = "HttpResponses", + Collection = new RecordCollection + { + Records = new List + { + new Record + { + PropertyValues = new List + { + new PropertyValue { Property = "ResponseCode", String = response.StatusCode.ToString(), }, + new PropertyValue { Property = "Examples" , Records = new List + { + new Record + { + Type = "Org.OData.Core.V1.InlineExample", + PropertyValues = new List + { + new PropertyValue { Property = "InlineExample", String = response.Body, }, + new PropertyValue { Property = "Description", String = response.ContentType }, + } + } + }}, + } + } + } + } + }, + new PropertyValue + { + Property = "SecuritySchemes", + Collection = new RecordCollection + { + Records = (method?.SourceFile?.AuthScopes?.Where(scp=>scp?.Scope != null && !scp.Scope.IContains("not supported")).Select(scp => + new Record + { + PropertyValues = new List + { + new PropertyValue { Property = "AuthorizationSchemeName", String = scp.Title }, + new PropertyValue + { + Property = "RequiredScopes", + Collection = new RecordCollection { Strings = scp.Scope?.Split(new[] {',',';',' ' }, StringSplitOptions.RemoveEmptyEntries).ToList() } + } + } + }))?.ToList() + }, + }, + }, + }; + + return record; + } + + private static void MergeAnnotations(string fullName, IODataAnnotatable annotatable, Dictionary schemaLevelAnnotations) + { + if (annotatable.Annotation?.Count > 0) + { + Annotations annotations; + if (schemaLevelAnnotations.TryGetValue(fullName, out annotations)) + { + foreach (var annotation in annotatable.Annotation) + { + var existingAnnotation = annotations.AnnotationList.FirstOrDefault(a => a.Term == annotation.Term); + if (existingAnnotation != null && annotation.Collection != null && annotation.Collection.Records?.Count > 0) + { + if (existingAnnotation.Collection == null) + { + existingAnnotation.Collection = new RecordCollection(); + } + + if (existingAnnotation.Collection.Records == null) + { + existingAnnotation.Collection.Records = new List(); + } + + existingAnnotation.Collection.Records.AddRange(annotation.Collection.Records); + } + else + { + annotations.AnnotationList.Add(annotation); + } + } + } + else + { + schemaLevelAnnotations[fullName] = new Annotations + { + Target = fullName, + AnnotationList = annotatable.Annotation, + }; + } + + annotatable.Annotation = null; + } + } + + /// + /// Scan the MethodDefintions in the documentation and create actions and functions in the + /// EntityFramework for matching call patterns. + /// + /// + private void ProcessRestRequestPaths(EntityFramework edmx, string[] baseUrlsToRemove, IssueLogger issues) + { + Dictionary uniqueRequestPaths = GetUniqueRequestPaths(baseUrlsToRemove, issues); + List pathsToProcess = uniqueRequestPaths.Keys.OrderBy(p => p.Count(c => c == '/')).ThenBy(p => p).ToList(); + + for (int attempts = 1; attempts < 10 && pathsToProcess.Count > 0; attempts++) + { + Dictionary pathsToRetry = new Dictionary(); + foreach (var path in pathsToProcess) + { + var methodCollection = uniqueRequestPaths[path]; + + ODataTargetInfo requestTarget = null; + try + { + // TODO: If we have an input Edmx, we may already know what this so we don't need to infer anything. + // if that is the case, we should just update it with anything else we know from the documentation. + requestTarget = ParseRequestTargetType(path, methodCollection, edmx, issues); + if (requestTarget.Classification == ODataTargetClassification.Unknown && + !string.IsNullOrEmpty(requestTarget.Name) && + requestTarget.QualifiedType != null) + { + CreateNewActionOrFunction(edmx, methodCollection, requestTarget.Name, requestTarget.QualifiedType, issues.For(requestTarget.Name)); + } + else if (requestTarget.Classification == ODataTargetClassification.EntityType) + { + // We've learned more about this entity type, let's add that information to the state + AppendToEntityType(edmx, requestTarget, methodCollection); + } + else if (requestTarget.Classification == ODataTargetClassification.NavigationProperty) + { + // TODO: Record somewhere the operations that are available on this NavigationProperty + AppendToNavigationProperty(edmx, requestTarget, methodCollection, issues); + } + else + { + // TODO: Are there interesting things to learn here? + //Console.WriteLine("Found type {0}: {1}", requestTarget.Classification, path); + } + } + catch (Exception ex) + { + pathsToRetry.Add(path, ex); + continue; + } + } + + if (pathsToRetry.Count < pathsToProcess.Count) + { + pathsToProcess = pathsToRetry.Keys.ToList(); + } + else + { + issues.Message($"Failed to resolve the following paths after {attempts} attempts:"); + foreach (var kvp in pathsToRetry) + { + issues.Warning(ValidationErrorCode.Unknown, $"Couldn't serialize request for path {kvp.Key} into EDMX: {kvp.Value}"); + } + + break; + } + } + } + + private void AppendToNavigationProperty(EntityFramework edmx, ODataTargetInfo navigationProperty, MethodCollection methods, IssueLogger issues) + { + EntityType parentType = edmx.ResourceWithIdentifier(navigationProperty.QualifiedType); + + var matchingProperty = + parentType.NavigationProperties.FirstOrDefault(np => np.Name == navigationProperty.Name) ?? + parentType.Properties.FirstOrDefault(p => p.Name == navigationProperty.Name); + if (null != matchingProperty) + { + // TODO: Append information from methods into this navigation property + StringBuilder sb = new StringBuilder(); + const string seperator = ", "; + sb.AppendWithCondition(methods.GetAllowed, "GET", seperator); + sb.AppendWithCondition(methods.PostAllowed, "POST", seperator); + sb.AppendWithCondition(methods.PutAllowed, "PUT", seperator); + sb.AppendWithCondition(methods.DeleteAllowed, "DELETE", seperator); + issues.Message($"Collection '{navigationProperty.QualifiedType + "/" + navigationProperty.Name}' supports: ({sb})"); + } + else + { + issues.Error(ValidationErrorCode.RequiredPropertiesMissing, + $"EntityType '{navigationProperty.QualifiedType}' doesn't have a matching Property '{navigationProperty.Name}' but a request exists for this. Sounds like a documentation error."); + } + } + + /// + /// Use the properties of methodCollection to augment what we know about this entity type + /// + /// + /// + private void AppendToEntityType(EntityFramework edmx, ODataTargetInfo requestTarget, MethodCollection methodCollection) + { + StringBuilder sb = new StringBuilder(); + const string seperator = ", "; + sb.AppendWithCondition(methodCollection.GetAllowed, "GET", seperator); + sb.AppendWithCondition(methodCollection.PostAllowed, "POST", seperator); + sb.AppendWithCondition(methodCollection.PutAllowed, "PUT", seperator); + sb.AppendWithCondition(methodCollection.DeleteAllowed, "DELETE", seperator); + } + + private void CreateNewActionOrFunction( + EntityFramework edmx, + MethodCollection methodCollection, + string name, + string boundToType, + IssueLogger issues, + bool recursing = false) + { + if (name.IEquals("$ref") || + name.IEquals("$value")) + { + issues.Message("Ignoring $ref and $value. No schema changes needed."); + return; + } + + if (name.StartsWith("{")) + { + var currentType = edmx.DataServices.Schemas.FindTypeWithIdentifier(boundToType) as ComplexType; + if (currentType?.OpenType == true) + { + issues.Message($"Ignoring {name} navigation into open type. No schema changes needed."); + } + else + { + issues.Warning(ValidationErrorCode.Unknown, $"Don't know what to do with {name}; it's not a function"); + } + + return; + } + + // Create a new action (not idempotent) / function (idempotent) based on this request method! + ActionOrFunctionBase target = null; + if (methodCollection.AllMethodsIdempotent) + { + target = new Validation.OData.Function + { + IsComposable = methodCollection.Any(mc => mc.RequestMetadata.IsComposable), + }; + } + else + { + target = new Validation.OData.Action(); + } + + if (this.options.ShowSources) + { + target.SourceFiles = string.Join(";", new HashSet(methodCollection.Select(m => m.SourceFile).Select(f => Path.GetFileName(f.FullPath)).ToList())); + } + + if (this.options.Annotations.HasFlag(AnnotationOptions.HttpRequests)) + { + AddHttpRequestsAnnotations(target, methodCollection, issues.For(name)); + } + + target.SourceMethods = methodCollection; + + target.Name = name.TypeOnly(); + target.IsBound = true; + target.Parameters.Add(new Parameter { Name = "bindingParameter", Type = boundToType, Nullable = false }); + foreach (var param in methodCollection.Where(m => m.HttpMethodVerb() == "POST").SelectMany(m => m.RequestBodyParameters)) + { + try + { + var existingParam = target.Parameters.FirstOrDefault(p => p.Name.IEquals(param.Name)); + if (existingParam != null) + { + existingParam.Type = ParameterDataType.ChooseBest(existingParam.Type, param.Type.ODataResourceName()); + } + else + { + target.Parameters.Add( + new Parameter + { + Name = param.Name, + Type = param.Type.ODataResourceName(), + Nullable = (param.Required.HasValue ? param.Required.Value : false) + }); + } + } + catch (Exception ex) + { + issues.Error(ValidationErrorCode.Unknown, $"Exception when adding parameter {param.Name} with type {param.Type}", ex); + throw; + } + } + + if (target.Name.Contains("(")) + { + target.ParameterizedName = target.Name; + var pathParameters = target.Name.TextBetweenCharacters('(', ')').Split(','); + target.Name = target.Name.Substring(0, target.Name.IndexOf('(')); + foreach (var param in pathParameters.Where(p => !string.IsNullOrEmpty(p))) + { + var paramName = param.Split('=')[0]; + var matchingParamDef = methodCollection.SelectMany(m => m.Parameters).FirstOrDefault(p => p.Name == paramName); + if (matchingParamDef == null) + { + issues.Error(ValidationErrorCode.Unknown, + $"Couldn't find definition for parameter {param} in {name} after looking in {string.Join(",", methodCollection.Select(m => m.SourceFile?.DisplayName))}"); + } + else + { + target.Parameters.Add( + new Parameter + { + Name = matchingParamDef.Name, + Type = matchingParamDef.Type.ODataResourceName(), + Nullable = (matchingParamDef.Required.GetValueOrDefault()) + }); + + // if we have an optional param, recursively call again without it + if (matchingParamDef.Optional.GetValueOrDefault() == true) + { + var overloadName = target.ParameterizedName.Replace($"{paramName}={{var}}", string.Empty); + CreateNewActionOrFunction(edmx, methodCollection, overloadName, boundToType, issues); + } + } + } + + if (!recursing) + { + // note: this is very rough and brittle. only intended for limited use right now. + // if there are other sample calling patterns for this function, create bindings for them too. + foreach (var sample in methodCollection.SelectMany(mc => mc.SourceFile.Samples).SelectMany(s => s.Samples)) + { + if (sample.EndsWith(")") && sample.Contains("(")) + { + var overloadName = target.ParameterizedName.ReplaceTextBetweenCharacters('(', ')', sample.TextBetweenCharacters('(', ')')); + CreateNewActionOrFunction(edmx, methodCollection, overloadName, boundToType, issues, recursing: true); + } + else if (sample.EndsWith(target.Name)) + { + // overload with no parameters + var overloadName = target.Name; + CreateNewActionOrFunction(edmx, methodCollection, overloadName, boundToType, issues, recursing: true); + } + } + } + } + + if (methodCollection.ResponseType != null) + { + target.ReturnType = new ReturnType { Type = methodCollection.ResponseType.ODataResourceName(), Nullable = false }; + var targetAction = target as Validation.OData.Action; + if (targetAction != null && + target.Parameters.FirstOrDefault(p=>p.Name=="bindingParameter")?.Type?.StartsWith("Collection(") == false) + { + targetAction.EntitySetPath = "bindingParameter"; + } + } + + var schemaName = name.HasNamespace() ? name.NamespaceOnly() : edmx.DataServices.Schemas.FirstOrDefault()?.Namespace; + var schema = FindOrCreateSchemaForNamespace(schemaName, edmx, true); + + // first see if this action or function is already bound to a base type with the same params. + // if so, this definition is redundant. + foreach (var existingFunction in schema.Functions.Cast().Concat(schema.Actions).ToList()) + { + if (existingFunction.CanSubstituteFor(target, edmx)) + { + return; + } + + // on the other hand, if the opposite is true... + if (target.CanSubstituteFor(existingFunction, edmx)) + { + if (target is Function) + { + schema.Functions.Remove((Function)existingFunction); + } + else + { + schema.Actions.Remove((Validation.OData.Action)existingFunction); + } + } + } + + if (target is Function) + { + schema.Functions.Add((Function)target); + } + else + { + schema.Actions.Add((Validation.OData.Action)target); + } + } + + /// + /// Walks the requestPath through the resources / entities defined in the edmx and resolves + /// the type of request represented by the path + /// + /// + /// + /// + /// + private static ODataTargetInfo ParseRequestTargetType(string requestPath, MethodCollection requestMethodCollection, EntityFramework edmx, IssueLogger issues) + { + string[] requestParts = requestPath.Substring(1).Split(new char[] { '/'}); + + EntityContainer entryPoint = (from s in edmx.DataServices.Schemas + where s.EntityContainers.Count > 0 + select s.EntityContainers.FirstOrDefault()).SingleOrDefault(); + + if (entryPoint == null) throw new InvalidOperationException("Couldn't locate an EntityContainer to begin target resolution"); + + IODataNavigable currentObject = entryPoint; + IODataNavigable previousObject = null; + + for(int i=0; i + /// Parse the URI paths for methods defined in the documentation and construct an entity container that contains these + /// entity sets / singletons in the largest namespace. + /// + private void BuildEntityContainer(EntityFramework edmx, string[] baseUrlsToRemove, IssueLogger issues) + { + // Check to see if an entitycontainer already exists + foreach (var s in edmx.DataServices.Schemas) + { + if (s.EntityContainers.Any()) + return; + } + + Dictionary uniqueRequestPaths = GetUniqueRequestPaths(baseUrlsToRemove, issues); + var resourcePaths = uniqueRequestPaths.Keys.OrderBy(x => x).ToArray(); + + EntityContainer container = new EntityContainer(); + foreach (var path in resourcePaths) + { + try + { + var methodCollection = uniqueRequestPaths[path]; + + if (EntitySetPathRegEx.IsMatch(path)) + { + var name = EntitySetPathRegEx.Match(path).Groups[1].Value; + var entitySet = new EntitySet + { + Name = name, + EntityType = methodCollection.ResponseType.ODataResourceName(), + SourceMethods = methodCollection, + }; + + if (this.options.Annotations.HasFlag(AnnotationOptions.HttpRequests)) + { + AddHttpRequestsAnnotations(entitySet, methodCollection, issues.For(path + "/HttpRequestsAnnotations")); + } + + container.EntitySets.Add(entitySet); + } + else if (SingletonPathRegEx.IsMatch(path)) + { + // Before we declare this a singleton, see if any other paths that have the same root match the entity set regex + var query = (from p in resourcePaths where p.StartsWith(path + "/") && EntitySetPathRegEx.IsMatch(p) select p); + if (query.Any()) + { + // If there's a similar resource path that matches the entity, we don't declare a singleton. + continue; + } + + var name = SingletonPathRegEx.Match(path).Groups[1].Value; + var singleton = new Singleton + { + Name = name, + Type = methodCollection.ResponseType.ODataResourceName(), + SourceMethods = methodCollection, + }; + + if (this.options.Annotations.HasFlag(AnnotationOptions.HttpRequests)) + { + AddHttpRequestsAnnotations(singleton, methodCollection, issues.For(path + "/HttpRequestsAnnotations")); + } + + container.Singletons.Add(singleton); + } + } + catch (Exception ex) + { + issues.Error(ValidationErrorCode.Unknown, $"BuildEntityContainer: error in path {path}", ex); + } + } + + Schema defaultSchema; + if (DocSet.SchemaConfig.DefaultNamespace != null) + { + defaultSchema = edmx.DataServices.Schemas.Single(s => s.Namespace.IEquals(DocSet.SchemaConfig.DefaultNamespace)); + } + else + { + // if the default schema isn't specified, assume the most common schema in the docs is the default + defaultSchema = edmx.DataServices.Schemas.OrderByDescending(s => s.EntityTypes.Count).First(); + } + + container.Name = this.options.EntityContainerName ?? defaultSchema.Namespace; + defaultSchema.EntityContainers.Add(container); + } + + private Dictionary cachedUniqueRequestPaths { get; set; } + + /// + /// Return a dictionary of the unique request paths in the + /// documentation and the method definitions that defined them. + /// + /// + private Dictionary GetUniqueRequestPaths(string[] baseUrlsToRemove, IssueLogger issues) + { + if (cachedUniqueRequestPaths == null) + { + Dictionary uniqueRequestPaths = new Dictionary(); + foreach (var m in Documents.Methods) + { + if (m.RequestMetadata?.OpaqueUrl == true) + { + // ignore requests with opaque urls, as they don't contribute to the schema + continue; + } + + if (m.ExpectedResponseMetadata != null && m.ExpectedResponseMetadata.ExpectError) + { + // Ignore things that are expected to error + continue; + } + + var path = m.RequestUriPathOnly(baseUrlsToRemove, issues); + if (!path.StartsWith("/")) + { + // Ignore aboslute URI paths + continue; + } + + if (m.RequestMetadata.Tags != null && + DocSet.SchemaConfig.SupportedTags != null && + !DocSet.SchemaConfig.SupportedTags.Intersect(m.RequestMetadata.TagList).Any()) + { + // ignore tagged request examples if the current service doesn't support that tag + continue; + } + + issues.Message($"Converted '{m.Request.FirstLineOnly()}' into generic form '{path}'"); + + if (!uniqueRequestPaths.ContainsKey(path)) + { + uniqueRequestPaths.Add(path, new MethodCollection()); + } + uniqueRequestPaths[path].Add(m); + + issues.Message($"{path} :: {m.RequestMetadata.ResourceType} --> {m.ExpectedResponseMetadata?.ResourceType}"); + } + + cachedUniqueRequestPaths = uniqueRequestPaths; + } + return cachedUniqueRequestPaths; + } + + /// + /// Find an existing schema definiton or create a new one in an entity framework for a given namespace. + /// + /// + /// + /// + private Schema FindOrCreateSchemaForNamespace(string ns, EntityFramework edmx, bool overrideNamespaceFilter = false, bool generateNewElements = true) + { + // Check to see if this is a namespace that should be exported. + if (!overrideNamespaceFilter && options.Namespaces != null && !options.Namespaces.Contains(ns)) + { + return null; + } + + if (ns.Equals("odata", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var matchingSchema = (from s in edmx.DataServices.Schemas + where s.Namespace == ns + select s).FirstOrDefault(); + + if (null != matchingSchema) + return matchingSchema; + + if (generateNewElements) + { + var newSchema = new Schema() { Namespace = ns }; + edmx.DataServices.Schemas.Add(newSchema); + return newSchema; + } + + return null; + } + + private void AddResourceToSchema( + Schema schema, + ResourceDefinition resource, + EntityFramework edmx, + IssueLogger issues, + bool generateNewElements = true) + { + ComplexType type = null; + var typeName = resource.Name.TypeOnly(); + + // First check to see if there is an existing resource that matches this resource in the framework already + var existingEntity = (from e in schema.EntityTypes where e.Name == typeName select e).SingleOrDefault(); + if (existingEntity != null) + { + type = existingEntity; + } + + // If that didn't work, look for a complex type that matches + if (type == null) + { + var existingComplexType = (from e in schema.ComplexTypes where e.Name == typeName select e).SingleOrDefault(); + if (existingComplexType != null) + { + type = existingComplexType; + } + } + + // Finally go ahead and create a new resource in the schema if we didn't find a matching one + if (null == type && generateNewElements) + { + type = CreateResourceInSchema(schema, resource, issues); + } + else if (null == type) + { + issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Type {resource.Name} was in the documentation but not found in the schema."); + } + + if (null != type) + { + LogIfDifferent(type.Name, resource.Name.TypeOnly(), issues, $"Schema type {type.Name} is different than the documentation name {resource.Name.TypeOnly()}."); + LogIfDifferent(type.OpenType, resource.OriginalMetadata.IsOpenType, issues, $"Schema type {type.Name} has a different OpenType value {type.OpenType} than the documentation {resource.OriginalMetadata.IsOpenType}."); + LogIfDifferent(type.BaseType, resource.OriginalMetadata.BaseType, issues, $"Schema type {type.Name} has a different BaseType value {type.BaseType} than the documentation {resource.OriginalMetadata.BaseType}."); + + AddDocPropertiesToSchemaResource(type, resource, edmx, generateNewElements, issues); + } + } + + private void AddDocPropertiesToSchemaResource( + ComplexType schemaType, + ResourceDefinition docResource, + EntityFramework edmx, + bool generateNewElements, + IssueLogger issues) + { + var docProps = (from p in docResource.Parameters + where !p.IsNavigatable && + !p.Name.StartsWith("@") && + (docResource.ResolvedBaseTypeReference == null || + !docResource.ResolvedBaseTypeReference.HasOrInheritsProperty(p.Name)) + select p).ToList(); + MergePropertiesIntoSchema(schemaType.Name, schemaType.Properties, docProps, edmx, generateNewElements, issues.For(schemaType.Name), schemaType.Contributors); + + var schemaEntity = schemaType as EntityType; + + var docNavigationProps = (from p in docResource.Parameters + where p.IsNavigatable && + !p.Name.StartsWith("@") && + (docResource.ResolvedBaseTypeReference == null || + !docResource.ResolvedBaseTypeReference.HasOrInheritsProperty(p.Name)) + select p); + + if (schemaEntity != null) + { + MergePropertiesIntoSchema(schemaEntity.Name, schemaEntity.NavigationProperties, docNavigationProps, edmx, generateNewElements, issues.For(schemaEntity.Name), schemaEntity.Contributors); + } + else if (docNavigationProps.Any()) + { + issues.Error(ValidationErrorCode.ExpectedTypeDifferent, $"Resource {schemaType.Name} has documented navigation properties, but we thought it was a complex type!"); + } + + if (schemaType.BaseType != docResource.BaseType) + { + issues.Warning(ValidationErrorCode.Unknown, $"Resource {schemaType.Name} has multiple declarations with mismatched BaseTypes."); + + if (schemaType.BaseType == null) + { + schemaType.BaseType = docResource.BaseType; + } + } + + if (schemaType.OpenType != docResource.OriginalMetadata?.IsOpenType && docResource.OriginalMetadata?.IsOpenType == true) + { + issues.Warning(ValidationErrorCode.Unknown, $"Resource { schemaType.Name} has multiple declarations with mismatched OpenType declarations."); + schemaType.OpenType = true; + } + + var docInstanceAnnotations = (from p in docResource.Parameters where p.Name != null && p.Name.StartsWith("@") select p); + MergeInstanceAnnotationsAndRecordTerms(schemaType.Name, docInstanceAnnotations, docResource, edmx, generateNewElements); + + schemaType.Contributors.Add(docResource); + } + + private void MergePropertiesIntoSchema( + string typeName, + List schemaProps, + IEnumerable docProps, + EntityFramework edmx, + bool generateNewElements, + IssueLogger issues, + HashSet allContributors = null) + where TProp : Property, new() + { + var documentedProperties = docProps.ToDictionary(x => x.Name, x => x); + foreach(var schemaProp in schemaProps) + { + ParameterDefinition documentedVersion = null; + if (documentedProperties.TryGetValue(schemaProp.Name, out documentedVersion)) + { + // Compare / update schema with data from documentation + var docProp = ConvertParameterToProperty(typeName, documentedVersion, issues.For(schemaProp.Name)); + LogIfDifferent(schemaProp.Nullable, docProp.Nullable, issues, $"Type {typeName}: Property {docProp.Name} has a different nullable value than documentation."); + LogIfDifferent(schemaProp.TargetEntityType, docProp.TargetEntityType, issues, $"Type {typeName}: Property {docProp.Name} has a different target entity type than documentation."); + LogIfDifferent(schemaProp.Type, docProp.Type, issues, $"Type {typeName}: Property {docProp.Name} has a different Type value than documentation ({schemaProp.Type},{docProp.Type})."); + LogIfDifferent(schemaProp.Unicode, docProp.Unicode, issues, $"Type {typeName}: Property {docProp.Name} has a different unicode value than documentation ({schemaProp.Unicode},{docProp.Unicode})."); + documentedProperties.Remove(documentedVersion.Name); + + AddDescriptionAnnotation(typeName, schemaProp, documentedVersion, issues); + } + else + { + // Log out that this property wasn't in the documentation + if (allContributors == null || + allContributors.All(c => c.Parameters.All(p => p.Name != schemaProp.Name))) + { + issues.Warning(ValidationErrorCode.RequiredPropertiesMissing, + $"UndocumentedProperty: {typeName} defines {schemaProp.Name} in the schema but has no matching documentation."); + } + } + } + + if (generateNewElements) + { + foreach(var newPropDef in documentedProperties.Values) + { + // Create new properties based on the documentation + var newProp = ConvertParameterToProperty(typeName, newPropDef, issues.For(newPropDef.Name)); + schemaProps.Add(newProp); + } + } + } + + private static void LogIfDifferent(object schemaValue, object documentationValue, IssueLogger issues, string errorString) + { + if ( (schemaValue == null && documentationValue != null) || + (schemaValue != null && documentationValue == null) || + (schemaValue != null && documentationValue != null && !schemaValue.Equals(documentationValue))) + { + issues.Warning(ValidationErrorCode.Unknown, errorString); + } + } + + private ComplexType CreateResourceInSchema(Schema schema, ResourceDefinition resource, IssueLogger issues) + { + var entityKey = resource.ExplicitOrInheritedKeyPropertyName; + ComplexType type; + + // Create a new entity or complex type for this resource + if (!string.IsNullOrEmpty(entityKey)) + { + var entity = new EntityType + { + BaseType = resource.BaseType, + HasStream = resource.OriginalMetadata.IsMediaEntity, + Name = resource.Name.TypeOnly(), + Namespace = resource.Name.NamespaceOnly(), + OpenType = resource.OriginalMetadata.IsOpenType, + }; + + if (resource.ResolvedBaseTypeReference == null || + entityKey != resource.ResolvedBaseTypeReference.ExplicitOrInheritedKeyPropertyName) + { + entity.Key = new Key { PropertyRef = new PropertyRef { Name = entityKey } }; + } + + entity.NavigationProperties = (from p in resource.Parameters + where p.IsNavigatable && + (resource.ResolvedBaseTypeReference == null || + !resource.HasOrInheritsProperty(p.Name)) + select ConvertParameterToProperty(entity.Name, p, issues.For(p.Name))).ToList(); + schema.EntityTypes.Add(entity); + type = entity; + } + else + { + type = new ComplexType + { + BaseType = resource.BaseType, + Name = resource.Name.TypeOnly(), + Namespace = resource.Name.NamespaceOnly(), + OpenType = resource.OriginalMetadata.IsOpenType, + }; + + schema.ComplexTypes.Add(type); + } + + type.Abstract = resource.Abstract; + type.Contributors.Add(resource); + + return type; + } + + private void MergeInstanceAnnotationsAndRecordTerms(string typeName, IEnumerable annotations, ResourceDefinition containedResource, EntityFramework edmx, bool generateNewElements) + { + foreach (var prop in annotations) + { + var qualifiedName = prop.Name.Substring(1); + var ns = qualifiedName.NamespaceOnly(); + var localName = qualifiedName.TypeOnly(); + + Term term = new Term { Name = localName, AppliesTo = containedResource.Name, Type = prop.Type.ODataResourceName() }; + if (!string.IsNullOrEmpty(prop.Description)) + { + term.Annotations.Add(new Annotation { Term = Term.LongDescriptionTerm, String = prop.Description.ToStringClean() }); + } + + var targetSchema = FindOrCreateSchemaForNamespace(ns, edmx, overrideNamespaceFilter: true, generateNewElements: generateNewElements); + if (null != targetSchema) + { + targetSchema.Terms.Add(term); + } + } + } + + private T ConvertParameterToProperty(string typeName, ParameterDefinition param, IssueLogger issues) where T : Property, new() + { + var prop = new T() + { + Name = param.Name, + Nullable = (param.Required.HasValue ? !param.Required.Value : false), + Type = param.Type.ODataResourceName() + }; + + // Add description annotation + AddDescriptionAnnotation(typeName, prop, param, issues); + return prop; + } + + private void AddDescriptionAnnotation( + string typeName, + T targetProperty, + ParameterDefinition sourceParameter, + IssueLogger issues, + string termForDescription = Term.DescriptionTerm) where T: Property, IODataAnnotatable + { + if (this.options.Annotations == AnnotationOptions.None) + { + return; + } + + if (!string.IsNullOrEmpty(sourceParameter.Description)) + { + if (targetProperty.Annotation == null) + { + targetProperty.Annotation = new List(); + } + + // Check to see if there already is a term with Description + var descriptionTerm = targetProperty.Annotation.Where(t => t.Term == termForDescription).FirstOrDefault(); + if (descriptionTerm != null) + { + LogIfDifferent(descriptionTerm.String, sourceParameter.Description, issues, $"Type {typeName} has a different value for term '{termForDescription}' than the documentation."); + } + else + { + targetProperty.Annotation.Add( + new Annotation() + { + Term = termForDescription, + String = sourceParameter.Description.ToStringClean(), + }); + } + } + } + + private class EnumComparer : IEqualityComparer + { + public bool Equals(EnumerationDefinition x, EnumerationDefinition y) + { + if (object.ReferenceEquals(x, y)) + { + return true; + } + + if (x == null || y == null) + { + return false; + } + + return x.MemberName.Equals(y.MemberName); + } + + public int GetHashCode(EnumerationDefinition obj) + { + return obj?.MemberName.GetHashCode() ?? 0; + } + } + } + + + public class CsdlWriterOptions + { + public string OutputDirectoryPath { get; set; } + public string SourceMetadataPath { get; set; } + public string MergeWithMetadataPath { get; set; } + public MetadataFormat Formats { get; set; } + public string[] Namespaces { get; set; } + public bool Sort { get; set; } + public string TransformOutput { get; set; } + public string DocumentationSetPath { get; set; } + public string Version { get; set; } + public bool SkipMetadataGeneration { get; set; } + public AnnotationOptions Annotations { get; set; } + public bool ValidateSchema { get; set; } + public bool AttributesOnNewLines { get; set; } + public string EntityContainerName { get; set; } + public bool ShowSources { get; set; } + } + + [Flags] + public enum MetadataFormat + { + Default = EdmxInput | EdmxOutput, + EdmxInput = 1 << 0, + EdmxOutput = 1 << 1, + SchemaInput = 1 << 2, + SchemaOutput = 1 << 3 + } + + [Flags] + public enum AnnotationOptions + { + None = 0x00, + Properties = 0x01, + Capabilities = 0x02, + HttpRequests = 0x04, + Independent = 0x08, + AllAnnotations = Properties | Capabilities | HttpRequests, + OnlyAnnotations = AllAnnotations | Independent, + } +} diff --git a/ApiDocs.Publishing/CSDL/MethodCollection.cs b/ApiDoctor.Publishing/CSDL/methodcollection.cs similarity index 94% rename from ApiDocs.Publishing/CSDL/MethodCollection.cs rename to ApiDoctor.Publishing/CSDL/methodcollection.cs index 25ff70cc..d88a4793 100644 --- a/ApiDocs.Publishing/CSDL/MethodCollection.cs +++ b/ApiDoctor.Publishing/CSDL/methodcollection.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,11 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +using System; using System.Collections.Generic; using System.Linq; -namespace ApiDocs.Publishing.CSDL +namespace ApiDoctor.Publishing.CSDL { /// /// Wrapper around a collection of methods that operate on the same REST path. @@ -55,7 +56,10 @@ public bool PutAllowed /// public bool AllMethodsIdempotent { - get { return this.All(x => x.RequestMetadata.IsIdempotent); } + get + { + return this.All(m => m.RequestMetadata.IsIdempotent || !m.HttpMethodVerb().Equals("POST")); + } } /// @@ -86,12 +90,11 @@ public Validation.ParameterDataType ResponseType else dataType = Validation.ParameterDataType.ChooseBest(method.ExpectedResponseMetadata.Type, dataType); } + return dataType; } } - - protected bool HttpVerbAllowed(string verb) { var query = from m in this @@ -100,7 +103,5 @@ where m.HttpMethodVerb().Equals(verb, System.StringComparison.OrdinalIgnoreCase) return query.Any(); } - - } } diff --git a/ApiDoctor.Publishing/CSDL/xmlsorter.cs b/ApiDoctor.Publishing/CSDL/xmlsorter.cs new file mode 100644 index 00000000..5ed4a02d --- /dev/null +++ b/ApiDoctor.Publishing/CSDL/xmlsorter.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using ApiDoctor.Validation.OData.Transformation; + +namespace ApiDoctor.Publishing.CSDL +{ + public class XmlSorter + { + private static HashSet attributesToDrop = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Nullable", // props + "Unicode", // string props + "EntitySetPath", // entitysets + "ContainsTarget", // navprops + }; + + private static HashSet attributesToIgnoreForSorting = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "SourceFiles", + }; + + private static Dictionary attributeValuesToRename = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["bindingparameter"] = "bindingParameter", + ["bindParameter"] = "bindingParameter", + ["this"] = "bindingParameter", + }; + + private bool learningMode; + private HashSet knownNames; + private HashSet unknownNames; + private HashSet keywordsToDropElements; + private HashSet keywordsToKeepElements; + + public XmlSorter(SchemaDiffConfig config) + { + this.keywordsToKeepElements = new HashSet(config?.KeepElementsContaining ?? new string[0], StringComparer.OrdinalIgnoreCase); + this.keywordsToDropElements = new HashSet(config?.DropElementsContaining ?? new string[0], StringComparer.OrdinalIgnoreCase); + } + + public bool KeepUnrecognizedObjects { get; set; } + + public void Sort(string firstXmlPath, string secondXmlPath) + { + this.knownNames = new HashSet(attributeValuesToRename.Keys, StringComparer.OrdinalIgnoreCase); + this.unknownNames = new HashSet(StringComparer.OrdinalIgnoreCase); + this.learningMode = true; + foreach (var input in new[] { firstXmlPath, secondXmlPath}) + { + var output = input.Replace(".xml", "-sorted.xml"); + if (File.Exists(output)) + { + File.Delete(output); + } + + using (var file = File.OpenRead(input)) + { + var settings = new XmlWriterSettings + { + Indent = true, + }; + + using (var writer = XmlWriter.Create(output, settings)) + { + var input1 = XDocument.Parse(File.ReadAllText(input)); + var sorted = Sort(input1.Root); + sorted.WriteTo(writer); + } + } + + this.learningMode = false; + } + } + + private bool SmartFilter(string name) + { + if (!string.IsNullOrEmpty(name)) + { + if (this.learningMode) + { + this.knownNames.Add(name); + } + else + { + bool known = this.knownNames.Contains(name); + if (!known) + { + this.unknownNames.Add(name); + return this.KeepUnrecognizedObjects; + } + } + } + + return true; + } + + public XElement Sort(XElement element) + { + var sorted = new XElement(element.Name); + foreach (var xa in element.Attributes(). + Where(xa => !attributesToDrop.Contains(xa.Name.ToString())). + OrderByDescending(xa => xa.Name == "Name"). + ThenBy(xa => xa.Name == "Term"). + ThenBy(xa => attributesToIgnoreForSorting.Contains(xa.Name.ToString())). + ThenBy(xa => xa.Name.ToString()). + ThenBy(xa => xa.Value.ToString())) + { + string newName; + if (attributeValuesToRename.TryGetValue(xa.Value.ToString(), out newName)) + { + sorted.Add(new XAttribute(xa.Name, newName)); + } + else + { + sorted.Add(xa); + } + } + + foreach (var xe in element.Elements(). + Where(el => SmartFilter(el.Name.ToString()) && SmartFilter(el.Attribute("Name")?.Value)). + Select(xe => Sort(xe)). + Where(xe => + xe.Attributes().Any(attr => keywordsToKeepElements.Contains(attr.Value)) || + xe.Elements().SelectMany(el => el.Attributes()).All(attr => !keywordsToDropElements.Contains(attr.Value))). + OrderBy(xe => xe.Name.ToString()). + ThenBy(xe => PrintAttrs(xe.Attributes())). + ThenBy(xe => PrintElems(xe.Elements()))) + { + sorted.Add(xe); + } + + foreach (var n in element.Nodes().Where(n=>n.NodeType != XmlNodeType.Element)) + { + sorted.Add(n); + } + + return sorted; + } + + public static string PrintAttrs(IEnumerable attributes) + { + var sb = new StringBuilder(); + foreach (var attr in attributes.Where(attr=> !attributesToIgnoreForSorting.Contains(attr.Name.ToString()))) + { + sb.Append(attr.Name).Append("=").Append(attr.Value).Append(";"); + } + + return sb.ToString(); + } + + public static string PrintElems(IEnumerable elements) + { + var sb = new StringBuilder(); + foreach (var el in elements.OrderBy(xe => xe.Name.ToString()). + ThenBy(xe => PrintAttrs(xe.Attributes()))) + { + sb.Append(PrintAttrs(el.Attributes())).Append("|"); + } + + return sb.ToString(); + } + } +} diff --git a/ApiDocs.Publishing/Html/ApiDocsConditionalTag.cs b/ApiDoctor.Publishing/Html/ApiDocsConditionalTag.cs similarity index 99% rename from ApiDocs.Publishing/Html/ApiDocsConditionalTag.cs rename to ApiDoctor.Publishing/Html/ApiDocsConditionalTag.cs index 24771ffe..d29b6eea 100644 --- a/ApiDocs.Publishing/Html/ApiDocsConditionalTag.cs +++ b/ApiDoctor.Publishing/Html/ApiDocsConditionalTag.cs @@ -4,7 +4,7 @@ * is copied here so it can be extended to add new conditional tags. */ -namespace ApiDocs.Publishing.Html +namespace ApiDoctor.Publishing.Html { using Mustache; using System; diff --git a/ApiDocs.Publishing/Html/DocumentPublisherHtml.cs b/ApiDoctor.Publishing/Html/DocumentPublisherHtml.cs similarity index 98% rename from ApiDocs.Publishing/Html/DocumentPublisherHtml.cs rename to ApiDoctor.Publishing/Html/DocumentPublisherHtml.cs index 967a2b4e..bd6424a0 100644 --- a/ApiDocs.Publishing/Html/DocumentPublisherHtml.cs +++ b/ApiDoctor.Publishing/Html/DocumentPublisherHtml.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Html +namespace ApiDoctor.Publishing.Html { using System; using System.Collections.Generic; @@ -31,9 +31,9 @@ namespace ApiDocs.Publishing.Html using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; - using ApiDocs.Validation; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Writers; + using ApiDoctor.Validation; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Writers; using MarkdownDeep; using Newtonsoft.Json; using Validation.Tags; diff --git a/ApiDocs.Publishing/Html/ExtendedElseTag.cs b/ApiDoctor.Publishing/Html/ExtendedElseTag.cs similarity index 97% rename from ApiDocs.Publishing/Html/ExtendedElseTag.cs rename to ApiDoctor.Publishing/Html/ExtendedElseTag.cs index a2c0a364..ac5dabaa 100644 --- a/ApiDocs.Publishing/Html/ExtendedElseTag.cs +++ b/ApiDoctor.Publishing/Html/ExtendedElseTag.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Html +namespace ApiDoctor.Publishing.Html { using Mustache; using System; diff --git a/ApiDocs.Publishing/Html/FileTagDefinition.cs b/ApiDoctor.Publishing/Html/FileTagDefinition.cs similarity index 96% rename from ApiDocs.Publishing/Html/FileTagDefinition.cs rename to ApiDoctor.Publishing/Html/FileTagDefinition.cs index c819e107..fc5858ca 100644 --- a/ApiDocs.Publishing/Html/FileTagDefinition.cs +++ b/ApiDoctor.Publishing/Html/FileTagDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,11 +23,11 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Html +namespace ApiDoctor.Publishing.Html { using System.Collections.Generic; using System.IO; - using ApiDocs.Validation; + using ApiDoctor.Validation; using Mustache; public class FileTagDefinition : TagDefinition diff --git a/ApiDocs.Publishing/Html/HtmlMustacheWriter.cs b/ApiDoctor.Publishing/Html/HtmlMustacheWriter.cs similarity index 99% rename from ApiDocs.Publishing/Html/HtmlMustacheWriter.cs rename to ApiDoctor.Publishing/Html/HtmlMustacheWriter.cs index d81577ba..fa70cc04 100644 --- a/ApiDocs.Publishing/Html/HtmlMustacheWriter.cs +++ b/ApiDoctor.Publishing/Html/HtmlMustacheWriter.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,15 +23,15 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Html +namespace ApiDoctor.Publishing.Html { using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; - using ApiDocs.Validation; - using ApiDocs.Validation.Writers; + using ApiDoctor.Validation; + using ApiDoctor.Validation.Writers; using Mustache; using System.Dynamic; using Newtonsoft.Json; diff --git a/ApiDocs.Publishing/Html/IfMatchTagDefinition.cs b/ApiDoctor.Publishing/Html/IfMatchTagDefinition.cs similarity index 97% rename from ApiDocs.Publishing/Html/IfMatchTagDefinition.cs rename to ApiDoctor.Publishing/Html/IfMatchTagDefinition.cs index e219b750..683136d5 100644 --- a/ApiDocs.Publishing/Html/IfMatchTagDefinition.cs +++ b/ApiDoctor.Publishing/Html/IfMatchTagDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Html +namespace ApiDoctor.Publishing.Html { using System.Collections.Generic; using System.Linq; diff --git a/ApiDocs.Publishing/PathExtensionMethods.cs b/ApiDoctor.Publishing/PathExtensionMethods.cs similarity index 97% rename from ApiDocs.Publishing/PathExtensionMethods.cs rename to ApiDoctor.Publishing/PathExtensionMethods.cs index f930dd6f..181c787b 100644 --- a/ApiDocs.Publishing/PathExtensionMethods.cs +++ b/ApiDoctor.Publishing/PathExtensionMethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing +namespace ApiDoctor.Publishing { internal static class PathExtensionMethods { diff --git a/ApiDocs.Publishing/Properties/AssemblyInfo.cs b/ApiDoctor.Publishing/Properties/AssemblyInfo.cs similarity index 100% rename from ApiDocs.Publishing/Properties/AssemblyInfo.cs rename to ApiDoctor.Publishing/Properties/AssemblyInfo.cs diff --git a/ApiDocs.Publishing/Swagger/SwaggerClasses.cs b/ApiDoctor.Publishing/Swagger/SwaggerClasses.cs similarity index 98% rename from ApiDocs.Publishing/Swagger/SwaggerClasses.cs rename to ApiDoctor.Publishing/Swagger/SwaggerClasses.cs index a28af396..e24d0862 100644 --- a/ApiDocs.Publishing/Swagger/SwaggerClasses.cs +++ b/ApiDoctor.Publishing/Swagger/SwaggerClasses.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Swagger +namespace ApiDoctor.Publishing.Swagger { using System.Collections.Generic; using System.Linq; diff --git a/ApiDocs.Publishing/Swagger/SwaggerExtensionMethods.cs b/ApiDoctor.Publishing/Swagger/SwaggerExtensionMethods.cs similarity index 97% rename from ApiDocs.Publishing/Swagger/SwaggerExtensionMethods.cs rename to ApiDoctor.Publishing/Swagger/SwaggerExtensionMethods.cs index 21871d2d..89b72804 100644 --- a/ApiDocs.Publishing/Swagger/SwaggerExtensionMethods.cs +++ b/ApiDoctor.Publishing/Swagger/SwaggerExtensionMethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,14 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Swagger +namespace ApiDoctor.Publishing.Swagger { using System; using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation; - using ApiDocs.Validation.Http; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation; + using ApiDoctor.Validation.Http; + using ApiDoctor.Validation.Json; using ResourceName = System.String; public static class SwaggerExtensionMethods @@ -61,7 +61,7 @@ internal static string ToSwaggerTypeString(this SimpleDataType type, string cust case SimpleDataType.Object: return "object"; case SimpleDataType.Collection: - throw new ArgumentException(); + throw new ArgumentException($"collection not supported: {customTypeName ?? type.ToString()}"); default: return type.ToString().ToLower(); } diff --git a/ApiDocs.Publishing/Swagger/SwaggerWriter.cs b/ApiDoctor.Publishing/Swagger/swaggerwriter.cs similarity index 67% rename from ApiDocs.Publishing/Swagger/SwaggerWriter.cs rename to ApiDoctor.Publishing/Swagger/swaggerwriter.cs index 76b46328..4f85cd12 100644 --- a/ApiDocs.Publishing/Swagger/SwaggerWriter.cs +++ b/ApiDoctor.Publishing/Swagger/swaggerwriter.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Publishing.Swagger +namespace ApiDoctor.Publishing.Swagger { using System; using System.Collections.Generic; @@ -32,8 +32,9 @@ namespace ApiDocs.Publishing.Swagger using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; - using ApiDocs.Validation; - using ApiDocs.Validation.Writers; + using ApiDoctor.Validation; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Writers; using Newtonsoft.Json; /// @@ -73,7 +74,7 @@ public SwaggerWriter(DocSet docs, string baseUrl) : base(docs) } #pragma warning disable 1998 - public override async Task PublishToFolderAsync(string outputFolder) + public override async Task PublishToFolderAsync(string outputFolder, IssueLogger issues) { this.SnapVariables(); @@ -112,13 +113,28 @@ public override async Task PublishToFolderAsync(string outputFolder) private object BuildSecurityDefinition() { + Console.WriteLine("building security definition..."); var foundScopes = this.Documents.AuthScopes; + var scopes = new Dictionary(); + foreach (var scope in foundScopes) + { + string existingDescription; + if (scopes.TryGetValue(scope.Scope, out existingDescription)) + { + Console.WriteLine($"ERR: duplicate scopes found. {scope.Scope}/{scope.Description} collides with {existingDescription}"); + } + else + { + scopes.Add(scope.Scope, scope.Description); + } + } + return new Dictionary { { this.AuthenticationParameters.ProviderName, new { type = this.AuthenticationParameters.AuthType, - scopes = foundScopes.ToDictionary(x => x.Scope, x => x.Description), + scopes = scopes, flow = this.AuthenticationParameters.OAuthFlow, authorizationUrl = this.AuthenticationParameters.AuthorizationEndPoint }}}; @@ -133,29 +149,35 @@ private object GenerateResourcesFromDocSet() var swaggerDefinitions = new Dictionary(); foreach (var jsonSchema in this.Documents.ResourceCollection.RegisteredSchema) { - if (this.IsDocFileInternal(jsonSchema.OriginalResource.SourceFile)) - continue; + try + { + if (this.IsDocFileInternal(jsonSchema.OriginalResource.SourceFile)) + continue; - var resourceName = jsonSchema.ResourceName.SwaggerResourceName(); + var resourceName = jsonSchema.ResourceName.SwaggerResourceName(); - var propertiesForThisResource = jsonSchema.Properties.ToDictionary( - property => property.Name.SwaggerResourceName(), - property => property.AsSwaggerProperty()); + var propertiesForThisResource = jsonSchema.Properties.ToDictionary( + property => property.Name.SwaggerResourceName(), + property => property.AsSwaggerProperty()); - var definition = new - { - properties = propertiesForThisResource - }; - - if (!swaggerDefinitions.ContainsKey(resourceName)) - { - swaggerDefinitions.Add(resourceName, definition); + var definition = new + { + properties = propertiesForThisResource + }; + + if (!swaggerDefinitions.ContainsKey(resourceName)) + { + swaggerDefinitions.Add(resourceName, definition); + } + else + { + Console.WriteLine("Found a duplicate resource type: " + resourceName); + } } - else + catch (Exception ex) { - Console.WriteLine("Found a duplicate resource type: " + resourceName); + Console.WriteLine($"FAILED GenerateResourcesFromDocSet {jsonSchema} with {ex}\r\n\r\n"); } - } return swaggerDefinitions; } @@ -235,51 +257,58 @@ private IDictionary> GeneratePathsFro var swaggerPathObject = new SortedDictionary>(new PathLengthSorter()); foreach (var method in this.Documents.Methods) { - if (this.IsDocFileInternal(method.SourceFile)) - continue; + try + { + if (this.IsDocFileInternal(method.SourceFile)) + continue; - string relativePath, queryString, httpMethod; - method.SplitRequestUrl(out relativePath, out queryString, out httpMethod); + string relativePath, queryString, httpMethod; + method.SplitRequestUrl(out relativePath, out queryString, out httpMethod); - // Skip things that look wrong - if (relativePath.StartsWith("https://")) - continue; + // Skip things that look wrong + if (relativePath.StartsWith("https://")) + continue; - // Create a node for the REST path if it doesn't exist - string knownPath; - if (!IsPathKnown(swaggerPathObject, relativePath, out knownPath)) - { - swaggerPathObject[relativePath] = new SortedDictionary(new HttpMethodComparer()); - knownPath = relativePath; - } - var restPathNode = swaggerPathObject[knownPath]; + // Create a node for the REST path if it doesn't exist + string knownPath; + if (!IsPathKnown(swaggerPathObject, relativePath, out knownPath)) + { + swaggerPathObject[relativePath] = new SortedDictionary(new HttpMethodComparer()); + knownPath = relativePath; + } + var restPathNode = swaggerPathObject[knownPath]; - // Add the HTTP Method to the restPathNode if we need to - httpMethod = httpMethod.ToLower(); - if (!restPathNode.ContainsKey(httpMethod)) - { - var swaggerMethod = method.ToSwaggerMethod(); - IEnumerable requiredScopes; - if (this.DefaultAuthScope != null) + // Add the HTTP Method to the restPathNode if we need to + httpMethod = httpMethod.ToLower(); + if (!restPathNode.ContainsKey(httpMethod)) { - requiredScopes = new string[] { this.DefaultAuthScope }; + var swaggerMethod = method.ToSwaggerMethod(); + IEnumerable requiredScopes; + if (this.DefaultAuthScope != null) + { + requiredScopes = new string[] { this.DefaultAuthScope }; + } + else + { + requiredScopes = from r in this.Documents.AuthScopes where r.Required == true select r.Scope; + } + + swaggerMethod.AddRequiredSecurityRoles(this.AuthenticationParameters.ProviderName, requiredScopes); + restPathNode.Add(httpMethod, swaggerMethod); } else { - requiredScopes = from r in this.Documents.AuthScopes where r.Required == true select r.Scope; - } + Debug.WriteLine("Couldn't save repeated method {0} for path {1} (from {2}). Copying query parameters.", httpMethod, relativePath, method.SourceFile.DisplayName); + var existing = restPathNode[httpMethod]; - swaggerMethod.AddRequiredSecurityRoles(this.AuthenticationParameters.ProviderName, requiredScopes); - restPathNode.Add(httpMethod, swaggerMethod); + // Make sure any query string parameters on this method are included in the existing definition + var missingQueryParameters = method.MissingRequestParameters(true); + existing.Parameters.AddRange(from qp in missingQueryParameters select qp.ToSwaggerParameter()); + } } - else + catch (Exception ex) { - Debug.WriteLine("Couldn't save repeated method {0} for path {1} (from {2}). Copying query parameters.", httpMethod, relativePath, method.SourceFile.DisplayName); - var existing = restPathNode[httpMethod]; - - // Make sure any query string parameters on this method are included in the existing definition - var missingQueryParameters = method.MissingRequestParameters(true); - existing.Parameters.AddRange(from qp in missingQueryParameters select qp.ToSwaggerParameter()); + Console.WriteLine($"FAILED GeneratePathsFromDocSet {method} with {ex}\r\n\r\n"); } } return swaggerPathObject; diff --git a/ApiDocs.Publishing/packages.config b/ApiDoctor.Publishing/packages.config similarity index 100% rename from ApiDocs.Publishing/packages.config rename to ApiDoctor.Publishing/packages.config diff --git a/ApiDocs.Validation.UnitTests/ApiDocs.Validation.UnitTests.csproj b/ApiDoctor.Validation.UnitTests/ApiDoctor.Validation.UnitTests.csproj similarity index 95% rename from ApiDocs.Validation.UnitTests/ApiDocs.Validation.UnitTests.csproj rename to ApiDoctor.Validation.UnitTests/ApiDoctor.Validation.UnitTests.csproj index 1bf3de74..de181872 100644 --- a/ApiDocs.Validation.UnitTests/ApiDocs.Validation.UnitTests.csproj +++ b/ApiDoctor.Validation.UnitTests/ApiDoctor.Validation.UnitTests.csproj @@ -7,8 +7,8 @@ {EE3453F1-FD69-406C-9BD7-0643D6E999F3} Library Properties - ApiDocs.Validation.UnitTests - ApiDocs.Validation.UnitTests + ApiDoctor.Validation.UnitTests + ApiDoctor.Validation.UnitTests v4.5 512 ..\ @@ -73,9 +73,9 @@ - + {33B10320-3802-49CF-8965-3510AE66D5EC} - ApiDocs.Validation + ApiDoctor.Validation {1569ed47-c7c9-4261-b6f4-7445bd0f2c95} diff --git a/ApiDocs.Validation.UnitTests/BrokenLinkTests.cs b/ApiDoctor.Validation.UnitTests/BrokenLinkTests.cs similarity index 70% rename from ApiDocs.Validation.UnitTests/BrokenLinkTests.cs rename to ApiDoctor.Validation.UnitTests/BrokenLinkTests.cs index 6176fb5b..3adb0f98 100644 --- a/ApiDocs.Validation.UnitTests/BrokenLinkTests.cs +++ b/ApiDoctor.Validation.UnitTests/BrokenLinkTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System; using System.IO; using System.Linq; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; using NUnit.Framework; [TestFixture] @@ -47,13 +47,13 @@ [Up one level](../anotherfile.md) [microsoft]: http://www.microsoft.com "; TestableDocFile file = new TestableDocFile(markdown); - ValidationError[] errors; + var issues = new IssueLogger(); - Assert.IsTrue(file.Scan(string.Empty, out errors)); - Assert.IsEmpty(errors.WarningsOrErrorsOnly()); + Assert.IsTrue(file.Scan(string.Empty, issues)); + Assert.IsEmpty(issues.Issues.WarningsOrErrorsOnly()); - Assert.IsTrue(file.ValidateNoBrokenLinks(false, out errors, true)); - Assert.IsEmpty(errors.WarningsOrErrorsOnly()); + Assert.IsTrue(file.ValidateNoBrokenLinks(false, issues, true)); + Assert.IsEmpty(issues.Issues.WarningsOrErrorsOnly()); } @@ -73,14 +73,14 @@ This link goes [up one level](../anotherfile.md) "; TestableDocFile file = new TestableDocFile(markdown); - ValidationError[] errors; + var issues = new IssueLogger(); - Assert.IsTrue(file.Scan(string.Empty, out errors)); - var realErrors = from e in errors where e.IsWarning || e.IsError select e; + Assert.IsTrue(file.Scan(string.Empty, issues)); + var realErrors = from e in issues.Issues where e.IsWarning || e.IsError select e; Assert.IsEmpty(realErrors); - Assert.IsFalse(file.ValidateNoBrokenLinks(false, out errors, false)); - realErrors = from e in errors where e.IsWarning || e.IsError select e; + Assert.IsFalse(file.ValidateNoBrokenLinks(false, issues, false)); + realErrors = from e in issues.Issues where e.IsWarning || e.IsError select e; Assert.AreEqual(1, realErrors.Count()); Assert.IsTrue(realErrors.First().Code == ValidationErrorCode.MissingLinkSourceId); } @@ -113,14 +113,14 @@ This link goes [up one level](../anotherfile.md) }) }; - ValidationError[] errors; + var issues = new IssueLogger(); - Assert.IsTrue(file.Scan(string.Empty, out errors)); - Assert.IsEmpty(errors.WarningsOrErrorsOnly()); + Assert.IsTrue(file.Scan(string.Empty, issues)); + Assert.IsEmpty(issues.Issues.WarningsOrErrorsOnly()); - Assert.IsFalse(file.ValidateNoBrokenLinks(false, out errors, false)); - Assert.AreEqual(1, errors.WarningsOrErrorsOnly().Count()); - Assert.IsTrue(errors.First().Code == ValidationErrorCode.LinkDestinationNotFound); + Assert.IsFalse(file.ValidateNoBrokenLinks(false, issues, false)); + Assert.AreEqual(1, issues.Issues.WarningsOrErrorsOnly().Count()); + Assert.IsTrue(issues.Issues.Any(i => i.Code == ValidationErrorCode.LinkDestinationNotFound)); } [Test] @@ -140,15 +140,15 @@ This link goes [up one level](../anotherfile.md) TestableDocFile file = new TestableDocFile(markdown); file.IsLinkValid = link => link != "../anotherfile.md"; - ValidationError[] errors; + var issues = new IssueLogger(); - Assert.IsTrue(file.Scan(string.Empty, out errors)); - Assert.IsEmpty(errors.WarningsOrErrorsOnly()); + Assert.IsTrue(file.Scan(string.Empty, issues)); + Assert.IsEmpty(issues.Issues.WarningsOrErrorsOnly()); - Assert.IsFalse(file.ValidateNoBrokenLinks(false, out errors, false)); - Assert.AreEqual(2, errors.WarningsOrErrorsOnly().Count()); - Assert.IsTrue(errors[0].Code == ValidationErrorCode.MissingLinkSourceId); - Assert.IsTrue(errors[1].Code == ValidationErrorCode.LinkDestinationNotFound); + Assert.IsFalse(file.ValidateNoBrokenLinks(false, issues, false)); + Assert.AreEqual(2, issues.Issues.WarningsOrErrorsOnly().Count()); + Assert.IsTrue(issues.Issues.Any(i => i.Code == ValidationErrorCode.MissingLinkSourceId)); + Assert.IsTrue(issues.Issues.Any(i => i.Code == ValidationErrorCode.LinkDestinationNotFound)); } } @@ -164,11 +164,11 @@ public TestableDocFile(string markdownContent) this.DisplayName = "testable_doc_file"; } - public override bool Scan(string tags, out ValidationError[] errors) + public override bool Scan(string tags, IssueLogger issues) { this.HasScanRun = true; this.TransformMarkdownIntoBlocksAndLinks(this.Markdown, tags); - return this.ParseMarkdownBlocks(out errors); + return this.ParseMarkdownBlocks(issues); } public Func IsLinkValid { get; set; } diff --git a/ApiDocs.Validation.UnitTests/DocFileForTesting.cs b/ApiDoctor.Validation.UnitTests/DocFileForTesting.cs similarity index 96% rename from ApiDocs.Validation.UnitTests/DocFileForTesting.cs rename to ApiDoctor.Validation.UnitTests/DocFileForTesting.cs index 8761d08c..e0836a7a 100644 --- a/ApiDocs.Validation.UnitTests/DocFileForTesting.cs +++ b/ApiDoctor.Validation.UnitTests/DocFileForTesting.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { public class DocFileForTesting : DocFile { diff --git a/ApiDocs.Validation.UnitTests/ExtensionMethods.cs b/ApiDoctor.Validation.UnitTests/ExtensionMethods.cs similarity index 94% rename from ApiDocs.Validation.UnitTests/ExtensionMethods.cs rename to ApiDoctor.Validation.UnitTests/ExtensionMethods.cs index 500cd325..c374864d 100644 --- a/ApiDocs.Validation.UnitTests/ExtensionMethods.cs +++ b/ApiDoctor.Validation.UnitTests/ExtensionMethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,11 +23,11 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; static class ExtensionMethods { diff --git a/ApiDocs.Validation.UnitTests/JsonPathTest.cs b/ApiDoctor.Validation.UnitTests/JsonPathTest.cs similarity index 98% rename from ApiDocs.Validation.UnitTests/JsonPathTest.cs rename to ApiDoctor.Validation.UnitTests/JsonPathTest.cs index dd685de5..dbab2077 100644 --- a/ApiDocs.Validation.UnitTests/JsonPathTest.cs +++ b/ApiDoctor.Validation.UnitTests/JsonPathTest.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System.Collections.Generic; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation.Json; using Newtonsoft.Json; using NUnit.Framework; diff --git a/ApiDocs.Validation.UnitTests/JsonRewriteTests.cs b/ApiDoctor.Validation.UnitTests/JsonRewriteTests.cs similarity index 94% rename from ApiDocs.Validation.UnitTests/JsonRewriteTests.cs rename to ApiDoctor.Validation.UnitTests/JsonRewriteTests.cs index 9cfa950c..088f38c6 100644 --- a/ApiDocs.Validation.UnitTests/JsonRewriteTests.cs +++ b/ApiDoctor.Validation.UnitTests/JsonRewriteTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -30,10 +30,10 @@ using System.Threading.Tasks; using Newtonsoft.Json; using NUnit.Framework; -using ApiDocs.Validation.Json; +using ApiDoctor.Validation.Json; using System.Collections.Specialized; -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { [TestFixture] public class JsonRewriteTests @@ -45,7 +45,7 @@ public void BasicRewrite() var map = new Dictionary(); map.Add("@microsoft.graph.downloadUrl", "@oneDrive.downloadUrl"); map.Add("@microsoft.graph.", "@oneDrive."); - var output = JsonRewriter.RewriteJsonProperties(json, map); + var output = JsonRewriter.RewriteJsonProperties(json, map, new Error.IssueLogger()); } } } diff --git a/ApiDocs.Validation.UnitTests/MultipartMimeTests.cs b/ApiDoctor.Validation.UnitTests/MultipartMimeTests.cs similarity index 97% rename from ApiDocs.Validation.UnitTests/MultipartMimeTests.cs rename to ApiDoctor.Validation.UnitTests/MultipartMimeTests.cs index 6b0cc793..59b91d60 100644 --- a/ApiDocs.Validation.UnitTests/MultipartMimeTests.cs +++ b/ApiDoctor.Validation.UnitTests/MultipartMimeTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using NUnit.Framework; using MultipartMime; diff --git a/ApiDocs.Validation.UnitTests/NullableTests.cs b/ApiDoctor.Validation.UnitTests/NullableTests.cs similarity index 84% rename from ApiDocs.Validation.UnitTests/NullableTests.cs rename to ApiDoctor.Validation.UnitTests/NullableTests.cs index a00a90a7..a758b2d6 100644 --- a/ApiDocs.Validation.UnitTests/NullableTests.cs +++ b/ApiDoctor.Validation.UnitTests/NullableTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Json; using Newtonsoft.Json; using NUnit.Framework; @@ -49,10 +49,10 @@ public void NullPropertyShouldBeOk() string json = JsonConvert.SerializeObject(exampleObj); var testExample = new JsonExample(json); - ValidationError[] errors; + var issues = new IssueLogger(); - Assert.IsTrue(nullableSchema.ValidateJson(testExample, out errors, new Dictionary(), new ValidationOptions())); - Assert.AreEqual(0, errors.Length); + Assert.IsTrue(nullableSchema.ValidateJson(testExample, issues, new Dictionary(), new ValidationOptions())); + Assert.AreEqual(0, issues.Issues.Count()); } [Test] @@ -69,12 +69,12 @@ public void NullPropertyShouldGenerateWarning() string json = JsonConvert.SerializeObject(exampleObj); var testExample = new JsonExample(json); - ValidationError[] errors; + var issues = new IssueLogger(); - Assert.IsFalse(nullableSchema.ValidateJson(testExample, out errors, new Dictionary(), new ValidationOptions())); - Assert.AreEqual(1, errors.Length); + Assert.IsFalse(nullableSchema.ValidateJson(testExample, issues, new Dictionary(), new ValidationOptions())); + Assert.AreEqual(1, issues.Issues.Count()); - var error = errors.First(); + var error = issues.Issues.First(); Assert.AreEqual(ValidationErrorCode.NullPropertyValue, error.Code); } diff --git a/ApiDocs.Validation.UnitTests/ObjectGraphMergerTests.cs b/ApiDoctor.Validation.UnitTests/ObjectGraphMergerTests.cs similarity index 97% rename from ApiDocs.Validation.UnitTests/ObjectGraphMergerTests.cs rename to ApiDoctor.Validation.UnitTests/ObjectGraphMergerTests.cs index d2a3e8d6..8f5e7b1c 100644 --- a/ApiDocs.Validation.UnitTests/ObjectGraphMergerTests.cs +++ b/ApiDoctor.Validation.UnitTests/ObjectGraphMergerTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System.Collections.Generic; - using ApiDocs.Validation.Utility; + using ApiDoctor.Validation.Utility; using Newtonsoft.Json; using NUnit.Framework; diff --git a/ApiDocs.Validation.UnitTests/Properties/AssemblyInfo.cs b/ApiDoctor.Validation.UnitTests/Properties/AssemblyInfo.cs similarity index 91% rename from ApiDocs.Validation.UnitTests/Properties/AssemblyInfo.cs rename to ApiDoctor.Validation.UnitTests/Properties/AssemblyInfo.cs index 72a04c75..59d208bd 100644 --- a/ApiDocs.Validation.UnitTests/Properties/AssemblyInfo.cs +++ b/ApiDoctor.Validation.UnitTests/Properties/AssemblyInfo.cs @@ -4,11 +4,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("ApiDocs.Validation.UnitTests")] +[assembly: AssemblyTitle("ApiDoctor.Validation.UnitTests")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ApiDocs.Validation.UnitTests")] +[assembly: AssemblyProduct("ApiDoctor.Validation.UnitTests")] [assembly: AssemblyCopyright("Copyright © 2014")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/ApiDocs.Validation.UnitTests/Properties/Resources.Designer.cs b/ApiDoctor.Validation.UnitTests/Properties/Resources.Designer.cs similarity index 98% rename from ApiDocs.Validation.UnitTests/Properties/Resources.Designer.cs rename to ApiDoctor.Validation.UnitTests/Properties/Resources.Designer.cs index de5f35c7..b9e1d530 100644 --- a/ApiDocs.Validation.UnitTests/Properties/Resources.Designer.cs +++ b/ApiDoctor.Validation.UnitTests/Properties/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace ApiDocs.Validation.UnitTests.Properties { +namespace ApiDoctor.Validation.UnitTests.Properties { using System.CodeDom.Compiler; using System.ComponentModel; using System.Diagnostics; @@ -44,7 +44,7 @@ internal Resources() { internal static ResourceManager ResourceManager { get { if (ReferenceEquals(resourceMan, null)) { - ResourceManager temp = new ResourceManager("ApiDocs.Validation.UnitTests.Properties.Resources", typeof(Resources).Assembly); + ResourceManager temp = new ResourceManager("ApiDoctor.Validation.UnitTests.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/ApiDocs.Validation.UnitTests/Properties/Resources.resx b/ApiDoctor.Validation.UnitTests/Properties/Resources.resx similarity index 100% rename from ApiDocs.Validation.UnitTests/Properties/Resources.resx rename to ApiDoctor.Validation.UnitTests/Properties/Resources.resx diff --git a/ApiDocs.Validation.UnitTests/ResourceStringValidationTests.cs b/ApiDoctor.Validation.UnitTests/ResourceStringValidationTests.cs similarity index 75% rename from ApiDocs.Validation.UnitTests/ResourceStringValidationTests.cs rename to ApiDoctor.Validation.UnitTests/ResourceStringValidationTests.cs index f8981deb..1703559e 100644 --- a/ApiDocs.Validation.UnitTests/ResourceStringValidationTests.cs +++ b/ApiDoctor.Validation.UnitTests/ResourceStringValidationTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Json; - using ApiDocs.Validation.UnitTests.Properties; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Json; + using ApiDoctor.Validation.UnitTests.Properties; using NUnit.Framework; [TestFixture] @@ -40,10 +40,10 @@ static DocFile GetDocFile() DocSet docSet = new DocSet(); DocFile testFile = new DocFileForTesting(Resources.ExampleResources, "resources.md", "resources.md", docSet); - ValidationError[] detectedErrors; - testFile.Scan(string.Empty, out detectedErrors); + var issues = new IssueLogger(); + testFile.Scan(string.Empty, issues); - Assert.IsFalse(detectedErrors.WereWarningsOrErrors(), "Detected warnings or errors when reading the example markdown file."); + Assert.IsFalse(issues.Issues.WereWarningsOrErrors(), "Detected warnings or errors when reading the example markdown file."); return testFile; } @@ -62,10 +62,10 @@ public void ResourceStringValidationValidExampleTest() var method = testFile.Requests.Single(x => x.Identifier == "valid-response"); - ValidationError[] detectedErrors; - bool result = schema.ValidateExpectedResponse(method, out detectedErrors); + var issues = new IssueLogger(); + bool result = schema.ValidateExpectedResponse(method, issues); Assert.IsTrue(result); - Assert.IsEmpty(detectedErrors, "Validation errors were detected"); + Assert.IsEmpty(issues.Issues.Where(i => i.IsWarningOrError), "Validation errors were detected"); } /// @@ -81,13 +81,13 @@ public void ResourceStringValidationBadTimestamp() var method = testFile.Requests.Single(x => x.Identifier == "bad-timestamp"); - ValidationError[] detectedErrors; - bool result = schema.ValidateExpectedResponse(method, out detectedErrors); + var issues = new IssueLogger(); + bool result = schema.ValidateExpectedResponse(method, issues); Assert.IsFalse(result); - Assert.IsTrue(detectedErrors.WereErrors()); + Assert.IsTrue(issues.Issues.WereErrors()); - Assert.IsNotNull(detectedErrors.SingleOrDefault(x => x.Code == ValidationErrorCode.ExpectedTypeDifferent)); + Assert.IsNotNull(issues.Issues.SingleOrDefault(x => x.Code == ValidationErrorCode.ExpectedTypeDifferent)); } /// @@ -103,13 +103,13 @@ public void ResourceStringValidationBadEnumValue() var method = testFile.Requests.Single(x => x.Identifier == "bad-enum-value"); - ValidationError[] detectedErrors; - bool result = schema.ValidateExpectedResponse(method, out detectedErrors); + var issues = new IssueLogger(); + bool result = schema.ValidateExpectedResponse(method, issues); Assert.IsFalse(result); - Assert.IsTrue(detectedErrors.WereErrors()); + Assert.IsTrue(issues.Issues.WereErrors()); - Assert.IsNotNull(detectedErrors.SingleOrDefault(x => x.Code == ValidationErrorCode.InvalidEnumeratedValueString)); + Assert.IsNotNull(issues.Issues.SingleOrDefault(x => x.Code == ValidationErrorCode.InvalidEnumeratedValueString)); } /// @@ -125,13 +125,13 @@ public void ResourceStringValidationBadUrlValue() var method = testFile.Requests.Single(x => x.Identifier == "bad-url"); - ValidationError[] detectedErrors; - bool result = schema.ValidateExpectedResponse(method, out detectedErrors); + var issues = new IssueLogger(); + bool result = schema.ValidateExpectedResponse(method, issues); Assert.IsFalse(result); - Assert.IsTrue(detectedErrors.WereErrors()); + Assert.IsTrue(issues.Issues.WereErrors()); - Assert.IsNotNull(detectedErrors.SingleOrDefault(x => x.Code == ValidationErrorCode.InvalidUrlString)); + Assert.IsNotNull(issues.Issues.SingleOrDefault(x => x.Code == ValidationErrorCode.InvalidUrlString)); } } diff --git a/ApiDocs.Validation.UnitTests/SchemaBuilderTests.cs b/ApiDoctor.Validation.UnitTests/SchemaBuilderTests.cs similarity index 98% rename from ApiDocs.Validation.UnitTests/SchemaBuilderTests.cs rename to ApiDoctor.Validation.UnitTests/SchemaBuilderTests.cs index 5213c6b7..7b50fb10 100644 --- a/ApiDocs.Validation.UnitTests/SchemaBuilderTests.cs +++ b/ApiDoctor.Validation.UnitTests/SchemaBuilderTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation.Json; using Newtonsoft.Json; using NUnit.Framework; using System.Linq; diff --git a/ApiDocs.Validation.UnitTests/SchemaValidatorTests.cs b/ApiDoctor.Validation.UnitTests/SchemaValidatorTests.cs similarity index 76% rename from ApiDocs.Validation.UnitTests/SchemaValidatorTests.cs rename to ApiDoctor.Validation.UnitTests/SchemaValidatorTests.cs index a749b341..14014608 100644 --- a/ApiDocs.Validation.UnitTests/SchemaValidatorTests.cs +++ b/ApiDoctor.Validation.UnitTests/SchemaValidatorTests.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,14 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.UnitTests +namespace ApiDoctor.Validation.UnitTests { using System; using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Http; - using ApiDocs.Validation.Json; - using ApiDocs.Validation.UnitTests.Properties; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Http; + using ApiDoctor.Validation.Json; + using ApiDoctor.Validation.UnitTests.Properties; using Newtonsoft.Json; using NUnit.Framework; @@ -57,9 +57,9 @@ public void SimpleTypeValidationValid() string json = JsonConvert.SerializeObject(newObj, Settings); - ValidationError[] errors; - Assert.IsTrue(schema.ValidateJson(new JsonExample(json), out errors, null, null)); - Assert.AreEqual(0, errors.Length); + var issues = new IssueLogger(); + Assert.IsTrue(schema.ValidateJson(new JsonExample(json), issues, null, null)); + Assert.AreEqual(0, issues.Issues.Count()); } [Test] @@ -68,7 +68,7 @@ public void SimpleTypeValidationWrongPropertyType() var schema = SchemaBuilderTests.SimpleSchemaExample(); // There are two errors here, the first is that stringProp is a number, and the - // the second is that numberProp is a string. MDS needs to catch both. + // the second is that numberProp is a string. API Doctor needs to catch both. var newObj = new { stringProp = 1231234, @@ -80,10 +80,10 @@ public void SimpleTypeValidationWrongPropertyType() string json = JsonConvert.SerializeObject(newObj, Settings); - ValidationError[] errors; - Assert.IsFalse(schema.ValidateJson(new JsonExample(json), out errors, null, null)); - Assert.IsTrue(errors.All(error => error.Code == ValidationErrorCode.ExpectedTypeDifferent)); - Assert.AreEqual(2, errors.Length, "Expected 2 errors in the response"); + var issues = new IssueLogger(); + Assert.IsFalse(schema.ValidateJson(new JsonExample(json), issues, null, null)); + Assert.IsTrue(issues.Issues.All(error => error.Code == ValidationErrorCode.ExpectedTypeDifferent)); + Assert.AreEqual(2, issues.Issues.Count(), "Expected 2 errors in the response"); } [Test] @@ -103,10 +103,10 @@ public void SimpleTypeValidationExtraProperty() string json = JsonConvert.SerializeObject(newObj, Settings); - ValidationError[] errors; - Assert.IsFalse(schema.ValidateJson(new JsonExample(json), out errors, null, null)); - Assert.AreEqual(1, errors.Length); - Assert.AreEqual(ValidationErrorCode.AdditionalPropertyDetected, errors.First().Code); + var issues = new IssueLogger(); + Assert.IsFalse(schema.ValidateJson(new JsonExample(json), issues, null, null)); + Assert.AreEqual(1, issues.Issues.Count()); + Assert.AreEqual(ValidationErrorCode.AdditionalPropertyDetected, issues.Issues.First().Code); } [Test] @@ -124,10 +124,10 @@ public void SimpleTypeValidationMissingProperty() string json = JsonConvert.SerializeObject(newObj, Settings); - ValidationError[] errors; - Assert.IsFalse(schema.ValidateJson(new JsonExample(json), out errors, null, null)); - Assert.AreEqual(1, errors.Length); - Assert.AreEqual(ValidationErrorCode.RequiredPropertiesMissing, errors.First().Code); + var issues = new IssueLogger(); + Assert.IsFalse(schema.ValidateJson(new JsonExample(json), issues, null, null)); + Assert.AreEqual(1, issues.Issues.Count()); + Assert.AreEqual(ValidationErrorCode.RequiredPropertiesMissing, issues.Issues.First().Code); } [Test] @@ -146,10 +146,10 @@ public void SimpleTypeValidationWrongTypeArray() string json = JsonConvert.SerializeObject(newObj, Settings); - ValidationError[] errors; - Assert.IsFalse(schema.ValidateJson(new JsonExample(json), out errors, null, null)); - Assert.AreEqual(1, errors.Length); - Assert.AreEqual(ValidationErrorCode.ExpectedNonArrayValue, errors.First().Code); + var issues = new IssueLogger(); + Assert.IsFalse(schema.ValidateJson(new JsonExample(json), issues, null, null)); + Assert.AreEqual(1, issues.Issues.Count()); + Assert.AreEqual(ValidationErrorCode.ExpectedNonArrayValue, issues.Issues.First().Code); } [Test] @@ -158,10 +158,10 @@ public void TruncatedExampleWithRequiredPropertiesTest() DocSet docSet = new DocSet(); DocFile testFile = new DocFileForTesting(Resources.ExampleValidateResponse, "test.md", "test.md", docSet); - ValidationError[] detectedErrors; - testFile.Scan(string.Empty, out detectedErrors); + var issues = new IssueLogger(); + testFile.Scan(string.Empty, issues); - Assert.IsEmpty(detectedErrors.Where(x => x.IsError)); + Assert.IsEmpty(issues.Issues.Where(x => x.IsError)); docSet.ResourceCollection.RegisterJsonResource(testFile.Resources.First()); @@ -171,10 +171,10 @@ public void TruncatedExampleWithRequiredPropertiesTest() var expectedResponse = parser.ParseHttpResponse(testMethod.ExpectedResponse); var actualResponse = parser.ParseHttpResponse(testMethod.ActualResponse); - testMethod.ValidateResponse(actualResponse, expectedResponse, null, out detectedErrors); + testMethod.ValidateResponse(actualResponse, expectedResponse, null, issues); - Assert.AreEqual(1, detectedErrors.Length); - var error = detectedErrors.First(); + Assert.AreEqual(1, issues.Errors.Count()); + var error = issues.Errors.First(); Assert.AreEqual(ValidationErrorCode.RequiredPropertiesMissing, error.Code); } @@ -184,10 +184,10 @@ public void TruncatedExampleSelectStatementOnChildren() DocSet docSet = new DocSet(); DocFile testFile = new DocFileForTesting(Resources.ExampleValidationSelectStatement, "test.md", "test.md", docSet); - ValidationError[] detectedErrors; - testFile.Scan(string.Empty, out detectedErrors); + var issues = new IssueLogger(); + testFile.Scan(string.Empty, issues); - Assert.IsEmpty(detectedErrors.Where(x => x.IsError)); + Assert.IsEmpty(issues.Issues.Where(x => x.IsError)); docSet.ResourceCollection.RegisterJsonResource(testFile.Resources.First()); @@ -197,9 +197,9 @@ public void TruncatedExampleSelectStatementOnChildren() var expectedResponse = parser.ParseHttpResponse(testMethod.ExpectedResponse); var actualResponse = parser.ParseHttpResponse(testMethod.ActualResponse); - testMethod.ValidateResponse(actualResponse, expectedResponse, null, out detectedErrors); + testMethod.ValidateResponse(actualResponse, expectedResponse, null, issues); - Assert.AreEqual(0, detectedErrors.Length); + Assert.AreEqual(0, issues.Errors.Count()); } [Test] @@ -208,10 +208,10 @@ public void TruncatedExampleSelectStatementOnChildrenExpectFailure() DocSet docSet = new DocSet(); DocFile testFile = new DocFileForTesting(Resources.ExampleValidationSelectStatementFailure, "test.md", "test.md", docSet); - ValidationError[] detectedErrors; - testFile.Scan(string.Empty, out detectedErrors); + var issues = new IssueLogger(); + testFile.Scan(string.Empty, issues); - Assert.IsEmpty(detectedErrors.Where(x => x.IsError)); + Assert.IsEmpty(issues.Issues.Where(x => x.IsError)); docSet.ResourceCollection.RegisterJsonResource(testFile.Resources.First()); @@ -221,12 +221,11 @@ public void TruncatedExampleSelectStatementOnChildrenExpectFailure() var expectedResponse = parser.ParseHttpResponse(testMethod.ExpectedResponse); var actualResponse = parser.ParseHttpResponse(testMethod.ActualResponse); - testMethod.ValidateResponse(actualResponse, expectedResponse, null, out detectedErrors); + testMethod.ValidateResponse(actualResponse, expectedResponse, null, issues); - Assert.AreEqual(2, detectedErrors.Length); - Assert.IsTrue(detectedErrors.Any(x => x.Code == ValidationErrorCode.RequiredPropertiesMissing), "Error with Code = RequiredPropertiesMissing"); + Assert.IsTrue(issues.Issues.Any(x => x.Code == ValidationErrorCode.RequiredPropertiesMissing), "Error with Code = RequiredPropertiesMissing"); Assert.IsTrue( - detectedErrors.Any(x => x.Code == ValidationErrorCode.SkippedSimilarErrors), + issues.Issues.Any(x => x.Code == ValidationErrorCode.SkippedSimilarErrors), "Error with Code = SkippedSimilarErrors"); } diff --git a/ApiDocs.Validation.UnitTests/packages.config b/ApiDoctor.Validation.UnitTests/packages.config similarity index 100% rename from ApiDocs.Validation.UnitTests/packages.config rename to ApiDoctor.Validation.UnitTests/packages.config diff --git a/ApiDocs.Validation.UnitTests/test-docs/ExampleResources.md b/ApiDoctor.Validation.UnitTests/test-docs/ExampleResources.md similarity index 80% rename from ApiDocs.Validation.UnitTests/test-docs/ExampleResources.md rename to ApiDoctor.Validation.UnitTests/test-docs/ExampleResources.md index bae47b98..7b197f98 100644 --- a/ApiDocs.Validation.UnitTests/test-docs/ExampleResources.md +++ b/ApiDoctor.Validation.UnitTests/test-docs/ExampleResources.md @@ -12,6 +12,26 @@ } ``` +### Properties + +| Name | Type | Description +|:----------------|:----------|:-------------- +| year | int | the year +| downloadUrl | string | download url +| createdDateTime | timestamp | created date time +| season | season | season enum +| ownerName | string | name of the owner +| contentType | string | mimetype + +#### season values + +| Value +|:--------- +| summer +| fall +| winter +| spring + ## Example request/response that's completely valid diff --git a/ApiDocs.Validation.UnitTests/test-docs/ExampleValidateResponse.md b/ApiDoctor.Validation.UnitTests/test-docs/ExampleValidateResponse.md similarity index 100% rename from ApiDocs.Validation.UnitTests/test-docs/ExampleValidateResponse.md rename to ApiDoctor.Validation.UnitTests/test-docs/ExampleValidateResponse.md diff --git a/ApiDocs.Validation.UnitTests/test-docs/ExampleValidationSelectStatement.md b/ApiDoctor.Validation.UnitTests/test-docs/ExampleValidationSelectStatement.md similarity index 100% rename from ApiDocs.Validation.UnitTests/test-docs/ExampleValidationSelectStatement.md rename to ApiDoctor.Validation.UnitTests/test-docs/ExampleValidationSelectStatement.md diff --git a/ApiDocs.Validation.UnitTests/test-docs/ExampleValidationSelectStatementFailure.md b/ApiDoctor.Validation.UnitTests/test-docs/ExampleValidationSelectStatementFailure.md similarity index 100% rename from ApiDocs.Validation.UnitTests/test-docs/ExampleValidationSelectStatementFailure.md rename to ApiDoctor.Validation.UnitTests/test-docs/ExampleValidationSelectStatementFailure.md diff --git a/ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/ComplexExample.md b/ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/ComplexExample.md similarity index 100% rename from ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/ComplexExample.md rename to ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/ComplexExample.md diff --git a/ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/IndentedCodeBlock.md b/ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/IndentedCodeBlock.md similarity index 100% rename from ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/IndentedCodeBlock.md rename to ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/IndentedCodeBlock.md diff --git a/ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/NonIndentedExample.md b/ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/NonIndentedExample.md similarity index 100% rename from ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/NonIndentedExample.md rename to ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/NonIndentedExample.md diff --git a/ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/SimpleBuLists.md b/ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/SimpleBuLists.md similarity index 100% rename from ApiDocs.Validation.UnitTests/test-docs/IndentedCodeBlock/SimpleBuLists.md rename to ApiDoctor.Validation.UnitTests/test-docs/IndentedCodeBlock/SimpleBuLists.md diff --git a/ApiDocs.Validation/ApiDocs.Validation.csproj b/ApiDoctor.Validation/ApiDoctor.Validation.csproj similarity index 96% rename from ApiDocs.Validation/ApiDocs.Validation.csproj rename to ApiDoctor.Validation/ApiDoctor.Validation.csproj index 26f2d16b..d4b46371 100644 --- a/ApiDocs.Validation/ApiDocs.Validation.csproj +++ b/ApiDoctor.Validation/ApiDoctor.Validation.csproj @@ -7,8 +7,8 @@ {33B10320-3802-49CF-8965-3510AE66D5EC} Library Properties - ApiDocs.Validation - ApiDocs.Validation + ApiDoctor.Validation + ApiDoctor.Validation v4.5 512 ..\ @@ -55,6 +55,7 @@ + @@ -89,6 +90,7 @@ + @@ -102,8 +104,10 @@ True Resources.resx + + diff --git a/ApiDocs.Validation/ApiDocs.Validation.nuspec b/ApiDoctor.Validation/ApiDoctor.Validation.nuspec similarity index 69% rename from ApiDocs.Validation/ApiDocs.Validation.nuspec rename to ApiDoctor.Validation/ApiDoctor.Validation.nuspec index 85980c28..f6be51f7 100644 --- a/ApiDocs.Validation/ApiDocs.Validation.nuspec +++ b/ApiDoctor.Validation/ApiDoctor.Validation.nuspec @@ -1,14 +1,14 @@  - MarkdownScanner.Validation + ApiDoctor.Validation $version$ - Markdown scanner API documentation validation class library - Ryan Gregg - rgregg@microsoft.com + API Doctor API documentation validation class library + Microsoft + dspektor@microsoft.com false Toolkit to enable validation of markdown-based documentation. - Check http://github.com/onedrive/markdown-scanner for details. + Check http://github.com/onedrive/apidoctor for details. http://msdn.microsoft.com/en-US/cc300389 Copyright Microsoft @@ -21,7 +21,7 @@ - + diff --git a/ApiDocs.Validation/AuthScopeDefinition.cs b/ApiDoctor.Validation/AuthScopeDefinition.cs similarity index 96% rename from ApiDocs.Validation/AuthScopeDefinition.cs rename to ApiDoctor.Validation/AuthScopeDefinition.cs index c73e4be4..9c179996 100644 --- a/ApiDocs.Validation/AuthScopeDefinition.cs +++ b/ApiDoctor.Validation/AuthScopeDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { public class AuthScopeDefinition : ItemDefinition { diff --git a/ApiDocs.Validation/AuthenicationCredentials.cs b/ApiDoctor.Validation/AuthenicationCredentials.cs similarity index 97% rename from ApiDocs.Validation/AuthenicationCredentials.cs rename to ApiDoctor.Validation/AuthenicationCredentials.cs index 0dece281..eb3311ea 100644 --- a/ApiDocs.Validation/AuthenicationCredentials.cs +++ b/ApiDoctor.Validation/AuthenicationCredentials.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Net; @@ -111,7 +111,7 @@ public class FirstPartyCredentials : OAuthCredentials internal FirstPartyCredentials() { - this.FirstPartyApplicationHeaderValue = "MarkdownScannerTool"; + this.FirstPartyApplicationHeaderValue = "ApiDoctorTool"; } public override void AuthenticateRequest(HttpRequest request) diff --git a/ApiDocs.Validation/BackoffHelper.cs b/ApiDoctor.Validation/BackoffHelper.cs similarity index 98% rename from ApiDocs.Validation/BackoffHelper.cs rename to ApiDoctor.Validation/BackoffHelper.cs index 9078d8dc..db403d8b 100644 --- a/ApiDocs.Validation/BackoffHelper.cs +++ b/ApiDoctor.Validation/BackoffHelper.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Diagnostics; diff --git a/ApiDocs.Validation/CodeBlockAnnotation.cs b/ApiDoctor.Validation/CodeBlockAnnotation.cs similarity index 52% rename from ApiDocs.Validation/CodeBlockAnnotation.cs rename to ApiDoctor.Validation/CodeBlockAnnotation.cs index 76fb3f0d..1cbbacb2 100644 --- a/ApiDocs.Validation/CodeBlockAnnotation.cs +++ b/ApiDoctor.Validation/CodeBlockAnnotation.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Diagnostics; @@ -31,31 +31,189 @@ namespace ApiDocs.Validation using Newtonsoft.Json; using Newtonsoft.Json.Converters; using System.Collections.Generic; + using System.IO; + using System.Linq; + using ApiDoctor.Validation.Error; - public class CodeBlockAnnotation + public class FlexibleLowerCamelStringEnumConverter : StringEnumConverter { - /// - /// The OData type name of the resource - /// - [JsonProperty("@type", NullValueHandling=NullValueHandling.Ignore )] - public string ResourceType { get; set; } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var val = value?.ToString(); + if (val != null && char.IsUpper(val[0])) + { + val = char.ToLowerInvariant(val[0]) + val.Substring(1); + } - [JsonProperty("@odata.type", NullValueHandling = NullValueHandling.Ignore)] - public string LegacyResourceType + writer.WriteValue(val); + } + } + + public class ODataAnnotations + { + [JsonProperty("property", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Property { get; set; } + + [JsonProperty("capabilities", DefaultValueHandling = DefaultValueHandling.Ignore)] + public ODataCapabilities Capabilities { get; set; } + } + + public class ODataCapabilities + { + [JsonProperty("changeTracking", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? ChangeTracking; + + [JsonProperty("computed", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Computed; + + [JsonProperty("countable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Countable; + + [JsonProperty("deletable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Deletable; + + [JsonProperty("expandable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Expandable; + + [JsonProperty("filterable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Filterable; + + [JsonProperty("insertable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Insertable; + + [JsonProperty("navigability", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Navigability; + + [JsonProperty("permissions", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Permissions; + + [JsonProperty("referenceable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Referenceable; + + [JsonProperty("searchable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Searchable; + + [JsonProperty("sortable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Sortable; + + [JsonProperty("selectable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Selectable; + + [JsonProperty("skippable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Skippable; + + [JsonProperty("toppable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Toppable; + + [JsonProperty("updatable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Updatable; + + + public void MergeWith(ODataCapabilities other, IssueLogger issues) + { + if (!string.IsNullOrEmpty(other.Navigability)) + { + if (string.IsNullOrEmpty(this.Navigability)) + { + this.Navigability = other.Navigability; + } + + if (!string.Equals(this.Navigability, other.Navigability)) + { + issues.Warning(ValidationErrorCode.Unknown, "Mismatched 'navigability' attributes."); + } + } + + if (!string.IsNullOrEmpty(other.Permissions)) + { + if (string.IsNullOrEmpty(this.Permissions)) + { + this.Permissions = other.Permissions; + } + + if (!string.Equals(this.Permissions, other.Permissions)) + { + issues.Warning(ValidationErrorCode.Unknown, "Mismatched 'permissions' attributes."); + } + } + + MergeNullables(ref this.ChangeTracking, other.ChangeTracking); + MergeNullables(ref this.Computed, other.Computed); + MergeNullables(ref this.Countable, other.Countable); + MergeNullables(ref this.Deletable, other.Deletable); + MergeNullables(ref this.Expandable, other.Expandable); + MergeNullables(ref this.Filterable, other.Filterable); + MergeNullables(ref this.Insertable, other.Insertable); + MergeNullables(ref this.Referenceable, other.Referenceable); + MergeNullables(ref this.Searchable, other.Searchable); + MergeNullables(ref this.Selectable, other.Selectable); + MergeNullables(ref this.Skippable, other.Skippable); + MergeNullables(ref this.Sortable, other.Sortable); + MergeNullables(ref this.Toppable, other.Toppable); + MergeNullables(ref this.Updatable, other.Updatable); + } + + private void MergeNullables(ref bool? original, bool? other) { - get { return ResourceType; } - set { this.ResourceType = value; } + if (other.HasValue) + { + original = other.GetValueOrDefault() || original.GetValueOrDefault(); + } } + } - [JsonProperty("@type.aka", NullValueHandling=NullValueHandling.Ignore )] - public string ResourceTypeAka { get; set; } + public class CodeBlockAnnotation + { + private string originalRawJson { get; set; } + private DocFile sourceFile { get; set; } + [JsonIgnore] + public string ResourceType + { + get { return this.NonAnnotatedResourceType ?? this.OdataAnnotatedResourceType; } + set + { + if (this.OdataAnnotatedResourceType != null) + { + this.OdataAnnotatedResourceType = value; + } + else + { + this.NonAnnotatedResourceType = value; + } + } + } /// /// Type of code block /// - [JsonProperty("blockType", Order=-2), JsonConverter(typeof(StringEnumConverter))] + + [JsonProperty("blockType", Order=-2), JsonConverter(typeof(FlexibleLowerCamelStringEnumConverter))] public CodeBlockType BlockType { get; set; } + /// + /// Specify that the result is a collection of the resource type instead of a single instance. + /// + [JsonProperty("abstract", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Abstract { get; set; } + + [JsonProperty("baseType", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string BaseType { get; set; } + + /// + /// Indicates that a resource is extensible with additional properties that + /// may not be defined in the documtnation. + /// + [JsonProperty("openType")] + public bool IsOpenType { get; set; } + + /// + /// Indicates that the URL in the example should not be used to infer API shape. + /// The URL is completely opaque and should be used as-returned from a previous API call. + /// This tool will avoid assuming the structure of the URL contributes to the resource model. + /// + [JsonProperty("opaqueUrl")] + public bool OpaqueUrl { get; set; } + /// /// Specify the name of properties in the schema which are optional /// @@ -80,6 +238,19 @@ public string LegacyResourceType [JsonProperty("isEmpty", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool IsEmpty { get; set; } + /// + /// Indicates that the function is composable. + /// Only required if there are no examples in the documentation of the function being composed. + /// + [JsonProperty("isComposable", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool IsComposable { get; set; } + + /// + /// Indicates that the resource is an odata media entity. Sets the HasStream attribute to true in the edmx. + /// + [JsonProperty("isMediaEntity", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool IsMediaEntity { get; set; } + /// /// Specifies that the example is truncated and should not generate warnings about /// missing fields unless those fields are shown in the example. @@ -108,6 +279,13 @@ public string LegacyResourceType [JsonProperty("nullableProperties", DefaultValueHandling = DefaultValueHandling.Ignore)] public string[] NullableProperties { get; set; } + /// + /// Examples typically include variables like {item-id} instead of example keys. + /// In cases where an example is needed, list the example key in this array. + /// + [JsonProperty("sampleKeys", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string[] SampleKeys { get; set; } + /// /// When provided indicates that the response is a long running operation that will return an /// asyncJobStatus response from a Location URL. When the job is complete, a Location URL will @@ -146,11 +324,51 @@ public string LegacyResourceType /// Space separated list of tags that represent capabilities required for this method to be invoked. /// [JsonProperty("tags", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Tags { get; set; } + public string Tags + { + get + { + if (this.TagList != null && this.TagList.Count > 0) + { + return string.Join(" ", this.TagList); + } - [JsonProperty("baseType", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string BaseType { get; set; } + return null; + } + set + { + if (string.IsNullOrEmpty(value)) + { + this.TagList = null; + } + + this.TagList = value. + Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries). + Select(s => s.Trim()). + ToList(); + } + } + + [JsonIgnore] + public List TagList { get; set; } + + /// + /// The OData type name of the resource + /// + [JsonProperty("@type", NullValueHandling = NullValueHandling.Ignore)] + public string NonAnnotatedResourceType { get; set; } + + [JsonProperty("@type.aka", NullValueHandling = NullValueHandling.Ignore)] + public string ResourceTypeAka { get; set; } + + [JsonProperty("@odata.type", NullValueHandling = NullValueHandling.Ignore)] + public string OdataAnnotatedResourceType { get; set; } + + [JsonProperty("@odata.annotations", NullValueHandling = NullValueHandling.Ignore)] + public ODataAnnotations[] OdataAnnotations { get; set; } + + [JsonIgnore] public string[] RequiredScopes { get @@ -162,6 +380,7 @@ public string[] RequiredScopes } } + [JsonIgnore] public string[] RequiredApiVersions { get @@ -173,6 +392,7 @@ public string[] RequiredApiVersions } } + [JsonIgnore] public string[] RequiredTags { get @@ -184,15 +404,38 @@ public string[] RequiredTags } } + public void PatchSourceFile() + { + if (this.sourceFile == null) + { + throw new InvalidOperationException("can only patch a resource that was read from a file"); + } + + var newJson = JsonConvert.SerializeObject(this, serializerSettings); + var originalFile = File.ReadAllText(this.sourceFile.FullPath); + var modified = originalFile.Replace(this.originalRawJson, newJson); + File.WriteAllText(this.sourceFile.FullPath, modified); + } + + private static JsonSerializerSettings serializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + Formatting = Formatting.Indented, + }; + /// /// Convert a JSON string into an instance of this class /// /// /// - public static CodeBlockAnnotation ParseMetadata(string json, MarkdownDeep.Block codeBlock = null) + public static CodeBlockAnnotation ParseMetadata(string json, MarkdownDeep.Block codeBlock = null, DocFile sourceFile = null) { var response = JsonConvert.DeserializeObject(json); + response.originalRawJson = json; + response.sourceFile = sourceFile; + // Check for Collection() syntax if (!string.IsNullOrEmpty(response.ResourceType)) { @@ -218,6 +461,11 @@ public static CodeBlockAnnotation ParseMetadata(string json, MarkdownDeep.Block return response; } + private static HashSet httpVerbs = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "GET", "POST", "HEAD", "DELETE", "PATCH", "PUT", "OPTIONS", + }; + /// /// Based on data in the codeBlock, see if we can infer what kind of block this is /// @@ -242,6 +490,17 @@ private static CodeBlockType InferBlockType(Block codeBlock, string resourceType return CodeBlockType.Response; } catch { } + + try + { + var lines = codeBlock.Content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + if (lines?.Length > 1 && + lines.All(line => httpVerbs.Contains(line.Substring(0, line.IndexOf(' ') + 1).Trim()))) + { + return CodeBlockType.Samples; + } + } + catch { } } else if (codeBlock.CodeLanguage == "json" && !string.IsNullOrEmpty(resourceTypeName)) { @@ -251,18 +510,16 @@ private static CodeBlockType InferBlockType(Block codeBlock, string resourceType return CodeBlockType.Unknown; } + [JsonIgnore] public ParameterDataType Type { - get { return new ParameterDataType(this.ResourceType, this.IsCollection); } + get + { + return this.ResourceType == null + ? null + : this.ResourceType.ParseParameterDataType(isCollection: this.IsCollection); } } - /// - /// Indicates that a resource is extensible with additional properties that - /// may not be defined in the documtnation. - /// - [JsonProperty("openType")] - public bool IsOpenType { get; set; } - [JsonProperty("target")] public TargetType Target { get; set; } } @@ -308,6 +565,11 @@ public enum CodeBlockType /// Example, + /// + /// Samples code block. Should be used to infer alternate ways of calling an API/function. + /// + Samples, + /// /// A simulated response, used for unit testing. /// diff --git a/ApiDocs.Validation/Config/ApiRequirementsFile.cs b/ApiDoctor.Validation/Config/ApiRequirementsFile.cs similarity index 98% rename from ApiDocs.Validation/Config/ApiRequirementsFile.cs rename to ApiDoctor.Validation/Config/ApiRequirementsFile.cs index e834b200..e9049289 100644 --- a/ApiDocs.Validation/Config/ApiRequirementsFile.cs +++ b/ApiDoctor.Validation/Config/ApiRequirementsFile.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Config +namespace ApiDoctor.Validation.Config { using Newtonsoft.Json; diff --git a/ApiDocs.Validation/Config/ConfigFile.cs b/ApiDoctor.Validation/Config/ConfigFile.cs similarity index 90% rename from ApiDocs.Validation/Config/ConfigFile.cs rename to ApiDoctor.Validation/Config/ConfigFile.cs index 7cdf0cbd..7d7880c9 100644 --- a/ApiDocs.Validation/Config/ConfigFile.cs +++ b/ApiDoctor.Validation/Config/ConfigFile.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,17 +23,18 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Config +namespace ApiDoctor.Validation.Config { public abstract class ConfigFile { public abstract bool IsValid { get; } - public string SourcePath {get;set;} + public string SourcePath { get; set; } /// /// Provide oppertunity to post-process a valid configuration after the file is loaded. /// - public virtual void LoadComplete() { + public virtual void LoadComplete() + { } } diff --git a/ApiDocs.Validation/Config/DocumentOutlineFile.cs b/ApiDoctor.Validation/Config/DocumentOutlineFile.cs similarity index 98% rename from ApiDocs.Validation/Config/DocumentOutlineFile.cs rename to ApiDoctor.Validation/Config/DocumentOutlineFile.cs index 793ff0c4..b239bd7c 100644 --- a/ApiDocs.Validation/Config/DocumentOutlineFile.cs +++ b/ApiDoctor.Validation/Config/DocumentOutlineFile.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Config +namespace ApiDoctor.Validation.Config { using System; using System.Collections.Generic; diff --git a/ApiDocs.Validation/DocFile.cs b/ApiDoctor.Validation/DocFile.cs similarity index 69% rename from ApiDocs.Validation/DocFile.cs rename to ApiDoctor.Validation/DocFile.cs index 87a01463..f638f19b 100644 --- a/ApiDocs.Validation/DocFile.cs +++ b/ApiDoctor.Validation/DocFile.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,37 +23,37 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.TableSpec; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.TableSpec; using Tags; using MarkdownDeep; using Newtonsoft.Json; using System.Threading.Tasks; + using ApiDoctor.Validation.OData; + using Newtonsoft.Json.Linq; /// /// A documentation file that may contain one more resources or API methods /// - public class DocFile + public partial class DocFile { private const string PageAnnotationType = "#page.annotation"; - - #region Instance Variables - protected bool HasScanRun; - protected string BasePath; - private readonly List resources = new List(); private readonly List requests = new List(); private readonly List examples = new List(); + private readonly List samples = new List(); + private readonly List enums = new List(); private readonly List bookmarks = new List(); - #endregion + protected bool HasScanRun; + protected string BasePath; #region Properties /// @@ -88,6 +88,10 @@ public MethodDefinition[] Requests public ExampleDefinition[] Examples { get { return this.examples.ToArray(); } } + public SamplesDefinition[] Samples { get { return this.samples.ToArray(); } } + + public EnumerationDefinition[] Enums { get { return this.enums.ToArray(); } } + public AuthScopeDefinition[] AuthScopes { get; protected set; } public ErrorDefinition[] ErrorCodes { get; protected set; } @@ -96,9 +100,12 @@ public string[] LinkDestinations { get { - var query = from p in this.MarkdownLinks - select p.Definition.url; - return query.ToArray(); + if (this.MarkdownLinks == null) + { + return new string[0]; + } + + return this.MarkdownLinks.Select(m => m.Definition.url).ToArray(); } } @@ -112,6 +119,8 @@ public string[] LinkDestinations public DocSet Parent { get; protected set; } public PageAnnotation Annotation { get; set; } + + public bool WriteFixesBackToDisk { get; set; } #endregion #region Constructor @@ -175,7 +184,7 @@ protected virtual string GetContentsOfFile(string tags) /// /// Read the contents of the file into blocks and generate any resource or method definitions from the contents /// - public virtual bool Scan(string tags, out ValidationError[] errors) + public virtual bool Scan(string tags, IssueLogger issues) { this.HasScanRun = true; List detectedErrors = new List(); @@ -187,18 +196,16 @@ public virtual bool Scan(string tags, out ValidationError[] errors) } catch (IOException ioex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ErrorOpeningFile, this.DisplayName, "Error reading file contents: {0}", ioex.Message)); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.ErrorOpeningFile, $"Error reading file contents.", ioex); return false; } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ErrorReadingFile, this.DisplayName, "Error reading file contents: {0}", ex.Message)); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.ErrorReadingFile, $"Error reading file contents.", ex); return false; } - return this.ParseMarkdownBlocks(out errors); + return this.ParseMarkdownBlocks(issues); } /// @@ -373,10 +380,8 @@ public List DocumentHeaders /// /// /// - protected bool ParseMarkdownBlocks(out ValidationError[] errors) + protected bool ParseMarkdownBlocks(IssueLogger issues) { - List detectedErrors = new List(); - string methodTitle = null; string methodDescription = null; @@ -402,18 +407,19 @@ protected bool ParseMarkdownBlocks(out ValidationError[] errors) { methodTitle = block.Content; methodDescription = null; // Clear this because we don't want new title + old description - detectedErrors.Add(new ValidationMessage(null, "Found title: {0}", methodTitle)); + issues.Message($"Found title: {methodTitle}"); } else if (block.BlockType == BlockType.p) { if (null == previousHeaderBlock) { - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.MissingHeaderBlock, null, "Paragraph text found before a valid header: {0}", this.DisplayName)); + issues.Warning(ValidationErrorCode.MissingHeaderBlock, + $"Paragraph text found before a valid header: {block.Content.Substring(0, Math.Min(block.Content.Length, 20))}..."); } else if (IsHeaderBlock(previousHeaderBlock)) { methodDescription = block.Content; - detectedErrors.Add(new ValidationMessage(null, "Found description: {0}", methodDescription)); + issues.Message($"Found description: {methodDescription}"); } } else if (block.BlockType == BlockType.html) @@ -424,22 +430,18 @@ protected bool ParseMarkdownBlocks(out ValidationError[] errors) { nextBlock = this.OriginalMarkdownBlocks[i + 1]; } + if (null != nextBlock && nextBlock.BlockType == BlockType.codeblock) { // html + codeblock = likely request or response or resource - ValidationError[] foundErrors = null; - List definitions = this.ParseCodeBlock(block, nextBlock, out foundErrors); - if (foundErrors != null) - { - detectedErrors.AddRange(foundErrors); - } + List definitions = this.ParseCodeBlock(block, nextBlock, issues); if (definitions != null && definitions.Any()) { foreach (var definition in definitions) { - detectedErrors.Add(new ValidationMessage(null, "Found code block: {0} [{1}]", definition.Title, definition.GetType().Name)); + issues.Message($"Found code block: {definition.Title} [{definition.GetType().Name}]"); definition.Title = methodTitle; definition.Description = methodDescription; @@ -450,47 +452,53 @@ protected bool ParseMarkdownBlocks(out ValidationError[] errors) } } } - else if (null == this.Annotation) + else if (this.Annotation == null) { // See if this is the page-level annotation - PageAnnotation annotation = null; try { - annotation = this.ParsePageAnnotation(block); + this.Annotation = this.ParsePageAnnotation(block); + if (this.Annotation != null) + { + if (this.Annotation.Suppressions != null) + { + issues.AddSuppressions(this.Annotation.Suppressions); + } + + if (string.IsNullOrEmpty(this.Annotation.Title)) + { + this.Annotation.Title = this.OriginalMarkdownBlocks.FirstOrDefault(b => IsHeaderBlock(b, 1))?.Content; + } + } } catch (JsonReaderException readerEx) { - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.JsonParserException, this.DisplayName, "Unable to parse page annotation JSON: {0}", readerEx.Message)); + issues.Warning(ValidationErrorCode.JsonParserException, $"Unable to parse page annotation JSON: {readerEx}"); } catch (Exception ex) { - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.AnnotationParserException, this.DisplayName, "Unable to parse annotation: {0}", ex.Message)); - } - - - if (null != annotation) - { - this.Annotation = annotation; - if (string.IsNullOrEmpty(this.Annotation.Title)) - { - this.Annotation.Title = (from b in this.OriginalMarkdownBlocks where IsHeaderBlock(b, 1) select b.Content).FirstOrDefault(); - } + issues.Warning(ValidationErrorCode.AnnotationParserException, $"Unable to parse annotation: {ex}"); } } } else if (block.BlockType == BlockType.table_spec) { - Block blockBeforeTable = (i - 1 >= 0) ? this.OriginalMarkdownBlocks[i - 1] : null; - if (null == blockBeforeTable) continue; + try + { + Block blockBeforeTable = (i - 1 >= 0) ? this.OriginalMarkdownBlocks[i - 1] : null; + if (null == blockBeforeTable) continue; - ValidationError[] parseErrors; - var table = this.Parent.TableParser.ParseTableSpec(block, previousHeaderBlock, out parseErrors); - if (null != parseErrors) detectedErrors.AddRange(parseErrors); + var table = this.Parent.TableParser.ParseTableSpec(block, headerStack, issues); - detectedErrors.Add(new ValidationMessage(null, "Found table: {0}. Rows:\r\n{1}", table.Type, - (from r in table.Rows select JsonConvert.SerializeObject(r, Formatting.Indented)).ComponentsJoinedByString(" ,\r\n"))); + issues.Message($"Found table: {table.Type}. Rows:\r\n{table.Rows.Select(r => JsonConvert.SerializeObject(r, Formatting.Indented)).ComponentsJoinedByString(" ,\r\n")}"); - foundElements.Add(table); + foundElements.Add(table); + } + catch (Exception ex) + { + issues.Error(ValidationErrorCode.MarkdownParserError, $"Failed to parse table.", ex); + throw; + } } if (block.IsHeaderBlock()) @@ -499,26 +507,22 @@ protected bool ParseMarkdownBlocks(out ValidationError[] errors) } } - ValidationError[] postProcessingErrors; - this.PostProcessFoundElements(foundElements, out postProcessingErrors); - detectedErrors.AddRange(postProcessingErrors); + this.PostProcessFoundElements(foundElements, issues); - errors = detectedErrors.ToArray(); - return !detectedErrors.Any(x => x.IsError); + return issues.Issues.All(x => !x.IsError); } /// /// Checks the document for outline errors compared to any required document structure. /// /// - public ValidationError[] CheckDocumentStructure() + public void CheckDocumentStructure(IssueLogger issues) { List errors = new List(); if (this.Parent.DocumentStructure != null) { - errors.AddRange(ValidateDocumentStructure(this.Parent.DocumentStructure.AllowedHeaders, this.DocumentHeaders)); + ValidateDocumentStructure(this.Parent.DocumentStructure.AllowedHeaders, this.DocumentHeaders, issues); } - return errors.ToArray(); } private static bool ContainsMatchingDocumentHeader(Config.DocumentHeader expectedHeader, IReadOnlyList collection) @@ -526,10 +530,8 @@ private static bool ContainsMatchingDocumentHeader(Config.DocumentHeader expecte return collection.Any(h => h.Matches(expectedHeader)); } - private ValidationError[] ValidateDocumentStructure(IReadOnlyList expectedHeaders, IReadOnlyList foundHeaders) + private void ValidateDocumentStructure(IReadOnlyList expectedHeaders, IReadOnlyList foundHeaders, IssueLogger issues) { - List errors = new List(); - int expectedIndex = 0; int foundIndex = 0; @@ -540,7 +542,7 @@ private ValidationError[] ValidateDocumentStructure(IReadOnlyList headerStack, Block block) { var header = CreateHeaderFromBlock(block); @@ -687,9 +688,7 @@ private PageAnnotation ParsePageAnnotation(Block block) /// /// Run post processing on the collection of elements found inside this doc file. /// - /// - /// - private void PostProcessFoundElements(List elements, out ValidationError[] postProcessingErrors) + private void PostProcessFoundElements(List elements, IssueLogger issues) { /* if FoundMethods == 1 then @@ -708,27 +707,16 @@ Attach all tables found in the document to the method. - Find request with matching parameters */ - List detectedErrors = new List(); - var elementsFoundInDocument = elements as IList ?? elements.ToList(); - - var foundMethods = from s in elementsFoundInDocument - where s is MethodDefinition - select (MethodDefinition)s; - - var foundResources = from s in elementsFoundInDocument - where s is ResourceDefinition - select (ResourceDefinition)s; - - var foundTables = from s in elementsFoundInDocument - where s is TableDefinition - select (TableDefinition)s; + var foundMethods = elementsFoundInDocument.OfType().ToList(); + var foundResources = elementsFoundInDocument.OfType().ToList(); + var foundTables = elementsFoundInDocument.OfType().ToList(); + var foundEnums = foundTables.Where(t => t.Type == TableBlockType.EnumerationValues).SelectMany(t => t.Rows).Cast().ToList(); this.PostProcessAuthScopes(elementsFoundInDocument); - PostProcessResources(foundResources, foundTables, detectedErrors); - this.PostProcessMethods(foundMethods, foundTables, detectedErrors); - - postProcessingErrors = detectedErrors.ToArray(); + PostProcessResources(foundResources, foundTables, issues); + this.PostProcessMethods(foundMethods, foundTables, issues); + this.PostProcessEnums(foundEnums, foundTables, issues); } private void PostProcessAuthScopes(IList foundElements) @@ -745,30 +733,128 @@ private void PostProcessAuthScopes(IList foundElements) this.AuthScopes = foundScopes.ToArray(); } - private void PostProcessResources(IEnumerable foundResources, IEnumerable foundTables, List detectedErrors) + private void PostProcessEnums(List foundEnums, List foundTables, IssueLogger issues) + { + // add all the enum values + this.enums.AddRange(foundEnums.Where(e => !string.IsNullOrEmpty(e.MemberName) && !string.IsNullOrEmpty(e.TypeName))); + + // find all the property tables + // find properties of type string that have a list of `enum`, `values`. see if they match me. + foreach (var table in foundTables.Where(t=>t.Type == TableBlockType.RequestObjectProperties || t.Type == TableBlockType.ResourcePropertyDescriptions)) + { + var rows = table.Rows.Cast(); + foreach (var row in rows.Where(r => r.Type?.Type == SimpleDataType.String)) + { + var rowIssues = issues.For(row.Name); + + // if more than one word in the description happens to match an enum value... i'm suspicious + var possibleEnumCandidates = row.Description.TokenizedWords(); + + var matches = possibleEnumCandidates?.Intersect(this.enums.Select(e => e.MemberName), StringComparer.OrdinalIgnoreCase).ToList(); + if (matches?.Count > 1) + { + rowIssues.Warning(ValidationErrorCode.Unknown, $"Found potential enums in parameter description declared as a string: " + + $"({string.Join(",", matches)}) are in enum {this.enums.First(e => e.MemberName.Equals(matches[0], StringComparison.OrdinalIgnoreCase)).TypeName}"); + + } + } + } + + foreach (var resource in this.resources) + { + foreach (var param in resource.Parameters.Where(p => p.Type?.Type == SimpleDataType.String)) + { + var possibleEnums = param.PossibleEnumValues(); + if (possibleEnums.Length > 1) + { + var matches = possibleEnums.Intersect(this.enums.Select(e => e.MemberName), StringComparer.OrdinalIgnoreCase).ToList(); + if (matches.Count < possibleEnums.Length) + { + issues.Warning(ValidationErrorCode.Unknown, $"Found potential enums in resource example that weren't defined in a table:" + + $"({string.Join(",", possibleEnums)}) are in resource, but ({string.Join(",", this.enums.Select(e => e.MemberName))}) are in table"); + } + } + } + } + } + + private void PostProcessResources(List foundResources, List foundTables, IssueLogger issues) { - if (foundResources.Count() == 1) + if (foundResources.Count > 1) + { + var resourceNames = string.Join(",", foundResources.Select(r => r.Name)); + issues.Warning(ValidationErrorCode.ErrorReadingFile, $"Multiple resources found in file, but we only support one per file. '{resourceNames}'. Skipping."); + return; + } + + var onlyResource = foundResources.FirstOrDefault(); + + if (onlyResource != null) { - var onlyResource = foundResources.Single(); foreach (var table in foundTables) { switch (table.Type) { + case TableBlockType.RequestObjectProperties: case TableBlockType.ResourcePropertyDescriptions: case TableBlockType.ResourceNavigationPropertyDescriptions: // Merge information found in the resource property description table with the existing resources + if (onlyResource == null) + { + if (table.Type != TableBlockType.RequestObjectProperties) + { + // TODO: this isn't exactly right... we want to fail RequestObjectProperties if the doc doesn't describe the resource anywhere else, either. + // but that check needs to happen after everything has finished being read. this whole thing should probably be a post-check actually. + // like we should aggregate all the object model data centrally, remembering where every bit of info came from, and then validate it all at the end + // and throw about any inconsistencies. + issues.Warning(ValidationErrorCode.ResourceTypeNotFound, $"Resource not found, but descriptive table(s) are present. Skipping."); + } + + return; + } + + table.UsedIn.Add(onlyResource); MergeParametersIntoCollection( onlyResource.Parameters, table.Rows.Cast(), - onlyResource.Name, - detectedErrors, - table.Type == TableBlockType.ResourceNavigationPropertyDescriptions); + issues.For(onlyResource.Name), + addMissingParameters: true, + expectedInResource: true, + resourceToFixUp: this.WriteFixesBackToDisk ? onlyResource as JsonResourceDefinition : null); break; } } + + // at this point, all parameters should have descriptions from the table. + var paramsToRemove = new List(); + foreach (var param in onlyResource.Parameters) + { + if (string.IsNullOrEmpty(param.Description) && !param.Name.Contains("@")) + { + if (onlyResource.OriginalMetadata.IsOpenType && + onlyResource.OriginalMetadata.OptionalProperties?.Contains(param.Name, StringComparer.OrdinalIgnoreCase) == true) + { + paramsToRemove.Add(param); + } + else + { + issues.Warning(ValidationErrorCode.AdditionalPropertyDetected, + $"Property '{param.Name}' found in resource definition for '{onlyResource.Name}', but not described in markdown table."); + } + } + } + + if (paramsToRemove.Count > 0) + { + foreach (var param in paramsToRemove) + { + onlyResource.Parameters.Remove(param); + } + } } } + /// /// Merges parameter definitions from additionalData into the collection list. Pulls in missing data for existing /// parameters and adds any additional parameters found in the additionalData. @@ -778,9 +864,10 @@ private void PostProcessResources(IEnumerable foundResources private void MergeParametersIntoCollection( List collection, IEnumerable additionalData, - string resourceName, - List detectedErrors, - bool addMissingParameters = false) + IssueLogger issues, + bool addMissingParameters = false, + bool expectedInResource = true, + JsonResourceDefinition resourceToFixUp = null) { foreach (var param in additionalData) { @@ -789,41 +876,51 @@ private void MergeParametersIntoCollection( if (match != null) { // The parameter was always known, let's merge in additional data. - match.AddMissingDetails(param); + match.AddMissingDetails(param, issues.For(param.Name)); } else if (addMissingParameters) { - // Navigation propeties may not appear in the example text, so don't report and error - if (!param.IsNavigatable) + if (expectedInResource) { - Console.WriteLine($"Found property '{param.Name}' in markdown table that wasn't defined in '{resourceName}': {this.DisplayName}"); - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.AdditionalPropertyDetected, this.DisplayName, $"Property '{param.Name}' found in markdown table but not in resource definition for '{resourceName}'.")); - } - else - { - detectedErrors.Add(new ValidationMessage(this.DisplayName, $"Navigation property '{param.Name}' found in markdown table but not in resource definition for '{resourceName}'.")); + // Navigation propeties may not appear in the example text, so don't report and error + if (!param.IsNavigatable) + { + issues.Warning(ValidationErrorCode.AdditionalPropertyDetected, $"Property '{param.Name}' found in markdown table but not in resource definition."); + + if (resourceToFixUp?.SourceJObject != null) + { + resourceToFixUp.SourceJObject.Add(param.Name, param.ToExampleJToken()); + resourceToFixUp.PatchSourceFile(); + Console.WriteLine("Fixed missing property"); + } + } + else + { + issues.Message($"Navigation property '{param.Name}' found in markdown table but not in resource definition."); + } } + // The parameter didn't exist in the collection, so let's add it. collection.Add(param); } - else if (!param.IsNavigatable) + else if (!param.IsNavigatable && expectedInResource) { // Oops, we didn't find the property in the resource definition - Console.WriteLine($"Found property '{param.Name}' in markdown table that wasn't defined in '{resourceName}': {this.DisplayName}"); - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.AdditionalPropertyDetected, this.DisplayName, $"Property '{param.Name}' found in markdown table but not in resource definition for '{resourceName}'.")); + issues.Warning(ValidationErrorCode.AdditionalPropertyDetected, $"Property '{param.Name}' found in markdown table but not in resource definition."); } } } - private void PostProcessMethods(IEnumerable foundMethods, IEnumerable foundTables, List errors) + private void PostProcessMethods(IEnumerable foundMethods, IEnumerable foundTables, IssueLogger issues) { - var totalMethods = foundMethods.Count(); var totalTables = foundTables.Count(); - if (totalTables == 0) + { return; + } + var totalMethods = foundMethods.Count(); if (totalMethods == 0) { this.SetFoundTablesForFile(foundTables); @@ -831,35 +928,71 @@ private void PostProcessMethods(IEnumerable foundMethods, IEnu else if (totalMethods == 1) { var onlyMethod = foundMethods.Single(); - SetFoundTablesOnMethod(foundTables, onlyMethod, errors); + SetFoundTablesOnMethod(foundTables, onlyMethod, issues); } else { - // TODO: Figure out how to map stuff when more than one method exists - if (null != errors) - { - var unmappedContentsError = new ValidationWarning(ValidationErrorCode.UnmappedDocumentElements, this.DisplayName, "Unable to map some markdown elements into schema."); + // maybe the methods are really all the same and the dupes are just different examples + var distinctMethodNames = foundMethods.Select(m => new Http.HttpParser().ParseHttpRequest(m.Request).Url).Select( + url => + { + var method = url.Substring(url.LastIndexOf('/') + 1); + var endIndex = method.IndexOfAny(new[] { '(', '?', '/' }); + if (endIndex != -1) + { + method = method.Substring(0, endIndex); + } - List innerErrors = new List(); - var unmappedMethods = (from m in foundMethods select m.RequestMetadata.MethodName?.FirstOrDefault()).ComponentsJoinedByString(", "); - if (!string.IsNullOrEmpty(unmappedMethods)) + return method; + }).Distinct().Count(); + if (distinctMethodNames == 1) + { + foreach (var method in foundMethods) { - innerErrors.Add(new ValidationMessage("Unmapped methods", unmappedMethods)); + SetFoundTablesOnMethod(foundTables, method, issues); } - var unmappedTables = (from t in foundTables select string.Format("{0} - {1}", t.Title, t.Type)).ComponentsJoinedByString(", "); - if (!string.IsNullOrEmpty(unmappedTables)) + return; + } + + // if there's no more than one of each table, we can assume + // they're meant to apply to all the methods. + if (foundTables.GroupBy(t => t.Type).All(g => g.Count() <= 1)) + { + foreach (var method in foundMethods) { - innerErrors.Add(new ValidationMessage("Unmapped tables", unmappedTables)); + SetFoundTablesOnMethod(foundTables, method, issues); } - unmappedContentsError.InnerErrors = innerErrors.ToArray(); - errors.Add(unmappedContentsError); + + return; } + + // TODO: Figure out how to map stuff when more than one method exists along with separate tables for each + List innerErrors = new List(); + var unmappedMethods = (from m in foundMethods select m.RequestMetadata.MethodName?.FirstOrDefault()).ComponentsJoinedByString(", "); + if (!string.IsNullOrEmpty(unmappedMethods)) + { + innerErrors.Add(new ValidationMessage("Unmapped methods", unmappedMethods)); + } + + var unmappedTables = (from t in foundTables select string.Format("{0} - {1}", t.Title, t.Type)).ComponentsJoinedByString(", "); + if (!string.IsNullOrEmpty(unmappedTables)) + { + innerErrors.Add(new ValidationMessage("Unmapped tables", unmappedTables)); + } + + var unmappedContentsError = new ValidationWarning(ValidationErrorCode.UnmappedDocumentElements, this.DisplayName, "Unable to map some markdown elements into schema.") + { + InnerErrors = innerErrors.ToArray(), + }; + + issues.Warning(unmappedContentsError); } } - private void SetFoundTablesOnMethod(IEnumerable foundTables, MethodDefinition onlyMethod, List detectedErrors) + private void SetFoundTablesOnMethod(IEnumerable foundTables, MethodDefinition onlyMethod, IssueLogger issues) { + var methodName = onlyMethod.RequestMetadata.MethodName?.FirstOrDefault(); foreach (var table in foundTables) { switch (table.Type) @@ -867,27 +1000,32 @@ private void SetFoundTablesOnMethod(IEnumerable foundTables, Me case TableBlockType.Unknown: // Unknown table format, nothing we can do with it. break; + case TableBlockType.AuthScopes: + // nothing to do on a specific method. handled at the file level + break; case TableBlockType.EnumerationValues: - // TODO: Support enumeration values - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.Unknown, this.DisplayName, $"Table '{table.Title}' for method '{onlyMethod.RequestMetadata.MethodName?.FirstOrDefault()}' included enum values that weren't parsed.")); + // nothing special to do with enums right now. break; case TableBlockType.ErrorCodes: + table.UsedIn.Add(onlyMethod); onlyMethod.Errors = table.Rows.Cast().ToList(); break; case TableBlockType.HttpHeaders: case TableBlockType.PathParameters: case TableBlockType.QueryStringParameters: + table.UsedIn.Add(onlyMethod); onlyMethod.Parameters.AddRange(table.Rows.Cast()); break; case TableBlockType.RequestObjectProperties: - onlyMethod.RequestBodyParameters.AddRange(table.Rows.Cast()); - break; - case TableBlockType.ResourcePropertyDescriptions: - case TableBlockType.ResponseObjectProperties: - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.Unknown, this.DisplayName, $"Table '{table.Title}' for method '{onlyMethod.RequestMetadata.MethodName?.FirstOrDefault()}' included response properties that were ignored.")); + table.UsedIn.Add(onlyMethod); + MergeParametersIntoCollection(onlyMethod.RequestBodyParameters, table.Rows.Cast(), issues.For(methodName), addMissingParameters: true, expectedInResource: false); break; default: - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.Unknown, this.DisplayName, $"Table '{table.Title}' ({table.Type}) for method '{onlyMethod.RequestMetadata.MethodName?.FirstOrDefault()}' was unsupported and ignored.")); + if (table.UsedIn.Count == 0) + { + // if the table wasn't used by anything else, we assume it's intended for this method, so we log a warning + issues.Warning(ValidationErrorCode.Unknown, $"Table '{table.Title}' of type {table.Type} is not supported for methods '{onlyMethod.RequestMetadata.MethodName?.FirstOrDefault()}' and was ignored."); + } break; } } @@ -935,23 +1073,19 @@ protected static List FindCodeBlocks(Block[] blocks) /// Convert an annotation and fenced code block in the documentation into something usable. Adds /// the detected object into one of the internal collections of resources, methods, or examples. /// - /// - /// - public List ParseCodeBlock(Block metadata, Block code, out ValidationError[] errors) + public List ParseCodeBlock(Block metadata, Block code, IssueLogger issues) { List detectedErrors = new List(); if (metadata.BlockType != BlockType.html) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MarkdownParserError, this.DisplayName, "metadata block does not appear to be metadata")); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.MarkdownParserError, "metadata block does not appear to be metadata"); return null; } if (code.BlockType != BlockType.codeblock) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MarkdownParserError, this.DisplayName, "code block does not appear to be code")); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.MarkdownParserError, "code block does not appear to be code"); return null; } @@ -960,12 +1094,11 @@ public List ParseCodeBlock(Block metadata, Block code, out Valid CodeBlockAnnotation annotation = null; try { - annotation = CodeBlockAnnotation.ParseMetadata(metadataJsonString, code); + annotation = CodeBlockAnnotation.ParseMetadata(metadataJsonString, code, this); } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MarkdownParserError, this.DisplayName, "Unable to parse code block metadata: {0}", ex.Message)); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.MarkdownParserError, $"Unable to parse code block metadata.", ex); return null; } @@ -978,42 +1111,38 @@ public List ParseCodeBlock(Block metadata, Block code, out Valid { try { - resource = new JsonResourceDefinition(annotation, code.Content, this); + resource = new JsonResourceDefinition(annotation, code.Content, this, issues); } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MarkdownParserError, this.DisplayName, "Unable to parse resource metadata [{1}]: {0}", ex.Message, annotation.ResourceType)); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.MarkdownParserError, $"Unable to parse resource metadata in {annotation.ResourceType}.", ex); return null; } } else { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MarkdownParserError, this.DisplayName, $"Unsupported resource definition language: {code.CodeLanguage}")); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.MarkdownParserError, $"Unsupported resource definition language: {code.CodeLanguage}"); return null; } if (string.IsNullOrEmpty(resource.Name)) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MarkdownParserError, this.DisplayName, "Resource definition is missing a name.")); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.MarkdownParserError, "Resource definition is missing a name."); return null; } this.resources.Add(resource); - errors = detectedErrors.ToArray(); return new List(new ItemDefinition[] { resource }); } case CodeBlockType.Request: { - var method = MethodDefinition.FromRequest(code.Content, annotation, this); + var method = MethodDefinition.FromRequest(code.Content, annotation, this, issues.For("RequestBlock")); if (string.IsNullOrEmpty(method.Identifier)) { method.Identifier = string.Format("{0} #{1}", this.DisplayName, this.requests.Count); } + this.requests.Add(method); - errors = detectedErrors.ToArray(); return new List(new ItemDefinition[] { method }); } @@ -1060,52 +1189,53 @@ public List ParseCodeBlock(Block metadata, Block code, out Valid throw new InvalidOperationException(string.Format("Unable to locate the corresponding request for response block: {0}. Requests must be defined before a response.", annotation.MethodName)); } } - errors = detectedErrors.ToArray(); + return responses; } case CodeBlockType.Example: { var example = new ExampleDefinition(annotation, code.Content, this, code.CodeLanguage); this.examples.Add(example); - errors = detectedErrors.ToArray(); return new List(new ItemDefinition[] { example }); } + case CodeBlockType.Samples: + { + var sample = new SamplesDefinition(annotation, code.Content); + this.samples.Add(sample); + return new List(new ItemDefinition[] { sample }); + } case CodeBlockType.Ignored: { - errors = detectedErrors.ToArray(); return null; } case CodeBlockType.SimulatedResponse: { var method = Enumerable.Last(this.requests); method.AddSimulatedResponse(code.Content, annotation); - errors = detectedErrors.ToArray(); return new List(new ItemDefinition[] { method }); } case CodeBlockType.TestParams: { var method = Enumerable.Last(this.requests); method.AddTestParams(code.Content); - errors = detectedErrors.ToArray(); return new List(new ItemDefinition[] { method }); } default: { - var errorMessage = string.Format("Unable to parse metadata block or unsupported block type. Line {1}. Content: {0}", metadata.Content, metadata.LineStart); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.MarkdownParserError, + $"Unable to parse metadata block. Possibly an unsupported block type. Line {metadata.LineStart}. Content: {metadata.Content}"); return null; } } } - #endregion #region Link Verification - public bool ValidateNoBrokenLinks(bool includeWarnings, out ValidationError[] errors, bool requireFilenameCaseMatch) + public bool ValidateNoBrokenLinks(bool includeWarnings, IssueLogger issues, bool requireFilenameCaseMatch) { string[] files; - return this.ValidateNoBrokenLinks(includeWarnings, out errors, out files, requireFilenameCaseMatch); + return this.ValidateNoBrokenLinks(includeWarnings, issues.For(this.DisplayName), out files, requireFilenameCaseMatch); } /// @@ -1115,7 +1245,7 @@ public bool ValidateNoBrokenLinks(bool includeWarnings, out ValidationError[] er /// Information about broken links /// /// True if all links are valid. Otherwise false - public bool ValidateNoBrokenLinks(bool includeWarnings, out ValidationError[] errors, out string[] linkedDocFiles, bool requireFilenameCaseMatch) + public bool ValidateNoBrokenLinks(bool includeWarnings, IssueLogger issues, out string[] linkedDocFiles, bool requireFilenameCaseMatch) { if (!this.HasScanRun) throw new InvalidOperationException("Cannot validate links until Scan() is called."); @@ -1125,12 +1255,10 @@ public bool ValidateNoBrokenLinks(bool includeWarnings, out ValidationError[] er // If there are no links in this document, just skip the validation. if (this.MarkdownLinks == null) { - errors = new ValidationError[0]; linkedDocFiles = new string[0]; return true; } - var foundErrors = new List(); foreach (var link in this.MarkdownLinks) { if (null == link.Definition) @@ -1138,8 +1266,8 @@ public bool ValidateNoBrokenLinks(bool includeWarnings, out ValidationError[] er // Don't treat TAGS or END markers like links if (!link.Text.ToUpper().Equals("END") && !link.Text.ToUpper().StartsWith("TAGS=")) { - foundErrors.Add(new ValidationError(ValidationErrorCode.MissingLinkSourceId, this.DisplayName, - "Link ID '[{0}]' used in document by not defined. Define with '[{0}]: url' or remove square brackets.", link.Text)); + issues.Error(ValidationErrorCode.MissingLinkSourceId, + $"Link ID '[{link.Text}]' used in document but not defined. Define with '[{link.Text}]: url' or remove square brackets."); } continue; @@ -1152,37 +1280,35 @@ public bool ValidateNoBrokenLinks(bool includeWarnings, out ValidationError[] er { case LinkValidationResult.ExternalSkipped: if (includeWarnings) - foundErrors.Add(new ValidationWarning(ValidationErrorCode.LinkValidationSkipped, this.DisplayName, "Skipped validation of external link '[{1}]({0})'", link.Definition.url, link.Text)); + issues.Warning(ValidationErrorCode.LinkValidationSkipped, $"Skipped validation of external link '[{link.Definition.url}]({link.Text})'"); break; case LinkValidationResult.FileNotFound: - foundErrors.Add(new ValidationError(ValidationErrorCode.LinkDestinationNotFound, this.DisplayName, "FileNotFound: '[{1}]({0})'. {2}", link.Definition.url, link.Text, suggestion)); + issues.Error(ValidationErrorCode.LinkDestinationNotFound, $"FileNotFound: '[{link.Definition.url}]({link.Text})'. {suggestion}"); break; case LinkValidationResult.BookmarkMissing: - foundErrors.Add(new ValidationError(ValidationErrorCode.LinkDestinationNotFound, this.DisplayName, "BookmarkMissing: '[{1}]({0})'. {2}", link.Definition.url, link.Text, suggestion)); + issues.Error(ValidationErrorCode.LinkDestinationNotFound, $"BookmarkMissing: '[{link.Definition.url}]({link.Text})'. {suggestion}"); break; case LinkValidationResult.ParentAboveDocSetPath: - foundErrors.Add(new ValidationError(ValidationErrorCode.LinkDestinationOutsideDocSet, this.DisplayName, "Relative link outside of doc set: '[{1}]({0})'.", link.Definition.url, link.Text)); + issues.Error(ValidationErrorCode.LinkDestinationOutsideDocSet, $"Relative link outside of doc set: '[{link.Definition.url}]({link.Text})'."); break; case LinkValidationResult.UrlFormatInvalid: - foundErrors.Add(new ValidationError(ValidationErrorCode.LinkFormatInvalid, this.DisplayName, "InvalidUrlFormat '[{1}]({0})'.", link.Definition.url, link.Text)); + issues.Error(ValidationErrorCode.LinkFormatInvalid, $"InvalidUrlFormat '[{link.Definition.url}]({link.Text})'."); break; case LinkValidationResult.Valid: - foundErrors.Add(new ValidationMessage(this.DisplayName, "Valid link '[{1}]({0})'.", link.Definition.url, link.Text)); + issues.Message($"Valid link '[{link.Definition.url}]({link.Text})'."); if (null != relativeFileName) { linkedPages.Add(relativeFileName); } break; default: - foundErrors.Add(new ValidationError(ValidationErrorCode.Unknown, this.DisplayName, "{2}: Link '[{1}]({0})'.", link.Definition.url, link.Text, result)); + issues.Error(ValidationErrorCode.Unknown, $"{result}: Link '[{link.Text}]({link.Definition.url})'."); break; - } } - errors = foundErrors.ToArray(); linkedDocFiles = linkedPages.Distinct().ToArray(); - return !(errors.WereErrors() || errors.WereWarnings()); + return !(issues.Issues.WereErrors() || issues.Issues.WereWarnings()); } protected enum LinkValidationResult @@ -1304,6 +1430,7 @@ protected virtual LinkValidationResult VerifyRelativeLink(FileInfo sourceFile, s var candidateFiles = from f in info.Directory.GetFiles() select f.Name; relativeFileName = StringSuggestions.SuggestStringFromCollection(info.Name, candidateFiles); } + return LinkValidationResult.FileNotFound; } @@ -1340,6 +1467,21 @@ public string UrlRelativePathFromRoot() var relativePath = this.DisplayName.Replace('\\', '/'); return relativePath.StartsWith("/") ? relativePath.Substring(1) : relativePath; } + + public override string ToString() + { + return this.DisplayName; + } + + public override int GetHashCode() + { + return this.DisplayName.GetHashCode(); + } + + public override bool Equals(object obj) + { + return this.DisplayName.Equals(obj); + } } } diff --git a/ApiDocs.Validation/DocSet.cs b/ApiDoctor.Validation/DocSet.cs similarity index 59% rename from ApiDocs.Validation/DocSet.cs rename to ApiDoctor.Validation/DocSet.cs index c6c12e5f..b7675490 100644 --- a/ApiDocs.Validation/DocSet.cs +++ b/ApiDoctor.Validation/DocSet.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Collections.Generic; @@ -31,24 +31,43 @@ namespace ApiDocs.Validation using System.IO; using System.Linq; using System.Text; - using ApiDocs.Validation.Config; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Http; - using ApiDocs.Validation.Json; - using ApiDocs.Validation.Params; + using ApiDoctor.Validation.Config; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Http; + using ApiDoctor.Validation.Json; + using ApiDoctor.Validation.OData.Transformation; + using ApiDoctor.Validation.Params; using Newtonsoft.Json; public class DocSet { - #region Constants private const string DocumentationFileExtension = "*.md"; - #endregion + private static readonly string[] XmlFileExtensions = new[] { "*.xml", "*.html", "*.htm" }; - #region Instance Variables - readonly JsonResourceCollection resourceCollection = new JsonResourceCollection(); - #endregion + private static readonly HashSet foldersToSkip = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "beta", + "templates", + }; + + private static readonly HashSet filesToSkip = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "changelog.md", + "contributing.md", + "issue_template.md", + "readme.md", + "license.md", + }; + + private bool writeFixesBackToDisk; #region Properties + + /// + /// Static schema config for those hard-to-reach places + /// + public static SchemaConfig SchemaConfig { get; private set; } + /// /// Location of the documentation set /// @@ -61,8 +80,9 @@ public class DocSet public ResourceDefinition[] Resources { get; private set; } public MethodDefinition[] Methods { get; private set; } + public EnumerationDefinition[] Enums { get; private set; } - public JsonResourceCollection ResourceCollection { get { return this.resourceCollection; } } + public JsonResourceCollection ResourceCollection { get; } = new JsonResourceCollection(); public List TestScenarios { get; internal set; } public List CannedRequests { get; internal set; } @@ -91,8 +111,9 @@ public IEnumerable ErrorCodes #endregion #region Constructors - public DocSet(string sourceFolderPath) + public DocSet(string sourceFolderPath, bool writeFixesBackToDisk = false) { + this.writeFixesBackToDisk = writeFixesBackToDisk; sourceFolderPath = ResolvePathWithUserRoot(sourceFolderPath); if (sourceFolderPath.EndsWith(Path.DirectorySeparatorChar.ToString())) { @@ -111,7 +132,10 @@ public DocSet(string sourceFolderPath) public DocSet() { - + this.SourceFolderPath = Path.GetTempPath(); + this.LoadRequirements(); + this.LoadTestScenarios(); + this.LoadTableParser(); } #endregion @@ -132,6 +156,18 @@ private void LoadRequirements() Console.WriteLine("Using document structure file: {0}", foundOutlines.SourcePath); this.DocumentStructure = foundOutlines; } + + SchemaConfigFile[] schemaConfigs = TryLoadConfigurationFiles(this.SourceFolderPath); + var schemaConfig = schemaConfigs.FirstOrDefault(); + if (schemaConfig != null) + { + Console.WriteLine($"Using schema config file: {schemaConfig.SourcePath}"); + SchemaConfig = schemaConfig.SchemaConfig; + } + else + { + SchemaConfig = new SchemaConfig(); + } } private void LoadTableParser() @@ -184,6 +220,12 @@ public static T[] TryLoadConfigurationFiles(string path) where T : ConfigFile var jsonFiles = docSetDir.GetFiles("*.json", SearchOption.AllDirectories); foreach (var file in jsonFiles) { + if (file.DirectoryName.Contains("beta")) + { + // hack: skip beta for now. + continue; + } + try { using (var reader = file.OpenText()) @@ -226,81 +268,222 @@ public static string ResolvePathWithUserRoot(string path) /// Scan all files in the documentation set to load /// information about resources and methods defined in those files /// - public bool ScanDocumentation(string tags, out ValidationError[] errors) + public bool ScanDocumentation(string tags, IssueLogger issues) { var foundResources = new List(); var foundMethods = new List(); + var foundEnums = new List(); - var detectedErrors = new List(); - - this.resourceCollection.Clear(); + this.ResourceCollection.Clear(); if (!this.Files.Any()) { - detectedErrors.Add( - new ValidationError( - ValidationErrorCode.NoDocumentsFound, - null, - "No markdown documentation was found in the current path.")); + issues.Error(ValidationErrorCode.NoDocumentsFound, + "No markdown documentation was found in the current path."); } foreach (var file in this.Files) { - ValidationError[] parseErrors; - file.Scan(tags, out parseErrors); - if (parseErrors != null) + file.Scan(tags, issues.For(file.DisplayName)); + foundResources.AddRange(file.Resources); + foundMethods.AddRange(file.Requests); + foundEnums.AddRange(file.Enums); + } + + // do a topological sort so that base types come first. + // also resolve and link up parent references. + // do a topological sort of the resources so that we process base types first. + var sortedResources = new List(); + var processed = new Dictionary(StringComparer.OrdinalIgnoreCase); + var resourceMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var resource in foundResources) + { + List resources; + if (!resourceMap.TryGetValue(resource.Name, out resources)) { - detectedErrors.AddRange(parseErrors); + resources = new List(); + resourceMap.Add(resource.Name, resources); } - foundResources.AddRange(file.Resources); - foundMethods.AddRange(file.Requests); + resources.Add(resource); + + if (resources.Count > 1) + { + var firstResource = resources[0]; + if (!string.Equals(firstResource.Name, resource.Name)) + { + issues.Warning(ValidationErrorCode.Unknown, + $"Inconsistent name casing found for {resource.Name} from {resource.SourceFile.DisplayName}" + + $" vs duplicate resource in {firstResource.SourceFile.DisplayName}"); + } + + if (!string.Equals(firstResource.BaseType, resource.BaseType)) + { + if (string.IsNullOrEmpty(resource.BaseType)) + { + resource.BaseType = firstResource.BaseType; + } + else if (string.IsNullOrEmpty(firstResource.BaseType)) + { + foreach (var duplicateResource in resources) + { + duplicateResource.BaseType = resource.BaseType; + } + } + else + { + issues.Error(ValidationErrorCode.ExpectedTypeDifferent, + $"Inconsistent base types for {resource.Name} : {resource.BaseType} from {resource.SourceFile.DisplayName}" + + $" vs duplicate resource in {firstResource.SourceFile.DisplayName} with base type {firstResource.BaseType} "); + } + } + } } - this.resourceCollection.RegisterJsonResources(foundResources); - this.Resources = foundResources.ToArray(); + foreach (var kvp in resourceMap) + { + if (kvp.Value.Select(r => r.SourceFile.DisplayName).Distinct().Count() > 1) + { + issues.Warning(ValidationErrorCode.Unknown, + $"Resource {kvp.Key} is defined in multiple files: {string.Join(", ", kvp.Value.Select(r => r.SourceFile.DisplayName))}"); + } + + // if we've already processed a node, we don't need to do it again. + if (!processed.ContainsKey(kvp.Key)) + { + TopologicalSortNode(kvp.Value, resourceMap, sortedResources, processed); + } + } + + this.ResourceCollection.RegisterJsonResources(sortedResources); + + this.Resources = sortedResources.ToArray(); this.Methods = foundMethods.ToArray(); + this.Enums = foundEnums.ToArray(); + + // suppressing until we have a better way that's less collission-prone + ////CheckForDuplicatesMethods(foundMethods, issues); - //CheckForDuplicatesResources(foundResources, detectedErrors); - //CheckForDuplicatesMethods(foundMethods, detectedErrors); + var definedTypes = new HashSet( + this.Resources.Select(r => r.Name). + Concat(this.Enums.Select(e => (SchemaConfig.DefaultNamespace + "." + e.TypeName).Trim('.'))). + Concat(new[] { "odata.error" })); - errors = detectedErrors.ToArray(); - return detectedErrors.Count == 0; + foreach (var resource in this.Resources) + { + var resourceIssues = issues.For(resource.Name); + if (!string.IsNullOrEmpty(resource.BaseType) && !definedTypes.Contains(resource.BaseType)) + { + resourceIssues.Error(ValidationErrorCode.ResourceTypeNotFound, + $"Referenced base type {resource.BaseType} in resource {resource.Name} is not defined in the doc set!"); + } + + foreach (var param in resource.Parameters) + { + if (param.Type.CustomTypeName != null) + { + EnsureDefinedInDocs(param.Type.CustomTypeName, definedTypes, resource.SourceFile, resourceIssues.For(param.Name)); + } + } + + if (!string.IsNullOrEmpty(resource.KeyPropertyName) && + !resource.Parameters.Any(p => p.Name == resource.KeyPropertyName)) + { + resourceIssues.Warning(ValidationErrorCode.RequiredPropertiesMissing, + $"Could not find key property {resource.KeyPropertyName} on {resource.Name}. Assuming doc bug, and ignoring."); + resource.KeyPropertyName = null; + } + } + + foreach (var method in this.Methods) + { + foreach (var param in method.Parameters.Concat(method.RequestBodyParameters)) + { + if (param.Type?.CustomTypeName != null) + { + EnsureDefinedInDocs(param.Type.CustomTypeName, definedTypes, method.SourceFile, issues.For(method.Identifier + "/" + param.Name)); + } + } + } + + return issues.Issues.All(issue => !issue.IsWarningOrError); } - /// - /// Report errors if there are any methods with duplicate identifiers discovered - /// - /// - /// - private void CheckForDuplicatesMethods(List foundMethods, List detectedErrors) + private static void TopologicalSortNode( + List duplicateResources, + Dictionary> allResources, + List sortedResources, + Dictionary processed) { - Dictionary identifiers = new Dictionary(); - foreach (var method in foundMethods) + var resourceName = duplicateResources[0].Name; + + bool sorted; + if (processed.TryGetValue(resourceName, out sorted) && !sorted) { - if (identifiers.ContainsKey(method.Identifier)) + throw new ArgumentException($"Circular dependency in {duplicateResources[0].SourceFile.DisplayName}. Depends on {duplicateResources[0].BaseType}."); + } + + if (!sorted) + { + // mark it processed but not yet sorted + processed[resourceName] = false; + + foreach (var baseTypeName in duplicateResources.Select(r => r.BaseType).Where(bt => !string.IsNullOrEmpty(bt)).Distinct()) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.DuplicateMethodIdentifier, null, "Duplicate method identifier detected: {0} in {1} and {2}.", method.Identifier, identifiers[method.Identifier].DisplayName, method.SourceFile.DisplayName)); + List baseTypes; + if (allResources.TryGetValue(baseTypeName, out baseTypes)) + { + // annoying how there are duplicates... + foreach (var resource in duplicateResources) + { + if (resource.ResolvedBaseTypeReference == null) + { + resource.ResolvedBaseTypeReference = baseTypes[0]; + } + } + + TopologicalSortNode(baseTypes, allResources, sortedResources, processed); + } } - else + + // we're done, so mark it sorted + sortedResources.AddRange(duplicateResources); + processed[resourceName] = true; + } + } + + private void EnsureDefinedInDocs(string type, HashSet definedTypes, DocFile sourceFile, IssueLogger issues) + { + if (!string.IsNullOrEmpty(type)) + { + if (!definedTypes.Contains(type)) { - identifiers.Add(method.Identifier, method.SourceFile); + var nameOnly = type.Substring(type.LastIndexOf('.') + 1); + var suggestion = + definedTypes.FirstOrDefault(t => t.IContains(nameOnly)) ?? + definedTypes.FirstOrDefault(t => nameOnly.IContains(t.Substring(t.LastIndexOf('.') + 1))) ?? + "UNKNOWN"; + + issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Referenced type {type} is not defined in the doc set! Potential suggestion: {suggestion}"); } } } - private void CheckForDuplicatesResources(List foundResources, List detectedErrors) + /// + /// Report errors if there are any methods with duplicate identifiers discovered + /// + private void CheckForDuplicatesMethods(List foundMethods, IssueLogger issues) { Dictionary identifiers = new Dictionary(); - foreach (var resource in foundResources) + foreach (var method in foundMethods) { - if (identifiers.ContainsKey(resource.Name)) + if (identifiers.ContainsKey(method.Identifier)) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.DuplicateMethodIdentifier, null, "Duplicate method identifier detected: {0} in {1} and {2}.", resource.Name, identifiers[resource.Name].DisplayName, resource.SourceFile.DisplayName)); + issues.Warning(ValidationErrorCode.DuplicateMethodIdentifier, $"Duplicate method identifier detected: {method.Identifier} in {identifiers[method.Identifier].DisplayName} and {method.SourceFile.DisplayName}."); } else { - identifiers.Add(resource.Name, resource.SourceFile); + identifiers.Add(method.Identifier, method.SourceFile); } } } @@ -312,36 +495,29 @@ private void CheckForDuplicatesResources(List foundResources /// Actual response from the service (this is what we validate). /// Prototype response (expected) that shows what a valid response should look like. /// A test scenario used to generate the response, which may include additional parameters to verify. - /// A collection of errors, warnings, and verbose messages generated by this process. - public static void ValidateApiMethod(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, ScenarioDefinition scenario, out ValidationError[] errors) + /// A collection of errors, warnings, and verbose messages generated by this process. + public static void ValidateApiMethod(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, ScenarioDefinition scenario, IssueLogger issues) { if (null == method) throw new ArgumentNullException("method"); if (null == actualResponse) throw new ArgumentNullException("actualResponse"); - List detectedErrors = new List(); - // Verify the request is valid (headers, request body) - method.VerifyRequestFormat(detectedErrors); + method.VerifyRequestFormat(issues); // Verify that the expected response headers match the actual response headers - ValidationError[] httpErrors; - if (null != expectedResponse && !expectedResponse.ValidateResponseHeaders(actualResponse, out httpErrors)) + if (null != expectedResponse) { - detectedErrors.AddRange(httpErrors); + expectedResponse.ValidateResponseHeaders(actualResponse, issues); } // Verify the actual response body is correct according to the schema defined for the response - ValidationError[] bodyErrors; - VerifyResponseBody(method, actualResponse, expectedResponse, out bodyErrors); - detectedErrors.AddRange(bodyErrors); + VerifyResponseBody(method, actualResponse, expectedResponse, issues); // Verify any expectations in the scenario are met if (null != scenario) { - scenario.ValidateExpectations(actualResponse, detectedErrors); + scenario.ValidateExpectations(actualResponse, issues); } - - errors = detectedErrors.ToArray(); } /// @@ -350,58 +526,45 @@ public static void ValidateApiMethod(MethodDefinition method, HttpResponse actua /// The MethodDefinition that generated the response. /// The actual response from the service to validate. /// The prototype expected response from the service. - /// A collection of errors that will be appended with any detected errors - private static void VerifyResponseBody(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, out ValidationError[] errors) + /// A collection of errors that will be appended with any detected errors + private static void VerifyResponseBody(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, IssueLogger issues) { - List detectedErrors = new List(); - if (string.IsNullOrEmpty(actualResponse.Body) && (expectedResponse != null && !string.IsNullOrEmpty(expectedResponse.Body))) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.HttpBodyExpected, null, "Body missing from response (expected response includes a body or a response type was provided).")); + issues.Error(ValidationErrorCode.HttpBodyExpected, "Body missing from response (expected response includes a body or a response type was provided)."); } else if (!string.IsNullOrEmpty(actualResponse.Body)) { - ValidationError[] schemaErrors; if (method.ExpectedResponseMetadata == null || (string.IsNullOrEmpty(method.ExpectedResponseMetadata.ResourceType) && (expectedResponse != null && !string.IsNullOrEmpty(expectedResponse.Body)))) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ResponseResourceTypeMissing, null, "Expected a response, but resource type on method is missing: {0}", method.Identifier)); + issues.Error(ValidationErrorCode.ResponseResourceTypeMissing, $"Expected a response, but resource type on method is missing: {method.Identifier}"); } else { var otherResources = method.SourceFile.Parent.ResourceCollection; - if ( - !otherResources.ValidateResponseMatchesSchema( - method, - actualResponse, - expectedResponse, - out schemaErrors)) - { - detectedErrors.AddRange(schemaErrors); - } + otherResources.ValidateResponseMatchesSchema( + method, + actualResponse, + expectedResponse, + issues); } var responseValidation = actualResponse.IsResponseValid( method.SourceFile.DisplayName, - method.SourceFile.Parent.Requirements); - detectedErrors.AddRange(responseValidation.Messages); + method.SourceFile.Parent.Requirements, + issues.For(method.SourceFile.DisplayName)); } - - errors = detectedErrors.ToArray(); } /// /// Scan through the document set looking for any broken links. /// - /// - /// /// - public bool ValidateLinks(bool includeWarnings, string[] relativePathForFiles, out ValidationError[] errors, bool requireFilenameCaseMatch, bool printOrphanedFiles) + public bool ValidateLinks(bool includeWarnings, string[] relativePathForFiles, IssueLogger issues, bool requireFilenameCaseMatch, bool printOrphanedFiles) { - List foundErrors = new List(); - Dictionary orphanedPageIndex = this.Files.ToDictionary(x => this.RelativePathToFile(x, true), x => true); List filesToCheck = new List(); @@ -419,14 +582,11 @@ public bool ValidateLinks(bool includeWarnings, string[] relativePathForFiles, o } } - foreach (var file in filesToCheck) + foreach (var file in filesToCheck.Where(f => f.LinkDestinations.Any())) { - ValidationError[] localErrors; - string [] linkedPages; - if (!file.ValidateNoBrokenLinks(includeWarnings, out localErrors, out linkedPages, requireFilenameCaseMatch)) - { - foundErrors.AddRange(localErrors); - } + string[] linkedPages; + file.ValidateNoBrokenLinks(includeWarnings, issues.For(file.DisplayName), out linkedPages, requireFilenameCaseMatch); + foreach (string pageName in linkedPages) { orphanedPageIndex[pageName] = false; @@ -442,13 +602,13 @@ public bool ValidateLinks(bool includeWarnings, string[] relativePathForFiles, o if (relativePathForFiles == null && printOrphanedFiles) { // We only report orphan pages when scanning the whole docset. - foundErrors.AddRange(from o in orphanedPageIndex - where o.Value - select new ValidationWarning(ValidationErrorCode.OrphanedDocumentPage, null, "Page {0} has no incoming links.", o.Key)); + foreach (var o in orphanedPageIndex.Where(o => o.Value)) + { + issues.Warning(ValidationErrorCode.OrphanedDocumentPage, $"Page {o.Key} has no incoming links."); + } } - errors = foundErrors.ToArray(); - return !errors.WereWarningsOrErrors(); + return !issues.Issues.WereWarningsOrErrors(); } /// @@ -475,11 +635,17 @@ private void ReadDocumentationHierarchy(string path) throw new FileNotFoundException(string.Format("Cannot find documentation. Directory doesn't exist: {0}", sourceFolder.FullName)); } - var fileInfos = sourceFolder.GetFiles(DocumentationFileExtension, SearchOption.AllDirectories); + var markdownFileInfos = sourceFolder.GetFiles(DocumentationFileExtension, SearchOption.AllDirectories); + var markdownFiles = markdownFileInfos. + Where(fi => foldersToSkip.All(folderToSkip => !fi.DirectoryName.Contains(folderToSkip)) && !filesToSkip.Contains(fi.Name)). + Select(fi => new DocFile(this.SourceFolderPath, this.RelativePathToFile(fi.FullName), this) { WriteFixesBackToDisk = this.writeFixesBackToDisk }); + + var supplementalFileInfos = XmlFileExtensions.SelectMany(x => sourceFolder.GetFiles(x, SearchOption.AllDirectories)).ToArray(); + var supplementalFiles = supplementalFileInfos. + Where(fi => foldersToSkip.All(folderToSkip => !fi.DirectoryName.Contains(folderToSkip)) && !filesToSkip.Contains(fi.Name)). + Select(fi => new SupplementalFile(this.SourceFolderPath, this.RelativePathToFile(fi.FullName), this)); - var relativeFilePaths = from fi in fileInfos - select new DocFile(this.SourceFolderPath, this.RelativePathToFile(fi.FullName), this); - this.Files = relativeFilePaths.ToArray(); + this.Files = markdownFiles.Concat(supplementalFiles).ToArray(); } internal string RelativePathToFile(DocFile file, bool urlStyle = false) diff --git a/ApiDoctor.Validation/EnumerationDefinition.cs b/ApiDoctor.Validation/EnumerationDefinition.cs new file mode 100644 index 00000000..b482e728 --- /dev/null +++ b/ApiDoctor.Validation/EnumerationDefinition.cs @@ -0,0 +1,50 @@ +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +namespace ApiDoctor.Validation +{ + public class EnumerationDefinition : ItemDefinition + { + /// + /// Enumerated member name + /// + public string MemberName { get; set; } + + /// + /// Enumerated type name + /// + public string TypeName { get; set; } + + /// + /// Attribute for IsFlags + /// + public bool IsFlags { get; set; } + + /// + /// Attribute for IsFlags + /// + public int? NumericValue { get; set; } + } +} diff --git a/ApiDoctor.Validation/Error/IssueLogger.cs b/ApiDoctor.Validation/Error/IssueLogger.cs new file mode 100644 index 00000000..4ecc8dde --- /dev/null +++ b/ApiDoctor.Validation/Error/IssueLogger.cs @@ -0,0 +1,284 @@ +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +namespace ApiDoctor.Validation.Error +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Runtime.CompilerServices; + using System.Text; + using System.Text.RegularExpressions; + + public class IssueLogger + { + private List children = new List(); + private Dictionary localSuppressions = new Dictionary(SuppressionComparer.Instance); + private HashSet globalSuppressions = new HashSet(SuppressionComparer.Instance); + private List messages = new List(); + private List warnings = new List(); + private List errors = new List(); + + private bool onlyKeepUniqueIssues { get; set; } + private bool similarIssuesFound { get; set; } + + public int IssuesInCurrentScope { get; private set; } + + public int DebugLine { get; set; } + + /// + /// All issues in current scope and child scopes (but not parents) + /// + public IEnumerable Issues => + this.Messages.Cast(). + Concat(this.Warnings). + Concat(this.Errors); + + /// + /// All errors in current scope and child scopes (but not parents) + /// + public IEnumerable Errors => + this.errors.Where(HandleSuppression).Concat(this.children.SelectMany(c => c.Errors)); + + /// + /// All warnings in current scope and child scopes (but not parents) + /// + public IEnumerable Warnings => + this.warnings.Where(HandleSuppression).Concat(this.children.SelectMany(c => c.Warnings)); + + /// + /// All messages in current scope and child scopes (but not parents) + /// + public IEnumerable Messages => + this.messages. + Concat(this.children.SelectMany(c => c.Messages)); + + public string Source { get; private set; } = string.Empty; + + public List UnusedSuppressions + { + get + { + return this.globalSuppressions.Except(this.UsedSuppressions, SuppressionComparer.Instance).ToList(); + } + } + + public List UsedSuppressions + { + get + { + return this.localSuppressions. + //Where(sup => sup.Value > 0). + Select(sup => sup.Key). + Union(this.children. + SelectMany(c=>c.UsedSuppressions)). + ToList(); + } + } + + public void Error(ValidationErrorCode code, string message, Exception exception = null, [CallerLineNumber]int lineNumber = 0) + { + LaunchDebuggerIfNeeded(lineNumber); + AddIfNeeded(new ValidationError(code, this.Source, message + $"\r\n{exception}"), this.errors); + this.IssuesInCurrentScope++; + } + + public void Warning(ValidationErrorCode code, string message, Exception exception = null, [CallerLineNumber]int lineNumber = 0) + { + LaunchDebuggerIfNeeded(lineNumber); + AddIfNeeded(new ValidationWarning(code, this.Source, message + $"\r\n{exception}"), this.warnings); + this.IssuesInCurrentScope++; + } + + public void Warning(ValidationWarning warning, [CallerLineNumber]int lineNumber = 0) + { + LaunchDebuggerIfNeeded(lineNumber); + AddIfNeeded(warning, this.warnings); + this.IssuesInCurrentScope++; + } + + public void Message(string message, [CallerLineNumber]int lineNumber = 0) + { + LaunchDebuggerIfNeeded(lineNumber); + this.messages.Add(new ValidationMessage(this.Source, message)); + this.IssuesInCurrentScope++; + } + + public IssueLogger For(string source, bool onlyKeepUniqueErrors = false) + { + var logger = new IssueLogger + { + onlyKeepUniqueIssues = onlyKeepUniqueErrors, + globalSuppressions = this.globalSuppressions, + DebugLine = this.DebugLine, + Source = this.Source + (string.IsNullOrEmpty(this.Source) ? "" : "/") + source, + }; + + this.children.Add(logger); + return logger; + } + + public void AddSuppressions(IEnumerable suppressions, [CallerLineNumber]int lineNumber = 0) + { + LaunchDebuggerIfNeeded(lineNumber); + foreach (var sup in suppressions) + { + this.globalSuppressions.Add(sup); + } + } + + public void LaunchDebuggerIfNeeded(int lineNumber) + { + if (lineNumber > 0 && lineNumber == this.DebugLine) + { +#if DEBUG + Debugger.Launch(); +#endif + } + } + + private bool HandleSuppression(ValidationError error) + { + if (this.globalSuppressions.Contains(error.ErrorText)) + { + int count; + this.localSuppressions.TryGetValue(error.ErrorText, out count); + this.localSuppressions[error.ErrorText] = count + 1; + + // do not return the warning (suppress) + return false; + } + + // return the warning (don't suppress) + return true; + } + + private void AddIfNeeded(ValidationError issue, List issues) where T: ValidationError + { + if (issue.Source == null) + { + issue.Source = this.Source; + } + + if (this.onlyKeepUniqueIssues) + { + if (issues.Any(i => i.Code == issue.Code && i.Message == issue.Message)) + { + if (!this.similarIssuesFound) + { + this.warnings.Add(new ValidationWarning(ValidationErrorCode.SkippedSimilarErrors, this.Source, "Similar errors were skipped.")); + this.similarIssuesFound = true; + } + } + else + { + issues.Add((T)issue); + } + } + else + { + issues.Add((T)issue); + } + } + + private class SuppressionComparer : IEqualityComparer + { + // matches the variable part of a stack trace: " at blah blah blah:line 12345" + private static Regex stackTracePattern = new Regex("\\sat\\s.*:(line\\s)?\\d+", RegexOptions.Compiled | RegexOptions.Singleline); + + // matches the variable part of known paths like: " /blah/blah/resources/someresource.md" + private static Regex knownPathPattern = new Regex(":\\s(/.*)/((api)|(resources))/.*\\.md", RegexOptions.Compiled | RegexOptions.Singleline); + + public static SuppressionComparer Instance { get; } = new SuppressionComparer(); + + private SuppressionComparer() + { + } + + public bool Equals(string x, string y) + { + return Sanitize(x).Equals(Sanitize(y)); + } + + public int GetHashCode(string obj) + { + return Sanitize(obj).GetHashCode(); + } + + private string Sanitize(string x) + { + return TrimWhiteSpace(TrimParentsOfKnownPaths(TrimStackTrace(x))); + } + + private string TrimWhiteSpace(string x) + { + if (string.IsNullOrEmpty(x)) + { + return string.Empty; + } + + var sb = new StringBuilder(x.Length); + foreach (var c in x.Where(c => !char.IsWhiteSpace(c))) + { + sb.Append(char.ToLowerInvariant(c)); + } + + return sb.ToString(); + } + + private string TrimStackTrace(string x) + { + var match = stackTracePattern.Match(x); + if (match.Success) + { + for (int i = 0; i < match.Captures.Count; i++) + { + x = x.Replace(match.Captures[i].Value, string.Empty); + } + } + + return x; + } + + // for well-known paths like /resources/ and /api/, + // trim any preceding hierarchy. + private string TrimParentsOfKnownPaths(string x) + { + var match = knownPathPattern.Match(x); + if (match.Success && match.Groups.Count > 2) + { + var parentPathGroup = match.Groups[1]; + for (int i = 0; i < parentPathGroup.Captures.Count; i++) + { + x = x.Replace(parentPathGroup.Captures[i].Value, string.Empty); + } + } + + return x; + } + } + } +} diff --git a/ApiDocs.Validation/Error/ValidationMessage.cs b/ApiDoctor.Validation/Error/ValidationMessage.cs similarity index 96% rename from ApiDocs.Validation/Error/ValidationMessage.cs rename to ApiDoctor.Validation/Error/ValidationMessage.cs index fce87470..283a0187 100644 --- a/ApiDocs.Validation/Error/ValidationMessage.cs +++ b/ApiDoctor.Validation/Error/ValidationMessage.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -24,7 +24,7 @@ */ -namespace ApiDocs.Validation.Error +namespace ApiDoctor.Validation.Error { public class ValidationMessage : ValidationError { diff --git a/ApiDocs.Validation/Error/ValidationResult.cs b/ApiDoctor.Validation/Error/ValidationResult.cs similarity index 97% rename from ApiDocs.Validation/Error/ValidationResult.cs rename to ApiDoctor.Validation/Error/ValidationResult.cs index b0cccef3..6c1133e6 100644 --- a/ApiDocs.Validation/Error/ValidationResult.cs +++ b/ApiDoctor.Validation/Error/ValidationResult.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Error +namespace ApiDoctor.Validation.Error { using System.Collections.Generic; using System.Linq; diff --git a/ApiDocs.Validation/Error/ValidationWarning.cs b/ApiDoctor.Validation/Error/ValidationWarning.cs similarity index 97% rename from ApiDocs.Validation/Error/ValidationWarning.cs rename to ApiDoctor.Validation/Error/ValidationWarning.cs index 92da7e6b..a99ca536 100644 --- a/ApiDocs.Validation/Error/ValidationWarning.cs +++ b/ApiDoctor.Validation/Error/ValidationWarning.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Error +namespace ApiDoctor.Validation.Error { public class ValidationWarning : ValidationError { diff --git a/ApiDocs.Validation/Error/ValidationError.cs b/ApiDoctor.Validation/Error/validationerror.cs similarity index 80% rename from ApiDocs.Validation/Error/ValidationError.cs rename to ApiDoctor.Validation/Error/validationerror.cs index f0302bbb..08ddc97b 100644 --- a/ApiDocs.Validation/Error/ValidationError.cs +++ b/ApiDoctor.Validation/Error/validationerror.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Error +namespace ApiDoctor.Validation.Error { using System; using System.Collections.Generic; @@ -118,7 +118,10 @@ public enum ValidationErrorCode SecondaryAccountMissing, AnnotationParserException, DuplicateMethodIdentifier, - ContentFormatException + DuplicateResource, + ContentFormatException, + + AmbiguousExample, } public class ValidationError @@ -167,6 +170,8 @@ public virtual bool IsWarningOrError get { return IsWarning | IsError; } } + private string cachedErrorText; + /// /// Returns a log-ready string that includes information about the specific error/warning/message. /// @@ -174,38 +179,45 @@ public string ErrorText { get { - StringBuilder sb = new StringBuilder(); - if (this.IsWarning) + if (this.cachedErrorText == null) { - sb.Append("Warning: "); + StringBuilder sb = new StringBuilder(); + if (this.IsWarning) + { + sb.Append("Warning: "); + } + else if (this.IsError) + { + sb.Append("Error: "); + } + + if (!string.IsNullOrEmpty(this.Source)) + { + sb.Append(this.Source); + sb.AppendLine(":"); + sb.Append(" "); + } + + sb.Append(this.Message); + + if (null != this.InnerErrors && this.InnerErrors.Length > 0) + { + sb.AppendLine(); + sb.AppendLine(this.InnerErrors.ErrorsToString(" ")); + } + + this.cachedErrorText = sb.ToString().ToStringClean(); } - else if (this.IsError) - { - sb.Append("Error: "); - } - - if (!string.IsNullOrEmpty(this.Source)) - { - sb.Append(this.Source); - sb.Append(": "); - } - sb.Append(this.Message); - if (null != this.InnerErrors && this.InnerErrors.Length > 0) - { - sb.AppendLine(); - sb.AppendLine(this.InnerErrors.ErrorsToString(" ")); - } - - return sb.ToString(); + return this.cachedErrorText; } } - public static ValidationError NewConsolidatedError(ValidationErrorCode code, ValidationError[] errors, string message, params object[] parameters) + public static ValidationError NewConsolidatedError(ValidationErrorCode code, IssueLogger issues, string message, params object[] parameters) { - var error = errors.All(err => err.IsWarning) ? new ValidationWarning(code, null, message, parameters) : new ValidationError(code, null, message, parameters); + var error = issues.Issues.All(err => err.IsWarning) ? new ValidationWarning(code, null, message, parameters) : new ValidationError(code, null, message, parameters); - error.InnerErrors = errors; + error.InnerErrors = issues.Issues.ToArray(); return error; } } diff --git a/ApiDocs.Validation/ErrorDefinition.cs b/ApiDoctor.Validation/ErrorDefinition.cs similarity index 96% rename from ApiDocs.Validation/ErrorDefinition.cs rename to ApiDoctor.Validation/ErrorDefinition.cs index 9a4bc5cd..99d26d73 100644 --- a/ApiDocs.Validation/ErrorDefinition.cs +++ b/ApiDoctor.Validation/ErrorDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { public class ErrorDefinition : ItemDefinition { diff --git a/ApiDocs.Validation/ExampleDefinition.cs b/ApiDoctor.Validation/ExampleDefinition.cs similarity index 98% rename from ApiDocs.Validation/ExampleDefinition.cs rename to ApiDoctor.Validation/ExampleDefinition.cs index 20a52698..465564a4 100644 --- a/ApiDocs.Validation/ExampleDefinition.cs +++ b/ApiDoctor.Validation/ExampleDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -25,9 +25,9 @@ using Newtonsoft.Json; using System; -using ApiDocs.Validation.Error; +using ApiDoctor.Validation.Error; -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { public class ExampleDefinition : ItemDefinition { diff --git a/ApiDocs.Validation/ExtensionMethods.cs b/ApiDoctor.Validation/ExtensionMethods.cs similarity index 70% rename from ApiDocs.Validation/ExtensionMethods.cs rename to ApiDoctor.Validation/ExtensionMethods.cs index 2f51a56c..08c6cedb 100644 --- a/ApiDocs.Validation/ExtensionMethods.cs +++ b/ApiDoctor.Validation/ExtensionMethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Collections.Generic; @@ -32,20 +32,90 @@ namespace ApiDocs.Validation using System.Linq; using System.Text; using System.Text.RegularExpressions; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Json; using MarkdownDeep; using Newtonsoft.Json.Linq; using System.Globalization; + using ApiDoctor.Validation.OData.Transformation; public static class ExtensionMethods { + private const char fancyLeftQuote = (char)0x201c; + private const char fancyRightQuote = (char)0x201d; + private const char singleQuote = '\''; - public static bool ContainsIgnoreCase(this string target, string value) + private static readonly Regex likelyBase64Regex = new Regex("^[a-fA-F0-9+=/]+$", RegexOptions.Compiled); + + + private static readonly string[] Iso8601Formats = + { + "yyyy-MM-dd", + @"HH\:mm\:ss.fffZ", + @"HH\:mm\:ssZ", + @"yyyy-MM-ddTHH\:mm\:ssZ", + @"yyyy-MM-ddTHH\:mm\:ss.fZ", + @"yyyy-MM-ddTHH\:mm\:ss.ffZ", + @"yyyy-MM-ddTHH\:mm\:ss.fffZ", + @"yyyy-MM-ddTHH\:mm\:ss.ffffZ", + @"yyyy-MM-ddTHH\:mm\:ss.fffffZ", + @"yyyy-MM-ddTHH\:mm\:ss.ffffffZ", + @"yyyy-MM-ddTHH\:mm\:ss.fffffffZ" + }; + + private static readonly string[] TimeOfDayFormats = + { + @"HH\:mm\:ss.fff", + @"HH\:mm\:ss", + }; + + public static bool IEquals(this string target, string value) + { + return string.Equals(target, value, StringComparison.OrdinalIgnoreCase); + } + + public static bool IContains(this string target, string value) { return target.IndexOf(value, StringComparison.OrdinalIgnoreCase) != -1; } + public static string IReplace(this string target, string oldValue, string newValue) + { + string result = target; + for (int i = 0; i < result.Length;) + { + int matchIndex = result.IndexOf(oldValue, i, StringComparison.OrdinalIgnoreCase); + if (matchIndex == -1) + { + break; + } + + result = result.Substring(0, matchIndex) + newValue + result.Substring(matchIndex + oldValue.Length); + i = matchIndex + newValue.Length; + } + + return result; + } + + public static string ToStringClean(this object value) + { + if (value == null) + { + return null; + } + + return value.ToString(). + Replace(fancyLeftQuote, singleQuote). + Replace(fancyRightQuote, singleQuote). + Replace('"', singleQuote). + Replace('\\', '/'); + } + + public static string[] TokenizedWords(this string value) + { + return value?.Split(' ', ',', ';', '.', '\'', '\"', '|', '`') ?? new string[0]; + } + public static string ComponentsJoinedByString(this IEnumerable source, string separator, int startIndex = 0) { StringBuilder sb = new StringBuilder(); @@ -175,11 +245,16 @@ public static bool WereWarningsOrErrors(this IEnumerable errors static MarkdownDeep.Markdown converter = new Markdown() { ActiveRenderer = new MarkdownDeep.Formats.RenderToPlainText(), ExtraMode = true }; - public static string ValueForColumn(this string[] rowValues, IMarkdownTable table, string[] possibleHeaderNames, bool removeMarkdownSyntax = true) + public static string ValueForColumn(this string[] rowValues, IMarkdownTable table, string[] possibleHeaderNames, List usedColumns = null, bool removeMarkdownSyntax = true) { + if (usedColumns == null) + { + usedColumns = new List(); + } + var headers = table.ColumnHeaders; - foreach (var headerName in possibleHeaderNames) + foreach (var headerName in possibleHeaderNames.Except(usedColumns)) { int index = headers.IndexOf(headerName); if (index >= 0 && index < rowValues.Length) @@ -191,6 +266,7 @@ public static string ValueForColumn(this string[] rowValues, IMarkdownTable tabl { tableCellContents = converter.Transform(tableCellContents).TrimEnd(); } + usedColumns.Add(headerName); return tableCellContents; } } @@ -217,58 +293,105 @@ public static int IndexOf(this string[] array, string value, StringComparison co /// /// /// - public static ParameterDataType ParseParameterDataType(this string value, Action addErrorAction = null, ParameterDataType defaultValue = null ) + public static ParameterDataType ParseParameterDataType(this string value, bool isCollection = false, Action addErrorAction = null, ParameterDataType defaultValue = null ) { + const string collectionPrefix = "collection("; + const string collectionOfPrefix = "collection of"; + const string collectionSuffix = " collection"; + if (value == null) { return null; } + if (value.StartsWith(collectionPrefix, StringComparison.OrdinalIgnoreCase)) + { + isCollection = true; + value = value.Substring(collectionPrefix.Length).TrimEnd(')'); + } + else if (value.StartsWith(collectionOfPrefix, StringComparison.OrdinalIgnoreCase)) + { + isCollection = true; + value = value.Substring(collectionOfPrefix.Length).TrimEnd(')'); + } + else if (value.EndsWith(collectionSuffix, StringComparison.OrdinalIgnoreCase)) + { + isCollection = true; + value = value.Replace(collectionSuffix, string.Empty); + } + // Value could have markdown formatting in it, so we do some basic work to try and remove that if it exists if (value.IndexOf('[') != -1) { value = value.TextBetweenCharacters('[', ']'); } - var lowerValue = value.ToLowerInvariant(); - SimpleDataType simpleType = ParseSimpleTypeString(lowerValue); + SimpleDataType simpleType = ParseSimpleTypeString(value.ToLowerInvariant()); if (simpleType != SimpleDataType.None) { - return new ParameterDataType(simpleType); + return new ParameterDataType(simpleType, isCollection); } - const string collectionPrefix = "collection("; - if (lowerValue.StartsWith(collectionPrefix)) + // some inferences for common descriptions + ParameterDataType inferredType = null; + if (value.IContains("etag")) { - string innerType = value.Substring(collectionPrefix.Length).TrimEnd(')'); - simpleType = ParseSimpleTypeString(innerType.ToLowerInvariant()); + inferredType = ParameterDataType.String; + } + else if (value.IContains("timestamp")) + { + inferredType = ParameterDataType.DateTimeOffset; + } + else if (value.IContains("string")) + { + inferredType = ParameterDataType.String; + } - if (simpleType != SimpleDataType.None) + bool isEnum = false; + if (value.IContains(" enum")) + { + isEnum = true; + value = value.IReplace(" enum", string.Empty).Trim(); + } + + if (inferredType != null) + { + if (isCollection) { - return new ParameterDataType(simpleType, true); + inferredType = ParameterDataType.CollectionOfType(inferredType); } - else + + return inferredType; + } + + // if there aren't any spaces or special characters, assume we parsed the name of a type correctly. + if (value.IndexOfAny(new[] { ' ', '/' }) == -1) + { + SchemaConfig config = DocSet.SchemaConfig; + + if (!(config?.NotLowerCamel?.Contains(value)).GetValueOrDefault()) { - return new ParameterDataType(innerType, true); + value = char.ToLowerInvariant(value[0]) + value.Substring(1); } - } - if (lowerValue.Contains("etag")) - return ParameterDataType.String; - if (lowerValue.Contains("timestamp")) - return ParameterDataType.DateTimeOffset; - if (lowerValue.Contains("string")) - return ParameterDataType.String; + if (value.IndexOf('.') == -1 && !string.IsNullOrEmpty(config?.DefaultNamespace)) + { + value = config.DefaultNamespace + "." + value; + } + return new ParameterDataType(value, isCollection, isEnum); + } if (defaultValue != null) + { return defaultValue; + } if (null != addErrorAction) { addErrorAction(new ValidationWarning(ValidationErrorCode.TypeConversionFailure, "Couldn't convert '{0}' into understood data type. Assuming Object type.", value)); } - return new ParameterDataType(value, false); + return new ParameterDataType(value, isCollection); } public static SimpleDataType ParseSimpleTypeString(string lowercaseString) @@ -284,16 +407,22 @@ public static SimpleDataType ParseSimpleTypeString(string lowercaseString) simpleType = SimpleDataType.String; break; case "int64": + case "long": case "number": case "integer": simpleType = SimpleDataType.Int64; break; + case "int": case "int32": simpleType = SimpleDataType.Int32; break; case "int16": + case "short": simpleType = SimpleDataType.Int16; break; + case "single": + simpleType = SimpleDataType.Single; + break; case "double": simpleType = SimpleDataType.Double; break; @@ -303,6 +432,7 @@ public static SimpleDataType ParseSimpleTypeString(string lowercaseString) case "guid": simpleType = SimpleDataType.Guid; break; + case "bool": case "boolean": simpleType = SimpleDataType.Boolean; break; @@ -311,6 +441,15 @@ public static SimpleDataType ParseSimpleTypeString(string lowercaseString) case "timestamp": simpleType = SimpleDataType.DateTimeOffset; break; + case "timeofday": + simpleType = SimpleDataType.TimeOfDay; + break; + case "date": + simpleType = SimpleDataType.Date; + break; + case "duration": + simpleType = SimpleDataType.Duration; + break; case "etag": case "range": case "url": @@ -319,23 +458,44 @@ public static SimpleDataType ParseSimpleTypeString(string lowercaseString) case "stream": simpleType = SimpleDataType.Stream; break; + case "binary": + simpleType = SimpleDataType.Binary; + break; + case "byte": + simpleType = SimpleDataType.Byte; + break; } if (lowercaseString.Contains("timestamp")) + { return SimpleDataType.DateTimeOffset; + } // Check to see if this looks like an ISO 8601 date and call it DateTimeOffset if it does var parsedDate = lowercaseString.ToUpperInvariant().TryParseIso8601Date(); if (parsedDate.HasValue) { - simpleType = SimpleDataType.DateTimeOffset; + return SimpleDataType.DateTimeOffset; + } + + // Check to see if it looks like a time of day + parsedDate = lowercaseString.TryParseTimeOfDay(); + if (parsedDate.HasValue) + { + return SimpleDataType.TimeOfDay; } // Check to see if this can be parsed as a GUID Guid testguid; if (Guid.TryParse(lowercaseString, out testguid)) { - simpleType = SimpleDataType.Guid; + return SimpleDataType.Guid; + } + + // check to see if it's a base64 example string + if (lowercaseString.StartsWith("base64")) + { + return SimpleDataType.Binary; } return simpleType; @@ -351,6 +511,16 @@ public static SimpleDataType ParseSimpleTypeString(string lowercaseString) return null; } + public static bool? IsOptional(this string description) + { + if (description?.StartsWith("optional", StringComparison.OrdinalIgnoreCase) == true) + { + return true; + } + + return null; + } + public static bool IsHeaderBlock(this Block block) { switch (block.BlockType) @@ -400,6 +570,22 @@ public static bool ToBoolean(this string input) throw new NotSupportedException(string.Format("Couldn't convert this value to a boolean: {0}", input)); } + public static int? ToInt32(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return null; + } + + int output; + if (int.TryParse(input, out output)) + { + return output; + } + + throw new NotSupportedException($"Couldn't convert '{input}' to an Int32"); + } + public static Regex PathVariableRegex = new Regex(@"\{(?.+?)\}", RegexOptions.Compiled); public static string FlattenVariableNames(this string input) @@ -505,7 +691,14 @@ internal static ExpectedStringFormat StringFormat(this ParameterDefinition param public static string[] PossibleEnumValues(this ParameterDefinition param) { if (param.Type != ParameterDataType.String) + { throw new InvalidOperationException("Cannot provide possible enum values on non-string data types"); + } + + if (param.OriginalValue == null) + { + return new string[0]; + } string[] possibleValues = param.OriginalValue.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); @@ -522,20 +715,23 @@ public static bool IsValidEnumValue(this ParameterDefinition param, string input return false; } - private static readonly string[] Iso8601Formats = + public static bool IsLikelyBase64Encoded(this string input) { - "yyyy-MM-dd", - @"HH\:mm\:ss.fffZ", - @"HH\:mm\:ssZ", - @"yyyy-MM-ddTHH\:mm\:ssZ", - @"yyyy-MM-ddTHH\:mm\:ss.fZ", - @"yyyy-MM-ddTHH\:mm\:ss.ffZ", - @"yyyy-MM-ddTHH\:mm\:ss.fffZ", - @"yyyy-MM-ddTHH\:mm\:ss.ffffZ", - @"yyyy-MM-ddTHH\:mm\:ss.fffffZ", - @"yyyy-MM-ddTHH\:mm\:ss.ffffffZ", - @"yyyy-MM-ddTHH\:mm\:ss.fffffffZ" - }; + if (likelyBase64Regex.IsMatch(input)) + { + if (input.Contains("=") || input.Any(char.IsDigit)) + { + return true; + } + + // even though it's base-64 decodable, + // the fact that it only contains text might mean that it's a real + // function name, like 'getByIds' + return false; + } + + return false; + } public static DateTimeOffset? TryParseIso8601Date(this string input) { @@ -548,6 +744,17 @@ public static bool IsValidEnumValue(this ParameterDefinition param, string input return null; } + public static DateTimeOffset? TryParseTimeOfDay(this string input) + { + DateTimeOffset value; + if (DateTimeOffset.TryParseExact(input, TimeOfDayFormats, DateTimeFormatInfo.InvariantInfo, System.Globalization.DateTimeStyles.None, out value)) + { + return value; + } + + return null; + } + /// /// Checks to see if a collection of errors already includes a similar error (matching Code + Message string) /// diff --git a/ApiDocs.Validation/Http/HttpParser.cs b/ApiDoctor.Validation/Http/HttpParser.cs similarity index 99% rename from ApiDocs.Validation/Http/HttpParser.cs rename to ApiDoctor.Validation/Http/HttpParser.cs index 07c2a1f0..1791c461 100644 --- a/ApiDocs.Validation/Http/HttpParser.cs +++ b/ApiDoctor.Validation/Http/HttpParser.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Http +namespace ApiDoctor.Validation.Http { using System; using System.IO; diff --git a/ApiDocs.Validation/Http/HttpParserRequestException.cs b/ApiDoctor.Validation/Http/HttpParserRequestException.cs similarity index 98% rename from ApiDocs.Validation/Http/HttpParserRequestException.cs rename to ApiDoctor.Validation/Http/HttpParserRequestException.cs index bde1b6f6..71a89f79 100644 --- a/ApiDocs.Validation/Http/HttpParserRequestException.cs +++ b/ApiDoctor.Validation/Http/HttpParserRequestException.cs @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Http +namespace ApiDoctor.Validation.Http { using System; using System.Runtime.Serialization; diff --git a/ApiDocs.Validation/Http/HttpValidationExtensionMethods.cs b/ApiDoctor.Validation/Http/HttpValidationExtensionMethods.cs similarity index 70% rename from ApiDocs.Validation/Http/HttpValidationExtensionMethods.cs rename to ApiDoctor.Validation/Http/HttpValidationExtensionMethods.cs index 47c06a3e..53ccbf91 100644 --- a/ApiDocs.Validation/Http/HttpValidationExtensionMethods.cs +++ b/ApiDoctor.Validation/Http/HttpValidationExtensionMethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Http +namespace ApiDoctor.Validation.Http { using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.Config; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Config; + using ApiDoctor.Validation.Error; public static class HttpValidationExtensionMethods { @@ -36,34 +36,28 @@ public static class HttpValidationExtensionMethods /// Evaluate the ApiRequirements file and return any inconsistencies /// as ValidationErrors. /// - /// The http request. - /// Request. - /// - /// - public static ValidationResult IsRequestValid(this HttpRequest request, string sourceFile, ApiRequirements apiRequirements) + public static ValidationResult IsRequestValid(this HttpRequest request, string sourceFile, ApiRequirements apiRequirements, IssueLogger issues) { if (null == apiRequirements || null == apiRequirements.HttpRequest) return new ValidationResult(true); - List errors = new List(); - var reqs = apiRequirements.HttpRequest; var requestMimeType = new MultipartMime.MimeContentType(request.ContentType); if (null != reqs.ContentTypes && null != requestMimeType.MimeType && !reqs.ContentTypes.Contains(requestMimeType.MimeType)) { - errors.Add(new ValidationWarning(ValidationErrorCode.InvalidContentType, sourceFile, "Request content-type header value is not in the supported list of content-types: {0}", request.ContentType)); + issues.Warning(ValidationErrorCode.InvalidContentType, $"Request content-type header value is not in the supported list of content-types: {request.ContentType}"); } if (reqs.HttpMethods != null && !reqs.HttpMethods.Contains(request.Method)) { - errors.Add(new ValidationError(ValidationErrorCode.InvalidHttpMethod, sourceFile, "Request HTTP method is not in the supported list of HTTP methods: {0}", request.Method)); + issues.Error(ValidationErrorCode.InvalidHttpMethod, $"Request HTTP method is not in the supported list of HTTP methods: {request.Method}"); } if (reqs.MaxUrlLength > 0 && request.Url.Length > reqs.MaxUrlLength) { - errors.Add(new ValidationError(ValidationErrorCode.UrlLengthExceedsMaximum, sourceFile, "Request URL is longer than the defined maximum URL length: [{0}] {1}", request.Url.Length, request.Url)); + issues.Error(ValidationErrorCode.UrlLengthExceedsMaximum, $"Request URL is longer than the defined maximum URL length: [{request.Url.Length}] {request.Url}"); } if (reqs.StandardHeaders != null && request.Headers.Count > 0) @@ -72,12 +66,12 @@ public static ValidationResult IsRequestValid(this HttpRequest request, st { if (!reqs.StandardHeaders.ContainsString(headerName, apiRequirements.CaseSensativeHeaders)) { - errors.Add(new ValidationWarning(ValidationErrorCode.NonStandardHeaderUsed, sourceFile, "Request includes a non-standard header: {0}", headerName)); + issues.Warning(ValidationErrorCode.NonStandardHeaderUsed, $"Request includes a non-standard header: {headerName}"); } } } - return new ValidationResult(!errors.Any(), errors); + return new ValidationResult(issues.IssuesInCurrentScope == 0); } public static bool ContainsString(this string[] array, string value, bool caseSenativeComparison = false) @@ -85,7 +79,7 @@ public static bool ContainsString(this string[] array, string value, bool caseSe return array.Any(x => x.Equals(value, caseSenativeComparison ? System.StringComparison.Ordinal : System.StringComparison.OrdinalIgnoreCase)); } - public static ValidationResult IsResponseValid(this HttpResponse response, string sourceFile, ApiRequirements requirements) + public static ValidationResult IsResponseValid(this HttpResponse response, string sourceFile, ApiRequirements requirements, IssueLogger issues) { if (null == requirements || null == requirements.HttpResponse) return new ValidationResult(true); @@ -98,10 +92,10 @@ public static ValidationResult IsResponseValid(this HttpResponse response, if (reqs.ContentTypes != null && null != responseMimeType && !reqs.ContentTypes.Contains(responseMimeType.MimeType)) { - errors.Add(new ValidationWarning(ValidationErrorCode.InvalidContentType, sourceFile, "Response Content-Type header value is not in the supported list of content-types: {0}", response.ContentType)); + issues.Warning(ValidationErrorCode.InvalidContentType, $"Response Content-Type header value is not in the supported list of content-types: {response.ContentType}"); } - return new ValidationResult(!errors.Any(), errors); + return new ValidationResult(!errors.Any()); } } diff --git a/ApiDocs.Validation/Http/HttpRequest.cs b/ApiDoctor.Validation/Http/httprequest.cs similarity index 97% rename from ApiDocs.Validation/Http/HttpRequest.cs rename to ApiDoctor.Validation/Http/httprequest.cs index 97b53a21..4cf49164 100644 --- a/ApiDocs.Validation/Http/HttpRequest.cs +++ b/ApiDoctor.Validation/Http/httprequest.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Http +namespace ApiDoctor.Validation.Http { using System; using System.IO; @@ -33,6 +33,7 @@ namespace ApiDocs.Validation.Http using System.Linq; using System.Collections.Generic; using Params; + using ApiDoctor.Validation.Error; public class HttpRequest { @@ -323,10 +324,10 @@ public string FullHttpText(bool showFullAuthorizationHeader = false) } - public async Task GetResponseAsync(IServiceAccount account, int retryCount = 0) + public async Task GetResponseAsync(IServiceAccount account, IssueLogger issues, int retryCount = 0) { var baseUrl = account.BaseUrl; - this.RewriteRequestBodyNamespaces(account); + this.RewriteRequestBodyNamespaces(account, issues); var webRequest = this.PrepareHttpWebRequest(baseUrl); this.StartTime = DateTimeOffset.UtcNow; @@ -334,7 +335,7 @@ public async Task GetResponseAsync(IServiceAccount account, int re HttpResponse response = await HttpResponse.ResponseFromHttpWebResponseAsync(webRequest); TimeSpan duration = DateTimeOffset.UtcNow.Subtract(this.StartTime); - response.RewriteResponseBodyNamespaces(account); + response.RewriteResponseBodyNamespaces(account, issues); var logger = HttpRequest.HttpLogSession; if (null != logger) @@ -350,7 +351,7 @@ public async Task GetResponseAsync(IServiceAccount account, int re await BackoffHelper.Default.FullJitterBackoffDelayAsync(retryCount); // Try the request again - return await this.GetResponseAsync(account, retryCount + 1); + return await this.GetResponseAsync(account, issues, retryCount + 1); } } response.RetryCount = retryCount; diff --git a/ApiDocs.Validation/Http/HttpResponse.cs b/ApiDoctor.Validation/Http/httpresponse.cs similarity index 86% rename from ApiDocs.Validation/Http/HttpResponse.cs rename to ApiDoctor.Validation/Http/httpresponse.cs index 17d98e1f..8abdcbae 100644 --- a/ApiDocs.Validation/Http/HttpResponse.cs +++ b/ApiDoctor.Validation/Http/httpresponse.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Http +namespace ApiDoctor.Validation.Http { using System; using System.Collections.Generic; @@ -32,7 +32,7 @@ namespace ApiDocs.Validation.Http using System.Net; using System.Text; using System.Threading.Tasks; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; using Newtonsoft.Json; public class HttpResponse @@ -189,13 +189,11 @@ public string FormatFullResponse(string body, bool prettyPrintJson, bool useChun /// and returns a set of validation errors for any important differences. /// /// The actual response that is compared with the expected (current object) response. - /// An array of errors that were generated by the validation + /// An array of errors that were generated by the validation /// A boolean that is True if the response is valid (no significant errors or warnings detected) or False if errors exist. - public bool ValidateResponseHeaders(HttpResponse actual, out ValidationError[] errors, IEnumerable allowedStatusCodes = null) + public bool ValidateResponseHeaders(HttpResponse actual, IssueLogger issues, IEnumerable allowedStatusCodes = null) { if (actual == null) throw new ArgumentNullException("actual"); - - List errorList = new List(); // Check the HTTP status code bool usedAllowedStatusCode = false; @@ -203,19 +201,27 @@ public bool ValidateResponseHeaders(HttpResponse actual, out ValidationError[] e { if (null != allowedStatusCodes && allowedStatusCodes.Contains(actual.StatusCode)) { - errorList.Add(new ValidationWarning(ValidationErrorCode.HttpStatusCodeDifferent, null, "Response uses an allowed status code that was different than the documentation indcaites: Expected status code: {0}, received: {1}.", this.StatusCode, actual.StatusCode)); + issues.Warning(ValidationErrorCode.HttpStatusCodeDifferent, + $"Response uses an allowed status code that was different than the documentation indcaites: Expected status code: {this.StatusCode}, received: {actual.StatusCode}."); usedAllowedStatusCode = true; } else { - errorList.Add(new ValidationError(ValidationErrorCode.HttpStatusCodeDifferent, null, "Expected status code: {0}, received: {1}.", this.StatusCode, actual.StatusCode)); + issues.Error(ValidationErrorCode.HttpStatusCodeDifferent, $"Expected status code: {this.StatusCode}, received: {actual.StatusCode}."); } } // Check the HTTP status message if (this.StatusMessage != actual.StatusMessage) { - errorList.Add(ValidationError.CreateError(usedAllowedStatusCode, ValidationErrorCode.HttpStatusMessageDifferent, null, "Expected status message {0}, received: {1}.", this.StatusMessage, actual.StatusMessage)); + if (usedAllowedStatusCode) + { + issues.Warning(ValidationErrorCode.HttpStatusMessageDifferent, $"Expected status message {this.StatusMessage}, received: {actual.StatusMessage}."); + } + else + { + issues.Error(ValidationErrorCode.HttpStatusMessageDifferent, $"Expected status message {this.StatusMessage}, received: {actual.StatusMessage}."); + } } // Check to see that expected headers were found in the response (headers listed in the @@ -231,7 +237,7 @@ public bool ValidateResponseHeaders(HttpResponse actual, out ValidationError[] e { if (!otherResponseHeaderKeys.Contains(expectedHeader, comparer)) { - errorList.Add(new ValidationError(ValidationErrorCode.HttpRequiredHeaderMissing, null, "Response is missing header expected header: {0}.", expectedHeader)); + issues.Error(ValidationErrorCode.HttpRequiredHeaderMissing, $"Response is missing header expected header: {expectedHeader}."); } else if (this.headersForPartialMatch.Contains(expectedHeader.ToLower())) { @@ -240,13 +246,12 @@ public bool ValidateResponseHeaders(HttpResponse actual, out ValidationError[] e if (!actualValue.ToLower().StartsWith(expectedValue)) { - errorList.Add(new ValidationError(ValidationErrorCode.HttpHeaderValueDifferent, null, "Header '{0}' has unexpected value '{1}' (expected {2})", expectedHeader, actualValue, expectedValue)); + issues.Error(ValidationErrorCode.HttpHeaderValueDifferent, $"Header '{expectedHeader}' has unexpected value '{actualValue}' (expected {expectedValue})"); } } } - errors = errorList.ToArray(); - return !errors.Any(x => x.IsError || x.IsWarning); + return !issues.Issues.Any(x => x.IsError || x.IsWarning); } public bool IsMatchingContentType(string expectedContentType) diff --git a/ApiDocs.Validation/HttpLog/HttpLogGenerator.cs b/ApiDoctor.Validation/HttpLog/HttpLogGenerator.cs similarity index 98% rename from ApiDocs.Validation/HttpLog/HttpLogGenerator.cs rename to ApiDoctor.Validation/HttpLog/HttpLogGenerator.cs index 2077cdd3..0383f14d 100644 --- a/ApiDocs.Validation/HttpLog/HttpLogGenerator.cs +++ b/ApiDoctor.Validation/HttpLog/HttpLogGenerator.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.HttpLog +namespace ApiDoctor.Validation.HttpLog { using System; using System.Collections.Generic; @@ -127,7 +127,7 @@ private static SessionMetadata CreateMetadataForSession(int sessionId, DateTimeO }; metadata.SessionFlags.Add(new SessionFlag { Name = SessionFlag.ClientIP, Value = "127.0.0.1" }); - metadata.SessionFlags.Add(new SessionFlag { Name = SessionFlag.ProcessInfo, Value = "apidocs.exe:1234" }); + metadata.SessionFlags.Add(new SessionFlag { Name = SessionFlag.ProcessInfo, Value = "ApiDoctor.exe:1234" }); return metadata; } diff --git a/ApiDocs.Validation/IServiceAccount.cs b/ApiDoctor.Validation/IServiceAccount.cs similarity index 98% rename from ApiDocs.Validation/IServiceAccount.cs rename to ApiDoctor.Validation/IServiceAccount.cs index cdc1f107..86a10215 100644 --- a/ApiDocs.Validation/IServiceAccount.cs +++ b/ApiDoctor.Validation/IServiceAccount.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Collections.Generic; diff --git a/ApiDocs.Validation/ItemDefinition.cs b/ApiDoctor.Validation/ItemDefinition.cs similarity index 96% rename from ApiDocs.Validation/ItemDefinition.cs rename to ApiDoctor.Validation/ItemDefinition.cs index 80e51a29..f2cb3c01 100644 --- a/ApiDocs.Validation/ItemDefinition.cs +++ b/ApiDoctor.Validation/ItemDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System.Collections.Generic; diff --git a/ApiDocs.Validation/Json/JsonExample.cs b/ApiDoctor.Validation/Json/JsonExample.cs similarity index 96% rename from ApiDocs.Validation/Json/JsonExample.cs rename to ApiDoctor.Validation/Json/JsonExample.cs index 97de9c97..9cded4c6 100644 --- a/ApiDocs.Validation/Json/JsonExample.cs +++ b/ApiDoctor.Validation/Json/JsonExample.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Json +namespace ApiDoctor.Validation.Json { public class JsonExample { diff --git a/ApiDocs.Validation/Json/JsonPath.cs b/ApiDoctor.Validation/Json/JsonPath.cs similarity index 99% rename from ApiDocs.Validation/Json/JsonPath.cs rename to ApiDoctor.Validation/Json/JsonPath.cs index 2fbd75e0..9cc9212d 100644 --- a/ApiDocs.Validation/Json/JsonPath.cs +++ b/ApiDoctor.Validation/Json/JsonPath.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Json +namespace ApiDoctor.Validation.Json { using System; using System.Collections.Generic; diff --git a/ApiDocs.Validation/Json/JsonSchema.cs b/ApiDoctor.Validation/Json/JsonSchema.cs similarity index 75% rename from ApiDocs.Validation/Json/JsonSchema.cs rename to ApiDoctor.Validation/Json/JsonSchema.cs index 9cc13abc..2c86d56c 100644 --- a/ApiDocs.Validation/Json/JsonSchema.cs +++ b/ApiDoctor.Validation/Json/JsonSchema.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,20 +23,21 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Json +namespace ApiDoctor.Validation.Json { using System; using System.Collections.Generic; using System.Globalization; using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Http; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; public class JsonSchema { - #region Properties + private Dictionary childTypes = new Dictionary(StringComparer.OrdinalIgnoreCase); + public string ResourceName { get { return this.Metadata?.ResourceType; } } public string ResourceNameAka { get { return this.Metadata?.ResourceTypeAka; } } @@ -51,12 +52,16 @@ public class JsonSchema public ResourceDefinition OriginalResource { get; set; } + public ParameterDefinition[] Properties { get { return this.ExpectedProperties.Values.ToArray(); } } - #endregion + public override string ToString() + { + return $"{ResourceName}"; + } #region Constructor public JsonSchema(string json, CodeBlockAnnotation annotation) @@ -68,39 +73,44 @@ public JsonSchema(string json, CodeBlockAnnotation annotation) public JsonSchema(ResourceDefinition resource) { this.Metadata = resource.OriginalMetadata; - this.ExpectedProperties = this.BuildSchemaFromJson(resource.ExampleText, resource.Parameters); + this.ExpectedProperties = this.BuildSchemaFromJson(resource.ExampleText, resource); this.OriginalResource = resource; } #endregion #region Json Validation Against Schemas + internal void RegisterChildType(JsonSchema schema) + { + this.childTypes[schema.ResourceName] = schema; + } + /// /// Checks that the expected response of a method definition is valid with this resource. /// /// - /// + /// /// - public bool ValidateExpectedResponse(MethodDefinition method, out ValidationError[] errors) + public bool ValidateExpectedResponse(MethodDefinition method, IssueLogger issues) { HttpParser parser = new HttpParser(); var response = parser.ParseHttpResponse(method.ExpectedResponse); JsonExample example = new JsonExample(response.Body, method.ExpectedResponseMetadata); var otherSchemas = new Dictionary(); - return this.ValidateJson(example, out errors, otherSchemas, null); + return this.ValidateJson(example, issues, otherSchemas, null); } /// /// Validate the input json against the defined scehma when the instance was created. /// /// Input json to validate against schema - /// Array of errors if the validation fails + /// /// /// /// /// True if validation was successful, otherwise false. - public bool ValidateJson(JsonExample jsonInput, out ValidationError[] errors, Dictionary otherSchemas, ValidationOptions options, JsonExample expectedJson = null) + public bool ValidateJson(JsonExample jsonInput, IssueLogger issues, Dictionary otherSchemas, ValidationOptions options, JsonExample expectedJson = null) { JContainer obj; try @@ -110,14 +120,12 @@ public bool ValidateJson(JsonExample jsonInput, out ValidationError[] errors, Di } catch (Exception ex) { - errors = new ValidationError[] { new ValidationError(ValidationErrorCode.JsonParserException, null, "Failed to parse json string: {0}. Json: {1}", ex.Message, jsonInput.JsonData) }; + issues.Error(ValidationErrorCode.JsonParserException, $"Failed to parse json string: {jsonInput.JsonData}.", ex); return false; } var annotation = jsonInput.Annotation ?? new CodeBlockAnnotation(); - List detectedErrors = new List(); - bool expectErrorObject = (jsonInput.Annotation != null) && jsonInput.Annotation.ExpectError; // Check for an error response @@ -129,16 +137,12 @@ public bool ValidateJson(JsonExample jsonInput, out ValidationError[] errors, Di string code = errorObject.code; string message = errorObject.message; - detectedErrors.Clear(); - detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObject, null, "Error response received. Code: {0}, Message: {1}", code, message)); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.JsonErrorObject, $"Error response received. Code: {code}, Message: {message}"); return false; } else if (expectErrorObject && null == errorObject) { - detectedErrors.Clear(); - detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObjectExpected, null, "Expected an error object response, but didn't receive one.")); - errors = detectedErrors.ToArray(); + issues.Error(ValidationErrorCode.JsonErrorObjectExpected, "Expected an error object response, but didn't receive one."); return false; } } @@ -146,14 +150,14 @@ public bool ValidateJson(JsonExample jsonInput, out ValidationError[] errors, Di { if (annotation.ExpectError) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObjectExpected, null, $"Expected an error object, but it doesn't look like we got one: {ex.Message}")); + issues.Error(ValidationErrorCode.JsonErrorObjectExpected, "Expected an error object, but it doesn't look like we got one.", ex); } } // Check to see if this is a "collection" instance if (null != annotation && annotation.IsCollection) { - this.ValidateCollectionObject(obj, annotation, otherSchemas, options.CollectionPropertyName, detectedErrors, options); + this.ValidateCollectionObject(obj, annotation, otherSchemas, options.CollectionPropertyName, issues, options); } // otherwise verify the object matches this schema else @@ -165,14 +169,13 @@ public bool ValidateJson(JsonExample jsonInput, out ValidationError[] errors, Di options.ExpectedJsonSchema = expectedJsonSchema; options.RequiredPropertyNames = expectedJsonSchema.ExpectedProperties.Keys.ToArray(); } - this.ValidateContainerObject(obj, options, otherSchemas, detectedErrors); + this.ValidateContainerObject(obj, options, otherSchemas, issues); } - errors = detectedErrors.ToArray(); - return detectedErrors.Count == 0; + return issues.Issues.Count() == 0; } - private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annotation, Dictionary otherSchemas, string collectionPropertyName, List detectedErrors, ValidationOptions options) + private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annotation, Dictionary otherSchemas, string collectionPropertyName, IssueLogger issues, ValidationOptions options) { // TODO: also validate additional properties on the collection, like nextDataLink JToken collection = null; @@ -188,14 +191,14 @@ private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annota } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonErrorObjectExpected, null, $"Unable to find collection parameter or array to validate: {ex.Message}")); + issues.Error(ValidationErrorCode.JsonErrorObjectExpected, $"Unable to find collection parameter or array to validate: {ex.Message}"); return; } } if (null == collection) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCollectionProperty, null, "Failed to locate collection property '{0}' in response.", collectionPropertyName)); + issues.Error(ValidationErrorCode.MissingCollectionProperty, $"Failed to locate collection property '{collectionPropertyName}' in response."); } else { @@ -203,22 +206,16 @@ private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annota { if (!annotation.IsEmpty) { - detectedErrors.Add( - new ValidationWarning( + issues.Warning( ValidationErrorCode.CollectionArrayEmpty, - null, - "Property contained an empty array that was not validated: {0}", - collectionPropertyName)); + $"Property contained an empty array that was not validated: {collectionPropertyName}"); } } else if (annotation.IsEmpty) { - detectedErrors.Add( - new ValidationWarning( + issues.Warning( ValidationErrorCode.CollectionArrayNotEmpty, - null, - "Property contained a non-empty array that was expected to be empty: {0}", - collectionPropertyName)); + $"Property contained a non-empty array that was expected to be empty: {collectionPropertyName}"); } foreach (var jToken in collection) @@ -226,8 +223,6 @@ private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annota var container = jToken as JContainer; if (null != container) { - List containerErrors = new List(); - var deeperOptions = new ValidationOptions(options) { AllowTruncatedResponses = annotation.TruncatedResult @@ -237,9 +232,7 @@ private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annota container, deeperOptions, otherSchemas, - containerErrors); - - detectedErrors.AddUniqueErrors(containerErrors); + issues.For("container", onlyKeepUniqueErrors: true)); } } } @@ -251,16 +244,40 @@ private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annota /// /// /// - /// - private void ValidateContainerObject(JContainer obj, ValidationOptions options, Dictionary otherSchemas, List detectedErrors) + /// + private void ValidateContainerObject(JContainer obj, ValidationOptions options, Dictionary otherSchemas, IssueLogger issues) { + var explicitType = obj["@odata.type"]; + if (explicitType != null) + { + var typeName = explicitType.Value(); + if (!string.IsNullOrEmpty(typeName)) + { + if (typeName.StartsWith("#")) + { + typeName = typeName.Substring(1); + } + + JsonSchema childType; + if (this.childTypes.TryGetValue(typeName, out childType)) + { + childType.ValidateContainerObject(obj, options, otherSchemas, issues); + return; + } + else if (!typeName.IEquals(this.ResourceName)) + { + issues.Warning(ValidationErrorCode.ResourceTypeNotFound, $"unrecognized type declaration {typeName}"); + } + } + } + var containerProperties = from p in obj - select ParseProperty(p, this, detectedErrors); + select ParseProperty(p, this, issues.For(p.Path)); - this.ValidateObjectProperties(containerProperties.Where(x => null != x), options, otherSchemas, detectedErrors); + this.ValidateObjectProperties(containerProperties.Where(x => null != x), options, otherSchemas, issues); } - private void ValidateObjectProperties(IEnumerable propertiesOnObject, ValidationOptions options, Dictionary otherSchemas, List detectedErrors) + private void ValidateObjectProperties(IEnumerable propertiesOnObject, ValidationOptions options, Dictionary otherSchemas, IssueLogger issues) { List missingProperties = new List(); missingProperties.AddRange(from m in this.ExpectedProperties select m.Key); @@ -273,18 +290,19 @@ private void ValidateObjectProperties(IEnumerable propertie if (null != options && (property.Type.IsCollection || property.Type.IsObject)) { var propertyOptions = options.CreateForProperty(property.Name); - this.ValidateProperty(property, otherSchemas, detectedErrors, propertyOptions); + this.ValidateProperty(property, otherSchemas, issues.For(property.Name), propertyOptions); } else { - this.ValidateProperty(property, otherSchemas, detectedErrors, options); + this.ValidateProperty(property, otherSchemas, issues.For(property.Name), options); } } this.CleanMissingProperties(options, missingProperties); if (missingProperties.Count > 0) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.RequiredPropertiesMissing, null, "Missing properties: response was missing these required properties: {0}", missingProperties.ComponentsJoinedByString(", "))); + issues.Error(ValidationErrorCode.RequiredPropertiesMissing, + $"Missing properties: response was missing these required properties: {missingProperties.ComponentsJoinedByString(", ")}"); } } @@ -315,7 +333,7 @@ private void CleanMissingProperties(ValidationOptions options, List miss } } - private static PropertyValidationOutcome ValidateSameDataType(ParameterDefinition expected, ParameterDefinition actual, List detectedErrors, bool relaxStringValidation) + private static PropertyValidationOutcome ValidateSameDataType(ParameterDefinition expected, ParameterDefinition actual, IssueLogger issues, bool relaxStringValidation) { if (expected.Type == actual.Type && actual.Type != ParameterDataType.String) { @@ -328,7 +346,7 @@ private static PropertyValidationOutcome ValidateSameDataType(ParameterDefinitio { return PropertyValidationOutcome.Ok; } - return ValidateStringFormat(expected, actual, detectedErrors); + return ValidateStringFormat(expected, actual, issues); } else if (expected.Type == ParameterDataType.String && expected.Type.IsLessSpecificThan(actual.Type)) { @@ -338,41 +356,22 @@ private static PropertyValidationOutcome ValidateSameDataType(ParameterDefinitio { if (relaxStringValidation) { - detectedErrors.Add( - new ValidationWarning( - ValidationErrorCode.ExpectedTypeDifferent, - null, - "Expected type {0} but actual was {1}, which is less specific than the expected type. Property: {2}, actual value: '{3}'", - expected.Type, - actual.Type, - actual.Name, - actual.OriginalValue)); + issues.Message( + $"Expected type {expected.Type} but actual was {actual.Type}, which is less specific than the expected type. Property: {actual.Name}, actual value: '{actual.OriginalValue}'"); return PropertyValidationOutcome.Ok; } - detectedErrors.Add( - new ValidationError( + issues.Error( ValidationErrorCode.ExpectedTypeDifferent, - null, - "Expected type {0} but actual was {1}, which is less specific than the expected type. Property: {2}, actual value: '{3}'", - expected.Type, - actual.Type, - actual.Name, - actual.OriginalValue)); + $"Expected type {expected.Type} but actual was {actual.Type}, which is less specific than the expected type. Property: {actual.Name}, actual value: '{actual.OriginalValue}'"); return PropertyValidationOutcome.BadStringValue; } else { // Type of the inputProperty is mismatched from the expected value. - detectedErrors.Add( - new ValidationError( + issues.Error( ValidationErrorCode.ExpectedTypeDifferent, - null, - "Expected type {0} but was instead {1}. Property: {2}, actual value: '{3}'", - expected.Type, - actual.Type, - actual.Name, - actual.OriginalValue)); + $"Expected type {expected.Type} but actual was {actual.Type}. Property: {actual.Name}, actual value: '{actual.OriginalValue}'"); return PropertyValidationOutcome.InvalidType; } } @@ -380,12 +379,7 @@ private static PropertyValidationOutcome ValidateSameDataType(ParameterDefinitio /// /// Verify that a property from the json-to-validate matches something in our schema /// - /// - /// - /// - /// - /// - private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProperty, Dictionary schemas, List detectedErrors, ValidationOptions options) + private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProperty, Dictionary schemas, IssueLogger issues, ValidationOptions options) { if (this.ExpectedProperties.ContainsKey(inputProperty.Name)) { @@ -395,13 +389,13 @@ private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProp // Check for simple value types first if (this.SimpleValueTypes(schemaPropertyDef.Type, inputProperty.Type) && this.AllFalse(schemaPropertyDef.Type.IsCollection, inputProperty.Type.IsCollection)) { - return ValidateSameDataType(schemaPropertyDef, inputProperty, detectedErrors, (null != options) ? options.RelaxedStringValidation : false); + return ValidateSameDataType(schemaPropertyDef, inputProperty, issues, (null != options) ? options.RelaxedStringValidation : false); } else if (null == inputProperty.OriginalValue) { if (null != this.NullableProperties && !this.NullableProperties.Contains(schemaPropertyDef.Name)) { - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.NullPropertyValue, null, "Non-nullable property {0} had a null value in the response. Expected {1}.", schemaPropertyDef.Name, schemaPropertyDef.Type)); + issues.Warning(ValidationErrorCode.NullPropertyValue, $"Non-nullable property {schemaPropertyDef.Name} had a null value in the response. Expected {schemaPropertyDef.Type}."); } return PropertyValidationOutcome.Ok; } @@ -411,39 +405,44 @@ private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProp if (schemaPropertyDef.Type.IsCollection && !inputProperty.Type.IsCollection) { // Expected an array, but didn't get one - detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedArrayValue, null, "Expected an array but property was not an array: {0}", inputProperty.Name)); + issues.Error(ValidationErrorCode.ExpectedArrayValue, $"Expected an array but property was not an array: {inputProperty.Name}"); return PropertyValidationOutcome.InvalidType; } else if (!schemaPropertyDef.Type.IsCollection && inputProperty.Type.IsCollection) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedNonArrayValue, null, "Expected a value of type {0} but property was an array: {1}", schemaPropertyDef.Type, inputProperty.Name)); + issues.Error(ValidationErrorCode.ExpectedNonArrayValue, $"Expected a value of type {schemaPropertyDef.Type} but property was an array: {inputProperty.Name}"); return PropertyValidationOutcome.InvalidType; } - return this.ValidateArrayProperty(inputProperty, schemas, detectedErrors, options); + return this.ValidateArrayProperty(inputProperty, schemas, issues, options); } else if (schemaPropertyDef.Type.IsObject && inputProperty.Type.IsObject) { // Compare the ODataType schema to the custom schema if (null == schemaPropertyDef.Type.CustomTypeName || !schemas.ContainsKey(schemaPropertyDef.Type.CustomTypeName)) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Missing resource: resource {0} was not found (property name '{1}').", schemaPropertyDef.Type.CustomTypeName, inputProperty.Name)); + issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Missing resource: resource {schemaPropertyDef.Type.CustomTypeName} was not found (property name '{inputProperty.Name}')."); return PropertyValidationOutcome.MissingResourceType; } else if (inputProperty.Type.IsObject) { var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName]; - ValidationError[] odataErrors; - if (null != inputProperty.Type.CustomMembers && !odataSchema.ValidateCustomObject(inputProperty.Type.CustomMembers.ToArray(), out odataErrors, schemas, options)) + if (null != inputProperty.Type.CustomMembers && !odataSchema.ValidateCustomObject(inputProperty.Type.CustomMembers.ToArray(), issues, schemas, options)) { - var propertyError = ValidationError.NewConsolidatedError(ValidationErrorCode.ConsolidatedError, odataErrors, "Schemas validation failed on property '{0}' ['{1}']", inputProperty.Name, odataSchema.ResourceName); - detectedErrors.Add(propertyError); + if (issues.Errors.Any()) + { + issues.Error(ValidationErrorCode.ConsolidatedError, $"Schema validation failed on property '{inputProperty.Name}' ['{odataSchema.ResourceName}']"); + } + else + { + issues.Warning(ValidationErrorCode.ConsolidatedError, $"Schema validation failed on property '{inputProperty.Name}' ['{odataSchema.ResourceName}']"); + } return PropertyValidationOutcome.InvalidType; } else if (null == inputProperty.Type.CustomMembers) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.NoCustomMembersFound, null, "Property '{0}' is of type Custom but has no custom members.", inputProperty.Name)); + issues.Error(ValidationErrorCode.NoCustomMembersFound, $"Property '{inputProperty.Name}' is of type Custom but has no custom members."); } return PropertyValidationOutcome.Ok; } @@ -452,31 +451,29 @@ private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProp var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName]; if (inputProperty.Type.CustomMembers == null) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.MissingCustomMembers, null, "Property {0} is missing custom members and cannot be validated.", inputProperty.Name)); + issues.Error(ValidationErrorCode.MissingCustomMembers, $"Property {inputProperty.Name} is missing custom members and cannot be validated."); return PropertyValidationOutcome.InvalidType; } else { - odataSchema.ValidateObjectProperties(inputProperty.Type.CustomMembers, options, schemas, detectedErrors); + odataSchema.ValidateObjectProperties(inputProperty.Type.CustomMembers, options, schemas, issues); return PropertyValidationOutcome.Ok; } } } else if (schemaPropertyDef.Type.IsObject) { - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.CustomValidationNotSupported, null, "Schemas type was 'Custom' which is not supported. Add a resource type to the definition of property: {0}", inputProperty.Name)); + issues.Warning(ValidationErrorCode.CustomValidationNotSupported, $"Schemas type was 'Custom' which is not supported. Add a resource type to the definition of property: {inputProperty.Name}"); return PropertyValidationOutcome.MissingResourceType; } else { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectedTypeDifferent, null, "Type mismatch: property '{0}' [{1}] doesn't match expected type [{2}].", - inputProperty.Name, inputProperty.Type, schemaPropertyDef.Type)); + issues.Error(ValidationErrorCode.ExpectedTypeDifferent, $"Type mismatch: property '{inputProperty.Name}' [{inputProperty.Type}] doesn't match expected type [{schemaPropertyDef.Type}]."); return PropertyValidationOutcome.InvalidType; } } else { - // Check to see if this property is on the ignorable list string[] ignorableUndocumentedProperties = this.OriginalResource?.SourceFile.Parent.Requirements?.IgnorableProperties; @@ -510,15 +507,20 @@ private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProp return PropertyValidationOutcome.Ok; } + if (this.OriginalResource?.OriginalMetadata?.IsOpenType == true) + { + return PropertyValidationOutcome.Ok; + } + // This property isn't documented - detectedErrors.Add(new UndocumentedPropertyWarning(null, inputProperty.Name, inputProperty.Type, ResourceName)); + issues.Warning(new UndocumentedPropertyWarning(null, inputProperty.Name, inputProperty.Type, ResourceName)); return PropertyValidationOutcome.MissingFromSchema; } } - private static PropertyValidationOutcome ValidateStringFormat(ParameterDefinition schemaProperty, ParameterDefinition inputProperty, List detectedErrorsCollection) + private static PropertyValidationOutcome ValidateStringFormat(ParameterDefinition schemaProperty, ParameterDefinition inputProperty, IssueLogger issues) { switch (schemaProperty.StringFormat()) { @@ -527,7 +529,7 @@ private static PropertyValidationOutcome ValidateStringFormat(ParameterDefinitio DateTimeOffset? output = inputProperty.OriginalValue.TryParseIso8601Date(); if (!output.HasValue) { - detectedErrorsCollection.Add(new ValidationError(ValidationErrorCode.InvalidDateTimeString, null, "Invalid ISO 8601 date-time string in property: {1}: {0}", inputProperty.OriginalValue, schemaProperty.Name)); + issues.Error(ValidationErrorCode.InvalidDateTimeString, $"Invalid ISO 8601 date-time string in property: {schemaProperty.Name}: {inputProperty.OriginalValue}"); return PropertyValidationOutcome.BadStringValue; } return PropertyValidationOutcome.Ok; @@ -541,7 +543,7 @@ private static PropertyValidationOutcome ValidateStringFormat(ParameterDefinitio } catch (FormatException) { - detectedErrorsCollection.Add(new ValidationError(ValidationErrorCode.InvalidUrlString, null, "Invalid absolute URL value in property {1}: {0}", inputProperty.OriginalValue, schemaProperty.Name)); + issues.Error(ValidationErrorCode.InvalidUrlString, $"Invalid absolute URL value in property {schemaProperty.Name}: {inputProperty.OriginalValue}"); return PropertyValidationOutcome.BadStringValue; } } @@ -549,7 +551,7 @@ private static PropertyValidationOutcome ValidateStringFormat(ParameterDefinitio { if (!schemaProperty.IsValidEnumValue(inputProperty.OriginalValue)) { - detectedErrorsCollection.Add(new ValidationError(ValidationErrorCode.InvalidEnumeratedValueString, null, "Invalid enumerated value in property {1}: {0}", inputProperty.OriginalValue, schemaProperty.Name)); + issues.Error(ValidationErrorCode.InvalidEnumeratedValueString, $"Invalid enumerated value in property {schemaProperty.Name}: {inputProperty.OriginalValue}"); return PropertyValidationOutcome.BadStringValue; } return PropertyValidationOutcome.Ok; @@ -576,7 +578,7 @@ private bool AllFalse(params bool[] input) return input.All(value => !value); } - private PropertyValidationOutcome ValidateSimpleArrayProperty(ParameterDefinition actualProperty, ParameterDefinition expectedProperty, List detectedErrors) + private PropertyValidationOutcome ValidateSimpleArrayProperty(ParameterDefinition actualProperty, ParameterDefinition expectedProperty, IssueLogger issues) { if (!actualProperty.Type.IsCollection || !expectedProperty.Type.IsCollection) { @@ -593,7 +595,7 @@ private PropertyValidationOutcome ValidateSimpleArrayProperty(ParameterDefinitio } else { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ArrayTypeMismatch, null, "Array expected members to be of type {0} but found: {1}", expectedProperty.Type, actualProperty.Type)); + issues.Error(ValidationErrorCode.ArrayTypeMismatch, $"Array expected members to be of type {expectedProperty.Type} but found: {actualProperty.Type}"); return PropertyValidationOutcome.InvalidType; } } @@ -605,7 +607,7 @@ private PropertyValidationOutcome ValidateSimpleArrayProperty(ParameterDefinitio /// /// /// - private PropertyValidationOutcome ValidateArrayProperty(ParameterDefinition actualProperty, Dictionary schemas, List detectedErrors, ValidationOptions options) + private PropertyValidationOutcome ValidateArrayProperty(ParameterDefinition actualProperty, Dictionary schemas, IssueLogger issues, ValidationOptions options) { JArray actualArray = null; try @@ -627,53 +629,51 @@ private PropertyValidationOutcome ValidateArrayProperty(ParameterDefinition actu if (memberSchema == null && string.IsNullOrEmpty(actualProperty.Type.CustomTypeName)) { - return this.ValidateSimpleArrayProperty(actualProperty, this.ExpectedProperties[actualProperty.Name], detectedErrors); + return this.ValidateSimpleArrayProperty(actualProperty, this.ExpectedProperties[actualProperty.Name], issues); } else if (memberSchema == null && !schemas.TryGetValue(actualProperty.Type.CustomTypeName, out memberSchema)) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Failed to locate resource definition for: {0}", actualProperty.Type.CustomTypeName)); + issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Failed to locate resource definition for: {actualProperty.Type.CustomTypeName}"); return PropertyValidationOutcome.MissingResourceType; } bool hadErrors = false; + var memberIssues = issues.For("member", onlyKeepUniqueErrors: true); for(int i=0; i memberErrors = new List(); - memberSchema.ValidateContainerObject(member, options, schemas, memberErrors); + memberSchema.ValidateContainerObject(member, options, schemas, memberIssues); // TODO: Filter out non-unique errors - hadErrors |= memberErrors.Count > 0; - detectedErrors.AddUniqueErrors(memberErrors); + hadErrors |= memberIssues.Issues.Count() > 0; } } return hadErrors ? PropertyValidationOutcome.GenericError : PropertyValidationOutcome.Ok; } - private bool ValidateCustomObject(ParameterDefinition[] properties, out ValidationError[] errors, Dictionary otherSchemas, ValidationOptions options) + private bool ValidateCustomObject(ParameterDefinition[] properties, IssueLogger issues, Dictionary otherSchemas, ValidationOptions options) { List missingProperties = new List(this.ExpectedProperties.Keys); - List detectedErrors = new List(); foreach (var inputProperty in properties) { missingProperties.Remove(inputProperty.Name); - this.ValidateProperty(inputProperty, otherSchemas, detectedErrors, options); + this.ValidateProperty(inputProperty, otherSchemas, issues.For(inputProperty.Name), options); } this.CleanMissingProperties(options, missingProperties); - errors = detectedErrors.ToArray(); - return detectedErrors.Count == 0; + return !issues.Issues.WereWarningsOrErrors(); } #endregion #region Schemas Building - private Dictionary BuildSchemaFromJson(string json, IEnumerable parameters = null) + private Dictionary BuildSchemaFromJson(string json, ResourceDefinition resource = null) { + var parameters = resource?.Parameters; Dictionary schema = new Dictionary(); try { @@ -691,6 +691,24 @@ private Dictionary BuildSchemaFromJson(string json, schema[propertyInfo.Name] = propertyInfo; } } + + // populate inherited properties too + if (resource != null) + { + resource = resource.ResolvedBaseTypeReference; + while (resource != null) + { + foreach (var param in resource.Parameters) + { + if (!schema.ContainsKey(param.Name)) + { + schema[param.Name] = param; + } + } + + resource = resource.ResolvedBaseTypeReference; + } + } } catch (Exception ex) { @@ -753,11 +771,11 @@ internal static ParameterDefinition ParseProperty(string name, JToken value, Jso var objectSchema = GeneratePropertyCollection((JObject)value); if (objectSchema.ContainsKey("@odata.type")) { - param.Type = new ParameterDataType(objectSchema["@odata.type"].OriginalValue); + param.Type = objectSchema["@odata.type"].OriginalValue.ParseParameterDataType(); } else if (objectSchema.ContainsKey("@type")) { - param.Type = new ParameterDataType(objectSchema["@type"].OriginalValue); + param.Type = objectSchema["@type"].OriginalValue.ParseParameterDataType(); } else { @@ -813,7 +831,7 @@ internal static ParameterDefinition ParseProperty(string name, JToken value, Jso string.IsNullOrEmpty(propertyType.CustomTypeName)) { // If we don't know what kind of object is here, let's record what we see as custom members - var firstValue = (JObject)value.First; + var firstValue = value.First as JObject; members = firstValue != null ? GeneratePropertyCollection(firstValue) : new Dictionary(); } ParameterDefinition[] customMembers = null; @@ -833,7 +851,7 @@ internal static ParameterDefinition ParseProperty(string name, JToken value, Jso return param; } - private static ParameterDefinition ParseProperty(JToken token, JsonSchema containerSchema, List detectedErrors = null) + private static ParameterDefinition ParseProperty(JToken token, JsonSchema containerSchema, IssueLogger issues = null) { ParameterDefinition propertyInfo = null; if (token.Type == JTokenType.Property) @@ -843,13 +861,9 @@ private static ParameterDefinition ParseProperty(JToken token, JsonSchema contai } else { - if (detectedErrors != null) + if (issues != null) { - detectedErrors.Add( - new ValidationWarning( - ValidationErrorCode.JsonParserException, - token.Path, - "Unhandled token type: " + token.Type)); + issues.Warning(ValidationErrorCode.JsonParserException, "Unhandled token type: " + token.Type); } else { diff --git a/ApiDocs.Validation/Json/ValidationOptions.cs b/ApiDoctor.Validation/Json/ValidationOptions.cs similarity index 98% rename from ApiDocs.Validation/Json/ValidationOptions.cs rename to ApiDoctor.Validation/Json/ValidationOptions.cs index b8ba3f78..a4fd1b52 100644 --- a/ApiDocs.Validation/Json/ValidationOptions.cs +++ b/ApiDoctor.Validation/Json/ValidationOptions.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Json +namespace ApiDoctor.Validation.Json { using System.Linq; diff --git a/ApiDocs.Validation/Json/JsonResourceCollection.cs b/ApiDoctor.Validation/Json/jsonresourcecollection.cs similarity index 70% rename from ApiDocs.Validation/Json/JsonResourceCollection.cs rename to ApiDoctor.Validation/Json/jsonresourcecollection.cs index da9068b7..3864381e 100644 --- a/ApiDocs.Validation/Json/JsonResourceCollection.cs +++ b/ApiDoctor.Validation/Json/jsonresourcecollection.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,13 +23,13 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Json +namespace ApiDoctor.Validation.Json { using System; using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Http; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Http; public class JsonResourceCollection { @@ -44,16 +44,19 @@ public void RegisterJsonResource(ResourceDefinition resource) { var schema = new JsonSchema(resource); this.registeredSchema[resource.Name] = schema; + + JsonSchema baseTypeSchema; + if (resource.BaseType != null && this.registeredSchema.TryGetValue(resource.BaseType, out baseTypeSchema)) + { + baseTypeSchema.RegisterChildType(schema); + } } /// /// Validates the value of json according to an implicit schmea defined by expectedJson /// - /// - /// - /// /// - public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, string actualResponseBodyJson, out ValidationError[] errors, ValidationOptions options = null) + public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, string actualResponseBodyJson, IssueLogger issues, ValidationOptions options = null) { List newErrors = new List(); @@ -61,7 +64,6 @@ public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, if (resourceType == "stream") { // No validation since we're streaming data - errors = null; return true; } else @@ -78,12 +80,8 @@ public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, schema = new JsonSchema(actualResponseBodyJson, new CodeBlockAnnotation { ResourceType = expectedResponseAnnotation.ResourceType }); } - ValidationError[] validationJsonOutput; - this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponseBodyJson, expectedResponseAnnotation), out validationJsonOutput, options: options); - - newErrors.AddRange(validationJsonOutput); - errors = newErrors.ToArray(); - return errors.Length == 0; + this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponseBodyJson, expectedResponseAnnotation), issues, options: options); + return issues.Issues.Count() == 0; } } @@ -92,68 +90,53 @@ public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, /// from the expected request (e.g. properties that are included in the expected response are required in the actual /// response even if the metadata defines that the response is truncated) /// - /// - /// - /// - /// - /// - internal bool ValidateResponseMatchesSchema(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, out ValidationError[] schemaErrors, ValidationOptions options = null) + internal bool ValidateResponseMatchesSchema(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, IssueLogger issues, ValidationOptions options = null) { - List newErrors = new List(); - var expectedResourceType = method.ExpectedResponseMetadata.ResourceType; switch (expectedResourceType) { case "stream": case "Stream": + case "Edm.stream": + case "Edm.Stream": // No validation since we're streaming data - schemaErrors = new ValidationError[0]; return true; case "string": case "String": case "Edm.String": case" Edm.string": - schemaErrors = new ValidationError[0]; return true; } // Get a reference of our JsonSchema that we're checking the response with var expectedResponseJson = (null != expectedResponse) ? expectedResponse.Body : null; - JsonSchema schema = this.GetJsonSchema(expectedResourceType, newErrors, expectedResponseJson); + JsonSchema schema = this.GetJsonSchema(expectedResourceType, issues, expectedResponseJson); if (null == schema) { - newErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Unable to locate a definition for resource type: {0}", expectedResourceType)); + issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Unable to locate a definition for resource type: {expectedResourceType}"); } else { - ValidationError[] validationJsonOutput; - this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), out validationJsonOutput, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options); - newErrors.AddRange(validationJsonOutput); + this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), issues, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options); } - schemaErrors = newErrors.ToArray(); - - if (schemaErrors.WereWarningsOrErrors() && schema.ResourceNameAka != null) + if (issues.Issues.WereWarningsOrErrors() && method.ExpectedResponseMetadata.ResourceTypeAka != null) { - var expectedResourceTypeAka = schema.ResourceNameAka; - schema = this.GetJsonSchema(expectedResourceTypeAka, newErrors, expectedResponseJson); + var expectedResourceTypeAka = method.ExpectedResponseMetadata.ResourceTypeAka; + schema = this.GetJsonSchema(expectedResourceTypeAka, issues, expectedResponseJson); if (null == schema) { - newErrors.Add(new ValidationError(ValidationErrorCode.ResourceTypeNotFound, null, "Unable to locate a definition for resource type: {0}", expectedResourceTypeAka)); + issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Unable to locate a definition for resource type: {expectedResourceTypeAka}"); } else { - ValidationError[] validationJsonOutput; - this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), out validationJsonOutput, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options); - newErrors.AddRange(validationJsonOutput); + this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), issues, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options); } - schemaErrors = newErrors.ToArray(); } - return !schemaErrors.WereWarningsOrErrors(); - + return !issues.Issues.WereWarningsOrErrors(); } /// @@ -161,20 +144,20 @@ internal bool ValidateResponseMatchesSchema(MethodDefinition method, HttpRespons /// creating a new temporary schema from the fallback. /// /// - /// + /// /// /// - public JsonSchema GetJsonSchema(string resourceType, IList errors, string jsonStringForFallbackIfMissingResource) + public JsonSchema GetJsonSchema(string resourceType, IssueLogger issues, string jsonStringForFallbackIfMissingResource) { JsonSchema schema; if (string.IsNullOrEmpty(resourceType)) { - errors.Add(new ValidationError(ValidationErrorCode.ResponseResourceTypeMissing, null, "Resource type was null or missing, so we assume there is no response to validate.")); + issues.Error(ValidationErrorCode.ResponseResourceTypeMissing, "Resource type was null or missing, so we assume there is no response to validate."); schema = JsonSchema.EmptyResponseSchema; } else if (!this.registeredSchema.TryGetValue(resourceType, out schema) && !string.IsNullOrEmpty(jsonStringForFallbackIfMissingResource)) { - errors.Add(new ValidationWarning(ValidationErrorCode.ResponseResourceTypeMissing, null, "Missing required resource: {0}. Validation based on fallback example.", resourceType)); + issues.Warning(ValidationErrorCode.ResponseResourceTypeMissing, $"Missing required resource: {resourceType}. Validation based on fallback example."); // Create a new schema based on what's avaiable in the expected response JSON schema = new JsonSchema(jsonStringForFallbackIfMissingResource, new CodeBlockAnnotation { ResourceType = resourceType }); } @@ -188,15 +171,20 @@ public JsonSchema GetJsonSchema(string resourceType, IList erro /// /// Schemas definition used as a reference. /// Input json example to be validated - /// Out parameter that provides any errors, warnings, or messages that were generated + /// /// /// - public bool ValidateJsonCompilesWithSchema(JsonSchema schema, JsonExample inputJson, out ValidationError[] errors, JsonExample expectedJson = null, ValidationOptions options = null) + public bool ValidateJsonCompilesWithSchema(JsonSchema schema, JsonExample inputJson, IssueLogger issues, JsonExample expectedJson = null, ValidationOptions options = null) { if (null == schema) + { throw new ArgumentNullException("schema"); + } + if (null == inputJson) + { throw new ArgumentNullException("inputJson"); + } string collectionPropertyName = "value"; if (null != inputJson.Annotation && null != inputJson.Annotation.CollectionPropertyName) @@ -209,7 +197,7 @@ public bool ValidateJsonCompilesWithSchema(JsonSchema schema, JsonExample inputJ options.AllowTruncatedResponses = (inputJson.Annotation ?? new CodeBlockAnnotation()).TruncatedResult; options.CollectionPropertyName = collectionPropertyName; - return schema.ValidateJson(inputJson, out errors, this.registeredSchema, options, expectedJson); + return schema.ValidateJson(inputJson, issues, this.registeredSchema, options, expectedJson); } internal void RegisterJsonResources(IEnumerable resources) diff --git a/ApiDocs.Validation/Json/JsonRewriter.cs b/ApiDoctor.Validation/Json/jsonrewriter.cs similarity index 82% rename from ApiDocs.Validation/Json/JsonRewriter.cs rename to ApiDoctor.Validation/Json/jsonrewriter.cs index 070b7074..2e6eaf89 100644 --- a/ApiDocs.Validation/Json/JsonRewriter.cs +++ b/ApiDoctor.Validation/Json/jsonrewriter.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -24,13 +24,14 @@ */ -namespace ApiDocs.Validation.Json +namespace ApiDoctor.Validation.Json { using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Linq; using System.Collections.Generic; + using ApiDoctor.Validation.Error; public class JsonRewriter { @@ -39,20 +40,18 @@ public class JsonRewriter /// Rewrite an input JSON string to adhere to the approriate output schema. This method will use the /// rewriteMap to convert from name -> value in the rewriteMap. /// - /// - /// /// - public static string RewriteJsonProperties(string sourceJson, Dictionary rewriteMap) + public static string RewriteJsonProperties(string sourceJson, Dictionary rewriteMap, IssueLogger issues) { if (string.IsNullOrWhiteSpace(sourceJson)) return sourceJson; JContainer source = (JContainer)JsonConvert.DeserializeObject(sourceJson); - ApplyMapToJsonObject(source, rewriteMap); + ApplyMapToJsonObject(source, rewriteMap, issues); return JsonConvert.SerializeObject(source); } - private static void ApplyMapToPropertyNames(JContainer source, Dictionary rewriteMap) + private static void ApplyMapToPropertyNames(JContainer source, Dictionary rewriteMap, IssueLogger issues) { if (null == source) return; @@ -70,23 +69,24 @@ private static void ApplyMapToPropertyNames(JContainer source, Dictionary rewriteMap) + private static void ApplyMapToJsonObject(JToken token, Dictionary rewriteMap, IssueLogger issues) { if (token is JArray) { JArray array = (JArray)token; foreach (var item in array) { - ApplyMapToJsonObject(item, rewriteMap); + ApplyMapToJsonObject(item, rewriteMap, issues); } - } else if (token is JContainer) + } + else if (token is JContainer) { - ApplyMapToPropertyNames((JContainer)token, rewriteMap); + ApplyMapToPropertyNames((JContainer)token, rewriteMap, issues); } else if (token is JValue) { @@ -94,10 +94,8 @@ private static void ApplyMapToJsonObject(JToken token, Dictionary public class MethodDefinition : ItemDefinition { - #region Constants internal const string MimeTypeJson = "application/json"; internal const string MimeTypeMultipartRelated = "multipart/related"; - internal const string MimeTypePlainText = "text/plain"; - #endregion + internal readonly static HashSet ContentTypesNotToValidate = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "text/plain", + "image/jpeg", + "application/octet-stream" + }; #region Constructors / Class Factory public MethodDefinition() @@ -61,7 +64,7 @@ public MethodDefinition() this.RequiredTags = new string[0]; } - public static MethodDefinition FromRequest(string request, CodeBlockAnnotation annotation, DocFile source) + public static MethodDefinition FromRequest(string request, CodeBlockAnnotation annotation, DocFile source, IssueLogger issues) { var method = new MethodDefinition { @@ -74,6 +77,37 @@ public static MethodDefinition FromRequest(string request, CodeBlockAnnotation a RequiredTags = annotation.RequiredTags }; method.Title = method.Identifier; + + try + { + var requestExample = new HttpParser().ParseHttpRequest(request); + if (!string.IsNullOrEmpty(requestExample.Body) && requestExample.Body.Contains("{")) + { + if (string.IsNullOrEmpty(annotation.ResourceType)) + { + annotation.ResourceType = "requestBodyResourceFor." + method.Identifier; + } + + var body = requestExample.Body; + if (requestExample.ContentType.StartsWith(MimeTypeMultipartRelated, StringComparison.OrdinalIgnoreCase)) + { + var multipartContent = new MultipartMime.MultipartMimeContent(requestExample.ContentType, body); + var part = multipartContent.PartWithId(""); + body = part.Body; + } + + var requestBodyResource = new JsonResourceDefinition(annotation, body, source, issues); + if (requestBodyResource.Parameters != null) + { + method.RequestBodyParameters.AddRange(requestBodyResource.Parameters); + } + } + } + catch (Exception ex) + { + issues.Warning(ValidationErrorCode.HttpParserError, $"Unable to parse request body resource for method {method.Identifier}", ex); + } + return method; } #endregion @@ -168,6 +202,11 @@ public void AddTestParams(string rawContent) } } + public override string ToString() + { + return $"id:{this.Identifier},title:{this.Title},desc:{this.Description}"; + } + #region Validation / Request Methods /// @@ -344,7 +383,7 @@ internal static void AddTestHeaderToRequest(ScenarioDefinition scenario, HttpReq "method-name: {0}; scenario-name: {1}", scenario.MethodName, scenario.Description); - request.Headers.Add("ApiDocsTestInfo", headerValue); + request.Headers.Add("ApiDoctorTestInfo", headerValue); } /// @@ -427,8 +466,7 @@ internal static void RewriteHeadersWithParameters(HttpRequest request, IEnumerab /// /// Check to ensure the http request is valid /// - /// - internal void VerifyRequestFormat(List detectedErrors) + internal void VerifyRequestFormat(IssueLogger issues) { HttpParser parser = new HttpParser(); HttpRequest request; @@ -438,20 +476,19 @@ internal void VerifyRequestFormat(List detectedErrors) } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.HttpParserError, null, "Exception while parsing HTTP request: {0}", ex.Message)); + issues.Error(ValidationErrorCode.HttpParserError, "Exception while parsing HTTP request.", ex); return; } if (null != request.ContentType) { - ValidateContentForType(new MimeContentType(request.ContentType), request.Body, detectedErrors); + ValidateContentForType(new MimeContentType(request.ContentType), request.Body, issues); } - var verifyApiRequirementsResponse = request.IsRequestValid(this.SourceFile.DisplayName, this.SourceFile.Parent.Requirements); - detectedErrors.AddRange(verifyApiRequirementsResponse.Messages); + var verifyApiRequirementsResponse = request.IsRequestValid(this.SourceFile.DisplayName, this.SourceFile.Parent.Requirements, issues.For(this.SourceFile.DisplayName)); } - private void ValidateContentForType(MimeContentType contentType, string content, List detectedErrors, bool validateJsonSchema = false) + private void ValidateContentForType(MimeContentType contentType, string content, IssueLogger issues, bool validateJsonSchema = false) { if (contentType.MimeType.Equals(MimeTypeJson, StringComparison.OrdinalIgnoreCase)) { @@ -462,59 +499,47 @@ private void ValidateContentForType(MimeContentType contentType, string content, if (validateJsonSchema) { - ValidationError[] schemaErrors; - if (!ContentMatchesResourceSchema(content, RequestMetadata.ResourceType, SourceFile.Parent.ResourceCollection, out schemaErrors)) - { - detectedErrors.AddRange(schemaErrors); - } + ContentMatchesResourceSchema(content, RequestMetadata.ResourceType, SourceFile.Parent.ResourceCollection, issues); } } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonParserException, null, "Invalid JSON format: {0}", ex.Message)); + issues.Error(ValidationErrorCode.JsonParserException, "Invalid JSON format.", ex); } } - else if (contentType.MimeType.Equals(MimeTypeMultipartRelated, StringComparison.OrdinalIgnoreCase)) + else if (contentType.MimeType.StartsWith(MimeTypeMultipartRelated, StringComparison.OrdinalIgnoreCase)) { // Parse the multipart/form-data body to ensure it's properly formatted try { MultipartMimeContent multipartContent = new MultipartMime.MultipartMimeContent(contentType, content); var part = multipartContent.PartWithId(""); - ValidateContentForType(part.ContentType, part.Body, detectedErrors, validateJsonSchema: true); + ValidateContentForType(part.ContentType, part.Body, issues, validateJsonSchema: true); } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ContentFormatException, null, "Invalid Multipart MIME content format: {0}", ex.Message)); + issues.Error(ValidationErrorCode.ContentFormatException, "Invalid Multipart MIME content format.", ex); } } - else if (contentType.MimeType.Equals(MimeTypePlainText, StringComparison.OrdinalIgnoreCase)) + else if (ContentTypesNotToValidate.Contains(contentType.MimeType)) { // Ignore this, because it isn't something we can verify } else { - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.UnsupportedContentType, null, "Unvalidated request content type: {0}", contentType.MimeType)); + issues.Warning(ValidationErrorCode.UnsupportedContentType, $"Unvalidated request content type: {contentType.MimeType}"); } } - private bool ContentMatchesResourceSchema(string content, string resourceType, JsonResourceCollection resources, out ValidationError[] schemaErrors) + private bool ContentMatchesResourceSchema(string content, string resourceType, JsonResourceCollection resources, IssueLogger issues) { List errors = new List(); - var resourceTypeSchema = resources.GetJsonSchema(resourceType, errors, null); - - ValidationError[] validationErrors; - if (!resources.ValidateJsonCompilesWithSchema(resourceTypeSchema, new JsonExample(content) { Annotation = new CodeBlockAnnotation { TruncatedResult = true } }, out validationErrors)) - { - errors.AddRange(validationErrors); - } - - schemaErrors = errors.ToArray(); - return errors.WereErrors(); + var resourceTypeSchema = resources.GetJsonSchema(resourceType, issues, null); + resources.ValidateJsonCompilesWithSchema(resourceTypeSchema, new JsonExample(content) { Annotation = new CodeBlockAnnotation { TruncatedResult = true } }, issues); + return issues.Issues.WereErrors(); } - /// /// Validates that a particular HttpResponse matches the method definition and optionally the expected response. /// @@ -522,35 +547,28 @@ private bool ContentMatchesResourceSchema(string content, string resourceType, J /// Actual response from the service (this is what we validate). /// Prototype response (expected) that shows what a valid response should look like. /// A test scenario used to generate the response, which may include additional parameters to verify. - /// A collection of errors, warnings, and verbose messages generated by this process. - public void ValidateResponse(HttpResponse actualResponse, HttpResponse expectedResponse, ScenarioDefinition scenario, out ValidationError[] errors, ValidationOptions options = null) + /// A collection of errors, warnings, and verbose messages generated by this process. + public void ValidateResponse(HttpResponse actualResponse, HttpResponse expectedResponse, ScenarioDefinition scenario, IssueLogger issues, ValidationOptions options = null) { if (null == actualResponse) throw new ArgumentNullException("actualResponse"); - List detectedErrors = new List(); - // Verify the request is valid (headers, etc) - this.VerifyRequestFormat(detectedErrors); + this.VerifyRequestFormat(issues); // Verify that the expected response headers match the actual response headers - ValidationError[] httpErrors; - if (null != expectedResponse && !expectedResponse.ValidateResponseHeaders(actualResponse, out httpErrors, (null != scenario) ? scenario.AllowedStatusCodes : null)) + if (null != expectedResponse) { - detectedErrors.AddRange(httpErrors); + expectedResponse.ValidateResponseHeaders(actualResponse, issues, (null != scenario) ? scenario.AllowedStatusCodes : null); } // Verify the actual response body is correct according to the schema defined for the response - ValidationError[] bodyErrors; - this.VerifyResponseBody(actualResponse, expectedResponse, out bodyErrors, options); - detectedErrors.AddRange(bodyErrors); + this.VerifyResponseBody(actualResponse, expectedResponse, issues, options); // Verify any expectations in the scenario are met if (null != scenario) { - scenario.ValidateExpectations(actualResponse, detectedErrors); + scenario.ValidateExpectations(actualResponse, issues); } - - errors = detectedErrors.ToArray(); } /// @@ -559,47 +577,38 @@ public void ValidateResponse(HttpResponse actualResponse, HttpResponse expectedR /// The MethodDefinition that generated the response. /// The actual response from the service to validate. /// The prototype expected response from the service. - /// A collection of errors that will be appended with any detected errors - private void VerifyResponseBody(HttpResponse actualResponse, HttpResponse expectedResponse, out ValidationError[] errors, ValidationOptions options = null) + /// A collection of errors that will be appended with any detected errors + private void VerifyResponseBody(HttpResponse actualResponse, HttpResponse expectedResponse, IssueLogger issues, ValidationOptions options = null) { - List detectedErrors = new List(); - if (string.IsNullOrEmpty(actualResponse.Body) && (expectedResponse != null && !string.IsNullOrEmpty(expectedResponse.Body))) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.HttpBodyExpected, null, "Body missing from response (expected response includes a body or a response type was provided).")); + issues.Error(ValidationErrorCode.HttpBodyExpected, "Body missing from response (expected response includes a body or a response type was provided)."); } else if (!string.IsNullOrEmpty(actualResponse.Body)) { - ValidationError[] schemaErrors; if (this.ExpectedResponseMetadata == null || (string.IsNullOrEmpty(this.ExpectedResponseMetadata.ResourceType) && (expectedResponse != null && !string.IsNullOrEmpty(expectedResponse.Body)))) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ResponseResourceTypeMissing, null, "Expected a response, but resource type on method is missing: {0}", this.Identifier)); + issues.Error(ValidationErrorCode.ResponseResourceTypeMissing, $"Expected a response, but resource type on method is missing: {this.Identifier}"); } else { var otherResources = this.SourceFile.Parent.ResourceCollection; - if ( - !otherResources.ValidateResponseMatchesSchema( + otherResources.ValidateResponseMatchesSchema( this, actualResponse, expectedResponse, - out schemaErrors, - options)) - { - detectedErrors.AddRange(schemaErrors); - } + issues, + options); } var responseValidation = actualResponse.IsResponseValid( this.SourceFile.DisplayName, - this.SourceFile.Parent.Requirements); - detectedErrors.AddRange(responseValidation.Messages); + this.SourceFile.Parent.Requirements, + issues.For(this.SourceFile.DisplayName)); } - - errors = detectedErrors.ToArray(); } #endregion diff --git a/ApiDocs.Validation/MultipartMime/MimeContentType.cs b/ApiDoctor.Validation/MultipartMime/MimeContentType.cs similarity index 97% rename from ApiDocs.Validation/MultipartMime/MimeContentType.cs rename to ApiDoctor.Validation/MultipartMime/MimeContentType.cs index 2947e4c9..3f7da044 100644 --- a/ApiDocs.Validation/MultipartMime/MimeContentType.cs +++ b/ApiDoctor.Validation/MultipartMime/MimeContentType.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.MultipartMime +namespace ApiDoctor.Validation.MultipartMime { using System; using System.Collections.Generic; diff --git a/ApiDocs.Validation/MultipartMime/MultipartMimeContent.cs b/ApiDoctor.Validation/MultipartMime/MultipartMimeContent.cs similarity index 99% rename from ApiDocs.Validation/MultipartMime/MultipartMimeContent.cs rename to ApiDoctor.Validation/MultipartMime/MultipartMimeContent.cs index aa5d95d2..971a24a1 100644 --- a/ApiDocs.Validation/MultipartMime/MultipartMimeContent.cs +++ b/ApiDoctor.Validation/MultipartMime/MultipartMimeContent.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -24,7 +24,7 @@ */ -namespace ApiDocs.Validation.MultipartMime +namespace ApiDoctor.Validation.MultipartMime { using Tags; using System; diff --git a/ApiDocs.Validation/OData/Action.cs b/ApiDoctor.Validation/OData/Action.cs similarity index 58% rename from ApiDocs.Validation/OData/Action.cs rename to ApiDoctor.Validation/OData/Action.cs index 6f8ac985..d6516dd5 100644 --- a/ApiDocs.Validation/OData/Action.cs +++ b/ApiDoctor.Validation/OData/Action.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,16 +23,15 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { - using Utility; using System; using System.Collections.Generic; using System.Linq; using System.Xml; - using System.Xml.Linq; using System.Xml.Serialization; using Transformation; + using Utility; /// /// Action in OData is allowed to modify data on the @@ -51,11 +50,14 @@ public Action() : base() } [Mergable(CollectionIdentifier = "ElementIdentifier", CollapseSingleItemMatchingProperty = "Name")] - public class ActionOrFunctionBase : XmlBackedTransformableObject + public class ActionOrFunctionBase : XmlBackedTransformableObject, IODataAnnotatable { [XmlAttribute("Name"), SortBy, MergePolicy(MergePolicy.EqualOrNull)] public string Name { get; set; } + [XmlIgnore] + public string ParameterizedName { get; set; } + [XmlAttribute("IsBound"), MergePolicy(MergePolicy.PreferGreaterValue)] public bool IsBound { get; set; } @@ -65,11 +67,83 @@ public class ActionOrFunctionBase : XmlBackedTransformableObject [XmlElement("ReturnType")] public ReturnType ReturnType { get; set; } + [XmlIgnore, MergePolicy(MergePolicy.Ignore)] + public object SourceMethods { get; set; } + + [XmlAttribute("SourceFiles"), MergePolicy(MergePolicy.EqualOrNull)] + public string SourceFiles { get; set; } + + [XmlElement("Annotation", Namespace = ODataParser.EdmNamespace), MergePolicy(MergePolicy.EqualOrNull)] + public List Annotation { get; set; } + protected ActionOrFunctionBase() { this.Parameters = new List(); } + // Func1 can substitute for Func2 if Func1 is contravariant on the input types and covariant on the output type. + // That is, if Func1 + // - is bound to the same type or a base type of what Func2 is bound to + // - returns the same type or a derived type of what Func2 returns + // then Func1 can be safely used instead of Func2. + // in practice, this is almost always just an identical function bound to a base type. + // but it could also indicate quirky examples. + // for now we'll match on exact return type because there aren't any scenarios that require otherwise. + // but if one arises, consider this comment permission to relax that check. + public bool CanSubstituteFor(ActionOrFunctionBase other, EntityFramework edmx) + { + if (other != null && + other.Name == this.Name && + other.IsBound == this.IsBound && + other.GetType() == this.GetType() && + other.Parameters?.Count == this.Parameters?.Count && + ((other.ReturnType != null && this.ReturnType != null && other.ReturnType.Equals(this.ReturnType)) || + (other.ReturnType == null && this.ReturnType == null))) + { + // each parameter must match contravariantly + foreach (var thisParameter in this.Parameters) + { + var otherParameter = other.Parameters.SingleOrDefault(p => p.Name == thisParameter.Name); + if (otherParameter == null || + otherParameter.Type == null || + thisParameter.Type == null || + otherParameter.Nullable != thisParameter.Nullable || + otherParameter.Unicode != thisParameter.Unicode) + { + return false; + } + + var thisTypeName = thisParameter.Type.ElementName(); + var otherTypeName = otherParameter.Type.ElementName(); + + if (thisTypeName != otherTypeName) + { + var otherTypeObj = edmx.DataServices.Schemas.FindTypeWithIdentifier(otherTypeName) as ComplexType; + + bool match = false; + while (!match && otherTypeObj != null) + { + if (otherTypeObj.Name == thisTypeName.TypeOnly()) + { + match = true; + break; + } + + otherTypeObj = edmx.DataServices.Schemas.FindTypeWithIdentifier(otherTypeObj.BaseType) as ComplexType; + } + + if (!match) + { + return false; + } + } + } + + return true; + } + + return false; + } #region ITransformable [XmlIgnore, MergePolicy(MergePolicy.Ignore)] diff --git a/ApiDocs.Validation/OData/Annotations.cs b/ApiDoctor.Validation/OData/Annotations.cs similarity index 97% rename from ApiDocs.Validation/OData/Annotations.cs rename to ApiDoctor.Validation/OData/Annotations.cs index 83bbcb8f..14970d2e 100644 --- a/ApiDocs.Validation/OData/Annotations.cs +++ b/ApiDoctor.Validation/OData/Annotations.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using Transformation; diff --git a/ApiDocs.Validation/OData/ComplexType.cs b/ApiDoctor.Validation/OData/ComplexType.cs similarity index 77% rename from ApiDocs.Validation/OData/ComplexType.cs rename to ApiDoctor.Validation/OData/ComplexType.cs index f5be0102..b285ef1c 100644 --- a/ApiDocs.Validation/OData/ComplexType.cs +++ b/ApiDoctor.Validation/OData/ComplexType.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; @@ -33,6 +33,7 @@ namespace ApiDocs.Validation.OData using System.Xml.Linq; using System.Xml.Serialization; using Transformation; + using ApiDoctor.Validation.Error; [XmlRoot("ComplexType", Namespace = ODataParser.EdmNamespace)] [Mergable(CollectionIdentifier = "Name")] @@ -44,6 +45,9 @@ public ComplexType() this.Annotation = new List(); } + [XmlAttribute("Abstract"), DefaultValue(false), MergePolicy(MergePolicy.PreferTrueValue)] + public bool Abstract { get; set; } + [XmlAttribute("Name"), SortBy] public string Name { get; set; } @@ -62,7 +66,10 @@ public ComplexType() [XmlAttribute("WorkloadName", Namespace = ODataParser.AgsNamespace), MergePolicy(MergePolicy.EqualOrNull)] public string WorkloadName { get; set; } - public virtual IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx) + [XmlIgnore] + public HashSet Contributors { get; } = new HashSet(); + + public virtual IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues) { if (this.OpenType) { @@ -71,20 +78,28 @@ public virtual IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx) throw new NotSupportedException("ComplexType cannot be navigated by key."); } - public virtual IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx) + public virtual IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) { - var propertyMatch = (from p in this.Properties - where p.Name == component - select p).FirstOrDefault(); - if (null != propertyMatch) + var func = this.NavigateByFunction(component, edmx, isLastSegment); + if (func != null) { - var identifier = propertyMatch.Type; - if (identifier.StartsWith("Collection(")) - { - var innerId = identifier.Substring(11, identifier.Length - 12); - return new ODataCollection(innerId); - } - return edmx.ResourceWithIdentifier(identifier); + return func; + } + + var match = this.Properties.Where(p => p.Name == component).Select(p => p.Type).FirstOrDefault(); + + if (match != null) + { + return edmx.ResourceWithIdentifier(match); + } + + // check for case-insensitive matches and throw if one exists + var otherCaseName = + this.Properties.Where(p => p.Name.IEquals(component)).Select(p => p.Name).FirstOrDefault(); + + if (otherCaseName != null) + { + throw new ArgumentException($"ERROR: case mismatch between URL segment '{component}' and schema element '{otherCaseName}'"); } return null; diff --git a/ApiDocs.Validation/OData/DataServices.cs b/ApiDoctor.Validation/OData/DataServices.cs similarity index 96% rename from ApiDocs.Validation/OData/DataServices.cs rename to ApiDoctor.Validation/OData/DataServices.cs index 26a0b951..5b457a7a 100644 --- a/ApiDocs.Validation/OData/DataServices.cs +++ b/ApiDoctor.Validation/OData/DataServices.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System.Collections.Generic; diff --git a/ApiDocs.Validation/OData/EntityContainer.cs b/ApiDoctor.Validation/OData/EntityContainer.cs similarity index 95% rename from ApiDocs.Validation/OData/EntityContainer.cs rename to ApiDoctor.Validation/OData/EntityContainer.cs index c7029366..5b40aeb2 100644 --- a/ApiDocs.Validation/OData/EntityContainer.cs +++ b/ApiDoctor.Validation/OData/EntityContainer.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.Linq; @@ -32,6 +32,7 @@ namespace ApiDocs.Validation.OData using System.Xml.Serialization; using Transformation; using Utility; + using ApiDoctor.Validation.Error; [XmlRoot("EntityContainer", Namespace = ODataParser.EdmNamespace)] [Mergable(CollectionIdentifier = "Name")] @@ -52,8 +53,7 @@ public EntityContainer() [XmlElement("Singleton"), Sortable] public List Singletons { get; set; } - - public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx) + public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) { string targetType = null; @@ -90,7 +90,7 @@ public IODataNavigable NavigateByUriComponent(string component, EntityFramework return null; } - public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx) + public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues) { throw new NotSupportedException(); } diff --git a/ApiDocs.Validation/OData/EntityFramework.cs b/ApiDoctor.Validation/OData/EntityFramework.cs similarity index 98% rename from ApiDocs.Validation/OData/EntityFramework.cs rename to ApiDoctor.Validation/OData/EntityFramework.cs index 22897e38..c7f86011 100644 --- a/ApiDocs.Validation/OData/EntityFramework.cs +++ b/ApiDoctor.Validation/OData/EntityFramework.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; diff --git a/ApiDocs.Validation/OData/EntitySet.cs b/ApiDoctor.Validation/OData/EntitySet.cs similarity index 87% rename from ApiDocs.Validation/OData/EntitySet.cs rename to ApiDoctor.Validation/OData/EntitySet.cs index 37369d06..d9f3ba0a 100644 --- a/ApiDocs.Validation/OData/EntitySet.cs +++ b/ApiDoctor.Validation/OData/EntitySet.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Newtonsoft.Json; using System.Collections.Generic; @@ -34,7 +34,7 @@ namespace ApiDocs.Validation.OData [XmlRoot("EntitySet", Namespace = ODataParser.EdmNamespace)] [Mergable(CollectionIdentifier = "Name")] - public class EntitySet : XmlBackedTransformableObject + public class EntitySet : XmlBackedTransformableObject, IODataAnnotatable { public EntitySet() { @@ -50,9 +50,14 @@ public EntitySet() [XmlElement("NavigationPropertyBinding"), Sortable] public List NavigationPropertyBinding { get; set; } + [XmlElement("Annotation", Namespace = ODataParser.EdmNamespace), MergePolicy(MergePolicy.EqualOrNull)] + public List Annotation { get; set; } + [XmlIgnore, MergePolicy(MergePolicy.Ignore)] public override string ElementIdentifier { get { return this.Name; } set { this.Name = value; } } + [XmlIgnore, MergePolicy(MergePolicy.Ignore)] + public object SourceMethods { get; set; } } [Mergable(CollectionIdentifier = "Path")] diff --git a/ApiDocs.Validation/OData/EntityType.cs b/ApiDoctor.Validation/OData/EntityType.cs similarity index 85% rename from ApiDocs.Validation/OData/EntityType.cs rename to ApiDoctor.Validation/OData/EntityType.cs index d3601583..12fc27e1 100644 --- a/ApiDocs.Validation/OData/EntityType.cs +++ b/ApiDoctor.Validation/OData/EntityType.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; @@ -32,6 +32,7 @@ namespace ApiDocs.Validation.OData using System.Linq; using System.Xml.Serialization; using Transformation; + using ApiDoctor.Validation.Error; [XmlRoot("EntityType", Namespace = ODataParser.EdmNamespace)] public class EntityType : ComplexType, IODataNavigable @@ -45,12 +46,12 @@ public EntityType() [XmlElement("Key", Namespace = ODataParser.EdmNamespace)] public Key Key { get; set; } - [XmlAttribute("Abstract"), DefaultValue(false), MergePolicy(MergePolicy.PreferTrueValue)] - public bool Abstract { get; set; } - [XmlAttribute("HasStream"), DefaultValue(false), MergePolicy(MergePolicy.PreferTrueValue)] public bool HasStream { get; set; } + [XmlIgnore] + public bool HasStreamSpecified => this.HasStream; + [XmlElement("NavigationProperty", Namespace = ODataParser.EdmNamespace), Sortable] public List NavigationProperties { get; set; } @@ -93,7 +94,7 @@ public bool GraphAddressContainsEntitySetSegmentSerializedValueSpecified [XmlAttribute("InstantOnUrl", Namespace = ODataParser.AgsNamespace)] public string GraphInstantOnUrl { get; set; } - public override IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx) + public override IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) { var navigationPropertyMatch = (from n in this.NavigationProperties where n.Name == component @@ -109,10 +110,20 @@ public override IODataNavigable NavigateByUriComponent(string component, EntityF return edmx.LookupNavigableType(identifier); } - return base.NavigateByUriComponent(component, edmx); + var otherCaseName = + (from n in this.NavigationProperties + where n.Name.IEquals(component) + select n.Name).FirstOrDefault(); + + if (otherCaseName != null) + { + throw new ArgumentException($"ERROR: case mismatch between URL segment '{component}' and existing nav prop '{otherCaseName}'"); + } + + return base.NavigateByUriComponent(component, edmx, issues, isLastSegment); } - public override IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx) + public override IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues) { throw new NotImplementedException(); } diff --git a/ApiDocs.Validation/OData/ExtensionMethods.cs b/ApiDoctor.Validation/OData/ExtensionMethods.cs similarity index 79% rename from ApiDocs.Validation/OData/ExtensionMethods.cs rename to ApiDoctor.Validation/OData/ExtensionMethods.cs index 8ae2ccfa..f937a91d 100644 --- a/ApiDocs.Validation/OData/ExtensionMethods.cs +++ b/ApiDoctor.Validation/OData/ExtensionMethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -25,7 +25,7 @@ using System.ComponentModel; -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.Collections.Generic; @@ -39,16 +39,21 @@ public static class ExtensionMethods { "Edm.String", SimpleDataType.String }, { "Edm.Int64", SimpleDataType.Int64 }, { "Edm.Int32", SimpleDataType.Int32 }, + { "Edm.Int16", SimpleDataType.Int16 }, { "Edm.Boolean", SimpleDataType.Boolean }, + { "Edm.Byte", SimpleDataType.Byte }, + { "Edm.Date", SimpleDataType.Date}, { "Edm.DateTimeOffset", SimpleDataType.DateTimeOffset }, + { "Edm.Duration", SimpleDataType.Duration }, { "Edm.Double", SimpleDataType.Double }, { "Edm.Float", SimpleDataType.Float }, { "Edm.Guid", SimpleDataType.Guid }, + { "Edm.TimeOfDay", SimpleDataType.TimeOfDay}, { "Edm.TimeSpan", SimpleDataType.TimeSpan }, { "Edm.Stream", SimpleDataType.Stream }, { "Edm.Object", SimpleDataType.Object }, { "Edm.Single", SimpleDataType.Single }, - { "Edm.Binary", SimpleDataType.Binary } + { "Edm.Binary", SimpleDataType.Binary }, }; internal static bool ToBoolean(this string source) @@ -74,61 +79,79 @@ internal static string AttributeValue(this XElement xml, XName attributeName) /// Resovles a fully qualified type identifier within a collection of schema. /// Will match on ComplexType, EntityType, Action, or Function. /// - /// - /// - /// public static object FindTypeWithIdentifier(this IEnumerable schemas, string identifier) { - // onedrive.item + if (identifier == null) + { + return null; + } + + bool isCollection = UnwrapCollectionIfNeeded(ref identifier); + + // namespace.name int splitIndex = identifier.LastIndexOf('.'); if (splitIndex == -1) + { throw new ArgumentException("identifier should be of format {schema}.{type}"); + } string schemaName = identifier.Substring(0, splitIndex); string typeName = identifier.Substring(splitIndex + 1); // Resolve the schema first - var schema = (from s in schemas - where s.Namespace == schemaName - select s).FirstOrDefault(); - if (null == schema) + var schema = schemas.FirstOrDefault(s => s.Namespace == schemaName); + if (schema == null) { return null; } - // Look for a matching complex type - var matchingComplexType = from ct in schema.ComplexTypes - where ct.Name == typeName - select ct; - if (matchingComplexType.Any()) - return matchingComplexType.First(); - - // Look for a matching entity type - var matchingEntityType = from et in schema.EntityTypes - where et.Name == typeName - select et; - if (matchingEntityType.Any()) - return matchingEntityType.First(); - - // Look up actions - var matchingAction = (from et in schema.Actions where et.Name == typeName select et); - if (matchingAction.Any()) - return matchingAction.First(); - - // Look up functions - var matchingFunctions = (from et in schema.Functions where et.Name == typeName select et); - if (matchingFunctions.Any()) - return matchingFunctions.First(); - - // Look up enums - var matchingEnums = (from et in schema.Enumerations where et.Name == typeName select et); - if (matchingEnums.Any()) - return matchingEnums.First(); + // now see if a data type matches + var match = + schema.ComplexTypes.FirstOrDefault(ct => ct.Name == typeName) as object ?? + schema.EntityTypes.FirstOrDefault(et => et.Name == typeName) as object ?? + schema.Enumerations.FirstOrDefault(e => e.Name == typeName) as object; + + if (match != null) + { + if (isCollection) + { + return new ODataCollection(identifier); + } + + return match; + } + + // now look for matching actions or functions. + match = + schema.Actions.FirstOrDefault(a => a.Name == typeName) as object ?? + schema.Functions.FirstOrDefault(f => f.Name == typeName) as object; + + if (match != null) + { + if (isCollection) + { + throw new ArgumentException("Can't have a collection of functions or actions"); + } + + return match; + } return null; } - public static T ResourceWithIdentifier(this IEnumerable schemas, string identifier) + private static bool UnwrapCollectionIfNeeded(ref string identifier) + { + bool isCollection = false; + if (identifier.StartsWith("Collection(", StringComparison.OrdinalIgnoreCase)) + { + identifier = identifier.Substring(11, identifier.Length - 12); + isCollection = true; + } + + return isCollection; + } + + public static T ResourceWithIdentifier(this IEnumerable schemas, string identifier) where T : class { var type = schemas.FindTypeWithIdentifier(identifier); if (type != null && type is T) @@ -136,6 +159,19 @@ public static T ResourceWithIdentifier(this IEnumerable schemas, stri return (T)type; } + bool isCollection = UnwrapCollectionIfNeeded(ref identifier); + + SimpleDataType simpleType = identifier.ToODataSimpleType(); + if (simpleType != SimpleDataType.Object) + { + if (isCollection) + { + return new ODataCollection(identifier) as T; + } + + return new ODataSimpleType(simpleType) as T; + } + throw new KeyNotFoundException("Unable to find type identifier '" + identifier + "' as '" + typeof(T).Name + "'."); } @@ -146,7 +182,7 @@ public static T ResourceWithIdentifier(this IEnumerable schemas, stri /// /// /// - public static T ResourceWithIdentifier(this EntityFramework edmx, string identifier) + public static T ResourceWithIdentifier(this EntityFramework edmx, string identifier) where T : class { return edmx.DataServices.Schemas.ResourceWithIdentifier(identifier); } @@ -309,8 +345,10 @@ public static SimpleDataType ToODataSimpleType(this string typeName) SimpleDataType? dataType = (from kv in ODataSimpleTypeMap where kv.Key == typeName select kv.Value).SingleOrDefault(); - if (dataType.HasValue) + if (dataType.GetValueOrDefault() != default(SimpleDataType)) + { return dataType.Value; + } return SimpleDataType.Object; } @@ -324,7 +362,7 @@ public static string ElementName(this string collection) { if (!collection.IsCollection()) { - throw new ArgumentOutOfRangeException(nameof(collection), $"'{collection}' is not a collction."); + return collection; } return collection.Substring(ODataParser.CollectionPrefix.Length, collection.Length - ODataParser.CollectionPrefix.Length -1); diff --git a/ApiDocs.Validation/OData/Function.cs b/ApiDoctor.Validation/OData/Function.cs similarity index 92% rename from ApiDocs.Validation/OData/Function.cs rename to ApiDoctor.Validation/OData/Function.cs index fb27f48e..492a055e 100644 --- a/ApiDocs.Validation/OData/Function.cs +++ b/ApiDoctor.Validation/OData/Function.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System.Xml.Serialization; @@ -42,5 +42,8 @@ public Function() : base() [XmlAttribute("IsComposable"), MergePolicy(MergePolicy.PreferLesserValue)] public bool IsComposable { get; set; } + + [XmlIgnore] + public bool IsComposableSpecified => this.IsComposable; } } diff --git a/ApiDocs.Validation/OData/IODataNamedElement.cs b/ApiDoctor.Validation/OData/IODataNamedElement.cs similarity index 96% rename from ApiDocs.Validation/OData/IODataNamedElement.cs rename to ApiDoctor.Validation/OData/IODataNamedElement.cs index afb215c2..8dc99525 100644 --- a/ApiDocs.Validation/OData/IODataNamedElement.cs +++ b/ApiDoctor.Validation/OData/IODataNamedElement.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { /// /// Interface for name OData elements diff --git a/ApiDoctor.Validation/OData/IODataNavigable.cs b/ApiDoctor.Validation/OData/IODataNavigable.cs new file mode 100644 index 00000000..88e9a982 --- /dev/null +++ b/ApiDoctor.Validation/OData/IODataNavigable.cs @@ -0,0 +1,188 @@ +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +namespace ApiDoctor.Validation.OData +{ + using System; + using System.Linq; + using System.Text.RegularExpressions; + using ApiDoctor.Validation.Error; + + public interface IODataNavigable + { + /// + /// Returns the next target pasted on the value of a component of the URI + /// + IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger logger, bool isLastSegment); + + /// + /// Returns the next component assuming that an entitytype key is provided + /// + IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues); + + string TypeIdentifier { get; } + } + + public class ODataCollection : IODataNavigable + { + public string TypeIdentifier { get; internal set; } + + public ODataCollection(string typeIdentifier) + { + this.TypeIdentifier = typeIdentifier; + } + + public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) + { + // NavigationByUriComponent for a collection means that either we have a hard coded key in the example path, + // or this is a function/action bound to a collection. + + long testLong; + Guid testGuid; + if (Guid.TryParse(component, out testGuid) || + long.TryParse(component, out testLong) || + component.IsLikelyBase64Encoded()) + { + issues.Warning(ValidationErrorCode.AmbiguousExample, + $"Assuming {component} under {this.TypeIdentifier} is a hard-coded key in the example path. Please fix to be a placeholder."); + return this.NavigateByEntityTypeKey(edmx, issues); + } + + var result = this.NavigateByFunction(component, edmx, isLastSegment); + if (result != null) + { + return result; + } + + // if the segment is itself a fully-qualified type, then it's a cast operator and should return a casted collection. + if (component.Contains(".")) + { + result = edmx.LookupNavigableType(component); + if (result != null) + { + return new ODataCollection(component); + } + } + + return null; + } + + public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues) + { + return edmx.ResourceWithIdentifier(this.TypeIdentifier); + } + } + + public static class OdataNavigableExtensionMethods + { + public static IODataNavigable NavigateByFunction(this IODataNavigable source, string component, EntityFramework edmx, bool isLastSegment) + { + var matches = + edmx.DataServices.Schemas.SelectMany(s => s.Functions). + Where(f => + f.IsBound && + (f.Name == component || f.ParameterizedName == component) && + f.ReturnType?.Type != null && + f.Parameters.Any(p => p.Name == "bindingParameter" && p.Type.TypeOnly() == source.TypeIdentifier.TypeOnly())). + ToList(); + + if (matches.Any()) + { + foreach (var m in matches) + { + m.IsComposable = true; + } + + var match = matches.First().ReturnType.Type; + return edmx.ResourceWithIdentifier(match); + } + + var otherCaseName = + edmx.DataServices.Schemas.SelectMany(s => s.Functions). + Where(f => + f.IsBound && + (f.Name.IEquals(component) || f.ParameterizedName.IEquals(component)) && + f.Parameters.Any(p => p.Name == "bindingParameter" && p.Type.TypeOnly() == source.TypeIdentifier.TypeOnly())). + Select(f => f.Name.IEquals(component) ? f.Name : f.ParameterizedName). + FirstOrDefault(); + + if (otherCaseName != null) + { + throw new ArgumentException($"ERROR: case mismatch between URL segment '{component}' and schema element '{otherCaseName}'"); + } + + return null; + } + } + + public class ODataSimpleType : IODataNavigable + { + public SimpleDataType Type { get; internal set; } + + public ODataSimpleType(SimpleDataType type) + { + this.Type = type; + } + + public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) + { + throw new NotSupportedException($"simple type can't navigate to {component}"); + } + + public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues) + { + throw new NotSupportedException(); + } + + public string TypeIdentifier + { + get { return Type.ODataResourceName(); } + } + } + + public class ODataTargetInfo + { + public ODataTargetClassification Classification { get; set; } + + public string QualifiedType { get; set; } + + public string Name { get; set; } + } + + + public enum ODataTargetClassification + { + Unknown, + EntityType, + EntitySet, + Action, + Function, + EntityContainer, + SimpleType, + ComplexType, + NavigationProperty, + TypeCast, + } +} diff --git a/ApiDocs.Validation/OData/IOdataAnnotatable.cs b/ApiDoctor.Validation/OData/IOdataAnnotatable.cs similarity index 96% rename from ApiDocs.Validation/OData/IOdataAnnotatable.cs rename to ApiDoctor.Validation/OData/IOdataAnnotatable.cs index 94eb17ad..f65d0e41 100644 --- a/ApiDocs.Validation/OData/IOdataAnnotatable.cs +++ b/ApiDoctor.Validation/OData/IOdataAnnotatable.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System.Collections.Generic; diff --git a/ApiDocs.Validation/OData/Key.cs b/ApiDoctor.Validation/OData/Key.cs similarity index 96% rename from ApiDocs.Validation/OData/Key.cs rename to ApiDoctor.Validation/OData/Key.cs index 2632788c..70ce94a9 100644 --- a/ApiDocs.Validation/OData/Key.cs +++ b/ApiDoctor.Validation/OData/Key.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.Xml.Serialization; diff --git a/ApiDocs.Validation/OData/NavigationProperty.cs b/ApiDoctor.Validation/OData/NavigationProperty.cs similarity index 97% rename from ApiDocs.Validation/OData/NavigationProperty.cs rename to ApiDoctor.Validation/OData/NavigationProperty.cs index 22a615e9..a392d507 100644 --- a/ApiDocs.Validation/OData/NavigationProperty.cs +++ b/ApiDoctor.Validation/OData/NavigationProperty.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System.Collections.Generic; using System.ComponentModel; diff --git a/ApiDocs.Validation/OData/ODataParser.cs b/ApiDoctor.Validation/OData/ODataParser.cs similarity index 96% rename from ApiDocs.Validation/OData/ODataParser.cs rename to ApiDoctor.Validation/OData/ODataParser.cs index fcbfe203..627a9797 100644 --- a/ApiDocs.Validation/OData/ODataParser.cs +++ b/ApiDoctor.Validation/OData/ODataParser.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.Collections.Generic; @@ -37,6 +37,8 @@ namespace ApiDocs.Validation.OData using System.Text; using System.Xml; using System.Xml.Serialization; + using ApiDoctor.Validation.Error; + /// /// Converts OData input into json examples which can be validated against our /// ResourceDefinitions in a DocSet. @@ -44,6 +46,7 @@ namespace ApiDocs.Validation.OData public class ODataParser { private static readonly IDictionary ODataSimpleTypeExamples = new Dictionary() { + { "Edm.Stream", "stream" }, { "Edm.String", "string" }, { "Edm.Boolean", false }, { "Edm.Int64", 1234567890 }, @@ -184,33 +187,33 @@ public static async Task ParseEntityFrameworkFromFileAsync(stri /// /// /// - public static List GenerateResourcesFromSchemas(IEnumerable schemas) + public static List GenerateResourcesFromSchemas(IEnumerable schemas, IssueLogger issues) { List resources = new List(); foreach (var schema in schemas) { - resources.AddRange(CreateResourcesFromSchema(schema, schemas)); + resources.AddRange(CreateResourcesFromSchema(schema, schemas, issues)); } return resources; } - private static IEnumerable CreateResourcesFromSchema(Schema schema, IEnumerable otherSchema) + private static IEnumerable CreateResourcesFromSchema(Schema schema, IEnumerable otherSchema, IssueLogger issues) { List resources = new List(); - resources.AddRange(from ct in schema.ComplexTypes select ResourceDefinitionFromType(schema, otherSchema, ct)); - resources.AddRange(from et in schema.EntityTypes select ResourceDefinitionFromType(schema, otherSchema, et)); + resources.AddRange(from ct in schema.ComplexTypes select ResourceDefinitionFromType(schema, otherSchema, ct, issues)); + resources.AddRange(from et in schema.EntityTypes select ResourceDefinitionFromType(schema, otherSchema, et, issues)); return resources; } - private static ResourceDefinition ResourceDefinitionFromType(Schema schema, IEnumerable otherSchema, ComplexType ct) + private static ResourceDefinition ResourceDefinitionFromType(Schema schema, IEnumerable otherSchema, ComplexType ct, IssueLogger issues) { var annotation = new CodeBlockAnnotation() { ResourceType = string.Concat(schema.Namespace, ".", ct.Name), BlockType = CodeBlockType.Resource }; var json = BuildJsonExample(ct, otherSchema); - ResourceDefinition rd = new JsonResourceDefinition(annotation, json, null); + ResourceDefinition rd = new JsonResourceDefinition(annotation, json, null, issues); return rd; } diff --git a/ApiDocs.Validation/OData/Parameter.cs b/ApiDoctor.Validation/OData/Parameter.cs similarity index 86% rename from ApiDocs.Validation/OData/Parameter.cs rename to ApiDoctor.Validation/OData/Parameter.cs index 232a3406..a73bab86 100644 --- a/ApiDocs.Validation/OData/Parameter.cs +++ b/ApiDoctor.Validation/OData/Parameter.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.ComponentModel; @@ -36,8 +36,6 @@ public class Parameter : XmlBackedTransformableObject { public Parameter() { - //Unicode = true; - //Nullable = false; } [XmlAttribute("Name"), SortBy, MergePolicy(EquivalentValues = "this=bindingParameter")] @@ -46,9 +44,11 @@ public Parameter() [XmlAttribute("Type"), ContainsType] public string Type { get; set; } - [XmlAttribute("Nullable"), MergePolicy(MergePolicy.Ignore)] - public bool ValueOfNullableProperty { get; set; } + public bool ValueOfNullableProp { get; set; } + + [XmlIgnore] + public bool ValueOfNullablePropSpecified => this.ValueOfNullableProp; [XmlIgnore, MergePolicy(MergePolicy.Ignore)] public bool ValueOfNullablePropertySpecified { get; set; } @@ -60,7 +60,7 @@ public bool? Nullable { if (ValueOfNullablePropertySpecified) { - return ValueOfNullableProperty; + return ValueOfNullableProp; } return null; } @@ -74,7 +74,7 @@ public bool? Nullable else { ValueOfNullablePropertySpecified = true; - ValueOfNullableProperty = value.Value; + ValueOfNullableProp = value.Value; } } } @@ -83,7 +83,7 @@ public bool? Nullable public bool UnicodePropertyValue { get; set; } [XmlIgnore] - public bool UnicodePropertyValueSpecified { get; set; } + public bool UnicodePropertyValueSpecified => this.Type == "Edm.String"; [XmlIgnore, MergePolicy(MergePolicy.PreferFalseValue)] public bool? Unicode @@ -94,17 +94,13 @@ public bool? Unicode { return UnicodePropertyValue; } + return null; } set { - if (!value.HasValue) - { - UnicodePropertyValueSpecified = false; - } - else + if (value.HasValue) { - UnicodePropertyValueSpecified = true; UnicodePropertyValue = value.Value; } } diff --git a/ApiDocs.Validation/OData/Property.cs b/ApiDoctor.Validation/OData/Property.cs similarity index 92% rename from ApiDocs.Validation/OData/Property.cs rename to ApiDoctor.Validation/OData/Property.cs index 4b7956c4..272d9c58 100644 --- a/ApiDocs.Validation/OData/Property.cs +++ b/ApiDoctor.Validation/OData/Property.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.Collections.Generic; @@ -37,8 +37,7 @@ public class Property : XmlBackedTransformableObject, IODataAnnotatable { public Property() { - Unicode = true; - Nullable = true; + Unicode = false; this.Annotation = new List(); } @@ -48,11 +47,15 @@ public Property() [XmlAttribute("Type"), ContainsType, MergePolicy(MergePolicy.PreferLesserValue)] public string Type { get; set; } - [XmlAttribute("Nullable"), DefaultValue(true), MergePolicy(MergePolicy.PreferTrueValue)] + [XmlAttribute("Nullable"), MergePolicy(MergePolicy.PreferTrueValue)] public bool Nullable { get; set; } - [XmlAttribute("Unicode"), DefaultValue(true), MergePolicy(MergePolicy.PreferFalseValue)] + [XmlIgnore] + public bool NullableSpecified => this.Nullable; + + [XmlAttribute("Unicode"), MergePolicy(MergePolicy.PreferFalseValue)] public bool Unicode { get; set; } + public bool UnicodeSpecified => "Edm.String".Equals(this.Type); [XmlElement("Annotation", Namespace = ODataParser.EdmNamespace), Sortable] public List Annotation { get; set; } diff --git a/ApiDocs.Validation/OData/PropertyRef.cs b/ApiDoctor.Validation/OData/PropertyRef.cs similarity index 96% rename from ApiDocs.Validation/OData/PropertyRef.cs rename to ApiDoctor.Validation/OData/PropertyRef.cs index 4c13d710..057d3e35 100644 --- a/ApiDocs.Validation/OData/PropertyRef.cs +++ b/ApiDoctor.Validation/OData/PropertyRef.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.Xml.Serialization; diff --git a/ApiDocs.Validation/OData/ReturnType.cs b/ApiDoctor.Validation/OData/ReturnType.cs similarity index 74% rename from ApiDocs.Validation/OData/ReturnType.cs rename to ApiDoctor.Validation/OData/ReturnType.cs index 472dc8dc..5656d5df 100644 --- a/ApiDocs.Validation/OData/ReturnType.cs +++ b/ApiDoctor.Validation/OData/ReturnType.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using System; using System.ComponentModel; @@ -45,6 +45,9 @@ public ReturnType() [XmlAttribute("Nullable"), DefaultValue(false)] public bool Nullable { get; set; } + [XmlIgnore] + public bool NullableSpecified => this.Nullable; + [XmlAttribute("Unicode"), DefaultValue(true)] public bool Unicode { get; set; } @@ -61,5 +64,23 @@ public override string ElementIdentifier } } + public override bool Equals(object obj) + { + var other = obj as ReturnType; + if (other != null) + { + return + string.Equals(this.Type, other.Type) && + this.Nullable == other.Nullable && + this.Unicode == other.Unicode; + } + + return false; + } + + public override int GetHashCode() + { + return this.Type.GetHashCode() ^ this.Nullable.GetHashCode() ^ this.Unicode.GetHashCode(); + } } } diff --git a/ApiDocs.Validation/OData/Schema.cs b/ApiDoctor.Validation/OData/Schema.cs similarity index 98% rename from ApiDocs.Validation/OData/Schema.cs rename to ApiDoctor.Validation/OData/Schema.cs index 13348140..3eb0fa99 100644 --- a/ApiDocs.Validation/OData/Schema.cs +++ b/ApiDoctor.Validation/OData/Schema.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; diff --git a/ApiDocs.Validation/OData/Singleton.cs b/ApiDoctor.Validation/OData/Singleton.cs similarity index 85% rename from ApiDocs.Validation/OData/Singleton.cs rename to ApiDoctor.Validation/OData/Singleton.cs index 1671119c..4df5c2c4 100644 --- a/ApiDocs.Validation/OData/Singleton.cs +++ b/ApiDoctor.Validation/OData/Singleton.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; @@ -33,7 +33,7 @@ namespace ApiDocs.Validation.OData [XmlRoot("Singleton", Namespace = ODataParser.EdmNamespace)] [Mergable(CollectionIdentifier = "Name")] - public class Singleton : XmlBackedTransformableObject + public class Singleton : XmlBackedTransformableObject, IODataAnnotatable { public Singleton() { @@ -49,11 +49,17 @@ public Singleton() [XmlElement("NavigationPropertyBinding"), Sortable] public List NavigationPropertyBinding { get; set; } + [XmlElement("Annotation", Namespace = ODataParser.EdmNamespace), MergePolicy(MergePolicy.EqualOrNull)] + public List Annotation { get; set; } + [XmlIgnore] public override string ElementIdentifier { get { return this.Name; } set { this.Name = value; } } + + [XmlIgnore] + public object SourceMethods { get; set; } } } diff --git a/ApiDocs.Validation/OData/Term.cs b/ApiDoctor.Validation/OData/Term.cs similarity index 98% rename from ApiDocs.Validation/OData/Term.cs rename to ApiDoctor.Validation/OData/Term.cs index 4000de72..b201e364 100644 --- a/ApiDocs.Validation/OData/Term.cs +++ b/ApiDoctor.Validation/OData/Term.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; diff --git a/ApiDocs.Validation/OData/Transformation/ITransformable.cs b/ApiDoctor.Validation/OData/Transformation/ITransformable.cs similarity index 93% rename from ApiDocs.Validation/OData/Transformation/ITransformable.cs rename to ApiDoctor.Validation/OData/Transformation/ITransformable.cs index b17accaf..dfcbfa70 100644 --- a/ApiDocs.Validation/OData/Transformation/ITransformable.cs +++ b/ApiDoctor.Validation/OData/Transformation/ITransformable.cs @@ -1,4 +1,4 @@ -using ApiDocs.Validation.Utility; +using ApiDoctor.Validation.Utility; using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +6,7 @@ using System.Threading.Tasks; using System.Xml.Serialization; -namespace ApiDocs.Validation.OData.Transformation +namespace ApiDoctor.Validation.OData.Transformation { public interface ITransformable { diff --git a/ApiDocs.Validation/OData/Transformation/PublishSchemaChangesConfig.cs b/ApiDoctor.Validation/OData/Transformation/PublishSchemaChangesConfig.cs similarity index 87% rename from ApiDocs.Validation/OData/Transformation/PublishSchemaChangesConfig.cs rename to ApiDoctor.Validation/OData/Transformation/PublishSchemaChangesConfig.cs index 56db3d1a..d1ad1448 100644 --- a/ApiDocs.Validation/OData/Transformation/PublishSchemaChangesConfig.cs +++ b/ApiDoctor.Validation/OData/Transformation/PublishSchemaChangesConfig.cs @@ -1,11 +1,36 @@ -using Newtonsoft.Json; +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace ApiDocs.Validation.OData.Transformation +namespace ApiDoctor.Validation.OData.Transformation { public class PublishSchemaChangesConfigFile : Config.ConfigFile { diff --git a/ApiDoctor.Validation/OData/Transformation/SchemaConfigFile.cs b/ApiDoctor.Validation/OData/Transformation/SchemaConfigFile.cs new file mode 100644 index 00000000..9db18701 --- /dev/null +++ b/ApiDoctor.Validation/OData/Transformation/SchemaConfigFile.cs @@ -0,0 +1,82 @@ +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using Newtonsoft.Json; + +namespace ApiDoctor.Validation.OData.Transformation +{ + public class SchemaConfigFile : Config.ConfigFile + { + [JsonProperty("schemaConfig")] + public SchemaConfig SchemaConfig { get; set; } + + [JsonProperty("schemaDiffConfig")] + public SchemaDiffConfig SchemaDiffConfig { get; set; } + + public override bool IsValid => this.SchemaConfig != null; + } + + public class SchemaConfig + { + /// + /// default namespace for types + /// + [JsonProperty("defaultNamespace")] + public string DefaultNamespace { get; set; } + + /// + /// Specifies the base service URLs included in method examples to be removed when generating metadata. + /// + [JsonProperty("baseUrls")] + public string[] BaseUrls { get; set; } + + /// + /// apiDoctor expects names to be lowerCamel. declare any exceptions here. + /// + [JsonProperty("notLowerCamel")] + public string[] NotLowerCamel { get; set; } + + /// + /// + /// + [JsonProperty("supportedTags")] + public string[] SupportedTags { get; set; } + } + + public class SchemaDiffConfig + { + /// + /// keep elements that have attributes containing any of these values + /// + [JsonProperty("keepElementsContaining")] + public string[] KeepElementsContaining { get; set; } + + /// + /// drop elements containing children with any of these values + /// + [JsonProperty("dropElementsContaining")] + public string[] DropElementsContaining { get; set; } + } +} diff --git a/ApiDocs.Validation/OData/Transformation/TransformationHelper.cs b/ApiDoctor.Validation/OData/Transformation/TransformationHelper.cs similarity index 99% rename from ApiDocs.Validation/OData/Transformation/TransformationHelper.cs rename to ApiDoctor.Validation/OData/Transformation/TransformationHelper.cs index 8dcca7a2..58753e45 100644 --- a/ApiDocs.Validation/OData/Transformation/TransformationHelper.cs +++ b/ApiDoctor.Validation/OData/Transformation/TransformationHelper.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace ApiDocs.Validation.OData.Transformation +namespace ApiDoctor.Validation.OData.Transformation { public delegate bool HandlePropertyModification(string propertyName, object modifiedValue); diff --git a/ApiDocs.Validation/OData/XmlBackedObject.cs b/ApiDoctor.Validation/OData/XmlBackedObject.cs similarity index 84% rename from ApiDocs.Validation/OData/XmlBackedObject.cs rename to ApiDoctor.Validation/OData/XmlBackedObject.cs index 6b2b3b74..00f6c3d7 100644 --- a/ApiDocs.Validation/OData/XmlBackedObject.cs +++ b/ApiDoctor.Validation/OData/XmlBackedObject.cs @@ -1,5 +1,5 @@ -using ApiDocs.Validation.OData.Transformation; -using ApiDocs.Validation.Utility; +using ApiDoctor.Validation.OData.Transformation; +using ApiDoctor.Validation.Utility; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +8,7 @@ using System.Xml; using System.Xml.Serialization; -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { public abstract class XmlBackedObject { diff --git a/ApiDocs.Validation/OData/XmlParseHelper.cs b/ApiDoctor.Validation/OData/XmlParseHelper.cs similarity index 96% rename from ApiDocs.Validation/OData/XmlParseHelper.cs rename to ApiDoctor.Validation/OData/XmlParseHelper.cs index 4c9dafe2..660663f2 100644 --- a/ApiDocs.Validation/OData/XmlParseHelper.cs +++ b/ApiDoctor.Validation/OData/XmlParseHelper.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using System.Xml.Serialization; -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { internal static class XmlParseHelper { diff --git a/ApiDocs.Validation/OData/Annotation.cs b/ApiDoctor.Validation/OData/annotation.cs similarity index 76% rename from ApiDocs.Validation/OData/Annotation.cs rename to ApiDoctor.Validation/OData/annotation.cs index a857b1af..e757dea2 100644 --- a/ApiDocs.Validation/OData/Annotation.cs +++ b/ApiDoctor.Validation/OData/annotation.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; @@ -36,41 +36,42 @@ namespace ApiDocs.Validation.OData [Mergable(CollectionIdentifier = "Term")] public class Annotation : XmlBackedTransformableObject { + private bool boolValue; + private bool boolSpecified; + [XmlAttribute("Term"), SortBy] public string Term { get; set; } [XmlAttribute("String"), DefaultValue(null), MergePolicy(MergePolicy.PreferGreaterValue)] public string String { get; set; } - - [MergePolicy(MergePolicy.EqualOrNull)] - public bool? Bool { get; set; } - - [XmlIgnore] - public bool BoolSpecified { - get - { - return Bool.HasValue; - } - } - - [XmlIgnore, MergePolicy(MergePolicy.Ignore)] - public bool BoolAttributeValue + [XmlAttribute("Bool"), MergePolicy(MergePolicy.EqualOrNull)] + public bool Bool { get { - if (Bool.HasValue) return Bool.Value; - return false; + return this.boolValue; } + set { - Bool = value; + this.boolValue = value; + this.boolSpecified = true; } } + [XmlIgnore] + public bool BoolSpecified => this.Bool || this.boolSpecified; + + [XmlElement("EnumMember", Namespace = ODataParser.EdmNamespace), DefaultValue(null), MergePolicy(MergePolicy.EqualOrNull)] + public string EnumMember { get; set; } + [XmlElement("Record", Namespace = ODataParser.EdmNamespace), DefaultValue(null), Sortable] public List Records { get; set; } + [XmlElement("Collection")] + public RecordCollection Collection { get; set; } + #region ITransformable [XmlIgnore, MergePolicy(MergePolicy.Ignore)] diff --git a/ApiDocs.Validation/OData/EnumType.cs b/ApiDoctor.Validation/OData/enumtype.cs similarity index 92% rename from ApiDocs.Validation/OData/EnumType.cs rename to ApiDoctor.Validation/OData/enumtype.cs index 2554a731..13a9d0d2 100644 --- a/ApiDocs.Validation/OData/EnumType.cs +++ b/ApiDoctor.Validation/OData/enumtype.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,13 +23,14 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System; using System.Collections.Generic; using System.Xml.Serialization; using Transformation; + using ApiDoctor.Validation.Error; [XmlRoot("EnumType", Namespace = ODataParser.EdmNamespace), Mergable(CollectionIdentifier = "Name")] @@ -52,6 +53,9 @@ public EnumType() [XmlAttribute("IsFlags")] public bool IsFlags { get; set; } + [XmlIgnore] + public bool IsFlagsSpecified => IsFlags; + [XmlElement("Member"), Sortable] public List Members { get; set; } @@ -62,12 +66,12 @@ public EnumType() [XmlIgnore] public string TypeIdentifier { get { return Name; } } - public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx) + public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues) { throw new NotImplementedException(); } - public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx) + public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) { throw new NotImplementedException(); } diff --git a/ApiDocs.Validation/OData/PropertyValue.cs b/ApiDoctor.Validation/OData/propertyvalue.cs similarity index 60% rename from ApiDocs.Validation/OData/PropertyValue.cs rename to ApiDoctor.Validation/OData/propertyvalue.cs index 3a774dba..72d481a0 100644 --- a/ApiDocs.Validation/OData/PropertyValue.cs +++ b/ApiDoctor.Validation/OData/propertyvalue.cs @@ -22,16 +22,20 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { - using Utility; + using System.Collections.Generic; using System.ComponentModel; using System.Xml.Serialization; + using ApiDoctor.Validation.Utility; [XmlRoot("PropertyValue", Namespace = ODataParser.EdmNamespace)] [Mergable(CollectionIdentifier = "Property")] public class PropertyValue : XmlBackedObject { + private bool boolValue; + private bool boolSpecified; + [XmlAttribute("Property"), MergePolicy(MergePolicy.EqualOrNull)] public string Property { get; set; } @@ -39,6 +43,42 @@ public class PropertyValue : XmlBackedObject public string EnumMember { get; set; } [XmlAttribute("Bool"), MergePolicy(MergePolicy.EqualOrNull)] - public bool Bool { get; set; } + public bool Bool + { + get + { + return this.boolValue; + } + + set + { + this.boolValue = value; + this.boolSpecified = true; + } + } + + [XmlIgnore] + public bool BoolSpecified => this.Bool || this.boolSpecified; + + [XmlAttribute("String"), DefaultValue(null), MergePolicy(MergePolicy.EqualOrNull)] + public string String { get; set; } + + [XmlElement("Annotation")] + public List AnnotationList { get; set; } + + [XmlElement("Record")] + public List Records { get; set; } + + [XmlElement("Collection")] + public RecordCollection Collection { get; set; } + } + + public class RecordCollection + { + [XmlElement("Record")] + public List Records { get; set; } + + [XmlElement("String")] + public List Strings { get; set; } } } diff --git a/ApiDocs.Validation/OData/Record.cs b/ApiDoctor.Validation/OData/record.cs similarity index 88% rename from ApiDocs.Validation/OData/Record.cs rename to ApiDoctor.Validation/OData/record.cs index 64093079..8a9b0add 100644 --- a/ApiDocs.Validation/OData/Record.cs +++ b/ApiDoctor.Validation/OData/record.cs @@ -22,7 +22,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { using Utility; using System.Collections.Generic; @@ -34,6 +34,9 @@ namespace ApiDocs.Validation.OData public class Record : XmlBackedObject { [XmlElement("PropertyValue", Namespace = ODataParser.EdmNamespace), DefaultValue(null), SortBy] - public PropertyValue PropertyValue { get; set; } + public List PropertyValues { get; set; } + + [XmlAttribute("Type"), MergePolicy(MergePolicy.EqualOrNull)] + public string Type { get; set; } } } diff --git a/ApiDocs.Validation/OData/SchemaValidation.cs b/ApiDoctor.Validation/OData/schemavalidation.cs similarity index 97% rename from ApiDocs.Validation/OData/SchemaValidation.cs rename to ApiDoctor.Validation/OData/schemavalidation.cs index 31a0e57e..5a40a783 100644 --- a/ApiDocs.Validation/OData/SchemaValidation.cs +++ b/ApiDoctor.Validation/OData/schemavalidation.cs @@ -1,11 +1,11 @@ -using ApiDocs.Validation.Error; +using ApiDoctor.Validation.Error; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { public static class SchemaValidation { diff --git a/ApiDocs.Validation/OData/SortCollectionsHelper.cs b/ApiDoctor.Validation/OData/sortcollectionshelper.cs similarity index 99% rename from ApiDocs.Validation/OData/SortCollectionsHelper.cs rename to ApiDoctor.Validation/OData/sortcollectionshelper.cs index 500a5897..646e3404 100644 --- a/ApiDocs.Validation/OData/SortCollectionsHelper.cs +++ b/ApiDoctor.Validation/OData/sortcollectionshelper.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace ApiDocs.Validation.OData +namespace ApiDoctor.Validation.OData { public delegate void WalkGraphAction(PropertyInfo property, object obj, Stack parentObjects); diff --git a/ApiDocs.Validation/ObjectGraph/ExtensionMethods.cs b/ApiDoctor.Validation/ObjectGraph/ExtensionMethods.cs similarity index 95% rename from ApiDocs.Validation/ObjectGraph/ExtensionMethods.cs rename to ApiDoctor.Validation/ObjectGraph/ExtensionMethods.cs index d8ad9386..36e0baf6 100644 --- a/ApiDocs.Validation/ObjectGraph/ExtensionMethods.cs +++ b/ApiDoctor.Validation/ObjectGraph/ExtensionMethods.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace ApiDocs.Validation.Utility +namespace ApiDoctor.Validation.Utility { internal static class ExtensionMethods { diff --git a/ApiDocs.Validation/ObjectGraph/ObjectGraphMerger.cs b/ApiDoctor.Validation/ObjectGraph/objectgraphmerger.cs similarity index 99% rename from ApiDocs.Validation/ObjectGraph/ObjectGraphMerger.cs rename to ApiDoctor.Validation/ObjectGraph/objectgraphmerger.cs index 805034ff..5f7ab31a 100644 --- a/ApiDocs.Validation/ObjectGraph/ObjectGraphMerger.cs +++ b/ApiDoctor.Validation/ObjectGraph/objectgraphmerger.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace ApiDocs.Validation.Utility +namespace ApiDoctor.Validation.Utility { public class ObjectGraphMerger where T : new() { diff --git a/ApiDocs.Validation/PageAnnotation.cs b/ApiDoctor.Validation/PageAnnotation.cs similarity index 96% rename from ApiDocs.Validation/PageAnnotation.cs rename to ApiDoctor.Validation/PageAnnotation.cs index e01c413a..1b7431e3 100644 --- a/ApiDocs.Validation/PageAnnotation.cs +++ b/ApiDoctor.Validation/PageAnnotation.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Linq; @@ -104,6 +104,8 @@ public PageAnnotation() [JsonProperty("footerAdditions")] public string[] BodyFooterHtmlTags { get; set; } + [JsonProperty("suppressions", NullValueHandling = NullValueHandling.Ignore)] + public string[] Suppressions { get; set; } /// /// Container for any unrecognized properties when deserializing the #page.annotation class. diff --git a/ApiDocs.Validation/ParameterDataType.cs b/ApiDoctor.Validation/ParameterDataType.cs similarity index 75% rename from ApiDocs.Validation/ParameterDataType.cs rename to ApiDoctor.Validation/ParameterDataType.cs index 6d9fa67a..5a7995a8 100644 --- a/ApiDocs.Validation/ParameterDataType.cs +++ b/ApiDoctor.Validation/ParameterDataType.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,13 +23,13 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.OData; + using ApiDoctor.Validation.OData; public class ParameterDataType { @@ -58,9 +58,7 @@ public ParameterDataType(SimpleDataType type, bool isCollection = false) /// /// Creates a new instance with a custom data type. /// - /// - /// - public ParameterDataType(string customDataType, bool isCollection = false) + public ParameterDataType(string customDataType, bool isCollection = false, bool isEnum = false) { // Remove the prefix '#' that @odata.type may include if (customDataType.StartsWith("#")) @@ -76,6 +74,7 @@ public ParameterDataType(string customDataType, bool isCollection = false) this.Type = SimpleDataType.Object; } this.CustomTypeName = customDataType; + this.IsEnum = isEnum; } public ParameterDataType(ParameterDataType existingType, IEnumerable customMembers) @@ -83,6 +82,7 @@ public ParameterDataType(ParameterDataType existingType, IEnumerable customMembers) /// public static ParameterDataType CollectionOfType(ParameterDataType type) { - if (type.IsCollection) - { - throw new ArgumentException("Cannot create a collection for a collection"); - } - return new ParameterDataType { Type = SimpleDataType.Collection, CustomTypeName = type.CustomTypeName, - CollectionResourceType = type.Type + CollectionResourceType = type.Type, + CollectionDimensions = type.IsCollection ? 2 : 1, + IsEnum = type.IsEnum, }; } @@ -151,6 +148,10 @@ public bool IsCollection get { return this.Type == SimpleDataType.Collection; } } + public int CollectionDimensions { get; set; } + + public bool IsEnum { get; private set; } + /// /// A dictionary that reprensets the known type members of an undefined object type. /// @@ -246,6 +247,69 @@ public string GetMarkDown() } #region Helper Methods + + /// + /// Returns true if the two types are perceivably different in json. + /// For example, Int64 and Int16 generally look the same. + /// Double and Int32 generally look different. + /// Guid and String generally look different. And so on. + /// + /// + /// + internal bool JsonLooksDifferentFrom(ParameterDataType type) + { + if (type == null) + { + return true; + } + + if (this.IsCollection) + { + if (type.IsCollection) + { + return new ParameterDataType(this.CollectionResourceType).JsonLooksDifferentFrom(new ParameterDataType(type.CollectionResourceType)); + } + else + { + return true; + } + } + else if (type.IsCollection) + { + return true; + } + + if (this.Type == type.Type) + { + return false; + } + + switch (this.Type) + { + case SimpleDataType.Boolean: + return type.Type != SimpleDataType.Boolean; + case SimpleDataType.Byte: + case SimpleDataType.Int16: + case SimpleDataType.Int32: + case SimpleDataType.Int64: + return + type.Type != SimpleDataType.Byte && + type.Type != SimpleDataType.Int16 && + type.Type != SimpleDataType.Int32 && + type.Type != SimpleDataType.Int64; + case SimpleDataType.Double: + case SimpleDataType.Float: + case SimpleDataType.Single: + return + type.Type != SimpleDataType.Double && + type.Type != SimpleDataType.Float && + type.Type != SimpleDataType.Single; + default: + // default to assuming the types look different + return true; + } + } + /// /// Returns true if the current ParameterDataType instance is less /// qualified than the type provided in the arguments. @@ -276,11 +340,30 @@ internal bool IsLessSpecificThan(ParameterDataType type) stream - We should never allow something from one three to be considered less specific than something in a different tree. + We should never allow something from one tree to be considered less specific than something in a different tree. */ + if (type == null) + { + return false; + } + + if (this.IsCollection) + { + if (type.IsCollection) + { + return new ParameterDataType(this.CollectionResourceType).IsLessSpecificThan(new ParameterDataType(type.CollectionResourceType)); + } + else + { + return false; + } + } + if (this.IsCollection != type.IsCollection) + { return false; + } if (this.Type == SimpleDataType.String && (type.Type == SimpleDataType.Guid || type.Type == SimpleDataType.DateTimeOffset || type.Type == SimpleDataType.TimeSpan)) @@ -295,6 +378,10 @@ We should never allow something from one three to be considered less specific th { return true; } + else if (this.Type == SimpleDataType.Object && type.Type == SimpleDataType.Object && this.CustomTypeName.IEquals("Edm.Object")) + { + return true; + } return false; } @@ -306,6 +393,21 @@ public static ParameterDataType ChooseBest(ParameterDataType a, ParameterDataTyp else return b; } + + public static string ChooseBest(string a, string b) + { + var paramA = a.ParseParameterDataType(); + var paramB = b.ParseParameterDataType(); + + if (paramB.IsLessSpecificThan(paramA)) + { + return a; + } + else + { + return b; + } + } #endregion #region Static property type definitions @@ -374,6 +476,7 @@ public enum SimpleDataType String, Boolean, + Byte, Int16, Int32, @@ -382,8 +485,11 @@ public enum SimpleDataType Float, Double, + Date, DateTimeOffset, + Duration, Guid, + TimeOfDay, TimeSpan, Stream, @@ -391,6 +497,7 @@ public enum SimpleDataType /// Specifies that the value is an array of another data type /// Collection, + /// /// Specifies that the value is an undefined resource (generic object) /// diff --git a/ApiDoctor.Validation/ParameterDefinition.cs b/ApiDoctor.Validation/ParameterDefinition.cs new file mode 100644 index 00000000..cbdb2c8e --- /dev/null +++ b/ApiDoctor.Validation/ParameterDefinition.cs @@ -0,0 +1,217 @@ +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +namespace ApiDoctor.Validation +{ + using System; + using System.Collections.Generic; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Json; + using Newtonsoft.Json.Linq; + + /// + /// Represents a parameter for a request + /// + public class ParameterDefinition : ItemDefinition + { + /// + /// The name of the parameter. + /// + public string Name { get; set; } + + /// + /// The type of object the parameter expects + /// + public ParameterDataType Type { get; set; } + + /// + /// The location of the parameter in the request/response + /// + public ParameterLocation Location { get; set; } + + /// + /// True if the parameter is required to have a value. + /// + public bool? Required { get; set; } + + /// + /// True if the function has an overload without this parameter. + /// + public bool? Optional { get; set; } + + /// + /// List of enumerated values for a parameter that uses enumeration + /// + public List EnumeratedValues { get; set; } + + /// + /// Text value of the original value of this parameter. + /// + public string OriginalValue { get; set; } + + /// + /// Indicates that the parameter should be represented as a navigation property + /// + public bool IsNavigatable { get; internal set; } + + public JToken ToExampleJToken() + { + if (this.Type != null) + { + switch (this.Type.Type) + { + case SimpleDataType.Boolean: + return new JValue(true); + case SimpleDataType.Byte: + return new JValue(byte.MaxValue); + case SimpleDataType.Date: + return new JValue(DateTime.Now.ToString("yyyy-MM-dd")); + case SimpleDataType.DateTimeOffset: + return new JValue(DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); + case SimpleDataType.Double: + case SimpleDataType.Float: + case SimpleDataType.Single: + return new JValue(2.75); + case SimpleDataType.Duration: + return new JValue("PT2H"); + case SimpleDataType.Guid: + return new JValue(Guid.NewGuid()); + case SimpleDataType.Int16: + case SimpleDataType.Int32: + case SimpleDataType.Int64: + return new JValue(12356); + case SimpleDataType.String: + return new JValue("string"); + } + + if (this.Type.CustomTypeName != null) + { + var obj = new JObject(); + obj.Add("@odata.type", new JValue(this.Type.CustomTypeName)); + return obj; + } + } + + return null; + } + + /// + /// Merge values from the param object into this object. + /// + /// + internal void AddMissingDetails(ParameterDefinition param, IssueLogger issues) + { + if (!this.Required.HasValue && param.Required.HasValue) + this.Required = param.Required; + + this.IsNavigatable = this.IsNavigatable | param.IsNavigatable; + + if (this.Type != param.Type) + { + if (this.Type.IsObject) + { + if (!this.Type.IsCollection && param.Type.IsCollection) + { + issues.Error(ValidationErrorCode.ExpectedTypeDifferent, + $"Type mismatch between example and table. Parameter name: {this.Name}; example type ({this.Type}) is not a collection, while the table description type ({param.Type}) is."); + } + + if (this.Type.CustomTypeName == null && param.Type.CustomTypeName != null) + { + this.Type = param.Type; + } + else if (this.Type.CustomTypeName != null && param.Type.CustomTypeName != null) + { + issues.Warning(ValidationErrorCode.ExpectedTypeDifferent, + $"Type mismatch between example and table. Parameter name: { this.Name}; example type: ({ this.Type.CustomTypeName}); table type: ({ param.Type.CustomTypeName})"); + } + } + else if (this.Type.IsCollection) + { + if (!param.Type.IsCollection) + { + issues.Error(ValidationErrorCode.ExpectedTypeDifferent, + $"Type mismatch between example and table. Parameter name: {this.Name}; example type ({this.Type}) is a collection, while the table description type ({param.Type}) is not."); + } + + if (param.Type.CollectionResourceType == SimpleDataType.Object && !string.IsNullOrEmpty(this.Type.CustomTypeName) && this.Type.CustomTypeName != param.Type.CustomTypeName) + { + issues.Warning(ValidationErrorCode.ExpectedTypeDifferent, + $"Inconsistent types between parameter ({this.Type.CustomTypeName}) and table ({param.Type.CustomTypeName})"); + } + else if (this.Type.CollectionResourceType != param.Type.CollectionResourceType) + { + if (this.Type.CollectionResourceType == SimpleDataType.String && param.Type.IsEnum) + { + // we allow json examples of enums to look like strings because we don't have a better way of representing them right now. + } + else + { + issues.Warning(ValidationErrorCode.ExpectedTypeDifferent, + $"Inconsistent types between parameter ({this.Type.CollectionResourceType}) and table ({param.Type.CollectionResourceType})"); + } + } + + // table should be authoritative. + this.Type = param.Type; + } + else + { + if (param.Type.IsLessSpecificThan(this.Type) && + param.Type.JsonLooksDifferentFrom(this.Type)) + { + issues.Warning(ValidationErrorCode.ExpectedTypeDifferent, + $"Parameter '{param.Name}' type changed from {this.Type.Type} --> {param.Type.Type} because the latter was in the table description. Update the resource to match."); + } + + this.Type = param.Type; + } + } + + if (string.IsNullOrEmpty(this.Title)) + { + this.Title = param.Title; + } + if (string.IsNullOrEmpty(this.Description)) + { + this.Description = param.Description; + } + if (param.EnumeratedValues != null) + { + this.EnumeratedValues.AddRange(param.EnumeratedValues); + } + } + } + + public enum ParameterLocation + { + Path, + QueryString, + Header, + JsonObject + } + + +} diff --git a/ApiDocs.Validation/Params/BasicRequestDefinition.cs b/ApiDoctor.Validation/Params/BasicRequestDefinition.cs similarity index 98% rename from ApiDocs.Validation/Params/BasicRequestDefinition.cs rename to ApiDoctor.Validation/Params/BasicRequestDefinition.cs index 6326d5cf..561b1066 100644 --- a/ApiDocs.Validation/Params/BasicRequestDefinition.cs +++ b/ApiDoctor.Validation/Params/BasicRequestDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Params +namespace ApiDoctor.Validation.Params { using System.Collections.Generic; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; using Newtonsoft.Json; public class BasicRequestDefinition diff --git a/ApiDocs.Validation/Params/CSharpEval.cs b/ApiDoctor.Validation/Params/CSharpEval.cs similarity index 95% rename from ApiDocs.Validation/Params/CSharpEval.cs rename to ApiDoctor.Validation/Params/CSharpEval.cs index 30fad0f3..113101c9 100644 --- a/ApiDocs.Validation/Params/CSharpEval.cs +++ b/ApiDoctor.Validation/Params/CSharpEval.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Params +namespace ApiDoctor.Validation.Params { using System; using System.Text; @@ -41,7 +41,7 @@ public static string Evaluate(string code, IReadOnlyDictionary v } string codeWrapper = - "namespace MarkdownScanner.Eval { " + + "namespace ApiDoctor.Eval { " + "using System; " + "using System.Collections.Generic; " + "public class MethodEval {" + @@ -59,7 +59,7 @@ public static string Evaluate(string code, IReadOnlyDictionary v throw new Exception("Unable to locate dynamic code module."); } - Type t = mod.GetType("MarkdownScanner.Eval.MethodEval"); + Type t = mod.GetType("ApiDoctor.Eval.MethodEval"); if (null == t) { throw new Exception("Unable to locate MethodEval type"); diff --git a/ApiDocs.Validation/Params/PlaceholderValue.cs b/ApiDoctor.Validation/Params/PlaceholderValue.cs similarity index 97% rename from ApiDocs.Validation/Params/PlaceholderValue.cs rename to ApiDoctor.Validation/Params/PlaceholderValue.cs index bbd43f14..acba743e 100644 --- a/ApiDocs.Validation/Params/PlaceholderValue.cs +++ b/ApiDoctor.Validation/Params/PlaceholderValue.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Params +namespace ApiDoctor.Validation.Params { using System.ComponentModel; using System.Runtime.CompilerServices; diff --git a/ApiDocs.Validation/Params/PlaceholderValueNotFoundException.cs b/ApiDoctor.Validation/Params/PlaceholderValueNotFoundException.cs similarity index 97% rename from ApiDocs.Validation/Params/PlaceholderValueNotFoundException.cs rename to ApiDoctor.Validation/Params/PlaceholderValueNotFoundException.cs index b59b33d7..7601ee37 100644 --- a/ApiDocs.Validation/Params/PlaceholderValueNotFoundException.cs +++ b/ApiDoctor.Validation/Params/PlaceholderValueNotFoundException.cs @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Params +namespace ApiDoctor.Validation.Params { using System; using System.Runtime.Serialization; diff --git a/ApiDocs.Validation/Params/ScenarioDefinition.cs b/ApiDoctor.Validation/Params/ScenarioDefinition.cs similarity index 98% rename from ApiDocs.Validation/Params/ScenarioDefinition.cs rename to ApiDoctor.Validation/Params/ScenarioDefinition.cs index 1f06f5b7..b6d5a0ce 100644 --- a/ApiDocs.Validation/Params/ScenarioDefinition.cs +++ b/ApiDoctor.Validation/Params/ScenarioDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Params +namespace ApiDoctor.Validation.Params { using System; using System.Collections.Generic; diff --git a/ApiDocs.Validation/Params/RequestDefinitionExtensions.cs b/ApiDoctor.Validation/Params/requestdefinitionextensions.cs similarity index 95% rename from ApiDocs.Validation/Params/RequestDefinitionExtensions.cs rename to ApiDoctor.Validation/Params/requestdefinitionextensions.cs index fb2140bc..9546a009 100644 --- a/ApiDocs.Validation/Params/RequestDefinitionExtensions.cs +++ b/ApiDoctor.Validation/Params/requestdefinitionextensions.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,15 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Params +namespace ApiDoctor.Validation.Params { using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; - using ApiDocs.Validation.Http; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Http; + using ApiDoctor.Validation.Json; using Newtonsoft.Json; public static class RequestDefinitionExtensions @@ -146,14 +147,14 @@ public static void RewriteRequestWithParameters(this HttpRequest request, IEnume /// /// /// - public static void RewriteRequestBodyNamespaces(this HttpRequest request, IServiceAccount account) + public static void RewriteRequestBodyNamespaces(this HttpRequest request, IServiceAccount account, IssueLogger issues) { if (account.Transformations?.Request?.Properties == null) return; if (request.IsMatchingContentType("application/json")) { - var translatedBody = JsonRewriter.RewriteJsonProperties(request.Body, account.Transformations.Request.Properties); + var translatedBody = JsonRewriter.RewriteJsonProperties(request.Body, account.Transformations.Request.Properties, issues); request.Body = translatedBody; } else if (request.IsMatchingContentType("multipart/related")) @@ -162,13 +163,13 @@ public static void RewriteRequestBodyNamespaces(this HttpRequest request, IServi var partsToRewrite = parsedBody.PartsWithContentType("application/json"); foreach(var part in partsToRewrite) { - part.Body = JsonRewriter.RewriteJsonProperties(part.Body, account.Transformations.Request.Properties); + part.Body = JsonRewriter.RewriteJsonProperties(part.Body, account.Transformations.Request.Properties, issues); } request.Body = parsedBody.ToString(); } } - public static void RewriteResponseBodyNamespaces(this HttpResponse response, IServiceAccount account) + public static void RewriteResponseBodyNamespaces(this HttpResponse response, IServiceAccount account, IssueLogger issues) { if (account.Transformations?.Response?.Properties == null) return; @@ -177,7 +178,7 @@ public static void RewriteResponseBodyNamespaces(this HttpResponse response, ISe if (response.Body.Length == 0) return; - var translatedBody = JsonRewriter.RewriteJsonProperties(response.Body, account.Transformations.Response.Properties); + var translatedBody = JsonRewriter.RewriteJsonProperties(response.Body, account.Transformations.Response.Properties, issues); response.Body = translatedBody; } @@ -271,12 +272,12 @@ public static PlaceholderValue ConvertToPlaceholderValue(string key, string valu private static string GenerateRandomFilename(string placeholder) { if (placeholder == (RandomFilenameValuePrefix + "!")) - return string.Format("apidocs-{0:D}", Guid.NewGuid()); + return string.Format("apidoctor-{0:D}", Guid.NewGuid()); string extension = placeholder.Substring( RandomFilenameValuePrefix.Length+1, placeholder.Length - RandomFilenameValuePrefix.Length - 2); - return string.Format("apidocs-{0:D}.{1}", Guid.NewGuid(), extension); + return string.Format("apidoctor-{0:D}.{1}", Guid.NewGuid(), extension); } public static bool MatchesContentTypeIdentifier(this string contentType, string expectedContentType) diff --git a/ApiDocs.Validation/Params/TestSetupRequestDefinition.cs b/ApiDoctor.Validation/Params/testsetuprequestdefinition.cs similarity index 95% rename from ApiDocs.Validation/Params/TestSetupRequestDefinition.cs rename to ApiDoctor.Validation/Params/testsetuprequestdefinition.cs index c28e9159..d29c869e 100644 --- a/ApiDocs.Validation/Params/TestSetupRequestDefinition.cs +++ b/ApiDoctor.Validation/Params/testsetuprequestdefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,14 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Params +namespace ApiDoctor.Validation.Params { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Http; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Http; using Newtonsoft.Json; public class TestSetupRequestDefinition : BasicRequestDefinition @@ -84,7 +84,12 @@ public override ValidationError[] CheckForErrors() /// /// /// - public async Task> MakeSetupRequestAsync(Dictionary storedValues, DocSet documents, ScenarioDefinition scenario, IServiceAccount account, Dictionary parentOutputValues = null) + public async Task> MakeSetupRequestAsync( + Dictionary storedValues, + DocSet documents, + ScenarioDefinition scenario, + IServiceAccount account, + Dictionary parentOutputValues = null) { // Copy the output values from parentOutputValues into our own if (null != parentOutputValues) @@ -146,7 +151,11 @@ public async Task> MakeSetupRequestAsync(Dictionary 0) { errors.Add(new ValidationWarning(ValidationErrorCode.RequestWasRetried, null, "HTTP request was retried {0} times.", response.RetryCount)); diff --git a/ApiDocs.Validation/Properties/AssemblyInfo.cs b/ApiDoctor.Validation/Properties/AssemblyInfo.cs similarity index 92% rename from ApiDocs.Validation/Properties/AssemblyInfo.cs rename to ApiDoctor.Validation/Properties/AssemblyInfo.cs index 63d5a7cc..af397e57 100644 --- a/ApiDocs.Validation/Properties/AssemblyInfo.cs +++ b/ApiDoctor.Validation/Properties/AssemblyInfo.cs @@ -4,11 +4,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("ApiDocs.Validation")] +[assembly: AssemblyTitle("ApiDoctor.Validation")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ApiDocs.Validation")] +[assembly: AssemblyProduct("ApiDoctor.Validation")] [assembly: AssemblyCopyright("Copyright © 2014")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/ApiDocs.Validation/Properties/Resources.Designer.cs b/ApiDoctor.Validation/Properties/Resources.Designer.cs similarity index 95% rename from ApiDocs.Validation/Properties/Resources.Designer.cs rename to ApiDoctor.Validation/Properties/Resources.Designer.cs index d078f4eb..8c4b48d1 100644 --- a/ApiDocs.Validation/Properties/Resources.Designer.cs +++ b/ApiDoctor.Validation/Properties/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace ApiDocs.Validation.Properties { +namespace ApiDoctor.Validation.Properties { using System; @@ -39,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ApiDocs.Validation.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ApiDoctor.Validation.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/ApiDocs.Validation/Properties/Resources.resx b/ApiDoctor.Validation/Properties/Resources.resx similarity index 100% rename from ApiDocs.Validation/Properties/Resources.resx rename to ApiDoctor.Validation/Properties/Resources.resx diff --git a/ApiDocs.Validation/ResourceDefinition.cs b/ApiDoctor.Validation/ResourceDefinition.cs similarity index 61% rename from ApiDocs.Validation/ResourceDefinition.cs rename to ApiDoctor.Validation/ResourceDefinition.cs index 64984c9a..ce767df3 100644 --- a/ApiDocs.Validation/ResourceDefinition.cs +++ b/ApiDoctor.Validation/ResourceDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,16 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; + using System.Collections.Generic; + using System.IO; using System.Linq; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; using Newtonsoft.Json; - using System.Collections.Generic; using Newtonsoft.Json.Linq; + /// /// Represents a class, resource, complex type, or entity type from an API. /// @@ -43,9 +45,11 @@ protected ResourceDefinition(CodeBlockAnnotation sourceAnnotation, string conten this.sourceAnnotation = sourceAnnotation; this.OriginalExampleText = content; this.SourceFile = source; + this.Abstract = sourceAnnotation.Abstract; this.Name = sourceAnnotation.ResourceType; this.NameAka = sourceAnnotation.ResourceTypeAka; this.KeyPropertyName = sourceAnnotation.KeyPropertyName; + this.BaseType = sourceAnnotation.BaseType; if (string.IsNullOrEmpty(sourceAnnotation.ResourceType)) { @@ -57,14 +61,34 @@ protected ResourceDefinition(CodeBlockAnnotation sourceAnnotation, string conten } } + /// + /// abstract + /// + public bool Abstract { get; set; } - #region Public Properties + /// + /// base type + /// + public string BaseType { get; set; } + + /// + /// resolved basetype ref + /// + public ResourceDefinition ResolvedBaseTypeReference { get; set; } /// /// For indexed resources, this specifies the property which is used as the index. /// public string KeyPropertyName { get; set; } + public string ExplicitOrInheritedKeyPropertyName + { + get + { + return this.KeyPropertyName ?? this.ResolvedBaseTypeReference?.ExplicitOrInheritedKeyPropertyName ?? null; + } + } + /// /// Metadata read from the code block sourceAnnotation /// @@ -99,40 +123,84 @@ public CodeBlockAnnotation OriginalMetadata /// The sourceFile file. public DocFile SourceFile {get; protected set;} - #endregion + public bool HasOrInheritsProperty(string name) + { + if (this.Parameters != null && this.Parameters.Any(p => p.Name == name)) + { + return true; + } + + if (this.ResolvedBaseTypeReference == null) + { + return false; + } + + return this.ResolvedBaseTypeReference.HasOrInheritsProperty(name); + } } public class JsonResourceDefinition : ResourceDefinition { - public JsonResourceDefinition(CodeBlockAnnotation sourceAnnotation, string json, DocFile source) + public JsonResourceDefinition(CodeBlockAnnotation sourceAnnotation, string json, DocFile source, IssueLogger issues) : base(sourceAnnotation, json, source, "json") { - ParseJsonInput(); + ParseJsonInput(issues); + } + + public JObject SourceJObject { get; set; } + + public void PatchSourceFile() + { + if (this.SourceFile == null) + { + throw new InvalidOperationException("can only patch a resource that was read from a file"); + } + + var newJson = JsonConvert.SerializeObject(this.SourceJObject, serializerSettings); + var originalFile = File.ReadAllText(this.SourceFile.FullPath); + var modified = originalFile.Replace(this.OriginalExampleText.Trim(), newJson); + this.OriginalExampleText = newJson; + File.WriteAllText(this.SourceFile.FullPath, modified); } - #region Helper Methods /// /// Parse the example resource conetent and populate the resource definition /// /// JSON string representation of an object /// - private void ParseJsonInput() + private void ParseJsonInput(IssueLogger issues) { try { - JObject inputObject = (JObject)JsonConvert.DeserializeObject(this.OriginalExampleText); - this.ExtractProperties(inputObject); + var inputObject = JsonConvert.DeserializeObject(this.OriginalExampleText); + + JObject parsedObject = this.SourceJObject = inputObject as JObject; + if (parsedObject == null) + { + JArray parsedArray = inputObject as JArray; + if (parsedArray == null) + { + throw new ArgumentException("failed to parse example as JObject or JArray"); + } + + if (parsedArray.Count > 0) + { + parsedObject = parsedArray[0] as JObject; + } + } + + if (parsedObject != null) + { + this.ExtractProperties(parsedObject); + } + this.ExampleText = JsonConvert.SerializeObject(inputObject, Formatting.Indented); } catch (Exception ex) { - Logging.LogMessage( - new ValidationError( - ValidationErrorCode.JsonParserException, - this.SourceFile.DisplayName, - "Error parsing resource definition: {0}", - ex.Message)); + issues.Error(ValidationErrorCode.JsonParserException, + $"Error parsing resource definition: {ex.Message}"); throw; } } @@ -155,7 +223,13 @@ private void ExtractProperties(JObject input) this.Parameters = parameters; } - #endregion + + private static JsonSerializerSettings serializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + Formatting = Formatting.Indented, + }; } } diff --git a/ApiDocs.Validation/EnumerationDefinition.cs b/ApiDoctor.Validation/SamplesDefinition.cs similarity index 72% rename from ApiDocs.Validation/EnumerationDefinition.cs rename to ApiDoctor.Validation/SamplesDefinition.cs index d7ae1f0f..ce7d9f22 100644 --- a/ApiDocs.Validation/EnumerationDefinition.cs +++ b/ApiDoctor.Validation/SamplesDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,14 +23,21 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { - public class EnumerationDefinition : ItemDefinition + using System; + + public partial class DocFile { - /// - /// Enumerated value - /// - public string Value { get; set; } + public class SamplesDefinition : ItemDefinition + { + public SamplesDefinition(CodeBlockAnnotation annotation, string content) + { + this.Samples = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + } + public string[] Samples { get; private set; } + } } + } diff --git a/ApiDocs.Validation/Scenarios.cs b/ApiDoctor.Validation/Scenarios.cs similarity index 97% rename from ApiDocs.Validation/Scenarios.cs rename to ApiDoctor.Validation/Scenarios.cs index 582bf373..d0ac2f6c 100644 --- a/ApiDocs.Validation/Scenarios.cs +++ b/ApiDoctor.Validation/Scenarios.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.Config; - using ApiDocs.Validation.Params; + using ApiDoctor.Validation.Config; + using ApiDoctor.Validation.Params; using Newtonsoft.Json; public class ScenarioFile : ConfigFile diff --git a/ApiDocs.Validation/SchemaBuildException.cs b/ApiDoctor.Validation/SchemaBuildException.cs similarity index 96% rename from ApiDocs.Validation/SchemaBuildException.cs rename to ApiDoctor.Validation/SchemaBuildException.cs index 9b61762e..1aaf15c5 100644 --- a/ApiDocs.Validation/SchemaBuildException.cs +++ b/ApiDoctor.Validation/SchemaBuildException.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; diff --git a/ApiDocs.Validation/SingleOrArrayConverter.cs b/ApiDoctor.Validation/SingleOrArrayConverter.cs similarity index 97% rename from ApiDocs.Validation/SingleOrArrayConverter.cs rename to ApiDoctor.Validation/SingleOrArrayConverter.cs index cd5ffe43..4fcab954 100644 --- a/ApiDocs.Validation/SingleOrArrayConverter.cs +++ b/ApiDoctor.Validation/SingleOrArrayConverter.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { /// /// Handle converting JSON properties that can either be a single value or an array of values. diff --git a/ApiDocs.Validation/StringSuggestions.cs b/ApiDoctor.Validation/StringSuggestions.cs similarity index 99% rename from ApiDocs.Validation/StringSuggestions.cs rename to ApiDoctor.Validation/StringSuggestions.cs index 94699548..cfc04310 100644 --- a/ApiDocs.Validation/StringSuggestions.cs +++ b/ApiDoctor.Validation/StringSuggestions.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { internal static class StringSuggestions { diff --git a/ApiDoctor.Validation/SupplementalFile.cs b/ApiDoctor.Validation/SupplementalFile.cs new file mode 100644 index 00000000..faafdcf3 --- /dev/null +++ b/ApiDoctor.Validation/SupplementalFile.cs @@ -0,0 +1,120 @@ +/* + * API Doctor + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the ""Software""), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using ApiDoctor.Validation.Error; +using MarkdownDeep; + +namespace ApiDoctor.Validation +{ + public class SupplementalFile : DocFile + { + private static HashSet LikelyUrlAttributes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "src", "url", + }; + + public SupplementalFile(string basePath, string relativePath, DocSet parent) + :base(basePath, relativePath, parent) + { + } + + protected override string GetContentsOfFile(string tags) + { + return File.ReadAllText(Path.Combine(this.FullPath)); + } + + public override bool Scan(string tags, IssueLogger issues) + { + try + { + var content = this.GetContentsOfFile(tags); + var xd = XDocument.Parse(content); + this.MarkdownLinks = AllAttribues(xd.Root). + Where(a => LikelyUrlAttributes.Contains(a.Name.ToString())). + Select(urlAttr => urlAttr.Value). + Select(url => new LinkInfo { Definition = new LinkDefinition(url, Fixup(url)), Text = url }). + Cast(). + ToList(); + this.HasScanRun = true; + return true; + } + catch (IOException ioex) + { + issues.Error(ValidationErrorCode.ErrorOpeningFile, $"Error reading file contents.", ioex); + return false; + } + catch (Exception ex) + { + issues.Error(ValidationErrorCode.ErrorReadingFile, $"Error reading file contents.", ex); + return false; + } + } + + private IEnumerable AllAttribues(XElement element) + { + foreach (var att in element.Attributes()) + { + yield return att; + } + + foreach (var el in element.Elements()) + { + foreach (var att in AllAttribues(el)) + { + yield return att; + } + } + } + + private string Fixup(string url) + { + if (url.StartsWith("/en-us/", StringComparison.OrdinalIgnoreCase)) + { + url = url.Substring(6); + } + + if (!url.EndsWith(".md", StringComparison.OrdinalIgnoreCase) && + !url.EndsWith(".htm", StringComparison.OrdinalIgnoreCase) && + !url.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) + { + url = url + ".md"; + } + + return url; + } + + private class LinkInfo : MarkdownDeep.ILinkInfo + { + public LinkDefinition Definition { get; set; } + + public string Text { get; set; } + } + } +} diff --git a/ApiDocs.Validation/TableSpec/TableAndHeaderConfig.json b/ApiDoctor.Validation/TableSpec/TableAndHeaderConfig.json similarity index 100% rename from ApiDocs.Validation/TableSpec/TableAndHeaderConfig.json rename to ApiDoctor.Validation/TableSpec/TableAndHeaderConfig.json diff --git a/ApiDocs.Validation/TableSpec/TableDefinition.cs b/ApiDoctor.Validation/TableSpec/TableDefinition.cs similarity index 92% rename from ApiDocs.Validation/TableSpec/TableDefinition.cs rename to ApiDoctor.Validation/TableSpec/TableDefinition.cs index d9cc61b8..e998b5a8 100644 --- a/ApiDocs.Validation/TableSpec/TableDefinition.cs +++ b/ApiDoctor.Validation/TableSpec/TableDefinition.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.TableSpec +namespace ApiDoctor.Validation.TableSpec { using System.Collections.Generic; using System.Linq; @@ -36,6 +36,8 @@ public class TableDefinition public string Title { get; set; } + public List UsedIn { get; } = new List(); + public TableDefinition(TableBlockType type, IEnumerable rows, string headerText) { this.Type = type; diff --git a/ApiDocs.Validation/TableSpec/TableParserConfig.cs b/ApiDoctor.Validation/TableSpec/TableParserConfig.cs similarity index 84% rename from ApiDocs.Validation/TableSpec/TableParserConfig.cs rename to ApiDoctor.Validation/TableSpec/TableParserConfig.cs index feffd533..92fd4c0e 100644 --- a/ApiDocs.Validation/TableSpec/TableParserConfig.cs +++ b/ApiDoctor.Validation/TableSpec/TableParserConfig.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.TableSpec +namespace ApiDoctor.Validation.TableSpec { using Newtonsoft.Json; using System.Collections.Generic; @@ -81,6 +81,12 @@ public string[] Titles get; set; } + [JsonProperty("parents")] + public string[] Parents + { + get; set; + } + /// /// Indicates the Type of TableRule used to parse a matching table. /// @@ -95,6 +101,27 @@ public TableRule ParseRule { get; set; } + + [JsonIgnore] + public string Stamp + { + get; + private set; + } + + [JsonIgnore] + public bool IsFlags + { + get; + set; + } + + public TableDecoder StampedWith(string stamp) + { + var clone = (TableDecoder)this.MemberwiseClone(); + clone.Stamp = stamp; + return clone; + } } public class TableRule diff --git a/ApiDocs.Validation/TableSpec/TableSpecConverter.cs b/ApiDoctor.Validation/TableSpec/tablespecconverter.cs similarity index 60% rename from ApiDocs.Validation/TableSpec/TableSpecConverter.cs rename to ApiDoctor.Validation/TableSpec/tablespecconverter.cs index f2162c08..58996c9b 100644 --- a/ApiDocs.Validation/TableSpec/TableSpecConverter.cs +++ b/ApiDoctor.Validation/TableSpec/tablespecconverter.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,13 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.TableSpec +namespace ApiDoctor.Validation.TableSpec { + using System; using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Json; using MarkdownDeep; /// @@ -61,7 +62,7 @@ private Dictionary GenerateDecoderRing(TableParserConfig c /// /// Creates a new instance of TableSpecConverter using the pre-programmed - /// table parser rules from Markdown Scanner. + /// table parser rules from API Doctor. /// /// public static TableSpecConverter FromDefaultConfiguration() @@ -74,11 +75,7 @@ public static TableSpecConverter FromDefaultConfiguration() /// /// Convert a tablespec block into one of our internal object model representations /// - /// - /// - /// - /// - public TableDefinition ParseTableSpec(Block tableSpecBlock, Block lastHeaderBlock, out ValidationError[] errors) + public TableDefinition ParseTableSpec(Block tableSpecBlock, Stack headerStack, IssueLogger issues) { List discoveredErrors = new List(); List items = new List(); @@ -86,14 +83,17 @@ public TableDefinition ParseTableSpec(Block tableSpecBlock, Block lastHeaderBloc var tableShape = tableSpecBlock.Table; TableDecoder decoder = new TableDecoder { Type = TableBlockType.Unknown }; - string headerText = null; + + var headerText = headerStack.Peek()?.Title; + // Try matching based on header - if (null != lastHeaderBlock && null != lastHeaderBlock.Content) + if (headerText != null) { - headerText = lastHeaderBlock.Content; - var matchingDecoder = FindDecoderFromHeaderText(headerText); + var matchingDecoder = FindDecoderFromHeaderText(headerStack); if (null != matchingDecoder) + { decoder = matchingDecoder; + } } // Try matching based on shape @@ -101,7 +101,9 @@ public TableDefinition ParseTableSpec(Block tableSpecBlock, Block lastHeaderBloc { var matchingDecoder = FindDecoderFromShape(tableShape); if (null != matchingDecoder) + { decoder = matchingDecoder; + } } switch (decoder.Type) @@ -111,24 +113,24 @@ public TableDefinition ParseTableSpec(Block tableSpecBlock, Block lastHeaderBloc break; case TableBlockType.PathParameters: - items.AddRange(ParseParameterTable(tableShape, ParameterLocation.Path, decoder)); + items.AddRange(ParseParameterTable(tableShape, ParameterLocation.Path, decoder, issues.For($"{decoder.Type}Table"))); break; case TableBlockType.ResourcePropertyDescriptions: case TableBlockType.RequestObjectProperties: case TableBlockType.ResponseObjectProperties: - items.AddRange(ParseParameterTable(tableShape, ParameterLocation.JsonObject, decoder)); + items.AddRange(ParseParameterTable(tableShape, ParameterLocation.JsonObject, decoder, issues)); break; case TableBlockType.ResourceNavigationPropertyDescriptions: - items.AddRange(ParseParameterTable(tableShape, ParameterLocation.JsonObject, decoder, true)); + items.AddRange(ParseParameterTable(tableShape, ParameterLocation.JsonObject, decoder, issues, true)); break; case TableBlockType.HttpHeaders: - items.AddRange(ParseParameterTable(tableShape, ParameterLocation.Header, decoder)); + items.AddRange(ParseParameterTable(tableShape, ParameterLocation.Header, decoder, issues)); break; case TableBlockType.QueryStringParameters: - items.AddRange(ParseParameterTable(tableShape, ParameterLocation.QueryString, decoder)); + items.AddRange(ParseParameterTable(tableShape, ParameterLocation.QueryString, decoder, issues)); break; case TableBlockType.EnumerationValues: @@ -140,15 +142,15 @@ public TableDefinition ParseTableSpec(Block tableSpecBlock, Block lastHeaderBloc break; case TableBlockType.Unknown: - discoveredErrors.Add(new ValidationMessage(null, "Ignored unclassified table: headerText='{0}', tableHeaders='{1}'", headerText, tableShape.ColumnHeaders != null ? tableShape.ColumnHeaders.ComponentsJoinedByString(",") : "null")); + var headers = tableShape.ColumnHeaders != null ? tableShape.ColumnHeaders.ComponentsJoinedByString(",") : "null"; + issues.Message($"Ignored unclassified table: headerText='{headerText}', tableHeaders='{headers}'"); break; default: - discoveredErrors.Add(new ValidationMessage(null, "Ignored table: classification='{2}', headerText='{0}', tableHeaders='{1}'", headerText, tableShape.ColumnHeaders != null ? tableShape.ColumnHeaders.ComponentsJoinedByString(",") : "null", decoder.Type)); + var hdrs = tableShape.ColumnHeaders != null ? tableShape.ColumnHeaders.ComponentsJoinedByString(",") : "null"; + issues.Message($"Ignored table: classification='{decoder.Type}', headerText='{headerText}', tableHeaders='{hdrs}'"); break; } - errors = discoveredErrors.ToArray(); - return new TableDefinition(decoder.Type, items, headerText); } @@ -171,29 +173,52 @@ private static IEnumerable ParseErrorTable(IMarkdownTable table return records; } - private static IEnumerable ParseParameterTable(IMarkdownTable table, ParameterLocation location, TableDecoder decoder, bool navigationProperties = false) + private static IEnumerable ParseParameterTable(IMarkdownTable table, ParameterLocation location, TableDecoder decoder, IssueLogger issues, bool navigationProperties = false) { - var records = from r in table.RowValues - select new ParameterDefinition - { - Name = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["name"]), - Type = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["type"]).ParseParameterDataType(defaultValue: ParameterDataType.String), - Description = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]), - Required = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]).IsRequired(), - Location = location, - IsNavigatable = navigationProperties - }; + var records = table.RowValues.Select(r => new ParameterDefinition + { + Name = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["name"]), + Type = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["type"]).ParseParameterDataType(defaultValue: ParameterDataType.String), + Description = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]), + Required = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]).IsRequired(), + Optional = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]).IsOptional(), + Location = location, + IsNavigatable = navigationProperties, + }).ToList(); + + var badRows = records.Count(r => string.IsNullOrEmpty(r.Name)); + if (badRows > 0) + { + var tableHeaders = $"|{ string.Join("|", table.ColumnHeaders)}|"; + if (badRows == records.Count) + { + issues.Warning(ValidationErrorCode.MarkdownParserError, $"Failed to parse any rows out of table with headers: {tableHeaders}"); + return Enumerable.Empty(); + } + + issues.Warning(ValidationErrorCode.ParameterParserError, $"Failed to parse {badRows} row(s) in table with headers: {tableHeaders}"); + records = records.Where(r => !string.IsNullOrEmpty(r.Name)).ToList(); + } + return records; } private static IEnumerable ParseEnumerationTable(IMarkdownTable table, TableDecoder decoder) { - var records = from r in table.RowValues - select new EnumerationDefinition - { - Value = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["value"]), - Description = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]) - }; + List records = new List(); + foreach (var r in table.RowValues) + { + var usedColumns = new List(); + records.Add(new EnumerationDefinition + { + MemberName = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["memberName"], usedColumns), + NumericValue = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["numericValue"], usedColumns).ToInt32(), + Description = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"], usedColumns), + TypeName = decoder.Stamp, + IsFlags = decoder.IsFlags, + }); + } + return records; } @@ -210,14 +235,60 @@ private static IEnumerable ParseAuthScopeTable(IMarkdownTab return records; } - - - private TableDecoder FindDecoderFromHeaderText(string lastHeader) + private TableDecoder FindDecoderFromHeaderText(Stack headerStack) { - TableDecoder decoder = (from key in CommonHeaderContentMap.Keys - where lastHeader.ContainsIgnoreCase(key) - select CommonHeaderContentMap[key]).FirstOrDefault(); - return decoder; + foreach (var kvp in this.CommonHeaderContentMap) + { + var lastHeader = headerStack.Peek().Title; + if (lastHeader.IContains(kvp.Key)) + { + var decoder = kvp.Value; + if (decoder.Parents != null) + { + foreach (var parentTitle in decoder.Parents.Where(t => t.Contains("{x}"))) + { + var matchKey = parentTitle.Replace("{x} ", string.Empty); + foreach (var parentHeader in headerStack.Skip(1).Select(h => h.Title)) + { + var matchIndex = parentHeader.IndexOf(matchKey); + if (matchIndex != -1) + { + var matchWord = parentHeader.Substring(0, matchIndex - 1).Split(' ').LastOrDefault(); + if (char.IsUpper(matchWord[0])) + { + matchWord = char.ToLowerInvariant(matchWord[0]) + matchWord.Substring(1); + } + + decoder = decoder.StampedWith(matchWord); + decoder.IsFlags = lastHeader.Contains("flags") || parentHeader.Contains("flags"); + return decoder; + } + } + } + } + + return decoder; + } + else if (kvp.Key.Contains("{x}")) + { + // eventually bring FriendlyWildcard into this code... + var matchIndex = lastHeader.IndexOf(kvp.Key.Replace("{x} ", string.Empty)); + if (matchIndex != -1) + { + var lastWord = lastHeader.Substring(0, matchIndex - 1).Split(' ').LastOrDefault(); + if (char.IsUpper(lastWord[0])) + { + lastWord = char.ToLowerInvariant(lastWord[0]) + lastWord.Substring(1); + } + + var decoder = kvp.Value.StampedWith(lastWord); + decoder.IsFlags = lastHeader.Contains("flags"); + return decoder; + } + } + } + + return null; } private TableDecoder FindDecoderFromShape(IMarkdownTable table) diff --git a/ApiDocs.Validation/Tags/TagProcessor.cs b/ApiDoctor.Validation/Tags/TagProcessor.cs similarity index 69% rename from ApiDocs.Validation/Tags/TagProcessor.cs rename to ApiDoctor.Validation/Tags/TagProcessor.cs index 883fe804..9290aa9d 100644 --- a/ApiDocs.Validation/Tags/TagProcessor.cs +++ b/ApiDoctor.Validation/Tags/TagProcessor.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -27,12 +27,12 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using ApiDocs.Validation.Error; +using ApiDoctor.Validation.Error; using System.Text.RegularExpressions; using MarkdownDeep; using System.Collections.Generic; -namespace ApiDocs.Validation.Tags +namespace ApiDoctor.Validation.Tags { public class TagProcessor { @@ -66,137 +66,138 @@ public TagProcessor(string tags, string docSetRoot, Action logM /// The file containing the Markdown contents to preprocess. /// The preprocessed contents of the file. public string Preprocess(FileInfo sourceFile) - { - StringWriter writer = new StringWriter(); - StreamReader reader = new StreamReader(sourceFile.OpenRead()); - - long lineNumber = 0; - int tagCount = 0; - int dropCount = 0; - string nextLine; - while ((nextLine = reader.ReadLine()) != null) + { + using (StringWriter writer = new StringWriter()) + using (StreamReader reader = new StreamReader(sourceFile.OpenRead())) { - lineNumber++; - - // Check if this is an [END] marker - if (IsEndLine(nextLine)) + long lineNumber = 0; + int tagCount = 0; + int dropCount = 0; + string nextLine; + while ((nextLine = reader.ReadLine()) != null) { - // We SHOULD be in a tag - if (tagCount > 0) + lineNumber++; + + // Check if this is an [END] marker + if (IsEndLine(nextLine)) { - if (dropCount <= 0) + // We SHOULD be in a tag + if (tagCount > 0) { - // To keep output clean if author did not insert blank line before - writer.WriteLine(""); - writer.WriteLine(nextLine); + if (dropCount <= 0) + { + // To keep output clean if author did not insert blank line before + writer.WriteLine(""); + writer.WriteLine(nextLine); + } + else + { + dropCount--; + } + + // Decrement tag count + tagCount--; } else { - dropCount--; + LogMessage(new ValidationError(ValidationErrorCode.MarkdownParserError, + string.Concat(sourceFile.Name, ":", lineNumber), "Unexpected [END] marker.")); } - // Decrement tag count - tagCount--; + continue; } - else + + // Check if this is a [TAGS] marker + if (IsTagLine(nextLine, sourceFile.Name, lineNumber)) { - LogMessage(new ValidationError(ValidationErrorCode.MarkdownParserError, - string.Concat(sourceFile.Name, ":", lineNumber), "Unexpected [END] marker.")); - } + string[] tags = GetTags(nextLine); - continue; - } + LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), "Found TAGS line with {0}", string.Join(",", tags))); - // Check if this is a [TAGS] marker - if (IsTagLine(nextLine, sourceFile.Name, lineNumber)) - { - string[] tags = GetTags(nextLine); + tagCount++; - LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), "Found TAGS line with {0}", string.Join(",", tags))); + if (dropCount > 0 || !TagsAreIncluded(tags)) + { + dropCount++; + } - tagCount++; + if (dropCount == 1) + { + LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), + "{0} not found in the specified tags to include, content will be dropped.", string.Join(",", tags))); + } + else if (dropCount > 1) + { + LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), + "Dropping content due to containing [TAGS]")); + } + else + { + // Keep line + writer.WriteLine(nextLine); + // To keep output clean if author did not insert blank line after + writer.WriteLine(""); + } - if (dropCount > 0 || !TagsAreIncluded(tags)) - { - dropCount++; + continue; } - if (dropCount == 1) - { - LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), - "{0} not found in the specified tags to include, content will be dropped.", string.Join(",", tags))); - } - else if (dropCount > 1) - { - LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), - "Dropping content due to containing [TAGS]")); - } - else + if (tagCount > 0 && dropCount > 0) { - // Keep line - writer.WriteLine(nextLine); - // To keep output clean if author did not insert blank line after - writer.WriteLine(""); + // Inside of a tag that shouldn't be included + LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), "Removing tagged content")); + continue; } - continue; - } - - if (tagCount > 0 && dropCount > 0) - { - // Inside of a tag that shouldn't be included - LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), "Removing tagged content")); - continue; - } - - // Remove double blockquotes (">>") - if (IsDoubleBlockQuote(nextLine)) - { - LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), "Removing DoubleBlockQuote")); - continue; - } - - // Import include file content - if (IsIncludeLine(nextLine)) - { - FileInfo includeFile = GetIncludeFile(nextLine, sourceFile); - if (!includeFile.Exists) + // Remove double blockquotes (">>") + if (IsDoubleBlockQuote(nextLine)) { - LogMessage(new ValidationError(ValidationErrorCode.ErrorOpeningFile, nextLine, "The included file {0} was not found", includeFile.FullName)); + LogMessage(new ValidationMessage(string.Concat(sourceFile.Name, ":", lineNumber), "Removing DoubleBlockQuote")); continue; } - if (includeFile != null) + // Import include file content + if (IsIncludeLine(nextLine)) { - if (includeFile.FullName.Equals(sourceFile.FullName)) + FileInfo includeFile = GetIncludeFile(nextLine, sourceFile); + if (!includeFile.Exists) { - LogMessage(new ValidationError(ValidationErrorCode.MarkdownParserError, nextLine, "A Markdown file cannot include itself")); + LogMessage(new ValidationError(ValidationErrorCode.ErrorOpeningFile, nextLine, "The included file {0} was not found", includeFile.FullName)); continue; } - string includeContent = Preprocess(includeFile); + if (includeFile != null) + { + if (includeFile.FullName.Equals(sourceFile.FullName)) + { + LogMessage(new ValidationError(ValidationErrorCode.MarkdownParserError, nextLine, "A Markdown file cannot include itself")); + continue; + } - writer.WriteLine(includeContent); - } - else - { - LogMessage(new ValidationError(ValidationErrorCode.ErrorReadingFile, nextLine, "Could not load include content from {0}", includeFile.FullName)); + string includeContent = Preprocess(includeFile); + + writer.WriteLine(includeContent); + } + else + { + LogMessage(new ValidationError(ValidationErrorCode.ErrorReadingFile, nextLine, "Could not load include content from {0}", includeFile.FullName)); + } + + continue; } - continue; + writer.WriteLine(nextLine); } - writer.WriteLine(nextLine); - } + if (tagCount > 0) + { + // If inTag is true, there was a missing [END] tag somewhere + LogMessage(new ValidationError(ValidationErrorCode.MarkdownParserError, + sourceFile.Name, "The file ended while still in a [TAGS] tag. All [TAGS] must be closed with an [END] tag.")); + } - if (tagCount > 0) - { - // If inTag is true, there was a missing [END] tag somewhere - LogMessage(new ValidationError(ValidationErrorCode.MarkdownParserError, - sourceFile.Name, "The file ended while still in a [TAGS] tag. All [TAGS] must be closed with an [END] tag.")); + return writer.ToString(); } - - return writer.ToString(); } /// diff --git a/ApiDocs.Validation/ValidationConfig.cs b/ApiDoctor.Validation/ValidationConfig.cs similarity index 97% rename from ApiDocs.Validation/ValidationConfig.cs rename to ApiDoctor.Validation/ValidationConfig.cs index 27dc7c6c..38c0ef27 100644 --- a/ApiDocs.Validation/ValidationConfig.cs +++ b/ApiDoctor.Validation/ValidationConfig.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { public static class ValidationConfig { diff --git a/ApiDocs.Validation/Writers/DocumentPublisher.cs b/ApiDoctor.Validation/Writers/DocumentPublisher.cs similarity index 99% rename from ApiDocs.Validation/Writers/DocumentPublisher.cs rename to ApiDoctor.Validation/Writers/DocumentPublisher.cs index cd5c0b45..94ea9c0e 100644 --- a/ApiDocs.Validation/Writers/DocumentPublisher.cs +++ b/ApiDoctor.Validation/Writers/DocumentPublisher.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Writers +namespace ApiDoctor.Validation.Writers { using System; using System.Collections.Generic; @@ -32,7 +32,7 @@ namespace ApiDocs.Validation.Writers using System.IO; using System.Linq; using System.Threading.Tasks; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; public abstract class DocumentPublisher { @@ -121,7 +121,7 @@ protected void LogMessage(ValidationError message) /// /// /// - public virtual async Task PublishToFolderAsync(string outputFolder) + public virtual async Task PublishToFolderAsync(string outputFolder, IssueLogger issues) { this.Messages.Clear(); diff --git a/ApiDocs.Validation/Writers/MarkdownPublisher.cs.cs b/ApiDoctor.Validation/Writers/MarkdownPublisher.cs.cs similarity index 96% rename from ApiDocs.Validation/Writers/MarkdownPublisher.cs.cs rename to ApiDoctor.Validation/Writers/MarkdownPublisher.cs.cs index be8a7ac6..362eb8c0 100644 --- a/ApiDocs.Validation/Writers/MarkdownPublisher.cs.cs +++ b/ApiDoctor.Validation/Writers/MarkdownPublisher.cs.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,12 +23,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Writers +namespace ApiDoctor.Validation.Writers { using System.IO; using System.Text; using System.Threading.Tasks; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; public class MarkdownPublisher : DocumentPublisher { diff --git a/ApiDocs.Validation/Writers/IPublishOptions.cs b/ApiDoctor.Validation/Writers/ipublishoptions.cs similarity index 98% rename from ApiDocs.Validation/Writers/IPublishOptions.cs rename to ApiDoctor.Validation/Writers/ipublishoptions.cs index 3dac6640..f14d3dcd 100644 --- a/ApiDocs.Validation/Writers/IPublishOptions.cs +++ b/ApiDoctor.Validation/Writers/ipublishoptions.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -29,7 +29,7 @@ using System.Text; using System.Threading.Tasks; -namespace ApiDocs.Validation.Writers +namespace ApiDoctor.Validation.Writers { public interface IPublishOptions { diff --git a/ApiDocs.Validation/Writers/OutlinePublisher.cs b/ApiDoctor.Validation/Writers/outlinepublisher.cs similarity index 94% rename from ApiDocs.Validation/Writers/OutlinePublisher.cs rename to ApiDoctor.Validation/Writers/outlinepublisher.cs index b37e66e6..01fa059c 100644 --- a/ApiDocs.Validation/Writers/OutlinePublisher.cs +++ b/ApiDoctor.Validation/Writers/outlinepublisher.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,11 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation.Writers +namespace ApiDoctor.Validation.Writers { using System.IO; using System.Threading.Tasks; + using ApiDoctor.Validation.Error; public class OutlinePublisher : DocumentPublisher { @@ -37,7 +38,7 @@ public OutlinePublisher(DocSet docset) } - public override async Task PublishToFolderAsync(string outputFolder) + public override async Task PublishToFolderAsync(string outputFolder, IssueLogger issues) { StreamWriter writer = new StreamWriter(Path.Combine(outputFolder, "outline.txt")) { AutoFlush = true }; foreach (var doc in this.Documents.Files) diff --git a/ApiDocs.Validation/Logging.cs b/ApiDoctor.Validation/logging.cs similarity index 96% rename from ApiDocs.Validation/Logging.cs rename to ApiDoctor.Validation/logging.cs index d1a5ebbf..d2d72d25 100644 --- a/ApiDocs.Validation/Logging.cs +++ b/ApiDoctor.Validation/logging.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,10 +23,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; - using ApiDocs.Validation.Error; + using ApiDoctor.Validation.Error; /// /// Provides an accessible Logger for use throughout the library and callers to the library diff --git a/ApiDocs.Validation/MethodValidationExtensionMethods.cs b/ApiDoctor.Validation/methodvalidationextensionmethods.cs similarity index 96% rename from ApiDocs.Validation/MethodValidationExtensionMethods.cs rename to ApiDoctor.Validation/methodvalidationextensionmethods.cs index 60950b6e..d78c85b0 100644 --- a/ApiDocs.Validation/MethodValidationExtensionMethods.cs +++ b/ApiDoctor.Validation/methodvalidationextensionmethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,7 +23,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Collections.Generic; @@ -31,10 +31,10 @@ namespace ApiDocs.Validation using System.Linq; using System.Text; using System.Threading.Tasks; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Params; - using ApiDocs.Validation.Http; - using ApiDocs.Validation.Json; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Params; + using ApiDoctor.Validation.Http; + using ApiDoctor.Validation.Json; public static class MethodValidationExtensionMethods { @@ -198,7 +198,8 @@ private static async Task ValidateMethodWithScenarioAsync( // Execute the actual tested method (the result of the method preview call, which made the test-setup requests) startTicks = DateTimeOffset.UtcNow.Ticks; - var actualResponse = await requestPreview.GetResponseAsync(primaryAccount); + var issues = new IssueLogger(); + var actualResponse = await requestPreview.GetResponseAsync(primaryAccount, issues); TimeSpan actualMethodDuration = new TimeSpan(DateTimeOffset.UtcNow.Ticks - startTicks); var requestResults = results[actionName]; @@ -209,24 +210,22 @@ private static async Task ValidateMethodWithScenarioAsync( { new ValidationWarning(ValidationErrorCode.RequestWasRetried, null, "HTTP request was retried {0} times.", actualResponse.RetryCount) }); } - requestResults.AddResults( new ValidationError[] { new ValidationMessage(null, "HTTP Response:\r\n{0}", actualResponse.FullText(false)) }); requestResults.Duration = actualMethodDuration; // Perform validation on the method's actual response - ValidationError[] errors; - method.ValidateResponse(actualResponse, expectedResponse, scenario, out errors, options); + method.ValidateResponse(actualResponse, expectedResponse, scenario, issues, options); - requestResults.AddResults(errors); + requestResults.AddResults(issues.Issues.ToArray()); // TODO: If the method is defined as a long running operation, we need to go poll the status // URL to make sure that the operation finished and the response type is valid. - if (errors.WereErrors()) + if (issues.Issues.WereErrors()) results.SetOutcome(actionName, ValidationOutcome.Error); - else if (errors.WereWarnings()) + else if (issues.Issues.WereWarnings()) results.SetOutcome(actionName, ValidationOutcome.Warning); else results.SetOutcome(actionName, ValidationOutcome.Passed); diff --git a/ApiDocs.Validation/packages.config b/ApiDoctor.Validation/packages.config similarity index 100% rename from ApiDocs.Validation/packages.config rename to ApiDoctor.Validation/packages.config diff --git a/ApiDocs.Validation/ScenarioExtensionMethods.cs b/ApiDoctor.Validation/scenarioextensionmethods.cs similarity index 81% rename from ApiDocs.Validation/ScenarioExtensionMethods.cs rename to ApiDoctor.Validation/scenarioextensionmethods.cs index 0c823f1f..149286fe 100644 --- a/ApiDocs.Validation/ScenarioExtensionMethods.cs +++ b/ApiDoctor.Validation/scenarioextensionmethods.cs @@ -1,5 +1,5 @@ /* - * Markdown Scanner + * API Doctor * Copyright (c) Microsoft Corporation * All rights reserved. * @@ -23,15 +23,15 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace ApiDocs.Validation +namespace ApiDoctor.Validation { using System; using System.Collections.Generic; using System.Linq; - using ApiDocs.Validation.Error; - using ApiDocs.Validation.Http; - using ApiDocs.Validation.Json; - using ApiDocs.Validation.Params; + using ApiDoctor.Validation.Error; + using ApiDoctor.Validation.Http; + using ApiDoctor.Validation.Json; + using ApiDoctor.Validation.Params; using Newtonsoft.Json.Linq; internal static class InternalScenarioExtensionMethods @@ -43,11 +43,11 @@ internal static class InternalScenarioExtensionMethods /// /// /// - public static void ValidateExpectations(this ScenarioDefinition scenario, HttpResponse actualResponse, List detectedErrors) + public static void ValidateExpectations(this ScenarioDefinition scenario, HttpResponse actualResponse, IssueLogger issues) { if (scenario == null) throw new ArgumentNullException("scenario"); if (actualResponse == null) throw new ArgumentNullException("actualResponse"); - if (detectedErrors == null) throw new ArgumentNullException("detectedErrors"); + if (issues == null) throw new ArgumentNullException("issues"); var expectations = scenario.Expectations; if (null == expectations || expectations.Count == 0) @@ -61,29 +61,29 @@ public static void ValidateExpectations(this ScenarioDefinition scenario, HttpRe switch (type) { case PlaceholderLocation.Body: - ExpectationSatisfied(key, actualResponse.Body, expectedValues, detectedErrors); + ExpectationSatisfied(key, actualResponse.Body, expectedValues, issues); break; case PlaceholderLocation.HttpHeader: - ExpectationSatisfied(key, actualResponse.Headers[keyIndex], expectedValues, detectedErrors); + ExpectationSatisfied(key, actualResponse.Headers[keyIndex], expectedValues, issues); break; case PlaceholderLocation.Json: try { object value = JsonPath.ValueFromJsonPath(actualResponse.Body, keyIndex); - ExpectationSatisfied(key, value, expectedValues, detectedErrors); + ExpectationSatisfied(key, value, expectedValues, issues); } catch (Exception ex) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonParserException, null, ex.Message)); + issues.Error(ValidationErrorCode.JsonParserException, string.Empty, ex); } break; case PlaceholderLocation.Invalid: case PlaceholderLocation.StoredValue: case PlaceholderLocation.Url: - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.InvalidExpectationKey, null, "The expectation key {0} is invalid. Supported types are Body, HttpHeader, and JsonPath.", key)); + issues.Warning(ValidationErrorCode.InvalidExpectationKey, $"The expectation key {key} is invalid. Supported types are Body, HttpHeader, and JsonPath."); break; } } @@ -95,16 +95,16 @@ public static void ValidateExpectations(this ScenarioDefinition scenario, HttpRe /// The name of the expectation being checked. /// The value for the expectation to check. /// Can either be a single value or an array of values that are considered valid. - /// A collection of validation errors that will be added to when errors are found. + /// A collection of validation errors that will be added to when errors are found. /// - private static bool ExpectationSatisfied(string key, object actualValue, object expectedValues, List detectedErrors) + private static bool ExpectationSatisfied(string key, object actualValue, object expectedValues, IssueLogger issues) { if (null == key) throw new ArgumentNullException("key"); - if (null == detectedErrors) throw new ArgumentNullException("detectedErrors"); + if (null == issues) throw new ArgumentNullException("issues"); if (actualValue == null && expectedValues != null) { - detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectationConditionFailed, null, "Expectation {0}={1} failed. Actual value was null and a value was expected.", key, expectedValues)); + issues.Error(ValidationErrorCode.ExpectationConditionFailed, $"Expectation {key}={expectedValues} failed. Actual value was null and a value was expected."); return false; } @@ -175,7 +175,7 @@ private static bool ExpectationSatisfied(string key, object actualValue, object } } - detectedErrors.Add(new ValidationError(ValidationErrorCode.ExpectationConditionFailed, null, "Expectation {0} = {1} failed. Actual value: {2}", key, expectedValues, actualValue)); + issues.Error(ValidationErrorCode.ExpectationConditionFailed, $"Expectation {key} = {expectedValues} failed. Actual value: {actualValue}"); return false; } } diff --git a/MarkdownScanner.sln b/ApiDoctor.sln similarity index 87% rename from MarkdownScanner.sln rename to ApiDoctor.sln index f74e6cc5..b8131760 100644 --- a/MarkdownScanner.sln +++ b/ApiDoctor.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2005 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Content", "Content", "{464F5D09-2636-459F-96C9-C968FF4CA7B5}" ProjectSection(SolutionItems) = preProject @@ -27,17 +27,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeep", "OSS\markdow EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft", "Microsoft", "{7C7E72AE-EC3D-4EB9-B919-165F794C4902}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDocs.Validation", "ApiDocs.Validation\ApiDocs.Validation.csproj", "{33B10320-3802-49CF-8965-3510AE66D5EC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDoctor.Validation", "ApiDoctor.Validation\ApiDoctor.Validation.csproj", "{33B10320-3802-49CF-8965-3510AE66D5EC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDocs.ConsoleApp", "ApiDocs.Console\ApiDocs.ConsoleApp.csproj", "{A6F3993F-59C6-4985-ACF1-4D837D61E98F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDoctor.ConsoleApp", "ApiDoctor.Console\ApiDoctor.ConsoleApp.csproj", "{A6F3993F-59C6-4985-ACF1-4D837D61E98F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDocs.Validation.UnitTests", "ApiDocs.Validation.UnitTests\ApiDocs.Validation.UnitTests.csproj", "{EE3453F1-FD69-406C-9BD7-0643D6E999F3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDoctor.Validation.UnitTests", "ApiDoctor.Validation.UnitTests\ApiDoctor.Validation.UnitTests.csproj", "{EE3453F1-FD69-406C-9BD7-0643D6E999F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDocs.Publishing", "ApiDocs.Publishing\ApiDocs.Publishing.csproj", "{B675CF73-AA42-4A54-B5E7-FF5F155DA4A7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDoctor.Publishing", "ApiDoctor.Publishing\ApiDoctor.Publishing.csproj", "{B675CF73-AA42-4A54-B5E7-FF5F155DA4A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDocs.DocumentationGeneration", "ApiDocs.DocumentationGeneration\ApiDocs.DocumentationGeneration.csproj", "{CD27998C-4021-4299-970B-91BE877FD01B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDoctor.DocumentationGeneration", "ApiDoctor.DocumentationGeneration\ApiDoctor.DocumentationGeneration.csproj", "{CD27998C-4021-4299-970B-91BE877FD01B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDocs.DocumentationGeneration.UnitTests", "ApiDocs.DocumentationGeneration.UnitTests\ApiDocs.DocumentationGeneration.UnitTests.csproj", "{32323786-6B69-4A7A-A5DA-DBBBF1148387}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDoctor.DocumentationGeneration.UnitTests", "ApiDoctor.DocumentationGeneration.UnitTests\ApiDoctor.DocumentationGeneration.UnitTests.csproj", "{32323786-6B69-4A7A-A5DA-DBBBF1148387}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -138,6 +138,9 @@ Global {CD27998C-4021-4299-970B-91BE877FD01B} = {7C7E72AE-EC3D-4EB9-B919-165F794C4902} {32323786-6B69-4A7A-A5DA-DBBBF1148387} = {7C7E72AE-EC3D-4EB9-B919-165F794C4902} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {94C1DEF9-05B7-4ABB-887D-CDD0830FF494} + EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = OneDrive.ApiDocumentation.CommandLine\OneDrive.ApiDocumentation.Console.csproj EndGlobalSection diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b692e2a4..8c0aaabf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,11 +10,11 @@ Thank you for your interest in Markdown Scanner! You can contribute to Markdown Scanner in these ways: -* Create issues based on bugs you've found when using markdown-scanner. -* Create issues based on desired but missing functionality for markdown-scanner. -* Contribute additional unit test cases that increase code coverage and validation of key scenarios for markdown-scanner. Tests should always pass on the latest master codebase. +* Create issues based on bugs you've found when using API Doctor. +* Create issues based on desired but missing functionality for API Doctor. +* Contribute additional unit test cases that increase code coverage and validation of key scenarios for API Doctor. Tests should always pass on the latest master codebase. * Contribute bug fixes and unit tests that validate the bug fix. -* Contribute new functionality and approriate test scenarios. +* Contribute new functionality and appropriate test scenarios. ## Before we can accept your pull request diff --git a/Deploy/DeployToAppVeyor.bat b/Deploy/DeployToAppVeyor.bat deleted file mode 100644 index 55d4ebde..00000000 --- a/Deploy/DeployToAppVeyor.bat +++ /dev/null @@ -1,4 +0,0 @@ -msbuild ../MarkdownScanner.sln /p:Configuration=Debug -nuget pack ../apidocs.nuspec -nuget push *.nupkg -ApiKey h6e37a0g5y1a052yo3mnuauq -Source https://ci.appveyor.com/nuget/rgregg-dju7gcli4m8k/api/v2/package -del *.nupkg \ No newline at end of file diff --git a/Deploy/nuget.exe b/Deploy/nuget.exe deleted file mode 100644 index 8dd7e45ae75d1a55fc669f09bdef4a49b16a95dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1664000 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

s0HCb|zhvuu4HkXyo61ZE5M zP2eRul1?{CmsA`GF`Vs*nm|447r5{%8C;A9X z)oQ^Z7?*qaYGl^uKKO;z)+e`O|Ge_+XSbtbC}p9xJDKEWqjhl`W*;hLi9{p{P)pP# ziUyK&Z^_A62$9=S(YK@H8E4?e+tI6OmG6?s+DTn^KN058pNE&$i4^VosY^`>t|%Lk z%tTmmN7gaY6s0-E-ee)IRCUma+hQD7L;=8dF9%p|fi#5P1|#r4n;r~ox0QkuC+j0$2NkqZcUZv3!PL$JQ^8JAOm)A^CG zQ2>CHo8mi~YP*|Ttx<2}G_V{gVM?8ej^t+Ee7R&fGQqJIry7^Oyvw6im>x7+S#%2v zg4WiXP?wTdXI@L-~ zYs(ln-G4>aIJ8ru=_d^x2~=dx5O)j(z{rj<2EeGGXBGn~0IWzlarWi?ykrs1Wu^!z zydn!rwb7Fz4E zCDUCoJ;0^JqW47lHI1p-a{W%^sY9R~lH1{02#eDx!L%Sfqq0HW(3uFuVJjrYN`Jt7 zi3v;}KuluNXf>k^BdsJE#MM8MS!2VfZbcLhx}fqqbIkGm#@owQD-8jxq9o0jDkFkg z+m2w%=S(;@Le-Gf3;X~|Ttvhea`J6lUz(ZDHG*=MMuvP7`Qnute)T5)yXmDKf<))E zAuvBnAS9@AAbh39&0J$+WVFq)BSKpMECQ*-5`rnpc?8wqQ+P}v8my`MTzlkx8ZZ-e2X|27YV4p*uAhBAKonKyhXx2<1{D{7$;uy;MQTV) zQdnF9c5{X!hVxfn=6f z(YYek&L}0JJE(K_z;iSaa51vbU`ug(1~?kPBw(>&Mh&YCkwD!%z~cf?=G52+*kv-lO*}hUYN8(q7y~ijjVJNa z!6JgI3GgI}Hp1^=jBAQ9qU^KC33`N&X)siV3+H!I{1M0@;}ABv6JWK0;z#*qcwdxn zCOSfkiZz37@Kr@#7xgY6?hMhdl`cyEQA*bk>`Jy5a0;U4DqJ0L?rz?$saUeA5|vZMx!9G|ZZc-QWS>y+?}BE3 zez?Dh0ip+YBY<*z~_X}rEQ7NEP~Gk zV8(zN4PVKrSv0xm9A%PYa`DARW(VU3goq{&AMXiUXDjPoUR6CrL3_~&mT>_tz?zyq zwlHKzQ91^3@Uq>H2&rfzBd8sG;A*o1A8V-hIL`!$N8t=+Sx^fU)W{}#fg0s|r0ka` zBZHjxBA}wSX7DX@>C>r{V+9|*x)%tv)Uxfe=GkS+=6e}@wsWS64VCY{1exPWQ)F*K zIYUTYM(-i^MA=i!gY5;KMTPOBT{#rqO<||tUFc>31*aGn;OQe!AR2%bF;;l3Xh#Ly zg=$BuP=9cC&<{xLuYJpAfId0{9$U-;GzX-2Y1UR?;dnMY_B&N`e4`_7U8=0HTq(R- zzWlJVHxeXV_CHFaOS0p5$~p(z)o2S)Go_$p&3)FSEq6x+o{Ij;`RK-^aU<3dgKY}< z#-q)L$mOta?7KLDKX5ph$HV~A9!Q%uq%I+*8?$;L&nbJ? zI94oUmgB9Bn}fWG;2S=3v1uS}L7MEy`mQ*F8@ z80$uArS*RTm*zoBIrUW*uBxD?@j*!3{`ayz7n0sYwDbKzWW$nB7JS3VMahpdNjrtD zZ@jC5au&58t4mf&kX6L0B2cxGy(6naKz!`N7=aukCbNQ+ z6fP7YD3!>fU~ykc=;N&oy;O zlDFN12x}4KNxBNg8d^2I3!D%}f8nX%z$Zl-O1FtB4LWjRfc>c!SzAC<&7BO0bNOdT z)rgjcRM-6ORy5aRI!B?h$2w(s@!!ZY3O}dOE_iOD5F8EqzfwH%q1ObW-v6! z;P9~7z+cauMLHHc54HiJMAn_4WD>R$@!S?(hN=9->XNvlBm@PJq;!4s-7R}@F0ZOo zzCW%!BfdN@7iUR!)8xDbxTZpaXObTZ=?j!sStH1=mHIgft(c zKND>k?(Dt7i$d{vLi|hp#zJ8ym>vSp@Q=XiBsoZ~eD){f4hNzc+p}>m(nxtTU zI|{WXcJZMCh8KfW5|1G3DjAKdVp=fSWCSpkoERxfS(aW5()p0`cPOpGz>2WdK%@>v zN(tbaP@J*r&yDmuvk_Y`s`-E+CM1&F3$wBFQ1J-4&V*4}K*FB|WK4Zuc~ z)$EZU6uXjfYuO67n%CvcEVWGqVnghXxvfzs0ty=2GemA2{RdybsOMiO)Y4>~|9Kz~ zq#Mc}!{TQwjZH4LlfR>2pFg-pB8jdc{}A?-5be*M>ec!3!p|q9I(6!-#1SwN2>>XS zF~M6X9Lir4kg4V!Vy6iV1+7(pekM4+xWzWOpVprK&khxe0zi=BJs5Oin05hn8n^J7 zn6P8KBN@Ni;F$vl>iT>3AMn=OefxRa|Ksg0HE$LUttHS~zB{tn>Y`J-8{!}Q&&|=9 z{@?3$r>LfnuSIqE{aVyd(l=4pUi&7hL91_~mRo%jRf!{X@HbJ>IAU=;AMs7pPh-A` zdT+uvQBfVAMg2B;dUVn6_o5p$tcZT2aYb~mmKD)vb`{YH_7%~Y1M%6RBKpYSis;c! z710AGS41zu5r@OJ`&Ww-wv3DU*PZy7t0nO<9UsTXYWOW3hEzaYP)8_5A2qZ2Hn;v6%_S zV)x*%SaB@2&aPvzpYAyp+cW=IY`rfMmi%^bVcZ`$mLFOew>o!WoOEJgT+Zo*aV@Sa zj9c(vVcf&2g>gNfE{xNee;hZv*~f9KTYen(`o@cK^=w?@U$=FQ@9~yv{P%CW#t#a2 zjZc{C8lQ-x#{$>*g9}~bM@GBGH&1qrk6Yy$zkQu+{HiA#6XyM~F`-?(w1io&r6tU7 zl9uq@>uCusI;ADF!BJ+NmN0y5TEf!lX$k8;#P2?732&#FC*Iy=p7`r-^Tf~3;P~D= zamyp~#0kHfC*F{nB>LBHk{E!)scVzOf7&!jEbrAMaa8?+#I79+68l>fB)-%ZBu;ZF zNZd81AaOU2E8`0ieI^wo_Vz4D+~HG@cqX7Aan$UBM9WWNmLH9YS>7%&X8D4(G0Xd` zk6FGiJ!bhK9C4qepDX8G@xca!zweor1#K6FKI`}7s- z(>tzmOE;{s-C4wrlHbNQ<*~*#Wmsq1luqq|JW&Nm}%UlJQ!O@kScmV@EjFLsf$zuLZ(csheslU%`_2FMv)|k_*nacg2>Z>k z3+y-Br`T_n*4l4wfW!4u`^{Oq>^Gm>W54;+Lq|3r{XNQX97n>_C_|h2(T2Ge(T3LT zq78>M(S~|mqYXm_L>syej5hQi6m4iTG}`dl=@0~Dcem!C6Q+2{% z|Jq4Ik;O^Fv~DL2eY7VH<^xU|hK)LD7(e=?Ve6%@4L^))^68DpZCl?zzH9qCGqScj z;JEP5tnDN8S=%26W^F$?KWqEIkF&PBFUs0}Z846ttnJ$mW^F%^o3%aYY}WR}7cTAi z{XkmAqP(<>t@&vgIR$AMy2`YSmcQV;$7vax-q@7UtK+5&I~CVS7W1!}cC23)_1FM{;@C-a5a8?X_qazBi{? z_}+?E;d=*l3E!*f8ou|f9^rdi+J*0J8niLH!<>!TKSgfLz81AHyFtRn?B#1WW>?|} zJ-9JD8b>US=f`nQ{>JS0E^N#``lw#caU2O%^>W(Osh=~~ynasW*X!pTYF|I6UZ?sw zL*A;N)3s0ioc{gm=d>AAKj&ws`Z*6CH#>Oyd9#BnESevj)~)%$={N$qH$Qk?+x%do ze>6Wh-nsd~hdA=zZGNz5O!I>ebj=TLoo8|I!NU=U9lq?6n?7S=ZhH%_+zvQ)w(!b5 z+{!DrTRX4Zt5#mQ>Ak#i+xGU#?f;fnZtMuJ+%@mw_px5N<0g6K8qzoAIz0G1_dwO> zxmHb24`L_9APSO+k=Tv;&r78Gh zkEUSuAx(kfVNJnE9Eqnj1(tiZ7C6MUIQxAUfG3XY#zoONy3^6d0REzdS=+wyGkADRC;ysPBg(?ccaUf{TRxa8d9ypnUypO>7Q zbh_l+$3-RQ-oJ*wzbiTC^{C`r_>+=zz3Y{pTVLMp{5uux&bN5f?)*2uw>$rKo%ZK@ zHE4f+m3jN~DLA$@Ykz*2Q~UF8k8Xe7d_w#4?IyNAf2Ci~^V3~=p8vzO=lK`^?0NoY z-=62k2lPBY3CBN!d!A2Q(DVFPt9qXQes$0D^|s=iT|LkDvtD`Ox4KmqK5bETVSmf2 z3#WSFaIU)W^~kCVAG}v}q349E3vc51r$^O={5e$@GUruY=)An@LdXlVi@9zKE>4@Y z;9`5<1s99_7F--ZXTiluIC_LFxR|kE!NsE=<2bzF;^LDFE}px*;Ntbd1s9#ZO1${a zwZx0Mvc!wC%M&kdeVKUCtyR)Rr*=sfZ`vhYyxTkJ;!ik!d@Jdq-^iqk!`@4}IPtBf zmzE7_dTE_w(@TD%n_i0cXnJXuZ_`VSXEnVvFA(2_HNA8(vgxHKOPXGi;+kG+faB)< zcP{DQU3qEM^xrNWYu@sUJRGB2wEW`C8!f+xuxk0m&|WRSIIL~?#TEOOUrg?YV{FSW zy7{#HqMyFy7e7U`{GzbxwXcRoM17Ul%cF2@PIjSd!smtS^DBzpIa^WG;;V|HZ~k3T z^!9fZMZJEgC|XroQIvvX+ue$yVI3=r-tJaeWd3GlQ9EsA(fGk}Uk@K1_w~`yabF+D zacNB4*S}1T`+A~Z+}Cf1$9+BPlen+<#m9Z!B{}ZvKC9xs9-k5Sb=`UI{CoMl75^R? z`QqP|&xT!j@p9Oe<_#RLBsF%ta;TZ(l`@UvmEf+9S6bRSUh(Ydc*P4xb2rB;nz52`VbFXfEIrnNBj{ASiy&7UQ@9K(f^R8Oj&b#XJ&b+I?44-$k z?wEO3TaBA{^<($?*Y4ifb#3q)b-t;v?Qwn6@nzSCo?3SO^$W|cue`PF`dS=@+sm#O z|FZ1*;K$3Z_kMx@YT~aqw~4=QWgCCp`R(}YUWH#@@Aq5(Z;P6|_gycajPHJaxc9rb z>}|!rAKq5{5=W!lZN&>uZ!7M1W?ON}g>A)ESGN_XUc=vywiWxn*j5}-XM6GPrrV3Z z&Ce;mfn!%ePI2n>oZ^0Wa*D6s%PH>qQ%>=KUvi2+th2v(z-#-9ovrs5yLa1P+`P~J z;)|ZAi$elV7iUDCEEf*mPZ#%!K3(jx>~!(egHcJq?`s9RMJcHb)dZTGFIf9$^XA&yyfvTogYE$h~rCRw-Kt+H-C>6CTrUtO|p zUG1KAt4Y7CTU8EOw;se6-ddVecx%_H!dpjE3UAr$F1%&fTX;)xy0Ztt)opxN^RAh0m9*D|&y|x?;!ott-yl zX z@q@2EUi{#r)r%kO&0PH8#LmSJ?i|JUrx!nP^S}9E)6=%UwtV;LuT6fP@~~sol!v`v zOnG><{?vyJ+fIF$+-~Z_EbFNc`#4U0xEMzaj^3lDK0NL+_2Ia2Qy;E~8&x$rWmJ{N zx=~g7eWR)zkK(@|$VGN%0HlWBKxuJp%~)yc_EuWw9# z`aO=lX~|DD+mfI5*^&Iz{#f$UqSMJwr(MJ6Tggw&%aflD`#t&T_~*$_SLQr?KIZd> z&!^=-d~Q?x@cFPC51$|X34d2Te7^IyhtJPFefaz}^GDD7TRwW8|Hh-|1vt!iTz*ke zT>N6yo#Gegel31+y{h=dm}kW=^mT8%Xx!w+i~P1XUOeu6o=U&DJ(X5Zd@8NMQ8MYNMlRrt8RxQz3OJ?^{RUqhjbF3Z}+OJt>{&EXl1Xu z-p_m0y*|X(?4P52%@&XKHCr{#*UT}(*X-B%_%6!VY~f;GGxH_BW=(MXxZT&RPNuKf z(%rsh;T?<28gwo)TVq{h_79sPv!OVa;pnF=GRx~zWY*?g{N`F@_SySIW``#incep; zGW#uLaJ@fpEDs%AZ*}RKLT(8Bd!SxpG8C>sS&ft1I4-c-VJ2$xA?29<( ztHJfI#@(*JWW}8Z8>i_Srs3%Mfv(}e4|NTp)^OLC*BYL`a;@R@@2)lU zyLGMMq^fHT4LEk-81w8}!w+sIHr}~=UE?DITbX-~YGoesZY%R)Ivn#`nRi~%%DiYz zEA#1_TA5cETA5ekIDN8}`K|m`=Cl6Q%DnyB5#}9m>`WbDet7)|^KOO_=2y3kFi+n% z!o2Ol5$635k1&tDIKq6*m-zkb5$5BHN0_f4?`{5IUXuCJMM>tnVw22|E=@AGNl!92 ze41pg&rULLcL2W~O)_tMBFQ}EMv{4#n@Q$fam@RzU(+rwGn;<*NBw3}^9IctW#9D2Ej!E% zYjwgWtkr;z!dm4-g|#|}V{uGaE34$NR-bGLYt>{=SS!0zVXX$A#&_q#T9p-swK|;G zq0P`09okG^)1gh@{TDcz!8OOGv7aZH}{L-=Q4>uj#{_-QvDRXT5 zRlQSfcegv$_ISrrZFg9mYTMiTRNJvVPqmHEo@!h4?y0t2Tu!y^ievitQ*D28Kh^fD z;8SfazN~9`;k&w)-+f=#^6rzmmZ$2NS>ClUvwYUl%+jTenPo)>Gs~sj%`C$l%q*P- zn^}&)vHyg*W#fWf`QQJoCY=$*zXBgh7oql|ycJ18pn@6K|jb}7! zm-2C=c3l=VYS$IVApMH=c|!`?_r1Np!{<--cPM(gzeA+Ofes(zc(28Q4k2w0ba>kH zK!?k{4|FK%d!WOvcMf#8<$9pQgZB<}c=G1#jy<)jJGL6Ix}%Hp>W))Jt?pP=VZYTKPx-IzcoxS$LRNR|8M(USv5zKO<>46p@nowrOD0=IBuus% zx^A-7;f<56u56lYHF*n;qm!+=eLdN#-_^-hKi!^e)ploqReDx{)$x4+R;w=uSgpZf z{qF#)p;rT}vWf$&KKvoTs%fJ@s}8RPS`D=bv^r}UXcd!`X_c}((`v`sOsfymGp&w( zn`vc!Bh#u0jvco$t@_{2v^rasX_fpa)2iWbnO5CiW?F4Ma;Ve5XM;7He@yCp_R6Eq z*RDS5+@b7I=aQds*i?0X(yOZTyuMYPT?SNj9*5%#hpNsoL#sNE7+=-7Y*JO{;&1y{ zw;Q?Ax+9K?Q7f&z+*VpgOjv1s?Sqxpn{_L#!+cj-w+UQn9kY0)b;=U_mblXTy)`SX z>$KY1Wp_l+u2su-bS+ES(RFItj;;r$y!*4c=H1uDns=YD&b+%j4zG>o-RoqT zcfWYty!*E&aPC#}?r#lo?s3DXsqLH#nx2h{G(CG=)AStlt)^$jFPfg3N1C2@rOrJ! z)bHH0QIpO+k!KgtKF{u2w>-PeJ@f3s z?DOo}49K&Kam}+!c@Mw2=h?kCJ3uBgxMG&~iKb7wLBP-q-Bx z+k9tdA9Ksje*KSj_BN$<_Pud*Ew{7ZU1?|EpuwB=3vAu`euSfOPq)4)E^d9>j&5tK0g=U@A=80{X0JSxc~dJa{8~k+i1YN z)R2MJpM?zUyCY!g1^8kb%P+gbuWA8ai-`b?Cs2 z|LQX+4M)%OeFhD@*k{o8!ajp0UF|cd)9pTkQcL>`I#}LkQ2iHu2DPr!chJE4eFuHn zwC|w%g;9fk^X&0X@15T7{L;+KVP|_YhhtV|4(A7&Ib3|l%wfwg{OxAu@ZAJ62ir+z z4)1%IIaq&S=FkntEMGH+?4SV-)9N@oOvlj+^#I$padtSMadw#0!`We5PiKcicFqpp zJK(<~oE-{2bauF+b9P7$a(0+!FbuWcYZ%%$+c4DeJdPg?Lnq!b3@xrS4Bh#&VQ3bP zTfZ5G4sX6?sBN1qL$~zWGIZdnH-@bnIm9U)$K6pwoUVK@#K}iD#Ocw@Ax;s#L!8zG z4sq%bg3pOVoEoed;?y>Eh|}2ZL!73K+TgTk(gvrMlQ%eRoq=Q72B)_YHaMM5-r#g% z+XkmbJ2p5q$B}S+gOl0m4Njf^wZZ8~+UHK5yFYgd$@<)B*jXGmKX>X}^|@2g@1HwO zuY1a=qW&qTN*t%Vo^rZnbINI!-6^Mq|L*NP=QStiMlGD2d$n&VdFTStcBIP=!lk)2()j=cB5){)PqZyotf_|}nQ|9yPaoaxCfjl7dxdif>03<^wk z$yl81qKQj(xw|IWWy3oBmY(bq|7o(z>!*`lw&o|hY{L=oW1&lOX`##JyM-=So))_N z^s>;U$sdI-F=j0>u~O5sJ<&et!x*1dJn zwbc(7T|aMh$+dHg(oX7v5_=w%Je3$EN+1;9fq_xG^l0Ft=pIKy# zdyXT%MaH<--^v)5+Am{V{dY3P9U7W3?vt?@jPv%*7#HS;--0s6Y3F8)TN1hJ zz0=KZzgN-h-g|HL(YsX))w?})(z~7Wz!9c*8$D0&Hfz4#?bbrQTV}l8ZCbM4?MkZN z&2zop%?roRDb^D%?XsS5V~_QO_x4*)2s~yzVeJX)3IDomJ>l2u))R(*YdygkN3&Av z2|w5AGGU=vmkD40zG*`A$CD@KkDcs3f6`=kgZE_jeLj=j$45?f|8e1D_wQFtcDG5H z?A{wk{<_KTyLL@>?~*;)-SOaL_pS#nxgT*4np}C-$gr8ArDT{ik02r2o{j%lc2flh}W1z{>tpn?%l-`oXXIsT=$bPVE`r!s9o? z3XcnI(>?ZgO82dHF3e2#m>iz&p^HfO z@WIjQ%}z5mzSU{Qk$#!-PKPA=(>J3KsO*`fbPRw{P)xVo%6Q? zbnZ6>=$hXfpsP2awdd;tTYL5x+S>E`QLQ}(EpF|Z5Zl@_5l4?@tvwGWwDugiqP1u9 zO|3oS46Qx4f7aTw{bE1Q4mftk_<0^);^)~d$M3KlStMzs=7xHrLN{ z%`yD`xu55_^M0P2&rJ0?-Sx58F}ufJKR7)0`epEAuP*OD_PRXlvDfL~$6hm{AA9Lz zAA1Gim>Kuj>)yu4UY~D!>}74c-208W0Y3Rl0(^?%0(=aH0G~el@!ul>K2cu<_#7_| z@ELO}z{eHGxsm`M>puc~hM5KW#0+fc+u)nozM9p8^k*&a=}YYI=^wpyPk++!p1z0v zp57z;p5AZXJ$)b!pZWLn|6X`ce|GUb{ZGmF^abng>GNWa`hU3WsQ=Z?NBt*kJL>O_ zqsxw?{+;rU`tQDc)c-^gj*6rHm+Bq!zfu2~e_ZQh{!=2a`FCIS$p66VNB&dRKk}b} zBlG_dcO8IH6m5GJY!p!tX%>!N4OO~8=ruvQSl~#GP!S>rB zO%C1;>HF^OkPlAY4*BZT?T{zGMA`3ehy1v=a@ei?mBaeHS2=9S$CblY0*+5AhgG;* zIqa9r%3*VVs~mRb&&pw}NR_aX#jAwfDp@6Lt~Dy`w~(l?=Fw4Mt%0XTMukln9~Cxx zVN_Vdl~G|KYofvq?T!jd+!GbHYhP5@+`~~}6Io)|hQ|}b_C1jp_D}i5Fx#t%VP*Oy zhD{ul7`Eh%#ITw}62s~NRfi^qZ5@^v_Sl5Pu#egm4{vvVilgPlDGuw+DUOJrra0C- zlHll7KEct>lHmBEVS?lO%L$I3fXhu29K(AgINJ70aNG`VJEFqswayoSj5TYWfm_!) zqqeVgesy52bLG*s_*>g+on=n1bx!Ho`G=aQ1MqgMhOOU;h{qT=l6 zm#fW=u2FY(^n&`cqnkFI9sR;9_|AWJ^y33&N0%8iJGw{k?CAYNTa9cptkuXjhqoHp zXlkpGje+-Pv>MrSL93DXmb4oA!w%q7tC8C-wHkTka;uTUZnYZ8=8qfMZRP!#M(gj# z*!SL#8M*&{%#9=WW44^VA2ama{g^J_-H#b~`F>2d>-S^6`s;qorN8gTqywd1zB6iI z*E^#I13z`UGwQb_|B*i_B*4#orv!y-5E7)=ABXRt-LcTCF#znt~Z_? z^FoE5W1<`P9CN>E&oRF>?>Xi+5Z9{bm{o&&j(Kue&oRe__Z$;8x96BK^YGo0o@1)7 z?m1>|MW3->H}V<#0}%eQ&sbMepRt!(`HcO%x6jyL`}mA~-GT2Se8v_X=`+@Ms?XS7 zGknH&R;G;|oV;;t;pdKyb&csdZbZ$DanqY;jJx|v#<*o|GR8IY%NVz$Z^pP=Lo&ww z4m=W&F)nc-zFV0wE@VT-xKSH3#yQ&hkMGgnfBc)T`H%nfP5<#XZT{oS*!{Hg zInjT7GH?QTd$#|0#}@zb|7`Ogf3=M|{=@)v{P}_E_?5xx`2WSLs}+@_BI^q4ySVj^Q(8Yt{1pg}{0sles>3af_C!N8aG6xIuv2fTsr z%LA)`S%nog8h}nT@*GBcn@%PR@ghhpTMS03d;oQVSN~uRe&EmD(sLCWADDm zSfQGX`2xp*3BZTd8A|}V0%xi*Rus#QH5hvgNCqYX0Sy>CQJ=B9fDhIu*JJD#;8oyx z;3~KeV>t`k2LaW9bw4QVw@!>50DL+#W(U>+KLE44FjfKR2aM^8x_~oS_r-GOS%no| zuP{Gg32+7YV~xUw09ydxwF;X7d<9fqr?4>K05E(JV?P4xfZGchI|$UndV4IN0LB3y zFJ`Pi@EkA)s8x)yRz(?`4qO0+K7nTniZHed%V&TSk24kt+{AY)0e_&+3Wc3tuCUTr z&IO)drm&Su6?PF=iuG@Ra@alqSO~n1&#hl&?4Mp}KYzwXV|fIK#(ERr0MG)T+x5oz zLAmjOg7vpjalGvmcKQZm`@1q$K8mp!Jrvd*INe=gMX~IRWdiURaI%}i+5;1TLcn{- zkqN*PUnwm4OXLc$<2-WV3*;G=!-3}b?lzWlQKl#G{MTq#EH5rW+v0foV7VALy;xx- zuxx|nL?B|3!s-C&3z2tN{tt*Qt}q|8)!AT$)wC-t9w>|TBSFYdEWgKR7K%EDD{MZ{ zEkt2IW4R8?XOZU(#wqORbcOk+Ggc0`l*X6~Xa#HrZsWV7z+@ohDq~B4p+Fs=QdOK+ zU^j}gG`+-d0cWiqXs9lY*!N7aHuzx@(Q07(Sk-x&;2g(5_dMc~}-~f{S zpg$lS=(!#GX{4}zz((LNAgM9V`^z{NfFIUx18tiq>?S_X!!jMX3(Xz@n*5Ao0(Js7 zfwjM2e}Euh@vn@%2wcMY>sUHQGWIRbc@)q#8oI>iby%*9VXQgu1wNl0#aP49j0Iy^ z7A8-V%vt_QUHEbu)($7A{JTa0Z# zp|D7xFwhF9n2h6iUt!w-6=(?*0_NcRfxt(=KD2w+Nzl$9w9jh_yEjl_Td^#R&x3%$ zucHj`1aSBbg_U|!VS!ja2@EKU+(>5Z+pIZu>*_W~Rfx7*`kL6E&7^{NiK)@gC z6M+iA&fbir0SB?Z13BRQTwyN*e}1O0Y3CI7A@B&+4+G->U!c|(=(A8}2QUol^Q$rT z!YlBlmI}KB)Nh6I`1}c$QPTfMvkD(a;d!16XlxM}z0=2F5->d)^0{wZ(n_slfBT&^GWk zFan>Ow?lpaAL6qU%kvB1(a52lx8M(GyBF4gdm{D)=nFIeGUCzBSgyk|4mbr=pM;zT ze#G}jf%fZg9Kd^Pp&y_S)=vTn>oHaUW?=t1V_6i-(^z)LGRTE~xHrixe4$Bfn*xNj!*~Zc zgYQNG&usxuUF2Ou^ur~gizM`Ys}=SEmWhc9I|mfP`U0R45C|N`_G!RtKqhcB4E@$h z?0-D>KZX7Jgu+^EMgD+$DsTeWiO-S1eXM6-S$!L0UjfSjD^O}X#y1<#{y^Q0=u0;# ztjT7D{ea~Ke2xbau-d+ zCPUL$-mZ^&{xy zG{%is{snY7gS-GP1NBg5JHD$EtFRb&!ATs)z}6TWKZ`!6H2NhhCzgT+fp*Ve%mFZb zKNx6HMq%G#`=3}Y0UFIkTObdvqTil1g|Ux-N1^Gq!1t3ej>PA_z^zH(2lfLm;rn1< z4X`yzVb>!SRvXI@pgPtARE4btg0Q|3NW*$%;F1e@4wU&2*BKvRYybpd{Qz(h>#--{ z-N0wS->1;00!4r=!0O`g#$u=wD1vnd&NkwB0Y`3&5}_LW$+`w4yQO^kVf1Hj-+=n*)dfomn8JMb9bgKgLG{aWDXL)Zp1 z07@T3Ebtky?J&FpD1HRGJc{#=WeAXf^(DVUx4_px*+0&L!{>knlIg9Kcne(?1FuSq#TN8`s#t z%fN8p%q(~kUEr^&;N>X27D$E*bYnuQij4qfeKi+VmTRT1>8dWm&I}mFli3*0SE?G z13o}W;46$54*|DE{EYa_H{IF=s*mTwq~0VZC+xx}&#me;;T{|sEj`YbF1Za^>Bp&={- zv0Mav1T4ho#n*661lWLkKm&Y#2FnhG;bVp1v%p;-PJxdycnNS8>*erWQ!M-Ab1~pu zoYSB%T*Cmr0vAK!??7d&HwlNw1MdP~IB+csJPOPLMl8p;W;(`fz`1G2J1k$v(gLJS z#aIe(01NTCF_w3cKgWR1YoSGqx5}kqf39HvfXr0vBhVD<+wpw>a2L3cj%#>mWfqnl zfHhyB?|1^*1Z=aN4SoIiu@KIdDVY%}WT)zT0&<;`f?ipYma{qsT|2^;me)quz z{0a1Ah1gnr*F!19?qGd2kc9P(+n{Hl_I9+zR_qh77DxlCPe+{&(1-}Lqlnn%?|A_00J-ALA$k^YlVD+!ynyEGB0DeaSTfpw&ykE9Z(wR3XB7K4}zxyQ-C8t_17`B!E!d1uLBXAFh<&laR{&t z>oxFdkl1U>|G0#blyn?jSoAfQ$=#;#x)j^!Sp&fDngg3#WmH>M|i6IhP* z-@9Y%3p4^g>xS_kFap@$1N{V04&@e>g${tKtpChV{KWF?X;7=Mu{&@wpe)?`%gO4!n-Cg@E70?j)YZ~?!D1q%Afib}2j}~IR9x23T0v`dF3l(B-6fVRn0_(6I4AcRh z!L~2)-4dWXynR?0#t?ut6yq9TPYC)U;89@vaLjuEMe#kua+4kU0!|NuZiAp7Km}R? zg@6J0{zYIm@KG?11E>hh0q)hrzAtC&w`I@^uot+q6m)gfMuWNm>c^ZV_1OwfUy>qCoW=tfdrh(uYl@Uzlm)t0e_$sG=6I| z=8MMQSb(DA;R|EoMZhJX!#K?20V()C3}`d~8U^BePzJ=f|=xVP5rLn#h z)z8HG5TFr&u7Q1n?-0%q#;O6EF2fsvU|`)P%>4m3upWrzkRLIZ`vdxKEUVyi5|)>N zGZ+t#y^rg6E5>?5;ip&*0U8cQKLw0@OJUCe$KJ%)56et^*L8@()&q}U!x;Hnl#9UF z6sR-;UWVmxEcZI#k-+S5oM+@xXDq(~D&xBlz*P)w4m2r_bAaW|BItjB+CT{4D2hG^ zXozjUV!0D&@Ekk>XVQdAoRpod1=V$O+pd{c2EIJ3T2L=IO06(T+d2mlg@IBCH1?mNofV#k= zz$oCAmB>TjXRHTe8I6;+7B~mo16s{MyH8ixQ&`%8g+Ma!3-AKAcLN+HaK3@-K=r5K zIY1(C5vcGqGzFXk<^v^4Vq6JSJb-@L2i!ncU<@$!CGY`dfh%=!4F-$@Qt^2wPzGiG zY=#`HjN`2YPXel9y_yB%$BJkREazd_4N!pn_}mp`7%=`g_HzfuEy?H$kOPM=fD3S9 z{ljk*_B5~%zWh1hi|?o5b5GzAtd9d~q`)6P#kDW+^I7D#zpvLBY^ zfVseAd}d?O{#f3|auo0iupc<{9mXIRG48dkjQtM?#rnm!aUBJO01bh6;oElse{3%UBmj30W1QF##|WJ6fc?SpW_!$G z0ONr!KuO?2JB&xMy(%ylxH|!U3cXefz%?AuBoOlnSkA#R6IgA-*c2Ez44#MO9xO{> z{4ft*@-A=_PzxiUfX=`tz+XTT5C?tr18M?0fSs$58^B|U@PFj=H^3wKJP4Qs)B~nq z+Z#XwU^h@F6?0@0(5`?#Fd4Ws9v%y92Fl@cD=Z%cJ{gDWb6^NCCk`5q#X4{SD2va1 zfnxD!3*Zy1-^a4mM2rowoCK`-3tEnY4uRKz7h|CbU?i{(_zbuY{D^IPfG6Y8cL3u@ zV7wZEu?KJpcod)80e68WPV^C2-o&!D3wpuQ1yn@8Qe+N1>Q!vRwbQW<@O$7s(6%Ge z3Cq9QqyGY$0f&GNop4PCypQd%!07jI-hf`fr|)9S1FXmTS}gxOj&lc`1!C~I3g8F4 zQxVrYyTTDTry=aJw}n+Ul$9#v)d4!u z$}ZN{yTU5=IE2NaFtRNSmDrJ4kvI^}(H_e%{AWcQ1hQ?!n13NH+b()vSY;&&yAP~j zW}a=WyemxEHp08Ylx=IgD@@t;nRkUL+a7zws{^ELd)d3flx;5W3RAZ2^tLe1wx7K# zOxafHQLhe=vTZC1zxeNL^M}?{KWH{&!~ zW?qy@r?O{@NZUJ};az0WkqY8u(UDS)%AzAR6)q~N?MQL2$f6@vM#!Qg<(`*CN9y(| zCaLYXy$@whS#;zQn`P0FlawkB)5$sR>EZSZ8yn09xIF~pLAQVSqo|K}MKwowQ*VgM zhU*LA%8u3(Lh7BSCxjHbSWgJ4aI2mWQri1^LP$-Y>Iosme5WUbRB>Hj2+#b#^@LD% z^V{-X=NM&h9fZ{9X&KIjAyb2LjpDnbzl&jh*sh`nQxpL-<1(Sw$ ziv^RK&Lg-s3Bv_>@OhXlnK&_e?1^t&_>X2}ZlQP+?~D6lXc_?{MGD1NzEp3E?#9Gz}1xdDo5egFS3q~kNz7}H?T*Ot3 zP>_`Q9Tj1Xkf3XWqM`RP)1>v1NSMl+>ks1+>Z&)4WYb@77)fQQ-Y}BK2)$t>iLrXa zNCs2&hEeJ-&>KeS&TrZ2$`ZP^*{U~;()ys@FiPbQ5mvK3W-e?P!Pzj5<1-sp^ieKc ztMEGoykDZZjX?G}3S(I4U12Tjf6yPKvg>+-c*g&xFNpH}Z+$_O?N8v&tB!`CTrZ<9 zh%&vVz97o;M*4y%%RA`{q8#s~H;9|~Yx;sHzpeU$D7!}>2$v*zB`lxY8;Qhuc!|*n zZu{^}TpIZYYuUfjIEl*k871NB_|zB)spSu2B&423cwWIk!;+d_H%3D0T4s!d)W#oG zH*h$lzF&-ycm`Od4DAW^KOK#ckXnO{k&t?KfuvPl0_=_6Y6NB;cuJ{xDEJ}>Iv63) zR9xs8DMTu(A%lpkxrcEgQgozoB2x7}<3yzF?~N0Yx~r8oeAYhsF#3!;p#tuKhO zyC;Ggc0lG1Lk36UI*a#R*`JW%+gzB?bK*LT?;JhLFa%*O^oMB~Kg&qC%65o{a}|6h z8cupBTvqVdNf~WL!$~6(M8iofheg9lH-CzTlY*L*6Kp!tk}4j~`J}p389StO-~4Ad7%nA&OY@QrSybQHrLS;LK)l*A+>p0@;gQ< zkh7;~j~*o4ed~vB4`hQ-+}*W!S6s{F5RovIjnN;*GkT`pFv{xHdc!EQPw5S#?EX=2 z7-je$dc!EoOFXCZyiuk%&>KeC-bsHL_wqq{!zk;+^oCL9&qi3y{4!sA$bjEHU5mo? zy)CR|{Z;)zDl7NA&YF0}chVO``94}-5M_I^z97o=-}MDirZ;#&S3^*q2kHx=EMKcH zh;saf-XNagJ}>GzCdzNCz97o(g9vJpcfw}h5}504jlvai^D=vZoLxfpExALr%&%^g zLS^lYQE*kfWrQLS{aqxRWQ2kgv&9Gn>F0AJ6r`rVjZl!bYFZ5B1u3n+5em}XIAauC zh3kw^kS0$Xp&*6c0Y$UyV#-Tq+Ee1$->7=F5@z8=gEf`DY@7sdU>POhN*!g4gtWTW z7zwHMLt`YQ*HmL9q}alh4V_}rti>1!skWCf64LEtqa<9p8;y~Wc9V^fkb19zq<=@u zpz&L@PCUmzS4!ddq_WQLYbpBI7gtuHx!&>6-jvj4m|gCt^HjY4=)g5r=_+`|J%8$f zEbHx$KJKZ8$OqGS{DBImX9`;dRkysTdLYn>`uakE5!mCL@1bTl}$^YH?+Fsy(JesESr{mv2qQ` zdrM9^S3WKG&f+yC?=87$lx$k^*PEd2;H}l<_NnX^_-ZZuFr*A3&8;@dB~)4A+7kDW zJJcKU2+5m{$|EFKs#QnYA(J0Xmq$oW^s78V@}O>Yr5!T4&vto)N3+6eA62pZ=_Km)2U_bpcb3X_xZK~)5%74YzB zel_P8sM*kS_43*Ef(caC%?JUvnJ94rkFAIkkc||sFWd%X8$HDd$R_5A6Ob)@B2GXy zP^p1%8<6a6MhN)8Vv{%l$@+#k0m-;aL)=}{4PW~Ma}xmBWh6_BU> zZe|7C`Pw%(t%=AV=a^Rkx#(Z0AjjyR#@+l)Y5qzZ-MI0?ScLyZ9RGPA%_V!Z$Tev5 zHc)fHF~$f~mSlv0yVytK1msfp#0kiS>b4YaL~@ya;soR(OT-DtCB79WAQ!0IO1KS3 z<#rdDX-M2achT373lO4{fPB4xa;FNoAIQC|=#V1wQu zp7o#W3!+T_LthYOccV7B`WQY+kHmPK_cGLNM4_kQnZW0lvNF9V0&~o5c^jx@y~7xR z%H|j$;A+?_PC&}|QJjEOQoOBjBa&hoiW87}g2V|(Nz=s%NL9zg2}ogAjSz6PmG>1s z3#7a*;sm6^=^&`t36sI}6UY3fxg*;>c+#Qv6ONlvxW2cAHMPC3KS*WY=?&tl`b}RD zDXCODUB^V~X{;}Z6yv8ah*T1xFNl;eOJ9&j4f=ve0U3IOc-BAFURRS-rsFrD^yD68 zcOL}R)vitWV`zL_j}`Ze1%w`%03kdklMFROzJq z7)cc)5LjDFa{3`c`VD*jBFw*kD)=m{pEVYDDzo&{Bf(S zAKlD3x@m!pmb^A7@(&YUl(5YLd}e)(2M`Or20LUi`$p-_dqPOyu8jLntv-mGOnVSg`* zsm!iC=*KbDq!Cz7y1=cjItrApkkB{Ulml0GQ#)O0``q^Vot%u zcvgH}P%+Aja=iv=Q+;&rD&<@Y{ehH)k@^EEFO&2KQl@^SKag_ww*ElM=9l{FZdJJ9eX$>!I*IZHPNL5G}QIU)i@D(!1J#gJ~UFjS`bmr5ek2Sk&jOJtAa7CBj$hY?vaj*Z30n`{M7F)k>&!Q z$R$+SPcjL)D?IUrv_mF$@R3JIE-_FZA-Tmkd4%K|Tjddwd!)!CBo`_7rnIw6Zqh|2 zA$OH1d4%LH%j6M~%X|dFf!>?LzpsOMY}Qp%fA=*Kf2NM#ow^G0A!d0 zp(a*AK3g^1)GD~k20Kiwf;@Ikgo#yY>an>NqY;tD@-cd>s?{e z!+CED^UVI;yTX*Cr6RpLDU@v;QMgRbMA9BL3d5!sAHGS;tdxR^sO;sad|7xFy`5hX z%AMz;^I@Tk*_>Yy%9Hvd^I@Uv*pXim%8A-B`LIwXq~}+J&i~|5`LNKOf0@zw6rpoH zJ-;G!hP#ZxJM1}6qd)GlYd_B4-+lj(|0$ZRmsidpq=~)|Eh92TLseE{tWd2ygI*E| zrOfIl5=t2tBN9rPm?RQP8G2SElrr}>kxROWvC`CG_y77~bY8xERmyqXiYCZ2rBVVwAZ9Clqi9l%;D6Dn=Q3 zr=Vh#eXU{(sFX5oRYAoltL_zCjAu}8e56^vyf8UVId0WE8 z-azq&-WS(0Cqg7lWmEKr@yuGLH;gjvUA{}#gPK0K8NGyXk)i-SL@WMlnswGzroOu_?NdIGfU zw}=I*tg%QSS3pPofuw=A^aqj(9Qp%E2MPKENeQd<2a*={>JKC}e4;;)^pGYJ$Q5x< ze;{e1^km(+N~&mqz^8eV^FPJY3E*#V_~FZPxxdu1+nV=Am4)WDk!R`Hhi{}zobm9D zlx?dXzL7F&_ro_**1Y%djg%Q*JbWXa{fviiq%-_?UK{yYEH>q#%|~ad;=?!6*=UZ9 zP56%wJ6H(L34ceQo*w3dwXA0-`8mqnQ+GvZYq3!6%({$HsBE1v3Z5mO8lj+EDxP4V zEhyvK7@?rN3^GDN*}KvR1?Bh$Mkq)L<)<2GPSQ(nV-#FfQ;blM_O=?KAcbav!lx&+ zne{6+1SEua`>@cDv@QKrVsD}aqMvh@4{ic#iWD5w}^Si*t=*yzql zqlE<&ql`%@s2F9&_C>f4!%uxq-_7o~{)_SVTJtZWWynZxi>hqBcSU)oeBn(|%9heg zyx3RD7(Z`{Qr5(KQyGfo*{?Bk z-nbh8?a`md;(t3q`+Z0L)@(2WTZsf}YFliKKxLm9A>fKDzEb!QNONt)2}pHO;sm6- zcf|=vdB2MjkoGFA5^h9N-&^7Yq`%ch2;2%3Cm;=0OcZVdQsFWX)Xy%a!HnL!;D64Z z-!tj}M7ZAu)BZ3@Ao~VE&GZIo>MNfl9E@jpMT5EOCW!@;;=UCNCbd;xEp*JJv~aOt zQrQ8qU{cs^v0zeH$Qq$zCS{!z4d$vUw^pcTQdFQ=FsUgO!9F=hi#eY~$oe%gZEmRF zI!x5*3(=GmEE=k^Nn)W~MXN+YNka!kLP z-Aob-<*HdB5=xpmAQDQ7xq#3f`KLesU2hwnu%;&?`Mty3zr>pT&m`XhVPBbqn%bV* zUG@8o|`jmqwrUk!J| zs+&!2H1fu`%&vxoa>HoF>f*ch{`A-_Fnel^^Ee=@ro z^5UvnO-=!F<_V~#cYfBXU6*yjoQDGr+`&En!JnYz`sTma7vuh$8~iuYt@s>-Zv)vO z(B^CSVzinQ|7<3`${KAmZST4BjxdX!9QUAE^yIW9wwu&A5~ebItqa zSE91EyG%Y3?x3;eRzg1dp1GBfn^xX!YIBjNMw?p+IqP|IDHjn-Dq_6y2_>{=UNZiR{1*@f5(LXu0;pHg)3?&Tr2B$FYbV#gAoGFor)Zk zLZq@XGKjbXRWVLPo>R{_5xGn|<3yh8SmQ+GBzEIOwfk&6ut8q1U1teq^a^N@nDr*5e?=_{8cQNG{_DM9W$w~gjg`??gg=6Qd~W; zVA5J^v0zeJcd=m7*Fe!=uB>5V!KA53v0zfu3@x@am$t(& za&khzuYFR$EfVi&GzW{7NvE>i^60o*{V0o$JgV%clG=`(sgo=^@}n`b=*Wfk$)Y3g zxgv{>9H&l-q*FmY^Oih1?k-DY(UGT|l0`>OQuG{SopQ=Nr<3%-eFdBQJ^o=35x>b9 z;huTauFSH2S-UZ){Nzw+j$)Hdsj}sADY>(JB$JZd<>}8Pwk0`CADNWoGIM28lGB`% zNl9)~=yQo}NsiN3CMCJfV!4#uc`nGLB=;%xg~YZb2U0;s@V9t}DRC?kw>+i;^2v4)IwQ(NKXi5bi}thYN2dazS9-r z9(C#op)9U;QAaJ5!M`9R$IUJN3?2VvVmt0n^HbN!^A3o=d(229N1pNS-=+DUv9{UM zUr(@><wN(7UP{WdbdKe=iMXAO}NMrMik&x<686zPb z{?8~0S7wDD4V@j*Y7=85q~5_G!Tsz==rkBlzVdgq_{nd*?f*^T) zL7H0kiU+IgE74%C&cc_3cAHe$NGzDtI9e>2RCrJXSTL!s#$}-fBeg|~1(V9& z5e??*N)rnvRh3N@I%ZPSC{CmWGQt^DQ1>EwlnTyEN_FhlOd1T52R*WrxJWxvIVr4JTdQ6AdS2 z)kzm@I?`4L(Qr~%fM_`BYn*5}DQuf)IBD#>csTE~Zi|MK&PrYtJaMG71_-a)+kLgh ze;btlwV&L7Lq_|JUSC|*g|a~iXsjndQ`cazK>VV(NFY~~Lw_J?DN=u+XSAn3kn|I$ zKaiA@pg)i_GfRIUsb#7DK+?%-kwC7HZTbUA8+-Hzk}3`(umvT#1HF*bJ^xIcsucMO)*bjlR%@g*NoF}H6TRB3@SsS`(~Z)QvVQ|j>)g`R zbenThy$fbTcz>8%_w6%r)7%(^rtk%_s8sfy94fB#7jH^9KvKIy1{Ep(TNzZO`i4JA zI6zYVTp3iP{!|%M$KDpc0ymZ?X< z9cR0lRge?a{msOqAcy+E%qqyadfYbgD9F*SnOOxnUF7d39tAmI)jv$Ef}HV`xm9q- z9B{|PqaY{!&de&vVHf}TV9Ma(4y)ZUrsp2A{xVsOyZ9-D{QNVoK+PSy7$d;kgAoGm zZ1Lg*F=hXQB)NYmpaVubPK;^zEC-+Qpx^N9iV;v0#y|X5lLA?5R1j!p1)8eg z{%+?zi^O>fOdfp$>N z!nyC4X1}b>Su$^0&7o^6Ceo|y7xU=38;>qz(%zE?k11>tJvndeBPP+4-v&Nv51xJmRLM}5L1dh*dhMNFgT?%BDhN%Z8I&lfX^o}6-KaYegl_wcLCoPXmu z_qArslZtlFP9$7&zU4A#@GOaO8t!E0jna@~-8D)>&Qzm>!6qgL8f27)oF>jF4LQnY zqcr3k8AfTyA*wxPu!-r(FMp#nr1&XDX-L^8K+`wp5G3bdCHuCAJ>aGNeo*eK+B6X7 zdr2%xthqtSr%kOuWu45dfIEoOyb8!$wwPA|xy}vqDj+|4p`>XinVhPxc@>a{O){?n za<{L{tAKp3Y$?;u0XbrK^C}>(9A#bwJoY$N#_x;jv zHVm_XS#N)_XB6hlPHAp+n&Y`+KKDXrLf&X>2uSmM@BY`-QeV*G+5h<5QV+5Czpj@0jg0@kT7IQnugrhk$kfL~{MXe|&+|R1&G!kL z?A^q}d!8QXTU@yJ#Gg**cb&8wzctG$`9EPJPObGQ{mrgIWpA5X1@9@6&8&iYg~H`b zJQvgh^fI#wa{H-fRzc2w*32r%#S4`;@m!E&_cOB!a_8mdR>7V43p1-A*L|#liRXeG zHW*b@?}NMZ{Qd24cqRP~1HV(BbJo+vR->qocSSV^Jfb&5WuNN{;SO~}PY5|q@#l0L z3pq%2Jt5=>ZS;hY!mWBjNUSdYo|ByRRCeUR()wwe{8t;IsM44-0@LS?&*QE&x(YlMOn!Co-X7Nn47 zMkq)zHX{_IphZR~NKyNZP>{lI8lfP?ResUH8KJqSH;hn_A}1T6AcY-M z(?br?S0<&W50*(u4zgb+B{|A%xs<$*Z(K>@=^@7nlu1brv<8&u=(FC)rJ3if-z}my z)V253`R`uS<$uA!F&*2!4 z*cpFTom^oA#whemdmwwPn!>snr_r3@Wtns;drKZ2cZeyn=*T7Z$)Y2t$dE-xZqcB+ zq%%v7F;W&CxyA}vbmSaqvgpV?s@9Oyc61dJD2t9tNw3572a+;f`U6RWvHAl^ zZBz9JlCGAB1oA$3z5YPb%5MFEq>2v^SUbNY?}&aiOB3f&yrK8SHAQ?c5~i~2`op*y zZtD#rE!@)^MoK7BOJ|di4xZH;Mk;twZy0HymfkQ@Km)yDl>g23hw<$1s5gvq-%oEC zW&RL^VLm$V3q^hxI_urX>?MN`n(t+5-l3WzHX5Z+*%!trxKbXgZQwviH7$%#kbFl`=) z&jDq>+UtbCR@nucdm1UPKuw(q#t2lFY=nR-^Dl7%k1lML&XV5jqi&SkQSe-FMJ53#D3xgq{C(61f;?u4HVWTe=#{&PmB>&Je?4XYbgF? zLeBD8zxUA@WW923%=w}Re`(8GGEJ?q@<~;8KsG5?^%c3Kr1ZKCrM4-lf3jRsk0Zz> zC6}n)Nb2E}a}1D6N^Y`FE-5+8PqIn*NW4j7sclM5lpvRs-03fn;$f$}NAcWHa&CX+ zS}=vCsJ~=U zsqD$7Quc}~uA&SoQeYDqRHVqBGN?$QHW^f;*wHemNWpK*pdv-@kU>QXe_sw2SNug8 zROA4^$)F-fc&S+dw}G;&-xqGcGn;!t_P^UX%>6`ID8$nqbiK2O3#(~?^$EhV=tIoCkY<~yB`-D+~*-_6Uv5jVGcDP_iZtQnmq5a8Pw!> zP21^kJU%HE26v=)3iEQj2vB4u{m61d{ss39uiYp0T?%Yh&6&YF81IujC&V zq^WgKH{oEFZ59pY>inNrFsU-2yU=cv8n255lM1apglZ=BT@VW<)wSy>R5PjV2eDvM zS%{xd%`{(fUM!eY)yZF|W>V8#1n2vLPIh5rS6y(g0-nmtD^OF_55@>oR_j#*d%)Ed zBTnF%Z4f6QmDzd=9|9@uqBsGmZAc&CJs`z>EKWeGbMzJ715(~!MhJM{7Sd064@iOE zixZFvH})@Z4PKEP+(x8sUl~MP{d7EH=rD;7*@ z{ZK5J6q+g)Osc#u7EDTf{&k`AOzP_*7VH_xiUxC)%@7MFWo-}(CN-TwFy6v_m@u^8 z!R^RKdY;L2u?!T)Te#j9*A!IY4S_I~b<-clRTHH*jP$ZWZx|^hQ*Rh)r2LyYn}pQi zr#FmrF-~t7DdK|OFw#QJw{#vGUC)K<4Ws;DrZmIVep=VZ{~mQk^f>N z{~M9~PALDKlNh{g$A6L4>bbzOvJyk_6umJDO=(SJQK@XO94fBD$ug)&lN)7FkwU+h zK}C8k79imal6qUqpdu{?$)F--uaZGUI{#J<6(2no50udC~}{1eH&2IrA}P zI27N`{R?TKSo6o4ie8rgyB8r%^o3}O|5Y?pWmRlKwQ@!Ki-eMbXNZJ)6e|)+3av9t z;8;nKsz@j)@Tf>ADXwOazz&nbrig`dMSU$2N(!nTEU?3*m?VT+@=Aa0YV0Bk)hwux zri#1X7gkv*yO+#7^Xqz7m~y?7cZDgt!@Vm^`8?gb!j!>VyemvO`+;|bDNAp8SD5nh z>ET|rGmYqKdsmopuZwquDcfB670&%v*k+;dzb=+EGtXUE%f7Gl2dT^wqO&HRjc@7; zqO9DYFNm_UM5wOaqbwb!FNm`BxV|9D+NZ;G?H*X#c6tjxUcpN*R^|;)%)}X zQFg!Nz+WxN%O-RF-I6F2&2ylVv#6HY3-pGlY`4A;p4A`g389?6rYD3lx@3fo!>4?H zSx*RM^P74?D3>Sc3873rtS5x>I743u&*Cy8bhH@da0fjhl))nrl4nQHZ(4@B$IN#3 zjMY38&eNZB7uK?PkNzN)eW^EyXYw!lf+&}tbm}@L%H}5ef+(M@`hqBIz97o$vMyb%N2867`hqC8N9qfr?B0%`e@&$JKrZvYGsW-M=N?q>UvlEti+pDG zEP~tSHg|7>wd}8<8YaR0dZQ#<9q$?=A+=P9G;}zmo&;kgq^94Ek&wEEMHxCAQrj70 zB&5EM(T4Vf)VS9u3Ga8Rjx@9VAGNIMu>-V z)ol_DC%vVMhLh6jj!{@uE}0O(!J>0L@sf3CvL6c8^`@YvrZ9a0Dx0JyfGcK&_W`7l z1KtOaDn9oFOjaOVe{R=T%V4(tq>?sO;HsxlUMqHSjEb$@>7x(k|WyP?iq$K7g`x zoc95grHj1}pe)_%eE?q{j_1s7R^jWKfZYACHxA25DxYi3}>z{a_hX$+(GKdrX@ES zB%79;Ws+=Ka+yuCX~}Uul}$_Tb4@laInk5xk`s(vsgrzKKK@W;(~?^)l}$^|^#N!b z=WR7v&cw^`CtEu(npeGzz*ZuGnj4jvXovuBAQ~ay4%0!LfV|`_aRPFUk>Uj84-3T! z$O*QK6OiUli4%~z)5Qr$$HgZJClZY*>WCANHoJ)vkP72LfEy8cT@K`)>U{VE6N^C7 z#VCoU&VBO8aIa4m8CUJEa>z)*PfeE6W~AmOa>z*8gXNHs$`j;}k>a<@AtUwwD2I%k z;K?ac&L+7+Q#oYh5H>kv+lX-MVQjM9*@OTC@% zd_b0H>96EvjVJ0NFrWE=yaP2gcQHnwvT!2=T)}h12}rx|ixZG)Z-^6+UW?2UJ`1E& zA8`WGXb*7$Qs-!K0@CF^BLrNL--{EF7VnD_kP2(eR@jTVC6n_J@}4My2h+8llH{1P zh^DL|-WFBaDDR58)#6Q2(!~yMijo4(cvF;e{jxVjDXahXrYPlasX1P>D`o1--V~*r ze9OC{Jp0CZQ|*_al))e952Sp)tUr*lyWBk8r-O3+4Us^e z`BU`=k`6ZL499`k<#u#Kn0)AJS9Lk5wi zB0sr=DqA9xkgM)nd4#0KmlsHDLsID?d4#0iba{lN>gEfj9WtqXoIFBufj{ICk~_S) zNZKLOjLBYkgybfF%OfP0nY9?N&&SHl2%|sRXsR{DQMB!kSwHFQM;#0+fNpgq8f=OtPBncfe z$*Y@KFo|lOSTITH8?j&#kk4wNVpHUL7wz zEKestEa{yHJ3gsw27><87Zwzxsr3`_V3j?!Nw8+F&aPs?q{>BN!KB7N#DYnMO*ac2 zGpR31ESOZcT`ZW?R&$HcZj;JpiU#w(@QPS4sjBi;q1`4mO+;|3+=Ilfm~D!3&nMY2 zTb*-myAQtSzfRo~3gdb9J_tG(A<$HIO%4$hw@tzxarHGdPDDx^YMh8vIo&uBDfFmu zB2w!O<3yy~vfB-xHBxbB<3yg(tqdafXiAeP??!X=ju;mxB_h$Q&vil0Zc^@sk`X-i*_1pjL?%iCbh1^zZpqOvl_4DJb6ej{Tfr1{s4k&xOa86zQ`Z!<*Iz_G3nXgC<#~cXk#R#;f2OXNWH&{@q2rtQy%in z7!TB5aKpl3oX`y<>jxh~H?0olOZMkGp>`@&s0r?4Srk!#-9#{5d~u8`T4 zaG%^^ZYAWN>E>2Ko?8DiQ%^iOt;^g>$ba{nTM4=HKjv0KUOo78Q_l%G_$IR};l6&w z+)BvptA1f>b5S2K50&(mWsq5uB;SK}kbGLraoKqj=~ecEdGy?6o0>&W{@T|pdUDor zX3>+UZZ(UZ-1NLz^yH)LOOq0l9JIAr^yHmE=FxN4oNN|7`QM~Xqr6=cgwn zc*s#8ZD;JYH7l+hUY8cT^lIE8PYpGDom^rMw5k@DBXX$2X+b5HK!;_hH8gqKiJ2jh zmf_YAhXr*hbvjs8o83|wB`RA2ZDE#P))1R&v4*VTT;j5% z#%@fFU6dNTJ~e($YJ7ZZ+^*F4BdKu*Qe(GVJAM>x>)v`SHFk;Ob92VVn{jJ1H%!P} zG@HU}W-dQcDBoV=a08;gf+x& zQ$20Vx42Iw4lB?eV0VPOn{7m7wq|puh9hUIwN06yax7(6%FdMCDThdwn){E#Idxq4YLJ@QX?TEd0$W{QqN`?U~{TAPsB^@9sE!o7AqaScFg^)p$@0b zWr=XO4_!OxhA7U2wEB#QTW_R6)&Z54H1lbvW&w;;m9+<5FWM zrp6*6@$cr;xIL-yD^ug~@6-oPmOf)j+V-W?%qWvKVXY^+Wya=>Hy0ncxj@U;jLnlW z5)WpsoRl#=A!GVlA4_K3*36}wEXdxBY3q2o8$0KEf?x5oKWOxTQQ_pzkRbr(E>fA^&vJvSYCco63n$hldbgQ?rj7HW68 z?4v9pj<85o+ZX>(t^~N-iB&ddsLkmq{Zdx;J0eslR4boH@?W1Y^?K4lk>tM~d*FKF zqz7k`&SCz^pJWN+>F#i;`6Yk7Y{_r&r-N`?d7ioN9}!?r-V^T0Wvh0`dE|b552udI z_m3Ebrqsmppn^Oyzsw|K@sDsnKQ}=$Fz-{p-h}99e~9JSK|8^;?@FRw*(|_3w1;}B55R| zVpF6uk~*p8y*y=UOcQN)VnCLB#Ayk{0Bn%m7MgWfG{UhtU8*fKRP|9hz|A7;q3+HB zwZx!Rv@uQudQFQh)DniFj{{a>QJvPI*5U5eK$}aohkM37K1#Fp&g4y2x1j`Lq{H_{ zYupG&qJ?jHS@$c!o}% z9%+d(AY;aa%=lQ#&G{2E5)(34PRv|3<7VtuV&$VIY|rX`c&Vi2nTacIPCJmX?uaGh z;EarU%Wp1Nd}B|dC39s$=CpS_9h1_oySu?1(N^B!xSI-RGE0D#z)-8jr8*<|`HjJ# zCcqZzh_(%PmvHAClCr(ijC>@iN58)A!3(d%w}1bxef%F%NwFKdkK!ECmXiK``t}~w@#Nu-eqB4#S4DlZ z0xPAgN=ZyvfYIz4&)Pz)FMtOwPFdjNKANKaUi0nUl|oB*x5dz2SunPP2}Onn`Y7en zryjdtD;hh$a$-xgP5PMW9WhoN+?Q>H+jlsoW;R7;UaO~x} z@KK)Q$7`{MTZ3Uf7)cF9dWKuWtbr$X`6xwsGuMo%=U2<3C^7v1h0_=BP!wN98LkvY zAE+n}&j!9(qQXa*+cqTbr+{9Vl4G6ezlb1ZrYT+^-_*LV71lj!q`$(iZFMVh^?d02llr)`J4i-_FE{`Q8dEg z;5(jS&-)39-QHJ-jlj4*|<@-1`Zqti}{A^9q`B) zw7F8DlY>WLlETeD3jB6Dt`WAL z2Mchm)&jE!ytiACIej_0w%Ip!Z?a^rUx5oSw^$RG-`MjGI=;;5+fX2L@me3HN-vr{ zz(rKF-5MAGSF)pvvpJ(}a2R(hw@@naJ;gB2%IAz8w1z!mva9ZdLhuFjT#Kus2 zXb`4goc3_LWhlR>!T_fQc0Ol@Wy>u-ZX_$Ok}b#9T{$rQ%ArGPa~AU{07^Eu_g8t4 zaG1J6AzU6|Xoy0d#B&#_@gVsgns$7TsmeD0PCws*GtWILB!c`?kdOVSTt|TvblO6Z zi|93y6SXe(`G;nr`@$3*aKrSiNjg%{-BbUZvs-4^2gkxae5(Ov0?@Xrd+yIzBXYJ zK40Cq3tyG#ck-AW-K{Ou6@m_Wcv$kLaJN5a<0$3l2pbA7fW1VzBCSq4EcRJ^2MfVe zQmEa9@JOfIs*B@mlu}_Vu)6F!Qi>*4(At*F6?-$*PekRV{j8&+t(M4>M`F|vM|8M7 zgnM&IHwRUgoaC;p5=KHtFp#7n=(!n~(>MlMVhqU#mV8b_DcLCr|TRMb{7V` zo!nPjcAE>X;+c?(c8{<^&=c73l`A0FX|v)UR!j0G6{$cS2YNGp9TtG=D|e)O zb`SgQhk;u1M)YDBsqwZ9w_|Ay1)o;0=i1DDF@{}sPe@a}n8KUKFR!xA-0`{Q&0Bdx z$L`QN5KLLm;Iq=6ODk=x*9z%l@l4xgT~1|ATxu~*>ImiiVwBCPJau*J`m|a5)0fXo zo0gQeWTX3wYfDyLIdX`9$G@PGp16}cl&1svK;O;cA$gWB?SYU8WQbuYufX9_o^fwR zI`D7ZCvKhT|&O z7N|UxwtsiptcB_O64DY9uP<4fgTBHq`0BbU7}5%5)61huOoeB(wl`ow!_3O81G3h`=nli_e!@!^0zS z31m~sP=Qd-ls0u5b_4>zj^Y9b`^pDSTmzWu#{HR9$}^d9+jx5KL}KsX zm^pby#_WwsseZ0VniH_120lGtQ%ZIm84wzYQJ=Om4qUpd;S7jc$nh4FepJp@Ehd zOfq3agb5$6c8o22>w^15trH4i$Egf_gPb`DJN725u z4~Bbj(T6y3uY_M)woy81LGCFpD{ksk#_mX^dY^&}`VO-;SM(7wRTl#x40B)zc zY$`^hZIt>N55M@OEN~_#aecU(<^AIw;Ow{*rpB;3+bAuvB48jco*{O0x(`D7I3k15 z%iBi1geg1TJ5e*Ddu^jMco1uKCMSAME=Ek~=B!j<|45kBgWAyet+f#3mAfI`SI<+m zTzimTOVG4e_bt7agmgo$@wOZwoj#OBdOq)WtDANwk3f;vMdZp=wX-2Rej04z$G6yJ2Njhv!`bu0SO88 zuq7nW3y_fovUo@!iI-4}6QbwCd_ePcVj^Y&l7~o5Pt3Q8n3$R0f33aGu$-D)9 zwSJDpIP2*c&U*7>v0De~r?!ubX0tVh9mb3eD=;xIGCI&?NuPSC`0+IjaZM|HVjssH z|7U}(p~fSfef`bJeP@1&NX0N$-0zJR57tTSfR-H*#|r84wzqj^hnbIuP12bC@E)J@ zqj=-QTZBoZ`rFXb-)27!k?KG?j9N56Y^b-A&abVm_NTRxe%#D;oS*or(tdq*bL{5i zuVYKnP2$!XZg%ti!^=@0JY-o-6K=O7gXMn+rB<%|;)QYy3Nf&5(Q}-o-jc-v~>ck?(E{4dMDl*+k>xvY@HGev*u z*OxTES>tzB^$A7-)P@>qpnkE#f*=@2lKGeSID#>5Pi_5B~ zcgROHD#xuBjE?lL*wLj}L%gjdX_KxHSC~PdCSUb?%Ca-Ir!l%3J$2c{zLo4F7nr9v zHHaV#;HHRgb>Uf9po$H25|qi8!ttjtaeO)mz~n2%BTzqv!Hn1c@>V-7J&cfDbYYfM z*w&yE@b*2NJ&1!L&O8&IDTfkY$=`(r@F!f2AFy3kb@Z#J+4&jfp4dp}Wf+6M@YA&P zeJ*p7uIXn!%8=x5VskS~3kilw@{bINyt;}h>2$rlHZr%bkLgm_={DR9vg?L_QX3(% zAI{*>!CFedf1pwMsl}-(L}sL>FGK=Wmf|H>cx6d-2+gXJe7D%Xp1jfre;OX_YgBvE z5qq-UJ=7R(EFY=F*A6Iz(5=^1LXYnlh#m0G{-ouZp1!{1AI?9s^ZcQ$QV}{_HkIL# zofqGE=fdm9Ctuv$cF(PinO>Xx{e=y?XkB}uQ|lK`JUqGaRMTyDd`R2N7S;O3 zaE7B+Ozb22%&>gKCepFr?d%<>uWYOy7;Sr$!KozuQbF&b;jY&1&)m7Lw3n3>9QC$u zTc4+SQ+V)N4y0(|{HKz1yG=RR#1o1TM&}(;C~u@b*l6qD%pKr38>Wp8Z%tOT#cv-BNtz50fcV%2)P)@-fA zk5N>$UMe-;zAspK2(^o^?mN5g-HRsyUaiYQ8_7UxTf94HU#(X@<#?+FYosHu*vbEDTTYyI&<8XCbdMBuaaDVj_bTmWR3UU>1`vEAp6{c7^W;~<3jUqXO{DK6|jcJbts=&rJSt_@-|-2SV{ z&0CB262tZWO#8RvRn}Z9wr0 z?8DW=BhA5PC7FLme^4&-wm6V}(&)i!WvZt~Fcx6SR+rVP&E08fC~hutE zQJ3E>DoK^(x_cD%!_CFanzyi89~kC#rg>x~4+>h*vVrFi5%esS$=yfEFqwSfx0Ab{ zJ$K~Qi?958^1(yrpFRp8({IqceUsaTig{XqS-a?RlA9U;M`R@aXE-U2+YQmqGT`mfx}3{BcUvCAM6Y_FKG;#*H6 zVOx=uhFi5&FxJAj@gTdZkNr*{cpyt$iDO6K7TTK>t}MK2us(4#Wm0$~-Wud&PY4Vu zTV=1oE7xixt@|yGKeo`t-4C7Lde9<=@0?u^`1xgB1%u-F=VFozU@_)Mo2e8-G&j+m z3Lt*)_~FZsKE-BAryhLj@~`)RgHAnn#lnlfd;;7*F76i&8Z?+VqTV3;xlw0F!woeGp`VIKBN%pEv%26uO!7^6+~ z-X;tC9C`0?`l5g%aH5H~hA`CR2}mbiY4q|x6K{=8oFLL4Qe*_g#P%>()x3`T2pLeC z)#2GlN+746*fm@+@xH<)9C^F6xUtSZ^O6vZw!PXn)_*#&<=oL7*$r(i=G*V~;`YlP zC~i4#>--zPI{*B`?G0tSa&c)_4%D)J8N&E`!+so1vkgg{ub%zd^3j!K(KMUYkIM2^ zWqoPLoquo zo8!W7ZSDN9xc_xtX5Gskx8yYrUvi1}80wl?x568%c?YLu+zR%jpfX zZ9&BgZ6my}AVdaq>S}}~NrLz=nV!iHwqmVtsjan@?b1c3a|aPa1UZu@02zw%ngv5ch?z!rcN3>kPr8Q4 zyudz+&?uOaCrJXUBA4^3$=w?Ob+)9Io`2@8$%l?zdTP7hH0$E_t-7h0M|yty_=W8! zNZ+`$<-o;PJ+CbJmU2kbu|bsh7)SAw+G7bljnlh`!Sxc@99?$mId6RSZDikZk}j*& z`-SnW4)K@b%~DGWnWmvD{(VXxwU2VQY&@zB{_Ho-yO$kXV+??4)bL3UH-Uu^hfm3& zV>RGn9a4DNe6Rc~?q_SkFn0{jElZhlfGdHyjA0wa*GE72b(&+*a))=2--$r0n6(Z!rp2hoRQ2|`3bfHrYhbE4~yBRs>I?(HdVlqaXA_0v1>^wo!YSt2Wi zxLO6BoC30WIs#m}CQ(Z0<8%o7QGMJtPp<_FJ?|Jpy1m1Z~K8p75xW$>@F?*qCpwc&M%e6^7$#U{I#!#)*Fi}KCW$9A8A*)2VJtuLifMM0?C&{`M!L0T#u1|%PqG! zG?Dax07bKow5}8hKZb?~el{7yE$!y(<+_&j(mDh}&uE+rD`!?qW7DpmZ?>+VmT_xe zMRUR`YI#Gp6|`l*NXHd5x8=T%v97+Ht*e%{d~tpLwkifTabI{5lnNQMdcNpxDoYrFftnGWgv5u)!fyNbW29WF$5s z&g5_G_6%!~W$p=fdimAkrBf*BDtKXrNw}ux?uW@(5y*tf&KV@7BI!88Lf2-Uc`-j7 zJk<{UDo?+AGY-$^?aT-#1DSC#@#vcm{OsrH&k(2S19SZkrV?NQ|6cU-bWwx5nQa8=qonFn;$g!{QfiPQIr{7B}nui2g8G^a!jS@u~PS)@pW5puKPT+g^)rdrgft zm9AQPoxhCV>kQ>>dPYu69Cj#_>)T)YAOm0!L&S*V9ehCJv`+g~_4lvE0|O6fTp zy$yvGK>1lK;qTV_07I&^1*`D|`}g44m^gvD9DbrJKdO$NdVR>7xhGx@Ki-_&E&z`2 zNjp}-=~R1^Ot7rh4a7qJLl$VHu<0W~xd$*K8jE=tg9#$~mU+q}Fy5LL0|dv!I9W>r4G!4FRdu|OnFnMzJS(b`pXr=0SS&cez>0Z_s8QmOCrSd_$ZXurfW?(t z?#wwoh3CYd;|UJKnG+NL5Zl5bhgGA~VUuEQl*}G>8>+bAw+y{A>s00W(72mxv-E&I z{C>2ElHHRk>p|fG*D*;|Zz=-$=Y=-56HNn zu}JZgA%W%nNdB>%=%?TwMM=JV01!W((C1HFKCrfx{zpo*GXPDOhOu~wblB|hW3edg zgld3=)ddxa5*M(H+=A%wt__<4@VR$Ese#l+<32fl*CY> zRZ|dTZ+@ z+`TR(?9`SwA9Prs+=Z?0$vv3ta5-99St_ox*a%tn#nh@W(BJ<1JiR`wPq;|NY`@mq zr#cSM_ZQ?dL-fOz2`Z9c)`eh5OHbp&KO*t!b?jtvy?x7_d4~@#GE$%{XS((h|FNcFaY}(=YaewE#mh29nR_$9$ubFn=ss}4uU+{7E zt=>4}&efY1wZ7>?cP`CNvv+L@7S_ARmI3{YTX-~=*osT!ySQ}#a6;y5;--8r%fTv6 zpT@9_;fHT$b>pt~?M(QxxSiR;Z+zI6E_La9ts_-FY1!9diV3~uBY5zA*HBjb56*ZFk^P{GOUzqtS1OV8|A%SqSdZ^kb@ zzxLwZLl+Jo1R8L0&m+3$x0^4m-Fb1(t9H*d^Q(QQcVUf&_bBo&rjh|=uPOJt2Y;xg zP$b_YFSx1XL!*x{*T`(iN%U(VF}3uL77_`Z_baP?99bmZERprqSSD{R-=(mBe4oh9 z!iV{5W-Y;06(jtGuP3I<;WgZ(#WCyp)k|ng|0G+RWTuMfpl9kid-Zkm ztHc_XSI4lkjaVX3A8B}O0f=Q{td8>+H3U_V8FfdL=_F=nf^Y(&M2I68qC@aJ4k1;+ zH>Jt7nU~I0K+cEU)I70OYyo6rYz472o?OLCXQkmjvM9E0G97OnB&w5-uh%qjlO=0r zX5m0>kbFeNa~nO7QI2ZeMkdu!UVn|pKQfdGfr5z>ed42ZlaP=TYj_x?)sh`&*;knr z7r&y8nI=bQF!gX3k+{U?R*4AQ;CiyOGsC8kfso<{Tt2bazuzBphFOK9ZJcaaKGGP5 zsFdSU@N-T+f@cOe3bBTqJvA=mHjB)2D;c?Y?m!d-<;87y3@<*VKEU!U5LB0&ft%5N2N{knye}%Qe9HXvqSy0iJku04|v@ z!ZMPy_I-dG4Db@4KGtjt^FSX54Bqds%=Z$DGhAQx9F#-a&r{dl|eg3Pj1<2Q!vl z;fcr35Zq-6pMQ8QB0FF9FFYhC%l?Ni{(7TUwB>7AqH^i};7!SIenB_Sy)QZ{vt>Rz z?L`o>1)2<85AvM%&rVli?6?nQK)aOAk%`PksU$z(lXSQQOB1A|`Ab&9!ivS?e6$uy zi|&!|ZD5Vy;k)r?qQoI0C^Vv)6$6C4nSdSn|6!j9--d+an!GE~t$fQpN--8QP&T6U zTY)R2_{3h8K^0`2hR?!o!S4}X&vgjp?lF%g?$8L|zz>70fyE`v3Qsmb;o;W^&vB`e zRMjXOC<9D6sFf977T{0V9zqc*?htzxJ$x7 z&J!m@#1xJa-~Jqk9k$t5vQ3ojtn94OPWfWI{(|o$U+%i{wRz69jgwIWiV9}F#(baP zl7tOzK61&nL)a(^9(LRkUyEC!up|F^5*RdTs&SViXt%AVG142VqcI zj@D&B_&5ded@uOdgCc{3EIGtYOUag&dqJeeJiO`r^RG?rdj%NBS-iJD*)tzZaApMAscPmpD?u7$fe3%2Y{ys%BSKC-&2g*e;6E@ZiK=%@Lq@4`!*=T#+j3QK8vc z3b)IJ?mAT^xofG-cGdf8nCjS7uq&J~WfwSjV8BM$4+17dW>V>B!XZhW1sn3ymi^@_ zR+v!^lQxH#C}%|9YWQG$n=FpWP58bJM5QgFK<>?UThP0_Q9C}s-*3g8{mR;@om=e` zd1J%WPSPk}J0gqZnZxGn4hme$@v-Dq@+F~+VYXHQOz$Pl;<>&CK}`k8Iw@ua>=9y+ z5iKcmY~FvGYpcL#miPooBk1;~uW3!_6lq@)VMVxZ=USlcf1f%9BZ7*@7_U#x?7~;Q zmQzFEVi_qo)x@`R1aJ3xb95_L623g#ZA9S$!|M!so-cTweq09;|C=RUcF-Dm@6k$f z3&MC7%SpPRt!DI)Q?Cb8C~)kdWeQV-tn?vK+X<0%^tQ=jXP{yE9uWr__{e-VUx_>mP>Lk*m_nsjCB z;j9JL(?w0V0K%K^_HVM18M8nN)CfNP--#l9P~$wcCp&;IzPoMmF|lG?-25`EIg?Ku z;OgZ5_3ocI|L~KOC)OfK%by*&+h3SGaP;DV7a*=yMDfwBlMfx|-mL8l`*(rmgHVbZ z92XxMzc7Au^5|RVf4%nnj+Zmx%$v`>vs1lz`~bNRm)1Ue@!f}@1G@0ofytNFPd>iw z(nF7@UDJMFSDItiy7ZY{>G>C4_OHnLk$q$GKWPLe*FCRqv{E0J#y6Bcf9nn|@iCX4 z-aNT=N6TzG@%-e$N6Fyfp7UECwou9D^Z{=yeD9OHex6=9u=djN7m2@4Za&FlKH+>T z+(bas{aXX5?pG8dS$}c=Bj>kr+q54#D;37dXEJ4==w16qBnLjRc-cV1!86g?z^2gY z)H|_j3@xbvc-q*G=8y=(g+X|~$yQ>8kJMI!QCH%VEoM-vnCQKn239Eo+W^$MqSd9P zfF(ni4$2sJKsAA$rAr54-&u`sD)xX-jwOrl>Kz*KTOm?Y9*bdLD`}@b_n$sMaG6$# zitr49<tW) zhL8*ZMLS`8YE>n{nJJY{JiUB5fpGJwjp0q38!qvaV%GC*KBHl zX&cN~0o9p>;i-%wg(Th$S`l~-nT3kfcRN2jVzZ}?`=$Z?o~kXNCfHflGN3D~D^OFA zzOr3;8hK0p8h^~~fjkXONrtVl(q-nZa2Nf9ECMLYf;=wwrc>8BaST9!iw_>6WvD+y z=$r{7Vpl-~pwdLqO#h{>46%=9+T0X82txWleqy0cV;?c6z0a)>eipW1*fK#;y%<3G zU;AcxYUmK#xVd@=mn&UZ)|l9@-5fVG00>``!>wDGapR-L1OyjkVc_(W%^qlF*zsgN zR>kp4Bk^3~yzLgN-B=v7`{W@2&$ODKCibqtA8m(I=uvSH+rEYsi^{0^8CImUG;XuP zNk={c132^GtwsyChgCr)8_CB%ad#b5uvZhH`*d2EDm&tBb&enm<(3YMb*Qhw^5Y3U znAYeQL%?E0@eCrdJaBP1XXA|J5nFw*MTP@7Egk))kQ-EkOkClFUa_{8E_3aw3 zLKgB`;yc-XR<0NRkQctR@o>=WPailtj%zb7GXl`;4}N;gyfzODXQ`qJw2s#lRNFH&;3_GXF1&OMdgGW8VC-~uluqt` zm>j?vhlHo&QOb8k7KLWR2#}bpavO#{}Z{jaQV=iQ_nqN znF&@msACRp?r}=n@85R$agk!fh`AdBLzxXbcVFInIDMcz6<(?Z44B?PwyXIOO6DWxqIUgQigTg2kYS6B}Et)^K%<~jpmV)4Ld&|ZDMOi3tYkp36y=w)HAPrq)!j&%ccCv|MnA0hQ>zMVCdk&mYfQ8 z!z3%$j4GF-dyG?~k}A@J#7{}yz93Zds^m9WTfNSaii9>ylX$1t&R+8U* zVs3WkIF&7;29YTr5aWmgl0#j*+c*)zp{;*S?3$Au*>XPAe_{TF!h!AaFjpJ!^ea0=|8neZ_kZ&G4o4_zFjnj)!7p3h@VIZ}4&}<=-3)qE4ngF##M_ zv6yL>J0BwF1k3`4x8oo3Qb6|4f0abzYHcM!s+ey9Er?txT1iRoaUU_BP3}7aPFm!jtD+eBW@<9?r-}UiafV; zgB(CLI&o@;1B_vL8ZmljpvS}{qXX2~0*U+7jR%#w6&~&lZIya%cU4D82I@xDoOX0( zr7+jRMXKU%bQBXstphqdwPUEsMXz=Uc>=0B|0A?F%6efGYSR1hJPy>y$ST7}bE8~G z7Ho#kttnNG(8a&83pnREsBzx-1N-uBuA zslNul6p)(p&mE?cYyJu?m-}aWi9_6Yi6{GWf4S9751oIUaxO1-YWR_4k<~wH4gp#% zF!cwdJLhG2?Bef^coTQdvO?8Zv)v%ov3f8#E(Ry|i6%099$+4-bg^;9nz|u0RT#=G z$%2S`?G@0qbwKAJQ~bqqa3mvqL{csbUvolge+({AcvFZ9JZvX^&JbS8FBR39?_*C(tVi!vA8%bf1*ECX6cmf&ft%ki~sE6-OI$224@W;aPM z@R_$D2Ii%W4hMhMuS)VCtX@IuP=r-Hf^nQd{5buy^_<5tc&nAGkOeJPgMR8>GF%vy z;nlLXVG1Jelma9vSpcQik1isY+;D_E-L@RCBc)QB5h^p(I^Z=BvKWm_QtQIlm~6_v zS&dKtGQ6KTQPu$d1YItC`O&21eP+o(A5^MNXWT(#Q6IPxW`q{_a27icMqkkF1AoCT+a@=s_B#&|VoM_Gq#Q!y*gLuvRSzyk)>yW&U&A?vbyd z%!|qVQp|*k#{>=C5>ZhT@2b3C(45De@A-djQ{;WEB*R{Nf-|F?04A<=f{HxOeJ2pLhjmhHoKK%ZTkwdgAH8@`UvcW77m*jKAWnl;4qABL;$5 zN$Q#qnvaK9RQcJ%B)X27mV7zWjY1?swW0F{zOg7k>tYsuRR*P1y`{i#eqYzU8Ke$U zDC^BI0Z8hsNt@Tpx{`0)M?lN`rP#t2N6xjUVq4d8U&yhRtTGQ@u1HD2%HR7nYg5*> z3%%?w>Nc0$N^-=gN^^eeR|`s()v5JL%=ry-*ta>-t(Zk^7XFRFB#-0! z^}Cdj@LD%FKFM<k8!Q_k-)n5Dn*#J(0!VGW9#FGlr83j05*bG~bv# zbnfJX{1#s83y~YL!ejmvi`Vbdv)`fMm_Z`OD73mN0(YVs|4t?4x>v(9K=pJmVk>BX;IKKRt-m&p&br|k6YP~C6I;)sW+rKgV#O&k(nHFu+Rz&}_( zgd&QZt~tIndlcHLxjubV+sITdELb1e63oE{)`|wI$dk=^e-4Uip7DBt0p|dIm72A9 zP~`BynIKofv9MtIN1L#~VN$L)*eXprHdsmSw6fG_sH|J>b8mGNqXBa+x9=)w`d)+3 z!4AZEA#8(ilRGIzNMR0@AFL$QY|GAVmJVUYO7c03>Td6kepObt8 zlMcm5hWadbCHd^d{YL;3Ts*KYz4z{g{@b6Oe{9qF*B;4#_`+PJg;ezeaA;cSM(;qD zB!9LLP|U#Cob1sr=oTy^qF;Vvor6zzezEm}m0i^p?-Tg9tWRGQXOp5o#~p;5cjR-I zUpRqmnA$+$pWXS--oHZkR&o5P?SFIckhy8CFVye!U%y6Q=E#L-N+evND3n${`W>`V zKuCVqXBUFx0h&y=W(|k-tQMMSg(+c59CD+{pZGr~k3E_Eng4ld`ymiulgECWd|I*0 zAEtsEl-)G3uNv-ZeF#bJ?*fqJ?R?JPb9NjEGx09?tx$7L z#Mi%naQEdMuMwSzuaA#?HoVG#UEb6e@%M_zQP|yMi~&rI;U$0SFYzxJt|wP7Y&7sy z!g7XI7hu-FE29a+PcBekBKFEKnSub!9lVax2qOYsNXp1PAW3jF9K=B!2@`*CbK&L} zn)Jh<3Y?kVIif?{ZnK5bDhMUc#*9@Tkt;$4KS3&}1wi zuaW$&HR(*h7ETj1y^tIdq*!~6`0F7$8*BE~y&hZm znxG_bEU`YLWi>15x=#(|Ru%WLO0km2m)r#O|%L(c@|8&%2q*pKmE|T<8NuZeqADQ6QBen8Av|6Ez7s|G;G~gnWsW* zL8DT%Lfp>S25_(-2dyVKh4qJv#Bb;7RcA71$Co?wKI1z`z5`AxDqCPl4XlJb4CDp| zui_Q%n9Wxu5_kzr&UMUv#=(fsO-y>fC~f4bnPGvlLAv@L)5`FCiqhG5ONwq#fxK-@ z&~0sK1jooYoSg=}Kvw`Xggl;ICPjgxwCi=qNvrU7vo)H5vk{D{EB^e{&KGd*fjHPQ zWts@YtFLj|I+1e;RPg=3w^;xH)gUR9ge7zCDMlY=L4OLPomB+O);G0b7i2mgg9&B- z(69}!dF$fvD&-m3252A*z|zh+%_xcZ#_XKOH^zT6uN%@Ya-G1;0sRw40 zIgDxqMHg6`?IVb~506#}17-Ql$-hS%pS$Fa^v)V-OaL0W&ubwIy3A0-YaEea90AUm z-C(;yXS6`m!(cP9-m_b^V=@CdG9b+Y(mZ#t&TEvv*^kV^`1nOuI@uncJn-P9U%d=s z_}r1FGW$d8Ui+tT2OGz+%je#GVRGL#oD}8jj${r|p04c0U8xsv*1p@2lwW<}@JodE zE7aWH6)9JD?vPg|qtwjX2#GC}BT{>vFEkoFW z0^YT>jKonHY;*1epN=rIp&oW&Rx8Pic_fPO>rnK%yH870qe*O=X!l^FJBF^nlTgz* z7mkFML(+^b?f~0j#z^6G6ThS@sP`IG7i6c`2rGf9FT9&}Dh}}}#b~SfIRg!HCVphIe zRku{W8~lvQcUzaW$N=XAZxl+eq*;b$G+P{{B+3iPc^^Uk7_Rn#*TG`UThjI1gVh|? zWe|`Zhi@WLt7pge$(z;6aYK;m5sE>cUH2>jD9H)|6ifGis4pOQew7~`|CI}v7CdBe zkW2N-x&NJa!RSZrJ!dMA$oZ{(#RucNEyc7$LGp|?i?0ZJzP^JeX7W%3k6Pj6D?YSy z?bM4e=K#WD*F(8Zq4{_CNatkiCCAf=lGlIqnK{0R1Pkrm`nH*t; z=5X@8jTV70>It>b;jvcN<(Y%C&7FyA-4>E7)~8>e<{#H3$*Yl`J5XIo(HE%|?rWrC zxT0vnEz!TAjjRlAu9hd75JLw>QhfWF*V2JirF$*BRvqmLWs?ngRb0?esDiNqVO5C1 z%|gB5Oprb=RbH7D#=sQq{0h7fR##wlcH~b(gU0cm5uhc7yF1gLad$?ol-x9}(3#8ZLuGq1ucc*^ zzMUz|rlxZO-(+Zjp4>ply}&*bvJG+~q?R|rHW3}fHl(?>f#Sh|*FGAzh7#kZC!GfpHy_$cum=}&=bg{(jmpSIx!-2;y$yFi#rsSRwCP*8h zg9v1AB9L>dm_ZnR?$J#=6z;gq;%i(D;RC9V*;j@p(&TUKMkx^Th@Rm1#!uUlyb$AL zNf@g40sC)$p#W+302dkcG`_0Zhdvk2GM0Xf8H6r z8lBALaqftURi)YU=ik^xC^+1xB*|@%k%}&eyQKHrz!5MDt`&dEJ#DZ>KevhK*+&E07n{Umz6^r^p)N}Kc|8TgCr4NLh5_9x*PGG-dF)qHUUc-NWZZ46`6Vf3-OIlh!Uxl28+A1Os zSzp-%VXTy{j*%3C?R>V8J;tved!W_%2NiU1C|_AJIf2#_Du?yvnYRTq2mX%6V`b+8 zN*onUQI)dSQ3bJnU<)}bw2Q+!>wdsTgSAEpQn5K;5P5u-3Wk;BCSRB7EWnq`8pE|& zoXW$D4GWH9)kBa)!&>AJt#C_y1Ye09?@&fpl5aakDD4W*cclu?oLDb=!?3dFpE}Cl z@j4H8Y4=^9y)O8;-+trLy2mJ3qla?ugx{q%>Y;mn5>P}W8-nCmCYB4-2xnlanZJN) z%oDqcfYyFhI~U8)N)`Nb5K??xsl=7!4xv24DCA$Ey}6o98qlXBt;kdvnInW{*a5}w zBC1c2jrC_RrQ(1xl&c6*Xj{sp6ld7Ibi6pAG==@r*QAI!r6w0f2vgol)MQ9hB&~Tb zFR`r4NOhrO9wz9+Bq-*C2IbP?fN{zjlBHZ7M8zXWO%BbyX5$tfVs1%e1pLu#anr|^ zStIP>f=#KNZkcB#%ktduU!NU+{OtH6xKiVzwswdm6&@~8=V_LIns?QAz1Z{I7A#OS zKk_Dg@Rv95t|%HXwRvap4Q7{y9Jg&#x$$_HiwML*Sbf-OE*gY^WL7Dy1OJ%tK>aX_ z8KuRlVCYG#)MeylqiaUqkHK+-*g@BPsucFEB=KJ+-f(jnt81h=u}_Ign(c7CDja{K zOB$Bk7@T4QmNx`uxHVZOE-sqPtS{0-u!s;6?>%ZFf>XD^djX!~S0+6S0AdQ5(sHJ= zp~7<4rmQ4`bCs1bKz>Kxsn^NY^mGx@FEYagjldI5(q&>}>gCRU{ZL8gVc$^Un;&Onm@H=3Cs%2Amk8gR)C@$~92jdFci9TUvmCvMJpj$B;m$rqIg13#p;OkC_+{!1?jkQyhoo${e4c-#~z)uayYN0eZ|0 z_}c=3c<7%B5|ntrGpBuwc7EQDkIVya#N9oJ#aVOFuqXkn;3fjpYpFyX*6Oc^S6u6H z`pJVYTs*M#-0{aUjm%4}Wcs+~>FeR%ZO>fX`K=lFUVqT=yKU|Ya_wviy1vX1w1H~^r?6$q3H$`hD<2W}(E9VjTzUiA7b z1yvsT&N}gLOq0L8NB5H7XMUVQ#g<&J=eTU5tA$l?q4Ak;I zAceZsd1V+C$psr-MJ_4bjsGb24k2TwB|1FXzpT-hK_=uej&l+aC1Ir=Io7&TU;EAE z<8PPN>)#aDtMQ)LZt?XBdDq45JMvX~!xgSt@1e(0eF5Ltc>yoAPjM5e5WFa_;~PG7 z9YZ~|Ph&#puMp--lkZ*0nohm9*c|4X56kze4y&6puddgSrl&yyJnB=5@_$x^Zz&@E zJcV)zotA9Z4_WOuStA8E^7}^)ey~lU)4)Ee!+wT^fBlRL-#?BNM|``5E%0|AvIRO0 zX!7^J&yK)Bs)}mLpH(g`@zhfI)U8$bJ~GUZuGz8x*x;4*;2-V0OOoqv6G4LsY$TMW zS9OmHu8mik)GKs*PGJLN>Ao;^^Uj@o;o_T5lh=0P&~dO-*vwvUK_@VoZciOWn)}|E z_O$-Nh3AgY3`P4w16I(521GIR+~K@|jwkwUOTB7~2<-r8)u}g)J|%U3Ohh~NK7&IC z3|Bb$1BA;xmQLxDEX~h9oV4v0eE|WIplN`up_Rfkv>9*9bfB%< z+RwAOR+MFokPYFT69ZH`1mqK`B#nIl!~`tP(wH1!<~dP?O+J4y^O5B(`HThLIVtd_ znlriDANb=MFpn~WnnQmk?3BFtifQGRkMxFt&$OKtE3ps>S@xv2>lUf!3b&jg@1RvV z<9e|GXFMV2&htcB4nF>mRObkn2-(pjeN*U;YQ!#Zv&*K-c>(MnPA3K6ddek%-QcgZ#d%dViq5RVoiiHI&Mn>+*i*`sQ`G*-LM7b1HE-QGbBqci6 zjH0|}2iYO-Kv8}amV`MD%-lh+Bt0`qkOOTJBqVEg5+c)7cG9#jyAmNn6~n2mkCZFC z_$rRFq(hCT%z=>H)=8EqhTK7(DB@y8pWF&7kYGY1S`aAl2nYhjDm;~?N`A9f@~1E@GU3I`E#{CIy_@+4KqD+!;cz0 zbbz$f*rjHBZ-e}>gs@#QEXVMO-8r9w_kOel4tRh;kw+r!YVCQK%3E-LdqRAc1eOg& zb2-jJ1VF^zN)$LLg*gdZo0DkZg#xD7Xo*fYgaG6$48O8-UHhQbEEO`g+u;ja-2ati z!5yJTGIA-M&9lZj0rO((WJ(s`G0A#_rR&p4zSokryCX$~@+NT|GKsMcL5cR(e253m zt{XR}AzDX}8ObeeEjpKl(k#t8nB{R@HpW{E6R0-i_TVH;ZoR(>Q@qtgjw8}((%Id{ zWt(Sho!aP}xTiK&l6i~cgh>sE^VQb3Sk6uO3{96p2ia^r&@pGz=xH-QUI#aS+RCSa z#ohX1iu$$wMD{`_iVdds)DH2QvV;W0yT`^C?JVtv;XF@gV4to-Lcn>+D}G1&BnES^s< z#e`D2uwVa7k@LfU`jj(t_&@#>x^2oRpj`OB{}o36s#-5O%Nm0}Rbz!O{Il42{5-p% z@Nr-2$bS$Tn|ko6%fH?;wQ-yO`!jbp#;6OkQT5+L;*>R4_~?IqW+$PeO{QUHpZw{G zhdiy^oB8A&P(y_0KSj}>OD~1?KQ+1a@k_h5_+6itGXq#)zyDL8NpLaz%P-dA5dOK} zyQe=(#`<)Sbn!LCmOmvr>YdMbYV-r#l1D>`At=C#n4HinvEx*Dz3 zo`Z)HN+gQ@0NxAQ>_B&4vj-7cC1}&XtZ&p1;1rp;4e!v(THm0;&F12iCG1{-%!L$+ z=qNe6l8h>de2C&RJI-%GKj^IMRS5df))r?e?1Pj0pAFv`3gEoC0l0sL=J$F|?LI4B zcJb9$*;pmzzYCG&S)07f{9icA4B--MORa$)*5~4xq;m~! zO|&_Gnc$_O&?UGgMwwwu=NiP*Pl{qqq=SvLHyd#Mhu~ zG29-40LG$N)zKycKipWxQpfKQ9N8X$L>Y+bt5OUyr&UUwH7#w(5X2m^bVb~$>DhN9Ci8|dFs-aWKkFK(`^q2r9|Ca7D zix5%_FsQ#a%y|u~G4Jo@q*%E&5R&rs*~X`@^=?Rh(JDo9UL09kt=rDWTM|WqvDna6 zrmk9V66v5;Sl_JBuJc3mKvNWan_=t(3%1ew-;x>KnHd{rYo4!*B}>G6DI}<_f0_i$ z*2I!>(HUp?EE0~kMo(w-w93u%Bre#q>mSyUc4({KzYdLTL#WmbQu#WN%F~xNlNkDs zNoU_{r(*Ix`10OPJ$-m;xiceuXN^(?JAu*H*)7qfjJdKd}KQqpq z5(fAqgnW!?K=2m$B5{eS%UJT;&Z|Xb6K>IZ|SBiRB2E{urVjOLUAsd+GW{B z2gO^d&=LXeVpp6(5ptg_{$!kyq-r}O=0RO4d)xUz#qNk-p=rmA?)TQChq#Qi#~J^jEU+Q*PP>oJYJv$LY#7J9MlvvD=sQ3db3S!)=3_W05C(%6!X0`;!tVVz zB>PEev=G2-bql!G=yGLE=>_F7d%yUIS`lZpm|XC1hK2aC)uZv`qv}V%G(%|NS6hgv z5rO-?6{V3YRg@eHI%zK^l=u5-Os^!rr^e(cRGsF^vqWdzlsZ`%3$>rHnF13L%% zzbAEd=nt$w9r^<+P?=mysc3_L$|!gK;JVb2Kd>$p_9Y0}f9O!xi4d~?A@h!$I|Nja7>c9VZ`jyijT|W4Dt~JGQ+ka6d>-9gRl9gfB;YJ5M zZi-h>o&Jz&*CSV~b`{(UWvwd^HRWbk5H>TYUzv<58)(1&MC3dCFQj|Pi6*RJ3=CzO`OO?MDWO6J$y}vR50{< zI2kL+-#W=d=5dmj$;cRq;*0i1&73>(s{8X~rYRQ;rU(5lf(Oe7bF7j#Y2**)@YCJj zUebU{n6h}m>xa{23|#dtDhoOd&gz0+h8LTIj@pa2DH&Q7hkW$AxRfV)Q3J@mhVBL= z3u0+o(wF*YFU2}CCY;<&5j2fnp&80uFVxYbuI4?IubOOXDt>ANPJ-xba zPXc?!F@VDXJIYgT!VoWLG3CQJ3BDXg5uk*7GH0TEpjI(6mIIW`IyIN4@*8OB{ey6q zlt>=Fx3F_DdwE<#4wt3qkaQpbgUx%KfoyQX0PPnS)+!}c&v0uaMM?+|SO>n+VPfR|tV{+4T3J^p#wJe6A9iwW=;Vy3Z?dI$hp$%QXdY*gi zLvdNLf=nD69waibi-LLS>d}$a4QS5%OgiP2hP3oJOfdH#YOo3H~jTw$LK3tE>^(+y=Pc|P&#FQC8ZT=WLmh@n(w}9u`$E$jNT*>jp~_r*_lb+Uwg^@B z^ucpSH%~tNs0YJ5x_18IwZzY|g*r)%dsBeU9eXVMG5C{q9|d@-1^Phty8L_ionym; zXTG2>9(w5f)7vL^!#ldq+w^z&Rb`Mq{OHB??@aD^JL{erUklAcA1GDl5yZo*1DWqZ z-&YYxh;y4kjoOEB38fUOc-s^R%VTG&ID69G-|eI*9Cexi9Hy%YGk33~0&fh6jdHl8 z;9yUmrv5gM?JoOTnEOoHfGOmgAsd<4qq06Z>SKD@>^dV_IBesAqt=TiqA} zAuMtm2=01jGnf40bsEvs?)8!yVCg`n6X|DnKb$O5o<=A`*j%QwB_HrkK`&c7ynv)x z0$vVKw>@u~prS+7p+T~$lv4Uv&))f~p9ddYp=}3WJ5dD#?~CO2qf^v}ufd zlr~gY&uuX+5J7Z;wzE|n`iJ{PnXzg~#4{eDke1g}43G}>bIiud z3`C8KF)M8?%4t|#g>rvNaVx3>levg^7-^D;qqj2}7R}VFO_HgOL!C2?XcnQw6-_G` zI4jm=utc-uP?EhvvaB;R0YPcoZwmeF-@*EJZW;ETLPVX4<>5tvFqBZzTFdvvi?aRf zI;!*$kRr-dM) z`4x)6CND8J^-NxH!Ey&T@BZNFhs%n_)}kB3h9}e)zhTVnnj-=!gQgEtnoV(Ytp6@zDU?Y zu^y5CiYggaSb6*13U1BgMbrndAg*saOAFCy!=~fxT}9O;<84I)r8rbk?kyC>SK68P9`5rj?TylsxC+ z2P9!zQo=XwVFW}mApz53>(;5aRME0M)8H!%frV??cK93J95191d}%>Hqo;^!A?6yo zI13wr!V4@#DfY^ei{^oT@H}1{XI1i=RfrkJ#WUdO{l5cQD>KbXhfzeZ3MZ)MilE(W zdR42kp>r$SFF^9`6d0F|Z@ajEH(@qcque6npIQRfI@86aolj7E6kFQ^VvYy$8~qpE zE4CPxNUVAdq!D`L#uz_&xrD5WLa3f_+X6~LWEj|`%Csbs$?-0gBU3KO9j@zmYq6y7 z^4qGY6+b|$SXqVNo&7LzEGv7!3W9Q}HpHgEE|`t%a)Xu^h{Ga8c#<3EE+b8!J<0m> zfnjVpz$y1zZBlJ~b*zFQJ!epP^~-g@LqP`*gmVSOdIp@bVeDt;e|DeD-JFShhL|(gX-lJeZw9?CtF>W5|1O--I{&O z3P*D5dK0?i$lUdqD&r=~MLK2ye@WpMUo)PT`#|zksW&L=mu;Y|wNfF0(MIp=6yC>? z0 zrn>Tld6~s4YnLn&F^IEPueoF8P;iC1BHpjXO*ZNhUYJgtfR+;HoE0hmtEHB64v_}G z$+Ct1Aqq+cpj|j`vnh(>aQ!di^@rkhCNy6BdVU0kNenrJdIxh7+V~s& z)A9gI^T~qbhSIdsp)cIwQ>t_^JqIpQ*6nYSFW(bpFC2V!6{lRd{15$WqD5j8U%`_o>*k_Xy&I*|A{-y4tLu;r-G&%_5x|J+0fAwE- zsB8XWe=K>J=29DWv0`WQrlU;{bEt(b>jZSNYhZYKC}L#fZPb+7$wnvs+1$Rq7L%hPpcWru9jwD6Qj#|{6hLenw>+upOtL_r7^W6`KpM2uc zbIx#K(*dvW7%+ZrE&o{TSddw92~&ODMh8;-OFFAGm$m?yPX3QyT(@I zCSlYo$*TG2;0$R=DyX%K9gQZm~#{9K6PC7Ia8C&=r_MA zHmW4|EY2iTestNKN_2toHd^K|@Fwz#%Y_x~9y-ogIgH|DKAfESwF=fxBek^h!QXVO zLPEG+(hqy;T?Kw5+GLTbKZ5}_1zGP1sREzR!RPwTiJ04x@fqwLplVF(CrbEBMKZj zRcNq+$mVH56u{qsMVLa%>S3q(IKaDka8wY3IWhMx`?Zp+KJVFEyY^$HLrX~1=;FKE zCLh}o-jvyYEaB|$ES!X>%Mc2kUJC4>!&@aF`!9Iv)~4O(pMD!?!lm&I`Agldl!UhW zR9PHKugj1P9lmk;*Zo%>lJbfQ9x_4(904ZXPcSfAwO3^dAwT3oOJFBHTiV*pOm zjzesSiB5z3Od{!RCR_+5DU0fUl7EJnEer1=e5Ehi+niKa;2Cn_3XG zn6Pg56Lqx@NtYKG?`rQ=7vT21Huzu@-%+*-J%ZFJ)MOTq)?!upMCAZg7^$LXq{A|V z|JSoi3yW}jOKb{Xl>4z5HA&?x#uUx}z`YbYVs{n7PoX27;2^^d2yWVk3HOy<=+L)E zvcA~?5glx7MS7OSpOg^Ari?KtdPI&esxoA8Rc9p06 zL6GX?-pP&YLp-S*ZQ|~7Y-;>h7al%pPPuY6*4pv*k>PA3?Ob)qJ8WbnM$Lq0*%i8* zc~Oj-3L@s`o(Og~Wxdl6wr-eu_9>lb*?l2wrQ2f`)XsEj6DM5pKi&x2R-aA3m~LR0KlR3$=EEqjt>XrZgnqvCdKd!H(iw=KO& zauzTnIK{kK&;SRqIe*yJdlBX7Sl&5hw==5%VGD zWO1zsHR|wogBrEJn@B&U4$yhAf3i}p)D|Bks^yWIMlfqEiXPB|U%q(qNjX%lAejoh z8hFDl>YV)2#S=$#`PHY|uch~y1wp(4tAGR<3=C@-10ytkt@|(rR#cZ^ZH^$MUqIY}Cc;q(LJ+%BO@{pe@PagqY3;ZBG5IpRkGL{ zYZM|;kZ6SeWMlG;oM(nDlzpa@)zXK47jM}{*8E{xwMVl+Rj`)jxqsMf>CAT@dfIh6 z%$r0;A}3V{Y`Wna?pYM*l{>7W;A(@igbLwnR-y*D?k7htdpn|uymXfzrFh>kZHqh& zuEXUD+cR$w!V1LH9o{J=aO)Th{?e|6==W&vSMJvk^7>ObY}$C z?VZ=|VOz@ZwOLoTyVviNzbzs=m2dgt0I#-4^KY9!dlnLoZVp*I?&*EXM`WQZqTMfK%6)A?7ENg)4$ zV_S3Aq4|N}+w%nlgHFDf@`A3um@v?_m;$WinZ&>%6dIcLa4qNe7S3aH00uskZ~|MX z$r$$ETia4|p|MjAqgG&e|_2um2obKds@A(&5p@*be0Lp5x;k#N;nJ{xNW`WjF{7bR#<6CBD4 zr#P9-o~Ju9z3;0JH>szxbfno+Zv=sZKZ+VFjTN)0_=cZOF%sPx6enPy76Rd<?e8fuk1>yl{TkgWNv(=+?=Hj&pC;#)bX6EdGV3) z3*$$r=!k3c{9Bu70c=bbv(dq@DSSoAP>;fBhL3QT6%G)6!HI|cJfT15xA2O!{Fpuv z%zf|9le>PNUO2G!((xA|KlI*AZazs(1RqXy<^M{M-T|jGgUzl5aqG%%MDiTcF^0jC zLEh60!#h@8Hbj7$^(hGHNOfh0BWT&F=T_prawe8=oux8TTg}0QUGA%b3wzT>FK0tM z`FL5CnkS`6A#(!N1oXG;c~S^)l~Y%dF02NLQ8@O1;|~cQtGL4HUA;pi1Jp2pwh^-8 z(KtJR~+^f z*kEj!b!sFvfR1o%AlDfzer);BmDLqMbrBZmmwFn}plADFPL+KChd3AjfoAg_C<}WS z(5{GJTyUhx|B`)TIQ?6l9DFBVDJ%!bf{9lVJ40c8(=^K+x+?stLzq8motFjOp`c%6 zbTh2%=4vQyGzQBW6Z;tr`&vmphRP>)xb;i?KOP0k?R}bJU@EOgfxeoet2HT4x-Xmr z`+eWe;J#@^E`0iv89@yKnV3WPJcF*3F~x%Utvx&bo3r8-w+fu=DYi+2uPQb%zDeN; zvD+wH$^JNiT2@-OLJp=6v}kHSr?)h_TcHh_9Y2WA=hD-(@MuRA^owWLKN`?A>wnE3 zJpGvdO<(9Bu@U0H0^ArBaqT)A7`c(`txmt=rl0tglmGog9Y|9K-1zh-o$x#bpW`E> z=tt&GGL>=j?x|-FQ)&A0lg|st>=dc6<6PcFRWMK>J9l5+dpLcdJR+gWk`-9r{!}`( ze%;iIPnav|nZrUxK9Id5egE)UMQ>hw;^*n*jYoKkZu?*zv6+45+j=h4e#&qtQyX@E zu>OT~>g_$mtUuVgH!?H6pBM_IpkPW&L_d@*Z#!>QptFf~D&JLK9UeSbdK$|_z$2@- z<%vvhEz<4iFk3?!*qEPRVd0ZWE(aA`hvYZB-Och&2qzg7Kxdtcn=|LZ3JcTMB<5~Qz#*UKb%p*(2jY7-dj4Qc2WzA~4aK;ehp zTT6iJ?M{w27l1MigyfdP?t~QmHa;MJBm$O^2vJw`jq*@fgpD_>MbkgZ>*+aMMZ8>m z{1nl#=ovqf9V;J6+SOXF=e^Aw?JN2&E24qkk3eq zgmOO-AK$?Hg^yHHZm6YxsnzLMHa6tY(^^^_N(0E6QoSBg1>xed0h<+3=>n*ey@krq zkIM>Y9td_37-dXDWeKs=s{-}Ba-Mq}L4UB~q9 znK*H3has^Bz(s(V#~BH#bI?~&!J9~n93mj+N9%-a{@Pp@-NC2GK4NuS3^CSx$MKe> zcNH^(r8`Qf{zY~*O9PIVx|X^lk)1E^h*b>0t5_&&4scPP80$mti2D>nRe(-;R>^gX z3K>vrB+JF-z%Kb3d7VSzO}qU@!NX`BaR3+RGt**eORDMzsIQnpWJOCkY0Fu=*51t_ z^bX*2{1rQbHgPKDq$dUs<3OD*5h%H#L;9^A$|d>iuUznxJPwt~ERk-6mTfh8{M+rxn!y>&LYR`&e2F za=*1FhkT1fF9Kxd7i(8STD@aFT65xPe&w-J2w+zH`HTT7PLb&2#PT>vi5Cw=KmjBnk*eg zSY}0r(=c!C8+_Ugd{=AhAz%iC?DF`wsb>yfej=DiE%s3i%eU|8c{A?mHKUf#NpTs- z3f5RkScf%obDV|NJ+3u@a&3uR#Y3)jk@bD#U61Oi?0@a~hfL4(e93nx(6&*!Zaf;% zYg@*Q5&}!g@6XOh>5I0E6)Fx3na8J>6SJ>CPMFf-(x@mKTojnpW*5&W55nPFln0?A zq{Q1aGT^3=$$M3EUirrc~>4Go8XQw zLM@{}J&ul*QLTjkfh%8F@C*F!UMY)5%*q*7>Qj0*R2H=%IV>}k-j1?vG()@N^LN+b z@!}KY-F6zHx|+Pc-JCK1+0nD(c>VCX9csz))k9YmO&IfEJ+gc17@(_S_S0!+wk~`8nYcV-#4O;s!HBWnNMhzE#HB>y57yvb6 zIaN}Z^vvXe2QU5Vim_aJ^i4OrrCeQgr@=z)vNoj5 z-=^X(OffUzfc)>wr^hrfXKD9JaxG!a?{jZNARI9>!_zB>#|+kRMx$hq7`{-E0Y1W- zcmF4-cPOn_G|2|$DC9W7sIag5rrk_@Nj>#@W8!0NfYQ8*BnWONaBCqlzmz&?;2@*uC*-5-qa_FM6-UJVHS8ntN9^qp&`UVOPN zFY<;vsr*5{B5{?W<+W9~$<<1pK$8`ds#3J13ayK*1MB3~b5((jA!2Lj8UJz_$M!vi z0s79FUs}#2UZj)nZDesNyB1c2lm&SFy~o~r$ewo#59bx>E&hgUGz_lld*gb|+=1#! z)*$$8a$I@{8d;X*>?>G@7WV!3-@+dMW_dlrq@41*s0FPj9oD6u$kt`JedhIf&4;f~ zS2Xxv>w4u1*LD2VAAYRWEuX2q?wcAZc990^ z%GP=WJL1nU{&OV`QN5h<56S>FFj8$u@>ds-WtmM%!lqK7ULm85ySq|SJP{@rwr=-d zB~>eKdBs0-bs}y zRa?!`sMT(cR^0M^eb#&L4}vlk>}!ay+>ipw$fR_UeZ)-*6rb%^y%5BWvn24BlF1Fv zmCjPS?W~Z78SeCU5Z3H?+?D*Qhr_7pKzH*Ye4t#H$Z5S=^lP&hkz=G=I1B3{J;@2r;`&5$aJTE%X!o`H2DzygE>L|xTADlO*cUT?|RX_q+}TlY<0~%ot)5b{4;Mkavp!& z`Au&#ZyXhz4i|Q=)8e-qcWw%dBk#P+T^HAG^vw}&eCdR4obzJ>{rEenWIhI5cLWy4 zlXVsj6brqK>nalD2xQfPaKMws4(W=0?J|S~unX^P7nGh-V#u|m2C{RHggldEKEw!4Nny(bfi7yX+GtE^Hnm9z@-ZbD<&crhEH=biED^ZUOt`mRoM&Zl(7b?Izt6 zbis&f`z;HKr9mfHBfua4aZS7`&WFfC!VW@{U7B^l_MK=0xfLX@J*$zYl3@Hw4v0NS zgfJtV7Uz!a0^W+k9P<_w4rf+KK`N)@*$(=l6LJ#Njax^D#MIWOD7bBoP|LK>csLJF zZ6)NJM{;jkWW{(%zUm#SL2{@NEgJ*{TERtanC;HG37^hDy|+3$U8RB+D1*bN$2D7v z(?wzPFtii9G`0*!Z2)bt2bi0%NsM1MDY#K8$s)9Gc{&agX9HNTaGTKd+6)#w6jslW z_a%x=h4)N73cx|zD#zd3LMQH2-Zy%A_o>(CYA<{j0vCv z%7RbLXVMK7GqE@MSJtRqg7M&w!5mXGG5J%UBW7h2W4|Z))cvEU$4_nShDfrOe3IsH z#;sOcpEZJppchI>Jaba71M`ydS*LfLdOi6U3ykj3dp-HYfvneCcj(W5xdf;(jvwe2 z<_2KWNb<=a<=y=ka|Pg`tA8@rKSBNU;*QB5dq0y;-k*2xU(5}C`*ZB*a?k$x0;tah zlAEP{hI1v%vwX`C2@M#QH_HITFj=i?QdC$pT%6%IcHR;1)7 zavNn*rk392%tkt*A=H{U(Kk|G(VpC9bi!HP(C~~>*yeM7hf}+_6A()}vIDkO1aytT z#*iVlcujmFUQX^rXU&Egj1rb@$ZMcm2&I6`R*fOBLm+!FL7PJZ;L4P#9GIn_l}TYV33d~7LloNM@d8K^YT0fwMR@RM#c7)DMzB* z3VDp4%3x^>A0`kAE#i+Thye(FmNhUDP>%Ym(+L6Hl;sEBkP}R+a3*VclqKeHSwB@& z(}6m9U|I2GYtM%ivTtWFo^wYY^Sy7OqRA~sFKv93jehR<@pJENEt=@syw_%rDgVy$ z>%(FH+!0k9Gy6Nd;%}JR7rEA52R_{Tx6#+VE^lXzGRU8k4EbeF1hnDZ{3@?hz zh+$u2$774sf$@rY3ZfNT=M-y6ID;mr%H*v3Sko?|IgqAg7NZ4 z_(qG$SrGvDukW@X3uiQC{d~f1O#U2`_WYqelMiljtIB`5xaaufgOB)J-I#pNAH*ob zJcZZqkaH8Cp zeD%Wr!`^#<$$6D|zE!X@uw)DvM_fq8l8w{~&a!NyXbF_8mTbe=i0ba@R!QAmO;xv~ z77QcHk}cx|#sTMmb2iR-2HDJBnBB2=nVd&%ImyH9usgFm?9ATZ|2^k?vAVk?n|XGh zdj+zp>O1K>=RI!@*F5va#Xk(D8*+~0@_$iuB=mXfdk^F3ZmK@x`)3so{0h1{g*euw ztc!W^GV?CCa)6C!4hkC0DxUE2)n23ah`UuN39>phU6(X_{qmEH_a%9@4k|ScpZd#q zy4GhEXRPpq&b=j(N$AqV(%CKHieZBxy25gQS=A0yr`q}*s?V|eiDyQ>eKXh+R@dkr zXOX3KNBbgAM$^RlCdi$eBOdOmhemF_Q6PH4R|xc37sX_eo-2`x%Uc7J)jSl%>~fvX zlVF=5Z4oVVY--X?Hd){U!md90*maw8xYq!?tDH6qQYXp?t9eP?_5b6vGqLM0U5lP|UFkerV05CsaTp;B1-S^#+ z;7hY%^T5)(BaMqZ6Fy|!rs=2#Xf10{E1934rNDXHLaj*mJO#KI+N zIFn805|LHS^l+ozBd42tNB2ku^>8hneorTP;$GgP%|RoGhvdFpq^?+x*j7BXg?rSM z)HQ}H5iZk7<6kC%*@AL1^!69(#IY}wor+UriXlx^-C4Dlm+HajQaOdF#de_&`nF}z z2={7v3-@a4eS5CGTlXpz8?|TKZRC~9UAGex*KK2@aebHW90%jHw%VO{{mzNiODJ2( zsNLF~L**Xpic1}eAX$%92(}I_`PkR6;`GX+-a@Bqen+G@8LZmop zxaB#pPsA+JAoF1K8-UoMCP)!zbgbzRUXU}NBlM{N^4iGBIAc-tHEmB zx#5g+!RmxT$}wRyc5!6K&2LR`}X2fO+Cn1k*YmoNx`SX3(lY+Uy*%({2 zHdtd+=elGGV}mywwr*~StPR0V8LSO86Oog~DoG8#*4voY2H!KXHk>|+?Xh)>L!^)l z7RUvoS|GD#hp{b@&hQZF5n8c5#En}wKbS^Bbr@PSKe+b9*Ecgi%zo)9Tr9fqps@6B z850C18<{KD;mKRxSXC?H-4i8W<~KJ(TCOHyg1r4rD)lxTjF@Qq@N(#aKc6=`=N9&_S%3^3g9* zA%Y@+)n3V-k&4`5k2q;@Y|sP)OGhv3MjIuAg2w{n8jGg5TBq#U@eujfal}aKc?gSn z{g%6+#F3?QTOrtt1kh9Eq%u!-{_cS#RIH`c}mO?zkIRSCNAAo|9ye z&FUN5fbeSD^7u*+a6z_d{LvwrbTAx)HC7dbme93CE)R)## zw3<7Hwh!3A=Mdl{j=-m~mpv-78N@2UKma&}^?2CIYc<%PYs*dXmnQ1#DF3C{0}{wN zMIZ$^fVddWtwxL>&uI{WJcDxOwWlzZIr^jw)s;=1y+e)a27vsb44p_=mgTqyS@2b% zl*~gJ_Y%57%kLrM)eBO&^NbqxsoeK2&@royw_Jd!3?w-R7-m++4bd-L08nEW`F_MMWSN+3Uu9e&rTk7t~>{+kk?1no)k*`hto z`0QUb?b%Xn&XPALKvvfH9a5l(gAY__9V_TIt4!=Tgg0C$Yv;)jHOhLD-BveSZd0tlS%u_i7Hi*6h2K#_&W@3E!jsAeZF&(Q6r<1Etmnm;Het5@ z(Y-&qnHB%&<{v$SN4sP9lLh31KGW;`r)72iZsnowdtTW;e^hSTZ>cmL!C37p!RHl_ z`2Z`M7p&ysV^d|+v8!FZV^376wm2tNQ^eNkTy(5FANz)h9g#OBCQ^T3a@fH38zF{> z`Ao#hhFauV!}Y@PbdhfJvX<&h-SM{KG}kV{>gBA|csMINCM$Vn$Ah%9sNX==%-MgqtqxDalaF24oCSmB+=dS09Hc?`K*4jp? z#NN8|;UU~slBa*0Q*tqu+mGeU?+3}KEvv#KHHMiYtq z-`;&4C64hIPi1^%{#{!ycRL&sMWjPSmUxH^NX*OpHztr*>8(p1*mKpRd%pLOPn$Zm z+g$ogFOUrmQAv&%uNim5ae&Wsc<@TY>&R}`GbA6TA>LIB;;EUS_2$~Bg zAdaW$5e#+M*6h50s!m{CfO(Uj@kYy4>OkVX!bQ9J3_%Ky^KvxKf)b}yPE*3~6L2>^ z`6WU#9&Zn+KpbwOq{;`r=GwtIj=~lCn1Vy|&O4nnEBAmth4fJvhIIGcXw3LzC1Qfa z40s(C`+$@7fbh$brLDJGiSC@aW46T>1I~0K=nENnN2Vr5vy^)oiLOk0ymIE-jR@?tAe%a#wEt(akKvXMv>TE9CJ8$zj$6dv;;^&tovO#v1`#P%V)5SFSYO-2b zR08nm;5+S{ush0)&LO1<2~}&|)|7prTfaY(Lnk?0_tm;ALy5dLG;C`4nXbIBe%o~} zu96qJ?cmG3Bez}1cKHI1p$?7q=~Q<$uKv!RZNB>A0H_W-yf&#OqV&S>}do}i5~ zSv~da>r{xo$^vw8i0<8tN=b8)!k_r(Xxh#43KfqO0p@y=uPSMmi$PfG*2j0(Tg+@I zt(O8+-vG$-W0U3v2PIf8|57so%n48_gv44btvI0tn9mi*+K&&g1sa4dlR_7Psa$Z& z_<)7+Ne%0vmAs(t?Hz>fI`5WUTVL39-UXIn`vvg$?r%RwlE5aAI?MW@fEA>^b@vr- zpSOitYjCD-Z+pm4_2XCUd7MPp%?hGSO91J{;aXSy#cMh+kze4~=N$fHluqD$ljNm- z8&t8(Z5vuywk>SHyisA)JRx7XK()mT(SmSg^>#)y^TvO|0WDNhXDH(byIA*dLe0dX zK0NAGH5i{`w2#kRS6!2J&`eld%ko^iIs6(dn86JG=$``iOA$&}ijez&A@|#+z|#`+9{LdTwMzBCo3G{b$l)=9jZl!pqe&HnHU3Z zMHd>Dg~(Tbz^6MTtD?9hrlj6pE`YN5wiUmWMAX5#-fwYRX-=ckv-4@?z|e`xat8`$ zX)cMc6Z!Cq7{LCg3>l+D)A-$=LpT9BBc^Vkn!Sz1jj-1oj3htDfhPt^M$^Q@R4JTZ zMvH7^00^>Slpag45oyiD=PRd1)jEOdM@{GOmIKyy6Okg|* zD&!VeS8CwnGKE+QzqypY3i+2BaZ31M0x9$KCO&^JVi@BLW!1@Z+8!(cPnk(@9S5Hc z5evpf4>!H$ae86{T(durA)I?|8N%V%E+#7H0w@$#4`Hhel`hX5S76Y4^MUM%3ec#}gH&@y|#g*s@VAgn7@udg(j0gogy zCS+*!rTXk=UEu><#zj0^oFL;m*b}_1()IbmU`I&hG9RJ1X=T3BWZNGmJ(&CGB?_)u z$aU&h^qD1bNxozuzA(vw7&I+yp~6m`co7~QmCRv?yy{jSmKg*gt13+|a0g?|s<@5q;L1G_&(I(Qu`N1Er>!$1ekS4lm{$%ak>aa~oY z@N8iVsC}4?17KYoE@}vl+Z%`*KKm{xW?U@aEY+@f7;{=cq{JLyiaQxDZTQj6dzw6zP~$?M1zc&4lHJ6b|qlO6Z## zczGB&Pf8$2DN53)DwZT#Y17ocm_^4WDVa4$&`3J~gs81n$Vyj>WF<4HS>&w>t|*JM z$d5r?`V!P75%$?cU)hY%cmhNE8?3%Q1&%(Bnk4vZ&G^WuWVV=DJcm~ z{MeU2_Mvx9kDdolQIsOesw3wvvvF*5cnl<#*>tcJlx!e4UC-Vqv=}{Z1~{Vn z41Tvh8v#JFRO~1);)=k=FmiB;BxNcrv^%Ee~&o z%cDXG;cgsnre@rzZa3-qiCyR2x$C?~S^$^NJ8(%9MZisIG+3FEfR&gZPgVp?f=g4u zIYM~KaiiBr)fPHt9GHO#j%pl$iX4MoJk+2jQRX1U7Z=ZpA*Z)SvLY`Ddkh6z-`j zzx?7Q**7Qsh_s_Cs|UET_MA|MGTv5Vtw*P4Rx4@GS>-5NaMLn(QnC_U6Yt8n5%nuAAP{62n}-%5#@jJvw345~q_WbL zq0B{7p!qBpVERodn;a$-ArNR{tyYRj%Pv?Zi>JI_n9ya?G?&s1dd$TMoMqzW(xXQ( zx+$#KRbse%*LnA<<{ee*M%NEyhT652oPxLQqO#q2lsZnCR|1dMOgYdOJ5IiR*`se= zLb5G<+DE}HnbRokA%*w#+eqkZAI!K-yqpB#K)-g8tTJX&4AuO=AEZsIWH5W zjq2eh3_VVD@Ni`6iMczM9OyBVOx5>!B@cS#!dEVSYKZcPydAv0+rhq^z+zV3Tt=+n9n-7)&|u1AUB8 zW<=`=ut}B%6Jk9qYUOF7B}~{s5`6R;YH)Q;{%_ z(-|sby6-X1F#xzuH(@u^3Y1axiLvR(YDwbroK*OQnh$b-9_98^Aq?G0s3n%VyaVZa zV#-VtD*(|fOEem-vRJa@itQE3Jf)xauRedAixq09WVCOg;F9M)-2OQus6mg&#jwin zrSC&=|JD5zQr_5aGy!@v%dMlh|L*JWdF`g#N51>qtIymvD}v(U{>U@CzkjJ9)jjg< z4_rq0#m-y$NK6T--g*D}^5!tqfvfvFDzaND1N7WVcV%twEC-v#gXY!xj0i%0A%Zp1 zMb$z#J<~%KI^tRXP(>Lv?8V}K)~f+x9E|!M;#O#p!PI4?JSzXaF!VfR&y^R4L~9oQ zbkFv;Zhk5}J(LrNheTqg{BxyKB{XNnCtEx?yuc=J!s#cR7C{37dE4Qd>g@TqVhH7jBte)i|#3XL~C4+?g_ar5@jK01$E z1i{buDgd%26-ZwAo=SAF%pprT8$SUWX+P13}>G zU0b~r5Qfd3hb#XYo*wAWLxiYv9iv9Kta$KpPnz6X?JS~c+wy1OoE0HF9FD8)`TQ$e zq8Cq`h8zaK`0K+OJ~L0Hu=RQadDVbPd@k#C^ueIy%+WmtqyKlOE_YJqde!)G4 zqq!p9jSWm58r^d%jqplu+USTCQP~r0Y9t8)tpxuwWl=171MB1kBzL2xxK;FCZT^g8 zOGVQxWrAgGgOPZ)8ztcp04&$P!|oU+V)H#LM@6({QMGqCB2^njZhxd)ytzc?8u6C{ zLu9|Tb`$2gbz$Oza}CaZQd*h?)!InQj56Q>|MFQ|%{B`tHdFF3wDk7Ul;0j4S!8(0 z*T$L(n^fM|wb^R!g4G6Jyk?-k_R@8o#EW=my|PN)`pudI*B={B*5c0+y!4RGtQ@W1 zgBdU1WabRr-Yts6P?^<-xwR^%;tE+kX(5F#N7m5KpBB9B`mCIa-aKIq72`lLyM9-` zt~755i*x@V+5M>PZdG!uC|g^Fs=Bsw6Gxu=9^pt1o?d_9JFnkzv8zTFnusTu_}X<3 zM2$vndZKBgsdLx~k~8+dc%2gWHwIMT9tRVITQiGfCO&dP^uq)LB7?5YL-ox>vH@ic z%aycNuBQ{)&3Z1kAZ)e>5dws$w|kGnQ{Sa!wh}vLsYf6o-mQkk=hNwKL@#crnyGMN zy1Q&758T9SqOXrL?H~X4_qXl2@xJsJvCZ$I+upwFLfoSA;4{UnCg~}ZlB-nfsC0tw zOZeb*uC4X4Mr?D3&(WQ+WKURwMtk9-UcLp?{}e^f9#RS-^K5cP6|&-{rcx<_q0=gZ z;Y3R>-9%Was@97fQl_ocHt)VUOCj@l4YCa)AOOk-c&M)YE`97%%=EV{kwfG6g5b zlfR~|JU-YMcr5eOW7qGw=;qQ}KiszGX3jgRm2hb06zv4v_1Cc-N*ecQoBg6a^3$4+ zfBDAT&3-J=gG-NWgToo8DiOuyD7>t#xL-V~ceG`$SSpBJ93&wt4iXh%#NYb+)4q$Hys+m)p%_7S3<@r)c~a;&|7 z7q>p{M3S8k88&I-GywM)atC;$$r2+#WH=uJ zG3r2HUKWzj2kjm}n_DVg4EQ(J`aohq2RM`l>l}eXpSe1usD~-Z4e3gnww03D%SpVH z1b3={jC)S;^c5cPbAu_HM92|A9SC_^xK!3&%jY_XUPII38-y`-g*%`8`WUi5WTq~K zi<7=#@u+4v>OqxIWHE=y}EHt;R$UWkiQK;e=8= zOV5@+ptceXgOs1dBqriVbK$4Bk@7l&_04`Hy!6lJbWoz@$3#eq!DuX9Ks-FWBB)d@ zrW`(lwiO3VhEW9a3knP|=h0`K6cfb~=ch)!c6n0chWnlovr!dw(GqY$J1ugfcE2W1 zhW!JaDmYPHeP*y|(K+KD9^TH!p`WLhk*I@Ze_TkwpDXU4zUWB0{)F?Ev2u<=Z|s(4ZLkZ`O`hnO07!FXWom7GIdFrW7-Oh~FHM$LtD zSa9Too`Qg8^2QMX5PXGCr#g(r(~Y;iIV^xgdcCF^beq={EaHKvuWapN2HIW=C4v;% zaa@>_Mn4__h-3fSfF`fVuuX`VRTUl$)mt4bL>&%ke*RhooeuI`sU5Ss)$VvTVCsNv zKouZ8AbYUUlTRj3Gg^R)CrccX*$m|0Eddjz#Ybv!9}ijW zOqFiA29kcgDt|S_MOu+Q6jq1Z=vzs5Bu}A~L%uLmYAjU3ekf(u&AQveRJhSH7Ct50 z4p~-;b2HBN^0<2I;w{Q@Vx8l|55ZD-t-$A36BR{>C#*np>R6>g+)#WlaA`o$6)F~Bz(VgjtCdD^F(QG`eQ4K)j=j6u1JpP);2C9Rc2wzA>)8p4*LS?AVOdqB@ZQKDJi~JUQZO;&RaHAsl7*lT3hjJc~43w9>nhY zmai^VK;3v8yyh&1ihK`^(wvU9L{{}wdx&JoT#lt^#L}7lb1U~CA6x?9HWhyYw8-ir zhCG~0xgqJqGzZ~4mG6H@MJ#G%BUjLAR04;7RD%5x3JByNgFbqoAUTR_b~^HTk{)N-TLYaTjj5B z1hsj-{B(+?^fgypWwDZ(H!%Iw!-^uiOK)u@)caAT+K&VXYj^ARJ?CGtXWPSsj45Kw zWl?IX7F_)m13yq0+obr(%(QK#GlwmDOzxEM!pIWC&YvHtz?BrlYsJ9NDiAu2baCMV z*{@k;^jZ`wq+DS-_%h%twNW}EQXEO2sal@rg2QBsH(8{3)`|?E8vjr9vJ(*$ zAJkD+e0m=Ui~lPt5qeI`N(7WaVW{J=o>a4yIG2%OwxX5jBCjH`4h~_8K&rAXDo{A+ zGBLaqaI)usZ>h1iM$|MWdwc;bB`qNpnE4gw>k1qwFp12nHIl1B*?dp)wNZ1aU*%Y( zf9e&59(9$5MZEmpwiOpB(cN_~?7rkyFy9-fSDyPGRCZ*`v%7a(<+I#Aa2^5IuWfsL z;JOnr zPZs86Vj>6#MM=G%9CzgWURkm5YrKg-Fa4?s$RTnLWu1+M)7J<`B71I~y4D~dMKdvF zq^TrP;YuICy;)u=Fd|k9kWp0C6yN;2zdpnXvsT{NCDSF$iIxcd^@?JeaE(gpSH9RT zDq#`csU@*_5y0S!gHYHvq>%ew+_m_kpe9_NXFIav0_@HsTW&*BLa-UJ z;CR68I(LUEgAnn$<=l{2*IbjfoSV_3fc{3#zhva;J9eG-J#&d7@@dR!sjK(gd@j{b z#p=BI(B&S>Q9$`RewMu_=bG3vk0RDN2NU45UO#ogv$|Jr0AvxJO3HxFylMY6yq% z*_Hm58H$FT!KD%$Ac7m#bnLFJkC{Km)m)D-nXVfb$LFkW=JfZuJ?DM@?JF)09duQE z;=_j@N)!)1e6>Ne{O#7GtLajn+6d^QH5_h70(>@m>#}3*h$4RZ2Xgm-0DI`dmJCpl zp7xdVMOxEP`BXIe5j(E}U9JyH4RBNkL=upLs3dym=tv|NVh41f00=;iMxMR`9nZ-2 z#~qXiD5k}|JZTjtZXiORSD(CM_XQWee)kXKTyUn)}?iPQ`+|zw=1{E+~v&=5iAsu2oBL-CP=PIZ*!7zj|Ls$bk$Zo zPXr-_+5O&m=~~On7pPC%LQs8{I=ph(6u99)SQuT=k;X$ON_*R_e&sT`sZz5;(>NjB z?Jz#4y1p?D=zO8N}X7Ots!(~6#zXotKGdqPqt%5UI0 zL7aM}UaykAxJUb_47yh2;f*UEi1Pou`t+qE58sG*{l<0Yk9=o`d?$ir50^P|{xzOV zoMZ~B*B-rf6Vxa6^@|L&`v9eMb^On;XZ2n69fFZ_}@@|&J9l_E$1lI!uHTaQyt z{0>M4${JK{kVHuL(or1nL%;9?-uJybpXF-w`h%1c0(;QKvjvv$75DsZ_|fuEw}c2P zSHttWT+4|BLlLU+9#T7@!=*3P=?P0Keiu&ki`@UtUJ7-2?NXFyNOnCyo4u}dI!`#Q z9tNn4msuOm!=8baj@IryMj#st$aUxFLv4N5fD7*gvc>R9T&FIW4GRH8 zTCw0yp0WZ42RpSD7uxZx2ROhemaJ$%NdV-sJvw-tA+B1JR5T22@#=T0+qLz6bFzOh zaVn@!age-k*H%!Dcg~Y4e3T?dj@Z8XWR&U%dh7Ea6~F2xd3)<^a@~l}y|m|<``)_w z!ads&(!XPwhn3MdmE9Ui~!=> z#h0%|zjx^?7kD^E&{kGq#O7R9BtexL(D+S? zSuVA&uMP}=wWLo!HDX(Yb1vRK@|}C}GjJ~QU@Q2SSD!my!ILh}RzO~_Kk>-xFI=Dj zPOw*tdb$WpdRn(T@;zU>66yO=^<3iJ>KVY&dTd6%?q{_iEiUG|3dRajV!^!kS8U;+ zD>kIIj;o1Y2o#2(Ll~0WrWoxUhxnm_CAl290mQjjFi}5oM&L1$&DRjEohw2TTd{#5 z?As8Qxoc5(dDkr<6 z3OOC^xX`0mFae(WQz&(eUL~2u0EdE}elSQ~-H)^R!9K1d^SGIv&pz>Taf$;DnD{9q zWA)qQG0wRc=Uw>0FC4cJhw95Da;DQ5_9m5CO&Jj&T^8;G@*>;^jN_PmZv$YLQjq0v zU6c|s>Gd!C^Y&%;y?y0faWFP{KHj<$pYU5A#h*}eu&c$E&@R6D@S|^S+ws3Ds%oMG6qGQo2t%+`cXqd)7!&X`ISLEf4{T!kIZV10)Ne<-gyYzN;i zMNV*BjH95)mD#6o8xgYD9ekXMN4ysKE;Z@k29wNlWDsGpt#tiiMEbqw(k+Opv8$2d z2~7_l);KkmU+@x(#1IyrY3LaNT}LED+$)V;{k)^-xyG@3P!!!j?mbVz-4$YQvr|O9 zV1(P)2lLIjC)Z2@gKAQ``=`Tg$ldovG)zZEJ;6_auA3S2jr5UU)tp8PLM?9 z2z!y-x+3=&Hyf(KX1*>!9!j3Hp!`^NEbqaO-7NbuPIRvN_m2n}LYt3dz%z0MUi<@t zlj6)%m|{>ALL|Fn9DzSvR_k9ZzbSlPs~aklxRzYIGUI5j zazRM!?$nGpA*(~2C=Zg#^NF^9*VfCdPAGQm%)-XY5&G4{a$S(jo*R(lGZjP=R0|Wh zT&J`1m|ZLw!V@~*Gl~6iJ`Vw~{&vd^PQW)-8M?l?3|p-NI(?X=236{5ozEufPyshT z{Zfq3R$MVBiXu(C0_Mv8+R#+Mq1J)22b;Hpg$WirXReB>M_QWz?~RGn;Y# z|4o_1pb3zbUCR`2GL$JEtMtkxQt2vo!(uz&u3@s}C?|EY@z`j3p@ODPPFuqm%%vjD z-r~06+OSWPxPcZjMD~!#l+}mmmlXQt+TT{1I|P@Fq8D{u9>5aANZIG>d-UT`z`bk0 zPk4DEN|Hqgac^xS-TKM|;GhCS8A6pzfEiM2@rt5%mNToC*U(z%rBw2qfESOm3y9)hJ2*Hn zYuYrXV2y3HdQV%acy7`AmiZOABwz6TSj0R69CK05wkGw4={a1`cYdalWYr6|y#9is zBVK>-{#T#6@U;tf?7rSoMqTFDuzTxWJPk@$msPCQWjwa+)n^`ex)Kb0ZW*uNbK~x9 zm$`Xfzw3Sy5K13=$JQo0!TRvd*7`v2rQ+R1u=<9IS^HvVq^kFe#>(d`0K@{o6$;%O ziZ8ok;UHw@lVmc^fhxN~MH!nLJMVVTvmQeXhl3qrpw=ZD8F~+pFv9OfjH3B{GM(pCRTjSLEkkP8Q8!~bt?XwJo;cNp)l_rE=nt|X1?2LV8nGC>2BCLuC zl3fgJROnk3UpgTNY7@{B(HNnf>hM9)DnZAC!ysJBm-+G7pr{!uB|_ipu6?K(ysHqW2tkVbg6R~eXQIT zKM5}u(gqUSes#*<(Hn)wd@p4g-@fCmC$D(xj{8b)fBPx9&&XO9@eHK9$h{PTwbI^F zNcpXMwj)GKQhxJ?=e_yJvlynYK^8~t_kAm5_QOZtd}JG6LQI#2w)AKz6LhI~Tk$Ia z|H+^%mmGg=W_xx?%m?ekP$Tm43GZzj8uLyffMc3Un!dnCj3;rMe>L6P&-P?R=2NMFmGz)Q3P*f#&O~pGr2b& zvWU1RrF(>*dT7ke4%yls_vTM{E-de`WzW&|Nm=9h1q-@zdPGdwE0>~@@IFTh)RxDQ zJSXAKSW1b{k4nQSMe3|3FIg%+UtH!83|>^j(mQM>-$CU0Ob!qB4_$w5e=x5Pcgrjz z4JbbOK8bO{LrM0Wa({2RuTk!WWnLf*Ss_HxyxMBLqKSe@S?S3H8R%NAI4$KA ztdv7|!R>t1JR(AmXfaTg;sH+TLYd@~2?VTOY-*PfXY0bBv`}$oE=NVYC)4g$l`J*&s?1X+LSc^UKuYU_tlc_KxR4Qy?$ zLNtRUWEzy+R8y5qsZ`qXf?26gNHFNJT{atkEk*%R@yNl)v7bpu z@~zeKeoJDr9p1$i`zSK$4%Z`a%M?S5#3$MEewqd_@X)P=0Y}|e%uu3Zm&5( z9E~v6C=$zOgD@bX@~O(|`mA|RYj+N~P5U!_{Y4eP!;Xz=Q@PiyU|g-6~Q(b`A0 zsmznj7CqGR@4d)p1ZlqGO_z=SA*?(a>l~}}^?Su+SVrVI>`(or2j%zS%vy%@4G7|g zE?t4km|Y_J=$0;3+FtKGMmUeQ(!8~u_!m#@t(JzA84`>dKb-=!4K-f69Ti2$CzS~E zOF&WFMa6THx{5RywZQ``aJ1dP-P|ty1y09#*|r{a|h3wJ(CF)ODNQGu)Wfr?dvz;G+ATxL8nPBsO%#cS)CmX_zI**Ts=j>;j3 z)?XCcv+*c0*oJE0+}WM?>%!x{=x;i*wHX;<>=efmiPcLW#wMbR62#CkgX!*)7L znGcomChhW2=Mbd2vlr(aExmtIuo?s%2Sx*@^nm2r5u(dTjA}*{`MlRFJs3hTi1SME zfV|NrOJ{?+rTIH;AbKorFV+a%9J*RfGTQq+S4>3f_}KqORVU_6S$aD&x?_`<2?qHz zB;Mq6xtEtN+xrS_OK*aSHiM^ENSjjbwY;67wIORpf)?hmS2egJIxH}Si1FZdxv1IXj|S~oMNaRT185SM7 zZ;D#R(0!&fTk~UUl`zR<<5Cj^kA#%Dr}&4(H=lgy?e9Ms2PL`Jj{_3PJZKIFazOBH zQKN{(ienh{7T%@GDN5wJ;C&R5yp-(>4b8xF^Oij~-Yz}N&9}XE_oJhLXR{-eX2#EW z&LkTuq*6-QivF>`6f(BX5cI z=H6wtpYF;!5CRLqg#fM1<}%6{Wle3?1l6!h*~DG;N&B>jrdVdx#+{F>YdVS;O;hv? zHwSIAW&KHUx%ea^60&I2(mjSut6jlUqtd@tDW#ob8JAu=E9Z^`lC>}v7ws@QMs=4J z6?l5ADJA(RG9;jYSHAAZF? z9=PdYH(PH~vxV`am#p;)$&+dUIr-j* zq9ZO19T`5`7X#xA<9XXo8SV9WKwaXns~#NEc98u`)VWhkfz1GSbBVvJ!u z6qUD)v~Z;3NSG#+GN9G)xf6Z0dZ&TF z!mt@<(wCm~Nh-$-m^&qTW^Ti!?$?If{c!QX+9}S<2g)VG&0aK%oeJ;?w@q!)@% zyx?F$)X?E^fW%Xp7*-BH!&KWzTrHg^@z8iw3<;n2<9{?uzH+oQ{dFESJJp<1Avk?@ ztQRQGS|*iZPSDN)l)vv}V*ouH^(Cfo$*3sU(Px~v@yn80Iw9fWt?Jj>1m*gE(mlsQ zBw;6GgPPwrq&2tmp*~|)04pBv#?ONu_TZ={Rufr<^2ltlx78{ogVW`v5ozRT3Z%^$ z4GjHKP34Oopc1{NGHsm_l08O@sIvYhOfaLNOgmx)VJf>%D$9ZAt|EAiaUfY_jm0ZY z6iA7cO0ze(9m;bnR8@|kRa!RSH<^`103H>bp5Qcy?j(F;?>`k)o1PMUpdB=K3K5RYq3xXu*DnDTv^N$P4v%q`mCEL39sfwY95=c3=Pp@HOavyK}x0;kXNRu8n$e; zAtl7S?FvG?4=WJPSjkXPAi(&=b_Q6{spK9oY3}`b2K9!P&B^IkXJ8B#sQ|W^) zePL?LtZk5-ks;j1{4P=#0Yal@{cxuXme&AT*~INva7^@Rh`#k} zg~EUO$bwv}-Gq?3Sfhfgc48+&uTaG*ACqI!TLGBxB2p4{@oATZk>M1Whb6` z_&rm&p$Yv|rr7~uJ^4LChN$Wl5q5(8+M z!NNxVt7(p++X>@$T0m2+Y)xS)-TDe0{I||pTIzfnO)I?0pmkbRSZl9!E}g$P{>;rg z>)jU=zRsG;?8Pc}=vIcCIsR}D7r_?EHs{?svk8rV2W z``zH}v~i2=gll}(26ovA=oQL9OQq05^WtQ}Bjiv=mJ}x{_v7@7K~e@ZB2CFKghmRY zK-zWJvXA0e5~({XgBvTzh`ObccnxjONVkDku+h!3Cdu4k^$Nwh(Z#tO?1!bh^r%=f zq9&nhHnxjIVTEDdR0GyoA>T@GlvwhR^k|sI8;PO(%Z8!ir@3*KxpCrc$!FFpPJshzbLL+i#7rEy9FlJDM5m12Gry%O48^lSNG(`kyH*?{~zt$QQ(|Mk;OUBZ7C&p-L-nKNfhKNjmMXBd`c z-jpm$#N*FC@_zvaKA3x^KOO_W|BwE7%g3L6_`LmAFTQZ&KmFwL!6W|Y+JP&7|HenJ z{? z|9zp!%geur38~k<-ZI9yfZk}zY-97yF zr}V4v+XS9{k>9T1-?^c@6Udm=RIKEz%)ycBDK>Gc*!R?8oZGP0xF~D)H4Uk>xrvbX@ zt#yqTypT?F^PN`No{DO9+n)BZ%T#INvSF4;`? z3c5`F5Yl$;Kp+1r^BTjWfWxtLn=)3n>=J*E4o8gDAzso4zzegIhn>Fb;&60P8>1|3 z3f+%srF(XXx6`q$m5%XJe^*P@B^7hh$+CJ3RyP~mCWmT%@PDVJw$eSj)UID=sjYO3 zm-;+irk`m?yb_jgIry?+`zRK;GRj{51)Yu>y;F8s6T#GnkKR09OgjyWJ76L3YYK5t zPOafB6uAGXt|sKZGzqb05*9ucZ#Bk@Vxt6?;MuaVraTBRD&;QaNBsynRFT(V|+a+I>Fm#rqSF+d;o{RetC*?Lw`D=J}Kak(AGBvdD zvD;Xyub&#UGSONFQ!hDI)6(K@=3?^baQIBHBKZkeg&fuT5Gt!m85zyjm{O`*h6C3V zVOx$a=n|v-5kH<^XSBTaf(C!g4i=J|!UEJ^yU{~tH+S5|j#pVxcxfUF@CIjn*u_aH zRU^AvSEgq;6SxK9$7-upaR|58ja8%! z&cp2inKOqyH%kR1e25%yxp&AO`d|%L!2scH#i=VS%cg+PIMQ*Nmloss-Zs#fL8-WU zb%0xuUP)W=x3pA5|Ne%;5Z$2BWeY&7=_bj^mOTnS1h5KumQ1%61v zSmbKM_q7}Fa|^J|NMIp1Tv#5!*b5FR&BN|QZ}OwsFxt}4L~qbm+^Dzq2W(K$`~Xv+ zG|KUL?}ir7u9QE!)FetRK~BGV`&ulI4z|M4S-a@q21k8^%)O3oj)ZWB>yyy8cgsI3 zbb5AyYT~PK1~w3PkukJIN*DowTenI1f>nA3(M?YZ@Ddu0Vd6xNX9$ z{|m(1jYvfLr1z5clWee(79Oqz1b4=?-n4`vy&r84a4`ZjsNh?kACXDKm6o-Ui`~X{>wGL_Nj+nI_|*VJoeaw-uKskbMTpW zz4y>#e`odKy=VXL!@v2l-#X%(N6nf%^wTd-{{F>(JLUdo|I5_R-Fwiq;~)Ll^pD)| ze!PU;6XeHy*Y28lk>j6{@4ZY`LoLxUS3_c=*HhabJ40>zq0t>?fIQ0Kfe3N%ifs%T>Dj54z2oM zjek6=)b|HpUV6gtIgh^CRbKIXf8Q~@^XEO|=OO;S!sm-VP$>NVL50GbjDI(L?H{A!SqLVKhMr* z%=`KK8e@KpHtmdY3*#R1p+ez2{vJcyE&MjXc$d@n4c4_AaJ`zbp8as4a2wxOG3R$b zUMQ?)-KQa>ZQBnu34cF8pZjV1ADQb_=9tQRETPZoeEw_J_F?*bn6=J4q)>R2G5&-; zZM^4yX6!rZdo9mC$Qq?)`8dxvv%Uq4wVZ!PnCBMu^DxGHoOw^8%~_0h8Q&lNXrVBP z_o=eBKc~$#jPdLA`vqhCFWT(K+<(ZseVX_DP4=yq_J6^=uke2FXRJQv{a1eb5o1VI zb1Llz_&dnD4x@b=&!_SHP4;GxbI_khN~RO(LXwx7L^@t6s^{4!Y2&4UkmMf6&PH@IMJyJv5Z8aykO4)G1Z|+bFE$ztVnJt&OzNUH0?@I!{lU4dz(`H$-eI zSizv3s6Cttxk{&F=Gj=Cj435V-4->#m`;t;Pje?0Yb~9&qOQVSuA|3E#mjJ@mFC`@C=Kcf*lSZWWN^Ete_US4V)r*QbL4rS~n zlBXB)x7Y96ZP_kMcvxeHoOVD>>xI?4MVCL0M&WnqG9fvO&ZLiq8&ZprGVi$Yn@J^A zqkD2u|4P>*#_Afce1{pJ}?vi#%YMNPeJ7w>?tzbY0vYQHDV~Df% zeh8ggTJ5Lp>*lY97Ptw_Z({I8`!e`kyBj*)02GbFqYSoWUj}o4D!|u$jR*W420VLT z29(jK*|D_SHR%NCgbq3jFEhaz`!c}_CXj`%&K|DiUl|OOnwdu7ErvF*OkPag@kIbm z!_<$lzVr+~lv5E;1P3nEs6mK(DCtUCK~X=G$V`wZ`0C1wogT zN&*CdNIQEa1ST`)opd;1J|Cqk)fPF-mS2qG9^!LEEV}7{+kAiB6(Ijlyp;)68T?r)gYD(|(%jwY5X)EUfP!pi%fM zx^-zB-HJL*McvYKHd1hqJR!k}JLGkKSXpI-HuGbA45E8P*zPRFkS#j4gtpW4Goh(~ zN)O#tb8kFYxPd0eoyLQ~3^Lc!1Cn&FE?MPDYl=T@Pe{(NXAb z>7J?iCt(ea2?$nJdc9bk>*!9elGuI+d11-V(s6%vj4$3oy6OP_JdBxq6xJJT7|WPk zHVsFt+{!P5M>!!qX0SlF({#|@LHh3o3)Owrpy8PRn9iki=kyUTW@&Y}p2_J(X>_6M zt5?@7Kc{h` zNnNyMKEs|6hLuZ1j}146HxdR)vPT*`O_=)V9KA-|S-YU+>n>y=`8>LulAljw2cBWP zS+8Pa8@e~HHdiH^d0^g-KT`^x^`)HX^w%l7_@-D zeoluk>`ey;*y&k61`~f=7$m)eX{48ab6u*rbdBaucg;7JeW=qQM+m2zZ#v`Q!r2V6 zFrHtOD{COy9WZ>)@4E4n@)M)cL0M+u-E6xvpE5j`tZ@47zi%+yEJshp*NG^}w=2v32JEd9i6y(P!UI}OzI&3MI)2%4JdS;HT$lEJ z0V@)UalC7z@RR1N$qzRSgVL3eGrJ0(MNVFsUybN%Co>JvB`_w!gW9t!GdT}R>4>cp zBgLbh%&;fqht&%7>n5BC^a4HX+*+fbLg9Kn{q3jo7n*fWQc~?mKOBj;MzCx%Z%Tz`iUNDtY9K`5qH+9dw$k+(@@3kTPDkz(4#8-A@^B z)+DDAKu(?q&hOwDR3k^_hj2_(9)qu@`GnD$3tJEI#$AQSX|^!mEWZH_FMW&-zJJM3 z$Fw%oVvc8l=M;tuGf~iVH}4qVHhI(tvIMjhZm2`}Vl{M>dZIPQ?HlIY68BgQZ1fhc zVyY(RknWnjnU38I-UvTsh$bMN9YXxC*5;HFW$udD`Y_0^p^I9Pf8`_>6eQc$Z|bL; z54ws~$a_`m+516HW6&n|McY|TNfF|;;xpYyx5?>u&Z{~CkY7UMS90_SF8K?nk6&cZfcVeFh8rqtTq) zT2rJW8fV^0``Mo%VeW0(Ib$*1Vt<50y9(RsJ~us=KvV=yn-VC2E|WM$VSb5`>kD&v zz6pnnib;@v$1sPdhmmkn_ma~BJ~@TrfoOzRNVidEw7^E@kKECIEmCM&J1coXT1vN$ zG-jB~pl7^0gJws$lA+n&y<2Y#-~UASQ}(udfD2KlwT0u+VSi!YhLAGKQH6^c{m^aNFl^Ppr+8K$vdqv@GI)#ns$R*Ols;l*gUsUM!C8 zPL&;y`_~%8s(|2iA+=jwz>uqKS zPC@`@*a~EXIG!fQtU~ya!XShf2sO_^5u*1OwbYV3GjH8N$K%Aohr6qJ1PkI4!aYsu zRTeXh25c0rqwDl+SMA^ED!}2w{aAS>jMeE(nB0YhpVMq+wwb`d`HE?avv^jT(j~IX zjor;ru&>kb5H(!SUnyp#{z_^XE~0^X)Q#341|?y&?6%%R_sQw;lWblZ^8S>@wvmk- zV7Rkh%Q5x>lmZxMzv(OdPdY6U;0Y^d0aYNGK2p7%*!n;KwCJ9QHm<<7HuFqt;xrdh z1_g|ero>Kfrz^`}x=kLf+ftS*J*8$6j9*RTCE3R5O>Z~rj;@9}2z&)fQZg&#VvxU1 za&fQy+YESic0j#%8mH)6R-%-MC}qK1yQF2N%|er2@8?YLv9nmoARJC0coQ*=7LRkD z6l=>3oLl7fM@=5G%3116!aB;h?^zAKu1Yrd8#9T%n?~Mb5)R?fL%l+xU2(*y z6KA2UJmo=>11j~t!snOQal)B9OnUfvE`CH+Y3ZbRI*Vt(PlFDcp8(1@+Mn?#?qlf> z@tZ2$H1L-Dk_G4A{5>%G46!P-*ac_kqnu`YOF5_3GVi_(fg5P-? z{2P;D+>>~(Np3Ybb|}Ih=PSifY$SZcjkEVN5PPWj+lF#SBqmVo#wasiju5m@m&oOpCNA=bqmk^zGAiBzgAPGd^( z6OpfDyp zM~+bb6$qOpWT`lLehhJL>HfA*AjX^={UPJRIFCw0T``2}RwNSykl48&*Hpg`0GYcI zebr55$RTV;_s(3Ud<;-%*jC(szK_?@J}RC{u~eMu47Z?AlhGCy&p*sZcWnh0w#!r& znM2rCz0MD{&d#BFo$IraWs~)SaG=C70YB88p%>dQOY;>!xxl({fvufA!9yy(RC*HL z&h_RF>FzKDC4l0F7J1~i%{|sW#qeP;JrUO{p%eZpwwg5V7Se?OVs{ll*XiRlwj!Aj zlH4&`!?3F{#^Y(LYLt^HleVcmG+0x`h)(d7^FXtEiKCq`7O|iMAd01{ok&_Bm|Kn0bX{*+!BOti z;z5a#qIw#xN?q~Ga!i{P7Kz6j_-vBjQaL|5+>Jw}a^dxXDC_uXDjLno2g*5)8Ucz( z>!UoScE6&$&;3fta(hu@D}`-e216>49OOvVuMBz&X9<{SESLGGv_f=%XyJjLIzhGG z)swf>?7c!!R<_3y6d0_v6l&IW^2`}WpLop7w&G`&N4sU4BH$HR1b};}I4{cZ1Jv}Z z-k+bny8_sIe{Mc;j1EY{k{MW8KHLGb+5nm^TDUY>IkXbatN{p}Pivi6@l&8u2|#${?7E!ac?1Wo*n2*ds6 zH~i-R=G1QhT-pHW^VLNcNEwGGb~;CW;J3_4^!A1|1|Qxs6_FL?iEDM2pq!fVHNup2 zoVaxkbyLVeDQK0f&*J+;)!0qD^B@xkwfr#Qb8`j;sH#!^+}Q>O?!SZ#>e`Z+>Kg*C zu%CU+khNq9<*|1B~QGKR8@!jX$wEIN3%|+ln9dBe;Q!2Qp1OM?g5% z{V)=H1ooi30j|hUs-q5uq^FPFAdkU{2e`Khn3pqjAtB)Ab~0HLUfT_{4GV~Ttcc%@ z&uK$LH%GZ5BCmb)^a{W2#3-;5fMK6W0)PJ<-;fzdgUOC-2)rB$IiU&pPkc+j5m~JT7 z>e&%z4Bv4|z~>I>3)=k^D|OVM$+NB0?J%29F@3tS|Aot5>=%BR)K~4F*DHK;hU1{Q(ot0> zA3+ds0OY;eEm|TN3g`ngLB;7P1RKmY+GW3{I7b;u#p&01Z4oTcOe~-HASJEXNT(rC z>hFcd(XJ1Js9dCXU#-hgk2rUnXpP2=Tl+|NP^Y_)Mim19_%McZ0y!v$n6CJSxLZV^ zx6u@%R74<&{0TM!{Kt%k5nTeQ9P?AFLv4P&5J8%M=!Q_HzMkmzYraIIe$L_9XlHatBlZ9K0E5zINNTR1NU(CSXnF@$7{<> z(pEg$!uR#sF(dltcpJ{uDdolRmxj*CT=h*0>Q4AEx}y}RoSW6}@9 z&V|~$exX~BPR`M^fYhQxcJWc2h6|(jbd!TR${k1yE)Bkp)Z{sMG}kF06SmQ45D9aR zm}ABG+;a#{lm;R|ho8SuoV3t%CL+YpJdtsOE%0oc-^K*c z(2%N^k0O3GCIDgQT^Jj3=V#oY3eZ`f9Bf5$7- zaZQ>!DJFew#ij3fP`6aW#jszxW0BYxu8jcA#Osq|Smo3}Fp{){5XHH+-sPhcR@U#^#drwT|{GJAS z6K>#_Q5{G>LWMD-k+r~OrzvhaIGbmY+CrECXoxXLz&{b(giC5VlpGfV=8KfHP9@_; z*P^mI0-HA4%eGNc&e(hs)P+^NZU^1X+8Qy&UxQ&bkL!ZCs~KaPmHMvZ|@%`)MxfM+yPGulIk;)@z{OU9;!LHm~yw7?r1yey3=jNFOH@#r5Ll*5=8Oa4npYeR+SRqxMs5! zVse>%>ZZO3gPlBKM%(lWD26%_8Txxpo-j1nJ@teMr=IfR56s@wnB^JlYS+mVTv@>h zAO1jzKaQpfZyByX*>pYbk>^YoqA;3Y=`ItgfAR$7zfzcfm+#I3`Wwkk_QR~c#$dCb zH_$IAUEW81@^pux%BaDHUQCA-W0wK^>>Ja(1LmGfRiOQZ~;{IpfO@P$=df2PS@M~I(lP3hhI<=SD+2lTF zCecXxlR-tJvh(2|yR4la82%Ed9^$)11so`(ak zc>cGTit+~i{}%Ij^u5HXxaLF}t`_&QgDgKJp4pepY%1#hTTJVm4zN6rG1_V%LA=uhmO7{2}ra?d3$61|7>?>7Tl~eP2Lxu-x;j z-^YlB#8Nco8;(N}g|1qsvH`h;USlu<$)?HAHrd)57$eC6jK(mxu=?!aY2q90({P|l z$zR4Jf61}+IgixMM35iaaib|QCp%X5yJLK9XIgyg(WHXx$l-ev);c_D_OCVfyE)E^ z^7|5LiM)a8|3)xPLsIsQ0H_2D`z`|T=)8BcQ_B9PqsU5N!S*P1Lx5GW@#D+p+0jS) z-fY%=7DPvzB>Veb!gI3wUch$fW?l9I;qB$>_F_f(+ZIWp|357V$JGQ^N4pg6K;nmY znG!@J|9^-O7zB?kgEvDHAkmbch}RXy`l*(M$rf(hIJYc+v!uMzrHYjmX-p5R-%oN; z*-1~HK#I&tQj-$FA|Et#YnWXvTf{TX*E_~?V^!oh6kSVieaYDu%@UwSy)&l*ZuwY~ zgNJ2hCvdc8-$AF9IWSMkOYz;Aqy<<_u4x>&4Na}tIIz>g%wxobUbY>B32W{=`!U|K;L|wg39} zcl_7?Uby1oUtad!Lh( zeE-Djpa1of?)t5{Up(uBzgRx?W5=w#^vdb0Zg_6Xm+zRk|8FeVwy5(De)#^bp~r9Q zI&9)a-5)*l2i@=6uk9PZxNOTe8^po?B^im9vh&OG}cYOXc z{+;|@@=NmU`?UK9(#ickd7D1S_jx1~`V-pT!@nP<-SgyP(Vxpn3w9T&5PreuZ_xLT z`2I5guIBj-^xr_Asf=+U<4pzJJGhHq+)B{(YLjN{7)$E+vJD4H3$vD6BZZP!WO3ucSW^0tw>~#_0@oMk_-tWGKZNtH_XYBPoEi+hXJTc#~ni>*=OM%+|vUy|R^| zlXZRaYjpwPadyO)7GS`sX)PA51I~M%x}i&4BFDk zFXlix?RXRr+sWYw78KGK!0B5I^@Y}6+NFU|thocIvn*^=DM+qmkR|VQ5Eo&pL_&jw zpE6*JryH*-d%EfJVY==u9z;B(i%WLt2Zp&p=^-11{kZREwesQ`!rj{ZX{Lk6;~3AW z4Bis5h{KUp&Bfgm1&P+V=Q?wRu6~ulT6%pQLmVcqoFSW7yN1cX{__mFs+HHz4(hTh zR6%koGm;3FO-h+Y*7v?|8Fe+u>C-$zb7b2Hr$DbBCf= zvFecTNzaw0q)~W)N%rwzHHb1FjEzH;c6J}PCnP7H10cHO!CJ9?RsODj*!S&8=F^I| zNvR?lDP-4G_!2@=OUzULaNF4&!HEQ;@CL*1wDBpZDp&8m!Tk7x>vWVHO&OF#86*My zPbO@M2Oj%vVy5~iWYM{;yg^!82#kGz|CtPP%H9v-Qg3KSdkSYWM6qp^oWh-#s2o}? zNCjtNSA!Ez>3(8Ea1r+o*Hj#eiuovzc!|P~75))o;(TnkIZrc+r5PxjBaQh{O>q^R zq%?B{oyZ>1OOY9?s-!R?W$Pg5Egnv;t2lA@;|)!bmA2x33yE&(Ej5M-(nM=^Rqby^G_}X2&ODLG$t2PI5KHXk6THzOI=|{1G~T%%!>Tg}Aho z{TCC0XO+*~S%)({G8KN>fUNEgxBYuik&J@5IM(2g>Z~nHEAp z@QW{V=XCDjdQj^w6dH5y5#u|D%W$<ulRehR?B}#Gdi2)< zsg( zIsL?$$LW_}LrFx@oEP6)nAmPM{f2&B@zQ$>o7TLiFmZKz;XfSy>xBouSu9MP+gt14 z*`{0GS3vh#n0VUi#q;@h2j9O+;o*tj;)}%=7G}#^;FNW1)~s4lt`}w}>cT}ot}3iL zqm1fvflEA6nC-7s)L^#EXLG_OkADu7L{l_*T` zGvij)%?FOfzgAltr#tT9osHuk7?`{yppE#a(N}bI+(?u;{x;7g_o&q4pBCf?j<8zX z%HAt|2y+R|;v&awa)%wjV`Wr9J#L)b+T4Bx->$(GF77Pu7M^q_4mGg^@?zz>EN}AL zvdYF>yV19EMq)qVs48`~k#IqR`Qn8wEw8I3c6Di$q-pU}b1zG6RuudnH?pA@rY;Ei zhx+0Mk?T?;{%vup#VQN_j)F$wX1(S3^I-gW6@}=b1BK-sDs?YrrO;lnV_m7(fW~0K zZxeLaGyJIMHr+oul?}Zr<}NE&Ac+qWY?VjTK{k#)MaT!DGug?3zb~& zgw~pM6xKl2tXe6)#f3FP4O~}!y-WWsfq@HaVA&L*RhT`mMp4cFE<u^lL zgGjqFKy8Y;M|8I^TezcrxFL*?1CqeQH#uZsGaUHyeHK-E2mGY@hkoOmJs@*Uf8|Vy zzCx}n`&Pb@hR**AA?Ogz9?m~nR_k9(dO;JgvcOQKj?RKqJxB|EJ z{H*Zwo`+b-#9}3~gD)B!99RLNBZXMkmny@;GlMWw;LgJAISr$j*)D5(D#XMPp(^CG zp<1u?OPGyNKy;wFxn?i7vcUNxT+=Dk6)%=m*o(1-1>N12)U4uBa(j70Whtz8e(+^A zLxA}Q^Q#T{MZrGe6;|nGx0Hl})#sazH_4g?sFXA^gB27f>kG4&4-F0s4bHbx;mvk; zc3=&%dpO%CJR0@+iYPBoewCtaOewf$&!Q?3P$4nIMH`bROCMzFnVATgu7XR@`f=Zu~1h| zsT!&BEQ05wqY7)N9oW@&Peu(;ob zZ6EIIU<2}}>7Pe$*X3WgNmEN_njgV;;2*!@e3QiuZGZ0f)=suX$}$5I&=RDyNP=dR zTO6-d2yKy-nmN6-d1oHmV!IYE+RU!PnmRl!c^2TTYV~l@!_qM>#InDn{TO12Ic(kE~TTtBSb3}+z&pO zF#ch2rv)?m$E%30@*h{iVw3F60@wUc34YQ40=r@P$mT~WfJI*$mA;N%l1(Dj=kNl{ zCGtP_^RgOCCC6<3$((^I6g>BP;L}iS7S>P-en77z^wovVR=dqYh*-`|w6t7budGhF zCoNs#7GZzJ%z|hnTvL-*Wm%p+=5d`zmkG@6Bj!9|1w;2%J3QdVzcmcY=6?35oldX_ zGG|2N@>A9`+}NYS?75tI^OGADl{dw)vzOI;evPoq+>X=4FL@4`orfFM{C)(6VgaBr z9PTN^DL)p%Qp(Xi-(uwu6fXC3OUHhEYbdX~wPrJ>Lt9Ca17gZ8XSQeS<|0mzW48%6 zx2D>~=|zc$ryA%x*8mme4BS#iD&5b{MaU%@u zxX4_KWg-B*h0}+sl|d+y1=ubmbEpvF88erlC+QqsZt023kWkaDx?@N-h&s%OKDCof z8Nv)}S@-##P6*HZET@51Ds|MK^$nG}M)SWrOcDRAJ@2asnhY@;{~BH@NP(V37Ok9v zBsS;(VScTX>L^2}RR;Ab0WcBY7;(S>I}F<#kOvc=3yyp=+9ht)`W<0d+!#-8^~ast zYjG+j!vzHg162x9p6$Y7yNHEEtLXDQG}6>t?&<#T|3+=Qx<^nl)afQZ31`DdP&%|Gy}g{dEIgTL6U3EKV9OpCgh)p z@y`DmowrO(O`b2XKP`%oGg@Vjk86>Xx5#(RXweKXqXYGfDVc%^ zw9(o^W}@Y3MaG{wQ?hJO07373Cg|if5*f^&baY7hQ+65TyvT6I43MBQ%7yPUGvANS zd_N}h{n*U+;|hMHRWQquN;aU~&m;m-XI^q3|x5P9DX%T(WnNP~;EXj2fV61Evf8h8k-fUWL9#8CPKg zHlUS9Dd(xbYZCtqD+4(ztm$S^&XC|fJ4UC!vWb$$)y7g7hKz#0FuR%LJliC_JP0mM z4v~MnqFz-h%sgFOqoh?FXceD3w$Px3J$C!*`L4ojZ>o%lbbf-!EIE3c#I8A5WRo@x z&x^LQVfPzlQt~G$NK|iQ71@7PRi&WAjlQhu_K)43QU$lx7!-RYPE`K}C5%3=VF&Q= zD)+i$u-QGUh9Wg#HCT>H(spB!i5qrO;RXcr`52EpI44?IEd=~Ooxzval){VxfK_=K z^0n$8Xoq;mU?)c4s374aziY$9TT04t4m-)l=E1}0q?&FdnzRp=2Z!LlyZ9}J+{VfZ z?ie`Uz^-Fc1#7ggY8O?f5tqE$KU7)V?eb)sO!fbg_O8us-N?Eyf6V!MToXHU*;THo zvK_~xvwb_3GM@L7ik2vuJ2I(FQckpg{rfz9!(}xVnK@O7%!Ph{g@tZ38jS|fu&U1c zWw~EpG2_`D&3=8hfi-3|(e>`jYhCS#X|ZT0xc7GA%EN14Z>~RXvG%WRwm=GI9e3Z# z;{{JEwZ|~nIb~L(Smx1JxBzJT4Qyps!^)i>^Jx5-%B<{3&MO6X)w)V-{mhZZ<#=C)`p2Q*1#bYx+HXo6`p%49ar8l11u83 zbr{Q30q=D>NFk^IQmUpwd0nm0WvoY6E|X~~tbxgm7Ap^L;TJ?e$v<#v82&}6Sf95C ziI(w)#l`l1@p5|r?_!{WyXNw{9>*PNT*+i-Uw>Qf9*Z-IgDLoW-6lTgSCFxHC}!H4 zH+OVLBPT2o3@_FTKdo+8SZK$T3vQ|IRB)q(nwEGyOu2Bo2*&79H>RfoM`l5V+*5^^ z&%ksAf-OGDjc6R)OxHnQt$x|9Q3(-ADnEByn6T&=-nbpiv*)2u38EUcYJUazi>vRx ztwe*xc%)CUFyOAgM+|*VFMlRcrGt=n3|Y2H?-yZyU}Bp4I-zFi z0~E&#?;>1?!`19RHGHSv2OlJ~;57ZMEW}RIFL!j|i1+Fsw~OMX!pJ=weDJkNM?Z(L zxwyp0xO?PafGz^%vE%AL*LpF-fjN4>xC}oA*4y!=4_}yBEMIjMb5Mpe3Tr(g80!1K zyg@1k@#wW?Dw{)Do=kPYGwK)oL<34Z6gqOS2N}R1+J(5^YVxJ#jABi?cTS)uN#N6G zr_Irxof=ql;`-{d6DP+%o><BW#nsAtF7w6hX8TF+YLK~(c->=K#4$%wKI8TSPKknITJX&S)}d^O z#5=J`2jdE+BA558)vuTe!|Jsu(BvbYe_X+8*&L{#?r`(w1R%2qnbwMqL%39^w<-3E ztdyxr#AF}yJFMc!Wm9Tj%Ep(KuI!v)%K&B$M&}5Ak}&;n!(IiKqxp(iKg@y-D_iP; z$`<7;MI$;DQ75+O$=>VNi_*0!*FBf7;e-+F(NwL+B4%Yf!893JoDNed?_}!(3W1>>>+Gu^WR-t zPL6xeZS`i-G?&~kqxQnyJ!5Ep9=!Z;FQJi$6~Q6^mV0;ZX`_CZq4@%PdqZ2FUAuIx zug#1#S3Ij}&iMz3?DOcGliEC2F`uSGZPVvQd7TpVWNG$BvmquBHGp}ss;;q1Q%U!7 zOIxhrZ@600Oq9jt7YtMoY)mcDdgzw8NM*`7OpU~Qh1t^Y3&yuBe1^Mtj_JoxBH*i9 zBk~j>X8Sbm6wNx9clrBFUUP>`8o^yyWXFUkn&qd}mxo(l;0Y^g`qj_5KlN@7@EKPS zon>9+>+0%P-gPZE22W~X)_%op2$)9b=pyrWqbh5y(RqDXg$64pE{vF{hCZ5t8ozWn zVWrzqE=46zYJ7>ypuUP<3Fd>ZQcqBz$&G*6?EDPjyc#AIbD>Fod5c@p=BhG(sr7p3 z72YE#`ZScUMpNt#FTP-Ym##S33b@9B?Nf*?I{BU=jS48AC zi*fU>%cgW-%{koXFCY1reG^s>w7glVh3RHShX9xEr=H(fP~c+c&)7xfR`g43I6MD- zrK;7#4Z>w$`@clY&k>||^|fFTwxA5#m$XY>6{K1;}VYnFgu<&ZdjCL&~UOg?nuK5f(rlgI6EWx8o<9R!B` z%FF@3*L!}qD8*=1SYiLUTEQ|l&hKA7M#+z2h{2VhZspI*yI-lnCpE&Fe=kWB3}z_a zLhiz#3xW5MuCCQBFjbb}#5nAA8aFLge0wkjz9H5NT(T>?Tq?PSMM`{I`=XK)hGHAP(VB5Lwy!PRK=95nDjLZrm6V+nWjU-aqhmNd#lv{~DVM-Kf}qUEj;liLSPC6)DC=IGs&yU&o#V zH%v;3sDE6p>HAI=Z`WVSgRB*oN7)omJ+8~X5G+aS2B-ezq5EYrrE6y}QQPE~E3EM8 znvi-Cu?lkCJpZz}_r(I&f$((OtYIJDV?kS{u)>2vU-YE2zh51~#5$8p6}g3Zf#g@5 z@cBAaMe-G+jP_!GKPXTsqcY!ODtLjjRX*%q(}#%Z$s1Ws+_j0ySg+IKJ(Muw9Mw4* z)qy{Cy38S?qdhLRQM@m$)I`hTB$;F#)?e0l zher!)z)0`aW>sF?t(SXF!J;yZ4#9*C191O6`^EqIU)qEI^!s1_^rt`l$JG!2`-d*= zPgj3MTI|y=0dxG4u!yTbI-hz;eH78l_C1SK<<3ZSk;zPy6& z#UorL>>{GLO1$SJXi`*_QWuHllwJD;KeMa*XS_*od6uwM2EKSq##kcTRrRfn6RrxMCMekHZ7=0!0wE4nb zF`Zn2odhRKDdD#I4KLF)7YzDoVzrsS6g9*5x`Dgx{3XBc=dYe+Lw|XW9sMQg+J}?+ zD|lQ02Z1UL*6z3JF%-qVO5_&2>ah!UswmjlUuuAz{qX>9a90STf=kS|MD6Y{>~PM) zRmS~dv-*u^oJ8E%0?U$I1ccK-*2qbG|M2Y##t8oW$nCIP{JHXY%}9Yy=L*Z0o7>Oy z@2Rg)>;mkmcX`||;IAU){2t@?ei4ES9==8K=emqag^JnTH75uyb__5hDKq$UV*UJ(Dz*67@*`|WEtOFP3 z=m6JhV&xx3x2DOlS8OKzDulf+;ELz+G)5$i6N_4DOXz#+-`X%)_0d3bGFsGfL0u+N z;X!Upzy$GypR#4XNVex|yM~u@58)<5h*&$3+Nd7_)o}dKTX-Y5Tge3#&YeAcfs*3V zX=#3P*uuSu?3x^wAod*mD~UX_1mmneI!oiLEEz(1vHY^I=sW(jY{GTaU1E&n;nmkQ zj&xvHI1Z#hv;mA*a%k}a_tetqzJOJMc_{nH8i67_U&A|=Cj_F9V2R${RHc5{NrD$@ zkIHxs0{C8wS7f`1PmzB(+h*^UvynoWCtU(A0j+h@B_K*4ENX2LdD*zC3(g<%x{~g!jR8^EdeeX2Ub$z7;?Myg%Vr2mVPMe2E`KiTChG5)64fUu5Uj|f?a``? zm7gNPnt!pA!-54j133Xw)DYEHYPGmwd;!;n+GGv$kiRw#h~nUC9fkjQ%CFWYI>kb2 zK-V+nIpCgfV0;`tLt+8Une?wJns%{A5Z+F;!Jf-n=pI_Or%4IgR+^-mNse1s$dQQP z{k0BM&;NdZd3lb3;LnTr`IiMeQ1HkgN>exS;L#38S->cNfa)&Cv6O7ExV}F&McKW@JiLX_L@z1UE=I>80-TjA}{BViYJ~&eG^X?K$hZ1GX4J^xRIPHp2 zu1KK&IxpIy4UZt=V_M*nha?UN+*mDIwswcxI1_^Fpf+%9aUK|PNBf89Gqtv8$9N2^7o!E7!B;nKM zpewZ#GRz_ioQ4r0(a7kh2tvkR1TtXthO4)(p#D~VBD}^^5f6bifuD>eg&! zFGxcrr;(vLO;|if2nG6cU*hwSqn=X5yrFe5Iy}Ey{?DV7vZpkmL+n?p4S$06ZM~^P z4_yKY`bN;XBl|#+A{a-y%tW~r#p_m%gph>(MQ%tM#IRalDvFF<7~ji7>Oz@PZEzOF zxa5yIQ0-x}b}^2)K5qZ`i#HM)`LGr zR3B8Q=qSv(pfVZxg=5$C2l{}0!XC2b%T(-&R(lLj)stmkJb#PzOPq1EhG+Adi$W7t z-${I@6~g|<;=NFG28<1p1p5NK(tdH+LkF)05=(J){sT0!VTIcqbxcuW9~o3AC#RC% zf185E0V!onzyRU}V$)ZYfFQ*3o2HGGj4-pRehZ7$qFR7=B3g)*?$g{zmbyYe@?X3V zE-?JS+l(pbyHJEogA$!Es!&lU?V}+Dg@%Udg6#?otz=>3`VQtg_1P8e8MI8`u5mOXe3Gm|9Md(}^!lNhIBnz-*2>yNJH zRI)qj-xg@h525rR5q9Vrw!3LC5oK(2Te*l`U&~KH(l9ag2o;>KYpHwq>ZqP=^;X(o1V|PnJJlbDqnxc-oG4 zlOLcGDTE?kaEIy>|1EIQ^5xtBjB3T7$Jv{O`JWUT`qWPUit!&KfG!hZHpnAfv;uoE zt*FrGH)EyI%i+*Chk+ssj&!0j8G4qkAO8HCPG8{RfS9$$pH5_~XqnW6x|If5`bR|b zicT`KCd`bBjs7f@ZRfK}y&C0_tSR ziQ~M5;-@*4l>R-1B5y2@J&=1s;X~NmKG{h4!IR(j*Vy!WwcGBuH-{&mS6`mMM-|S( zC+DB4YU3zlz4hXpeiUg2ji%{S0ep?s>fK5S9<{SjbV_<;YxHPJyGe|a*0i`9UrM#1EoNh2$ z$&@@yVcOR*5p$X9r6$2$%HL2~sV!b*Z{PE^_PhG&-vM)rQ(BzLE@HlTUeJ8S)4C57 zeRd1p$I=sG26=1AY9Z0u*id_h4cvG4l9Ohj*^+Gi?y(fX0y$&FQOd#q|IPI3BxRx1 z&!oGnlrQk#n$E3O%~{HlKRbfK-xDv8+W*s!RF>rOM09&Ll>oXVqVOGdva%qmM^exI zcvn_E9)uzaA0xc3B?`SwBC0qq+{;1WG}Rd7Jbn`b(Q*_ukk8hmm!-n?lP`N?2QP0o z)ebcQuC}ZV&rA*cMVZ!U*q0VtbuNX>}*=p;Y;8qNsusQkaYwWriwOLJMQvN{D6+w>jK-Ciao^Sicf^ zRl(_tQ4U=`fn|0;nrGC~IwpIDy8GF8VPCJmJ$yUIXlTX6=%a<(v>}#?k*7o8M01bF zU>3f*QfYii_h>y5i)FTr|B)b16@j<9Sb#UE{V$@-+LLXBA9ix=CEl&owjUQ;mlqkK zbyF12f#TM~*NiBZLMqnl(WN}ifIV6Yv;Lnnv-Dutsv{D-Kpi6_2RtfSGJLWr4=$Wc)F&Bn+z?m%i z%pdr&I47MsymYX@Y{ejhIam>b5o;=(mrvBv!?G|KEg2U;&qO^M`eg&%px2FE{UB&B z@;cQYfrhi>=$<+Wtqm%!2T?)E43}TWMaY=$#)E#hZV0r-vZ)DX9v}e-p|l6x3T7W_X;6@i09ZlY#^N*%TH73a_OwFxe!y<| zy`C_i)8ZKV=V_fNpP2a7H4~8)iH!O&3FC5IWhOz6lZumqrKABx0(mYmgN$?FrQMnY zYg23&Ayd($_+>Se^9{*ajZh=fkB1jyClNIB$Q$ofldFC9-{ zbU?E>OYHSBLQ!>dN+agLn-#CmK%}=kNddHn%qWm&YfSrM-`XifK_;}q8=M4L^#F*eHUcOkRXObiqMT1PJxRTq@lNnvEs?EFq5QgiRyQb=0UzI;YX`NrV)Er(v97QWv{hZ%f{@(FH!vOI zo2EZoFecx=*31griDtn%}@$*#afnUdC+^>LgwHZeG0= zPF%z4u*5LCrG>eX7D-w)=>{_qRHT5Kj_OA**tC zy#xsLJDPfGh4?{ra>5P!2vx(0i%4ZxtWO-vqK2wAS-pa_9X(xahlNE)eXX{ z(Gb-m+ePn@z#W9pJS5l8r1uvySgUmIZ}b0RIYfTx)d2F-Zwrtvi-0$Bk;l~VUs*rV z84ZF7e9>42i2s8#QY9=+ScAb&3)T?kJ_}m~67-f^htAi zdf%FsK50x#_qG(%3cf2-S?YJB|ID?jwC+(QPbLlwJ`F0%XoIH+n z-X2Ffw~r&8|HqLo5hox`Mvfz0YK|jaj!enXfVgxEg9HeBmryI#CDe*<3AJKcLajKKP%CyN)QVRLwPI98EnG^d6^jz;EPwPk zvoV*HKF_Pb%y7&xb9^(*EcXmE%R|G=a?&uf{4~rgR}C}ETf@w9*fDc_Hq0!y4HG;s z%THJ|tk%n_xoi*$my=vYqozdXYeo!)X2jrRMhq5a#Nb^<45nqo;8sQqHl;-8Peu&J zWWDCJ}iN;%qtQqK0El*2tJ<@5xU zal8kmobN%I1Tf+aL0?sGR8?GYZVE}IAcalkAcG~6kiaroNMM;XB(O{#5?Ce?2`rO| z1eQrf0?XtggC&xYz%toLV3~9@u@9AuM1({t37#v}*oByIJ%|~vgP3tRh#6mlm~k_R z84rV)aW04%zX~zoQV=uV1kpM2lim$zvygrxz>pfenL}s?ok19v#t3KC7~%XHBb;et zgmZ3;aQ2N6&ciXn899S6ZjKSo(lMgrt1gAYiJ$k z8d}G*hSqVbp>=#}XdQ8MSaHqvrf!v>_JXrtDF| zfIC6VnB&BhH%81^W5k>@M$8#w#GEfi%-LeZoGV7mnc~EhCq~R!V#JgqKcTyn!$qcy zY3VtCCYXdxJtpH;he?^%VN%|8n3RPbCgo&@Ng3K64a4IL#g<#c4o z+DYXCtwMJq+NAF^w2R?sXqU&+&@Py#pc@yYZ?0V>o%yrN?f$N}i($+!eM6H9)$yo=T6S59ECt)3QPP}^PjBIt# zIl=0nxm5A!KnF(bR4t|mBf!N4h)$9K-ifwr$*G`CAX5SBA&FphkW8RDNG3=fBom+x zk_k=+$pof@WP;K`G6Cr!iC}b)OdvW)D+s>-*M&f1pi}}Tw2r?Sy=HDgZ#X-pw``r# zTb@qoEkmdDmYY+0%gQOe<>QpzGBKey9Gucy_D$(6@7`~nYwWt%R_HO!h*0Btbl0Iz zfp;7}F-y(#)3P*ja8j05G)~IWO3O)ES^+vKODj_+WogCiq%5uEos^{&#?!Ji@_AC0 zR%B1g(n>MZlN~2lb}(1tOh)dcbO5QPGKVx$m_k~qOChb4rI1#tQb;RBDWsK}6w*pb z3TdSxhcr@$Bx)?up_p->xeDa zI%3POj@WXlBep#1i4Au;V#}9~*m0ye0B6dVgxqr{hcrA&AuXpeNXM@X(s3<=biB(T z9S1W=$HxrPaWjK-JWU}jXER90-wfhh{&@+b;M$HSKo6CJDCqyl2*KF_QF)pZjhiXa z`Ir)&gDKH@mlB<8Dbe|r5}i{i(Rq{;jXNpP`H~WYBVY(EGw#xR^>vHeZQ}SPnrc&0 z;mU|?d>N3PGdVeUlaqrxIXU=~lY>J!Ie3(lgG)I%_%t9pr*d-eDks<6LclF{marGq z9%Z$*J4(Z=Nj^{us}gF(sVTK))RbEDX-cixG^N&Dno?^fO{q1Hrqr563AN(Tlv*=r zN)7%n61vfARYKuLn`lgE620x~#Axz5FNH8%gu>s>wTx8UCvHJ=kf>5$CHgOwpNIAp0>aqGLX0g zmWkRFmde-!mJ8PemP^$HmW$H_mdnuumJ84XmP^hAmW#|3mdeTmmJ7)QmP?1Jr5ATQ z?DBrJjm=Zll)(m4GJs~1l0#EzNujyKq|jVyQfMwYDKwX!6q-v=3eBY`h31l!LsMx= zp}9n*&{?U9!Oq4&KYL;*UxN(uk~YpXBXh${bJ91;G%JUrOtTU>$}}sRqfE0>I?6OF zucJ(}k~_*YE5pM~bJ9G@G%MGmOh+VKPMpP*o2ThQ=Zc+}46)XE>>R%ie@wo!cOcKK zc;|WMr8~`YM7YyDM`SzAb40Y$JVzuu&2vPs(>zDyI?Z!Ltn)ncQk~{GBGhS~BGWQA zX%BoiWgUr?bhhY91bafrSw|?s)Deorbc7-n9ii~PBNPUAgu=y+P*~Lyf*&2BGl#qfy0(+FuGu85YcWad8cWi;c9OKNi8QTk9ZBmN zM$)=A(Ss)b$t$cRKESl4e0NXl+bW?q?KY*iZ8)KK?Kz=$Z9AcN?L47(Z9buQ?LVP+ zyqM5CzD(&Yk0$hvUlV%AJ3GNj4LISLru2M@)P_5r+VaFwJ5E??*Z!8;wYsHtZEdMt z3tMW}u9n)hrl+=TXsKPxS*o`eZ=^H_(B)fr=wToHveWg-oD?lMBx_TS$kwIGax&Y2IPcWI3HJozSYxNWh|78R}FQ-s+v0G zR7Fh~RZ$Z@Rn&w{6*b{fMNODgQ4=0j)PzMfb;_ZNnlPxM7XEx+{ftW%WT<$HqvH5b zaUl}C;XNVR+7e3hT0&`2ODK(K38mdEp){2xl-99?(jcA?ZD0xIvn?UM6xX=ITPJ~% za?3^BH)rxVrkD1Z#aI)~VZ7C5Fwu}Rm}uJ>Of>flCO9yI38u_of=@G;VBH+XxjBOg z2G3xe=kIVCJ9yrYy&~)bTJU;IS3VEv#^Vv)`8%RJZ%1_J>xk|=9nqbiBf9f)M0Y+8 z>Bhqm-T6172k-3O8(R#cy^9n6{cNT8TRlvDkP=*Ma7b0&b*aX&Hr4smraE`pROd;X z>YQj(z5Ux%Z}m3S+qz4&7H(6$UE5S|O{ji55w>ozCar3-KHXcbOOJMI(@V>>>80)3 z^wN55dTGBly|iGPUfQrtFRj?6M?1FZr6t>RZ_5uiH|s08p13-RSWXe8^**FV`_3V> zrDqV<=3|7n{utqW7$ck`V}$c&jBqZE5zenM!Z|mCFdmK(&do6*_L7P0opA}y(@_b*(h(pyIsybkM}Xkx2oUTX0fL(&KrnL%a9)l8 z!O9V!<)nFrKCf|E>%)PemUc#hav~#l+#EoxlS(aeNW;+-((*Kgv|LRgEnibe%h?pt z@-~IE+)W`Ze{)E~;S|#HIE6TuZ8~Yr00{%5l_1vLfEN56)0MeHx^Z?yceak`&eIXy z89JgnH%D}5<%sTl9MPSLL%MNrM0fU$=mXxNYhn|d1XIXnU(24casVB1b_`D$J%s0c zAHfG=FoF-HVgw%u$p}7>nGt*-LL>M3MJU@G zr^@Ce*GVGU=Q>SNyIdyi+nuWzxuk|+?wgz>hc-%K+flLWHLURBbD#D9Jy@I<;dlFE=MlY zb2)N(p39NT@?4Hwj^}dZGCZ3jmEXA>x$Ms6I9+ZLDDuZdw`BMO(%WP@Re2xu!GMz5TRi@L0xyp39JXe`c7wIa~=~7)~I$f~qOef2BmFaZxt}^8kZquLiO1)Mq z50C?y9-}j19->nz9-(vb9iel%9iel99iekc9iej(9iemC9HDce9HLWc9HDbD9H9sD z$GZ#rGebMU>mVbEo5E62o4|4z>tO@o>R|(^>R|(M>R|&p>R|%`>R|)P>0twrnZR;c z>0tvQ>0vX{;RjqKvm&`R%jt7+QDKHcQ7~hP2+WK)1ZGAS0y85Bftitlz|4q1U}pFq zm>K35%$W0mnPGciW_W(FKBR6oVYm?Ixb1);s|_&bvjS$AtbiE~D`1Ab3Yg)o0%jPi zfElhTV1}gz81qvBGt5*#&dK*%THIJttuMyq0#>WvGuVK;18BzQ9GY@Gh333Zp}7>K z&|Dr;Xf7EkG?$SSnoCOx&E+PCrV^Awb6HBE$E2!tJtV_JDYbJmOH&7 zvsG~kY9NC-G84ZPnMz)U%!MvP=JJ*yb5YBXxs+wdT);A9E?XHg7poMRN>ql-g(*Yk za&&&bx?0~n#ue!$y57pLHOLe9`yT%!^b7=YA>^u9MExP z2&&mL0yTUZfLewPKrQD6pq7OLP|M2!sAcK^)N*$KYS}yjHT)icTE-7RApsBjuQt5$ z?8=ATRkgY89r2 zDJNS%!pRnpaT0t(4>p{t0NB;0Hf89Q4<#?Ka!F|IaZ4nt;TZHo!hbZtSF+Wm^G3~giLztSAf}=ySvU5Z>UJl64$N|~8I3PO< z2W02pfb7g0kezb_va@YOHl7X0&aeTw;+7vTdcWEoFg?B6?hvzn;5{hLv@xRQ-7G3t zIEN~noIzC#ok3N6ok3OXok3Mxo2v^C<0oM_jVXap){e;`tcHh>UxTN6c?cKF)nSd%f6<^EHAIKB(bb6j5TPNqK zqPtH@x=*f91c|AQ(k=>K?RC0WBo?NEsFo4^pY+Cox!Y#_P#HIQ7|8b~f&4J4PP29k?T1IZ<&f#ibGLQ*+sAUWF`NX}*5 z%H=@|Kdov0YHz^eSOGmSrFfCuGYcXtrEhk7mmy_;|Kds*h&NCH-i&T>5R+ZJG^*z&ZL3iwMixu>UK;pFs~~em+Mg z?z1^kNuSG+3;A4*T)yXWD{e$arTLhijNo(dfutPCl8McHmQ;pjv*ZFbnk-sj)bm!&yRmhDmQ)5Ls``$UP) zbDt{wdG1sDL7w~6E|KRxwRhyXPwgmq?o<0rp8M49Gst~nPs($j+Ntu~C-yHMA-K9e z+i;$E$}mymPwh)xo|Ae|o9DEC)8sj^w={W9>?2K{6MIIJ=fwWdjgN5xwwa7Gu1b z!#Iy-Fu|)COz><56TF+j1P^C0!OIy;@N@lw_5=Rd7(mN;Ti`Me+V zKEVxTAjjsikYWcikzq%&kzq$Nl3_=(l3_(CoqNcP2HISDanMq8F zOl2lR=2DX(bGgZox#VQXTy`>KE2VsX$1Ty^@0kxtKf_i1&@r3OsO443e5f*I(D440{g1efx5ipzOC#pS%7 z;&Pr(aXIg&xLgLNxLg*dxLhU@Tq+w=TrMM1TrMjrA#xhIZQQz4GUlg4 zW;kk)Ii4D1j;jWl+t~8)U%c`)gbzSj>ow(;Z~Q z?I|qf_ym@7y@w4r-@^vn?_mQ8=wSmX=wSm%=wSnC=wSnin80$W=wSoN=wTrpI8V}? zKv>%69h2`Lcvhgw#ei5OVhk`@7y?`hMu6b|2oRhf0fOfvKyZ5m2tJPh!Ql}gcsm3* zS4V*0=LnE+l2Pb#NT8nKNPZfR3nk-WL!B_NrcSw6Q4`ix)P!#pHDOvsO*mFj6LwY9 zgjW?cVN^|>a;c&wEUKu^pI;=x;$r)1yZMeYa(FC8IlOg53@*(8lu=`V@oETgb`1f} zu_3^jHUv1|h5%>X5a8S!0-S+kfbnn$a5fGBJttrPe%LLq4lnlm)weHqkMCB8B?i}} zs7^0rfSK@hjBD9D#C2RA;d*9|a6QjQxSsVRTrUA5TrUVCTrUqJTrUuQ%6npQs)h+Q>*po5;qL-pa&;-pN8w?`5E;_q^}vJGf#tI>1JP<J1To`A5L1rmPWcH#ntkv-z_If~v>oN}rV6HWy&<5UncP6aXJR1h;x1u^4P z5Hn5%G2>JrCY%al#;G9ooO-*x!gS3USsjq(oXG$a4h=vpr*crou@uyEE(P@*OhG*- zQ&7**6x4Gz1@#Qf-IoB)1faMip z!0!q%V0MKVaJoVa*jyn7JgyJ}2G@w3yA@)<+6qzl`s-%FT%A-$P8{$9!498FP-|oKr_|!L}o~;NcNmF!Ts6xO)T_tUiJZz8}Gb5FEp~ zL>$3|cpSmyvhsj~r8kGW$4?J(c)|OJH!IwTVq;%Z;s802n=v|*o*_Dwp%FTlq!Buo zrx7}rsu4Putr0qxun{_!vk^L%wjnx|xe+>-yb(H;Kasaz?k~1K!TMH%s}{P3oJ(2{ zNyMvzWHQx4QbB4Vsr0mvRAgF6Dkm)@6^<5?N<<4u#h`;^yl){X!&^woWuHTFc573t zDX$Yy&hZ?W@I3`)+|Pii3}nDm5;9;a4;e6(iVT>_Mg~kJBm<^$k^(bn$$+WMWWZU; z(dn0nS~@y;fh}%cekRx%5$dsXQq*B*1*yZ%%2J1&6{ik6D^VSGR;W7ctXy^2S<&jT zbJEpeX9cXo<}xNDP8Y<{Sj+lQ5CggD$^j~qs0k{Sr5=?_QIE>yr$^hU6I3c2Jt~)q9yOK+%%4QtB;yndaq^5LRFIJv6v$8p0x}kWfQ-2xkTL56 zGUj_g#!L^$nBxH%v%5fsybj2i(E(|=j8r?Gt+2MFwft<-D<;)kGJ05s>X(y*KFz%nry9b@ekBA{bGXRyFF~Fo{2yhu00$ehN0GEd$ zz@=aaaNZ9A&ha6@`8);~cZUGy=@4*?lfGOkL*=Lx2OZ^X1)S$_0UqIW0FQAzfX6r= zz+(~+z+;jSz+(~-z+;jTz+)0pfJY=HfX5^zfX5`~Vtud`)REW-;;76xYF=OrbwpYe zbxc$gbxckabxcSUbxcAObxb@IbxbxCbxbe}bwnx@bxb4_HIavh`@6M-RM;(rIES~@ zA_-tB4LPV2h7{DxLI#?MLI#>hLI#=$LI#@1K?a(LK?a&gK?a%#K?>?+AOlTAAOrOh zzz~Z1>i35GJ#oVM1kiGQ3g|dK0rcFS0D4YO06mu{fS$t>K+oL?py%ua&~tSP=r}q7 z^xT{PdQRHuYGxGjMCEte>(ifJhRBHs4UxSZ{j%W-!TM&sx(;viV`e_g0bNfu zDrhc)$n*&P^LlmHTBlV}LYzo%0%(PH3g~2Y0_a6_0_Y`k0_X*D0_f#%0_eqW0_de~ z0_cTp3g~2P0_a6+0-!|Uo{jC@_tlHdHT=tKIdVa+PY@QLKiuDMcbIY5vL5yjt~NAJ z_lt|&Bf=Riozt8}`?z1YXpva&@zyf;A9pw#^lr7>$IcLzz@UP=_5a}fsKXBaZ&Z(` zNxjEukUaJMXH0~+9eMHU>vD6ulArv8TSKkTftLbZyvtI4=ylzFV$03*4cE;Q$O->E_GVo2%_Lo8fG;<~U*Wmm$Qcm33kbqdz=z%I|9)8BU2JpYTipIUF2|`y3h@tqzui9E@a%uORZMwj@onVs+Ctl4EW2 z7c6q_-mbqaj^H5A`}Or|h*7t|JxL-DKfNXC!q8|je&w)`IdK0>x z7pGs&rbBPNUv3_~v&wHxzkl4VZxLLi-PoVNj#!6B2jXV>cxJv(l+m7QuYX(~zKYPj zzPX_;`VM7>6ch(Oua@70rpMq2fp6^|+nmpzT;Xza1jvp=aC zhb?Ps-EX0sA0X6%yjb39ouX-q^a#Mj(hn#2w7S8-fEsA7@?I!MD+mKhN8~Cy+b+<) z?CzzCEE6$FI8%e=Vz~n&rI709>))^L9?T#2ZoPSRw?zmd7pbvg@j!^7DW(PuU^%60WBJ`v#lHR;~-zIS=V@b64 z_q-l=@w_RRs+8453pVLYgH%t_Ai0w?v<1_W>PbUDpytSsnK?4Vg5>rarCh5@Q1Gde ze%mYcsrv+xi1Lu?+GWPc{KgBY;`iixfCdozMTu5ke zn9u)-9nc$1SUg8@6O4Qb3v`N)N0Rh=^?DzY7XMuD*M|>Z{tKp}{*Ec8P=xu&?WtZM zCaET)QIggudkL!rehw?4)xJeDo^AGO3KpaDPt{B`$i!Nln&}obVckZPJ7YgS?7wQV z@T$dQE(EJ~#knu7b@eTvbNcm;!Y&DaL$#&12?yUJ+lqdR2Xql|Mf;)ukfa9k6AabE zN>TdzTpa{bCAK1OH(GthLf`kxr-Z?_a?`Ko|1?HAlw%0NB11~Sc`rIQ}ov@WwU0MpxYE;KJM1r9sL>18V~rq*PHKa)R*o5&n2u}c$O9~ z_C?-aK@L_6oXQUoxH`ODLgk(>Z&qjU6>%%&*7smIqO(P7wv_4;btUOmx1yktK*0k0)3hRvXf;nx|`g z@5=|wNq4Jjx>`_EHUet>)9PyXc<PPKXb;{?jtIZ2cwXqKs z-S3tSc#dY-!*4B}Sf=9gdQ)dQuiDd(Whob19rj8Q)?QH%pZ~1Am`dyJF1%bS8-3y} zBm(CQ_sJBLBpEef(|p=)55Mfz@|yF*^6-F5i*0TQ1$Iy6+6pkI;#9P2xRg`|1gdT0oJz``}OVMON)a6y)zETTv|D$m?}KY;@+b{vQ!&J$vG8 z&qN{+lmapLHlWP>=W2ET;tuW(|Mv5*+lRaBNPf9O^{>y6M?dpy|Ne0I?ZfUrS37w8 zw!(b!oSt0tDf)opkxlwacS2*>Tf%p?05^6A1<+f1e%RiZrw^NRgyJLJUk1-Jns{}7 zaqcM?a6YZzaE7}BsTbQ{;g)1u`fq%ZkGV{jm5icWFV0_M@9lPtkqbmo9_;esytM`p znW58qv%32k&ZBL(PUQQciyx?|^v(RhD3K3VO$w9_NGM((@V?JyKkM)R3m515MhMi| z^2#vi+*H`3xHNI(!P3)4mUod!O{pZAHGKu&?v~Pb1DQ4l$rnLvNf^LU7ZTUpyz9ykygL!EVm8nb{{mn@z979Ftt2IgMbAPk*g&(JR>>-|^z2KVr_*8a4WRZgrJ zY49jUo|ad?E^k+PX*7)XFP9Kw$lw(63dSt>Jt2tMozz5GtPKjivc87$;KG5M_4a7i zwY;1!e*HTf*ViiyCU+QM#5Vo-05_)EFEsHdHGrV++}fC>!vFz&P*<28VeSbf(!^hG6cz7p0Uv2SF#Ff7c16x@44$R#g+K51?*TNl~`j7fZ@BQ4Y@7vc&Y$YA>rpFiHl9 zSQ+D&O$#XNX-@NXu2IoWPYZTpDb~V%gH3(0J-sQmCzfJk-HYqRx{}tHoIskw6M9nq z1;&j14QyEQZ}^G^mA|EdG%sSOutkjI1r@a~xFVGTwJ}UYF1FjdeP8Gl(G)x-m%^v( zl6~P*L{s>bTnhiZ-h8_Fz@ZVoi@w~3*c91NN@?PsSl-p1hAXelfdM`D3^!1m%X1SK~b-NunDpL2YZD8LqHw zzrDYd6?{(S@%wCpX#?>3|{35r_#M;stIlZiS1i7Y3wLE8(Zic*;RM*#0psMNqSWRc%(~_98rnK5--M&d(J_) zu&d*5IaDL2W~P#9xfC4w;QEfe?G6zKqA(>2r_=P|q>UIeT1nGXYShnPmxr?((fjt3 zsp|f#ie>9sGl(-0@jBNFYu`!w6wFaGMUzm$V@5wIy{2Rtrf-NGBN_b6jZ(YlVEU?uOi-{HN*_Iq{DN`%cGwZTMfx!)tu#ofiSBvIZY)|##^ zw29(-t7}9gjLfDII+uxbhgxd#;3)dPw-Nt4q4GR*T=b1J>Kz1@l9t?GI@ zD|V-7@F{FN=oP#8v3;elnI&cN+hm~#Llyie()3J21xKZkthZ3uTg+6HnU`pys>)P1 z)gv{EYoEiuu{D!qqdVowB3G8{{Yt!9A2zQZui%CgJ@xDQz5p#}Id4@%_X0pSCUZ#) zVhL)rkxXgtmb+ig4z{1np#^Xk=(@fl1gJLKZ=!v?o?KWBL+1wE0; z;?-&MF!@J(EL@uND(B7$B>T&D<+{gn3Aph>GP#mB-ZErl4EBQR`C z?Bpj?>FKp$`lmHw&SbiS2Ji=SUH^54@VI5ZD?h;ofX8g^6F9BnEEbb{F{d;af(z;(=h{D0v_!HYy-59L}QdLty8f=Cma9FTG?6x+H0Z4&XeLU+dnxg zOASw2@GTnMSHu|KE6=}Uza!?xHi?7qi)uHe_J1E%@S&oV&_tzh(f1!HJ*#WrxH`xY zKz=gI`NJ1xCl}vtZ*9j#MZjXk4vdn;E#&--%?O{Pzlk-nRP_wFel0anxt1}esfRXH zp8gk8c+v%3z7`3HD4Cu)s^rx;u9 zm3(}rzA7D&)iHpzFO*-Wz{#UshBc@5dwW)j4AojMVA2Ep+zLk_JatLnLx%9f5N9- ztiIjzeT!F!3fxiD^#@4eIFx!I6`HhOFnabkDb~idsL9=;6vG3656@A}Ezqw^tNJcK=}9l(HoOc3+EtHPt4d zi`A77sWIErDuq74pJnBa72m1~|WVT1l+lc4E+kx(f0Gnzx!cU~Oe4EO@i zKG)P?V!~ls79yX46|%r=o&;NfwkN&=EBo8c7N=+**0!u=Cu(!H3@5=iIKkf{ZbRe~ zD~YSakAE)niE}L6>H*w6{Flvd)Nd+1Tuyy)yV@`?o0A@7izJ;)?zS+a_xl&$p;u+d z-l-=5GS?B%c|NXIzrq&yZ;n1vudrXFnaUGC@Z$>m0Jw+a+dZ8;g2>X~@2^KhoyF^V zw5ohK@96zPjkr&xRrphyIrkZxNe2RAhrVvF7oKW`Jj%y&_(Z{wOFCWb!`nm~FJDr} zu_us;xnA|>ix93>Ks)lTtt`0Bn$;1t<}D+Gh}?ai`Cs^|73a#lP@wT;t)K3!BJQ zCHj7>%C@2#ST|`^iMQ0U^iX0A9jl5O2)O+A<$7r$f=k9_!=QlUQXy)}i`|JnyEg?V z8j%}2@U^mFdc`OLhB}zW6N`*XpdEp1{*&l1GjZZ0#EK~9NTkc~WlK$#M`msDRn$zV zMm)oJ%y_aU$)*F6ihzz?8D_&vG#t~RqfNTy2%{^Dn8RT853`vo z?(b4?N@1S&tKZD56JYg7DmEb2)asE`UpA{U(dbK(iDnoT2eW_dR_Ziv5^6;KN(Xrw zaUKb<`?!T1TjZZD>wDV8_FpDj6gZ8V!5(Tc#~8}@nrjRD#q)yG9sogq3A+zn>eK34 zyaJq7GBjzBT)o9V+>m36hNV zdL+`nb1{c=920yb$@ans?fTcZJ5DYgCmmk;Q!)A|u;q!>c00`J%b+Ohsdyi_6UCDqO+oqMQped34*iywY1w8MuRFvSy)>PZz9MwF+2Ko_`M-b=8&X}`MKWC^*6 zee3!_2)^hu)@~3%xzq4L>B!>v{t4Usm%H=z?Zzmp-_@U6btRV9btNWMUC9u2S7MF4 zpOWmqKjOpytXKc*`Vd-2d!j}Yi_}YT-LrO>V)MvX_A9M6;jrl|<~EG*q9%)I`&eeQ z{;b3g&=PwFK4W-VeAv|af&}mIhfQQxpsEHhBZz?Nw78w9%IXAl7S`kt`OL5El3-j# zja8`T8_N+<`5emx1lb>h-e}fYMTwi~C_@*0kT$Pe_P{+rI;Qt7a#w8;Y-3j_2Hf?D{b3UyWeCNh3CI=y!`7-aUN_c~XKl@khiio9J?@ zH2W?b*Ro@`ygT08Mr&9&xEPx{ykbzB{Z^u6P4g%U&Y`4Tm>(9ZH$$l*TZcqU_r^rb za-EnoAhW}oK7xR4eFOnZ+gZUPf57*aR+khQ{#^H9&)jcNVh^e-jwTr9k0Mk06I$K30T$9yrm|l4B zFLayUONt6bFb+%{Ct3~d5#zR)Q5Y3%1*B;Qwm*7$-x^FtH_zIHB=qBsgq9&8mRXYa zA^oOw-nKPJ`~mipe}p`vV$HK!QwXHL!4^AgOpy_&dZdJ$?JaP@i2kHBj{=b;j|(>y zpmUcR9nz+Aup&hI5eR{6x&HktnyZr zDv#F9D`4=FNTkm$2$GywDAqNxYkf!e?D%4!nV!Hn@Ktiu&n&TeesKrW4cj}u^}v>J zwr@lE*?M!K|8OkC+x<7+>@v;#{=5EW3%Br3!Xv3H3Z`+HGc*aU!664k*!F;y;=NK3 zFgVnfcVk2Cdutgiw8I-&HIt6?rT;?9^hq{%^Ag%2PsaFa-xM0?f@d4cik&%J4Ism3Az}%- zGu*`J(-GGcFoIX&;25=P`415DJQ`itm>9_sF)VOM?!nzTE1| zDJHOEPQqdAJ_!e3wCuJd|>;7%9z`sCnO zQ7WA$;1(*z+P+}GXWA#=k1d};9qZn}4>)dbiSB3qdu(&v{)SDRFvHAn__VrO!SC~V zwfyzkB4Is#YPRu(F;#rBlrg9^gmV z{!0G{`zF{VqHcBb<&@Z^6af!{`BSP$5GbIC3i)%{?kYMT8WkHFi_g(a1oi6<%sjY-jvEUy;@0z%$?qewV&6chD5jswK1DHBD-_Rm6{cyl9_ zc9K}9%&hSvS{?;0R_4O=6N?ZFgjp3>IhEK+?MPDDt?S5C8i-xhCHbk@9N1iH42nAF zxWTnQ8Lg8_iUc7rffz7I-;Jrz&w69II4s~LwjPT_G2TS~h;9P4reZwS?A$-!gpk%Gq zFEN|@6){FSn}g$~`6lR;LMqJQreiDwUr{X}n)PU*YzegM2}Mhrakxra94atPhf4y& zUh2s*GziPSF-#EllL$^?dp8vJcFQ5#Kj8$>Rpzss_ZVQX!mW{%eW_HL+>9E9Nqy>) z?E4vz+tP=uDUE0|6j&Wq}gMl+fRO&ro0gC0jJ{7Wj%@J{ISpe5m|sXI%0{ zYDd6X3$Lhz*h*@7hLw2%hbo7Z_z! z2!y8}j3pfcl%_+7(sX=qnvT&iO=o0CX9UQ5F)pX+;@6_YXH*MgM8bQf&tX zf;JH-d{{g$?^HpVq`>RYVPL#;Dd2aPqA{iaI2`WPed5QW&@z~&-XaxM$HsH)ORad0 zHDAtU#8&Y5{1wB!@X59kB7sl>fS}=cY8|bmHLSNNalPY7JLTq9`uNlo;LGt&wcK{Q z?T)SI{Xmbn7?X7e&`+x`54ZdR-HRb_?9wi_HbC)pN~5s^lLpGz^hcFXClkWVJe8CF zNr|3o%M&B2e4Q>58@SXXp++oESO@ioW*MrHbiQPhYUv5{RlLNJwqfb}(Q8MteB>M( zhlSkT>6Y#z)y8;wv;N)6^rZoeeYOeT%@6$|r*lPHh3(z?cNdhn3b>yQGn)Frs^Pc4 z)tPD;m#taY$Ml_sSuMUwc)#Vc7H2XP$n~w%TS*3faR0H!;YDM+G6zIGP6G$jpNn8OBG7N6i*sH2G$G$smZ|xXVXE1KH#JgZ!+8mFXCN zuFt|n_rz=LCBhh*gy(jCPE2TOfVBf0Dq080WwKDl*kvR+!hB5?HEJ{-D`PhZZ9fr0ZC8-i8L z15qnCs8Z-i#G~nXnA-2BNSNVyS+<&`px1rGGItcR~aoPsG)TF7pc0ERl|E8Qro-(Ltxu4A~y#|Tno~D zVZ3fX-}D7%)93cJ&JE+6?JmhNzFsy5vIR_a9`OPKdjY2P&GiNGb*kJ>+;@`uMZ;tO`#+bvcI$s0E@Fr)FnRzxgAQ*-Y@ zb%=&a#MJeTy>c*Aq$r7}MC-k`^N3kI!*F@s&db${?!H0E^2>&HCT1;-XGGR08#+TA z>$sYPh%+pk)QLACqk8o9FPmR^6t@YgA?qZc7}QN`ncCi5g~Il7M>5iJ$4J>}BVrxtfjjXnJ=-wEF7a1h1)>iQ*a{e9Tu_+cKCf{66y z4Kd`G0UAMAcjEpIRiOfa5J&C6aDKrwk>LhE-_-s`W#2R@d|=EKaj%#{Hc@l%1X#i1 zDww(f;M?ik09dE!Qn*slC&7u-Crv#HKfr6;qy7fxF9M%+Qp3<rC+jb9j>)mw+>D8$ zVz?{kX6o^%JY8FTHuO$;_oEZkk23fD%La=_2)A_3NMH+@7NAm)Rm1vOU*p{d%XoN+ z@Jt0W6^h~DRH!a@SM71f$tiy{#+&V8k7SW~8P+QWy~ zC+{A~m6o!0Z$;#k9|yTYF1nbc-B%RRd$$-Gz+xUPE$WWXdpRpjkh}JH-Ft-aYdMvt zM-4zU*pBB2vw-J+|9W{TRvBAwb2yXf0B%^=VebP00(N!9g!R#k8Z7qR1{5m#}66^%ZqLksHPn7c|W?X9BUVEsA8PAf> z6Yab=EevZPH3ojH`VsT_`f;Xd^*gt?1A!MYV4&dG$r`>}{+CUYG5xk*x4Z2FV>Go| z;$!zSl%)i*#+eztNUw#waUZjOAJw5`zS9%)aCd|*EWXF?X*tNsJ>(dJ)z0@H9C%BR z3zi#p2fzIV_MHE8<44RE4H408#-cndN(&1Fj2*H4$LPCcSCb}9vBSYPRCr>S%2_CC zK7Qoc(O6sX2wQ*ThrPhwZ~&rn<3IFa^GZVeF!+T01ZBXPAU{ z(h+a4D1eJ`4$I$*(_~t%-V7T>qwQ3E=cOy!ylBo4k%>{shAjijT){Rcj@pb$#zM-D zhvIR+P)t0M8o;5n1L*o+(WQ@pz&n`C!}qd0|9ZRkcou7GT7J2(_M>SXWc-Y;F6&A- zIwWr{agwa<#4t#@b@6G*?!$cjJDkzSs^MVh4DMh&20!uRLKwX~aS6ZQ#`lg|B{IsY zknT%G&vPY{w@hS;R>U;x=$A(>Xrn*+BI#X!G?RSa z(e4lD(wCiBx2|UYUaq5pVBcdNi4$K^fBV0Y!rgC5$jS`q0&;s9O3MAha8)gH(*G=1o#Ev011@>;o z^ktM8P+u|zxuE+RE!LApq8Ko$17%H6@FiH)OS_XzD z8R|xkWWalPa16;TRPil zYX=Phf)8J&_1nXfOZk!cr7W$~63R1^%VZn82q;!K;PsKaLA64pTyPOls>&yn>{1J{ z;DHc8?q!IqNw(0T8}`WI(oEoPsSTw|pY(+|PewLLoNfFFD_LT3$2@Bg30%zK_^1_@ z4U4dY2t|M5>W{P;rHW}E=mm*}quAipVw;D(Ko5t!fmGXI*hxv1+KAxK>)jsv-f#{O z^VTCa)Uob4CV^Gs4cepJnxa!^1S`f23GcK(;easyEAE0c*i>@SYt1 zxxKev?-87ZD2}ZUgr$1PU44{;bc6pLTY=-l8Tkari6llx6=x{ z*lLnEvaO(=4J;8Ye2yh3NRE{$%ocSMUJuLeZBN8|Nb31vbX4%%AnJoxudZx?Iuu6*DHQ=*SgwtQ%C(pDrXk3kHn0^1m}(U+nd9a&#Ny_kQ|Ej$r+ydtTvE8 zsePQR4mDamh^wEloU9i_G3c!ntPI9EZ(g?`*}y(YN2A=Uu@_}JhI?`&v3}7)Cu2D+ zQXx2g?6e65Cj}4*@1=3bEZW$@eVY1XN*G|oUF8vqc)0Iv{yP%)Qd8D}?o`!V86WYFGQ zV}{DKj#htMxx|&b|5W{9b_0_f>%Ef1ib`;kDQha{`k8b+B1Td%1Hw>=aZqAM2_6!e z6V~v*?&X*z79Tm?Qch`7BNt(wYzw16_4pcx&U4HsLI>&1}tnN?R=AMnR5vu?Y!oGP@{~8D;rb zJiANo41j2JWv!YA;QcPxwx3w`%dF%Y4K99)XCeQ7-M;$l*-d)@B4vr@dWv3JdLGKH@g`At($x86+VvzF*RtC^& zR+%%sMu3*{6a9%h<>lV<^tu`5uV2-~s6EAkF~m07>?s7!o-ejlbK{5%zw=9%Vx=9i zdk_t7A|NR$lb9Ex<+-WFm7X>`_S15qkk;vtNiYKh_k`Bp z3vQMa<7WTF8Xn^+1Px1c|8F1ihhJ8I7sd-lE<(j|d?$0Cf}e7ji(0NsLQ07!L?mV0 z>44tO!fcLYGOPY{mmiB6Dlye2K%9SCK4@_6l#v|hK$S2dPUXagzc6+GZ#HGg-ZV~C z6+IuG(~{rC9?@UVKYv|qUThxiqx8rt`W2-mMfWL*#j=?_{*$&){jBuWow!2e1wXI- zNvfnlJ$6DVbBmF?!Lj_CTST=mHp};{onM@bIboHm!}u8e#AL+!WXU`OKt@Inx{CgW zBP-nyv`-@gp)YPIJ5;OzB^|NDLbnJ#!;u0@Vn9KN(i^xOJzhM@b6wwD0L%G?j^-Fi zG)&pVPn3L6Q4zI;%Kk9)xj0Z-gFj$*Vml?gHEh>-c-~%?$;!*N_x#FCl4aNWn2b-? z)*W6H$wKfwHvDKuVj9?GqbQ$LD}(?0gPat{v>(OHMO-sN%G4i$Q%R7g=yme+AHvD{g2D@4)I_FE5-W4bR(~*ilK#?|QXLI-s%p=d;!C@f z(=~5U}GnMBbPwg6w5mrpa&hkHDkegU^RbivnGPb^EY$L19D*$0j^QWHw z>(d1m;9JblNh~H&4q}RQBLp{Wq8@ZgArqg76S|bRs8^k6+;JSBn_L zQOqa(T3%*U+S@(??_sN!Czi#_=;4)wAtc1=5kqw_+b7sXWLt{~@;kXI2VwxWp~3oN z0a|_c^bbx^+&n+?*tvhREwzgzr8O-a9y?l1+M>|sbTNAZ8lp}3lf@JK!b_(_tHM== zSW!4ER7F>609s>`W+tB;O(kkaTqQc?#~AFRw45OczcSBcV8x(fj-J1bDjdZ~@8$74 z-o7COZYjf)@18Gj1a@&qG#-&LJB^h!`~jm`^R-d3P$5HODsKy&j%Yui@#7$lG8qT9 z8&}ZIRZ5JUTI=Zet?dUD-W-)EmHacdREbLx>O2Nl^Kq7EG=(u)$H2wtfI!sL9rBjJ zuV02e2Gy?(!Z%~QXr94JlZ1G!j!OWR*GCHr4eg(w1cquT_f*PY_~5Y|^}_e-$R5Xt zk?~R{>S6Or=^i}XQ8M;tld%jkaZJP)S3Hu|))dY(F@?oM*V;%w6tyINKn3?^s9^jI z72ir~tEYazm!egaBiN&1XJDZgCO?y-z~qya9ojLI>9VwZMU1ji_+S9vz3ez1jaJp9W<$467&L{9u#o;l?;(*m0{y_(S{SB)@7DZ^S>I zAHDhH_fNOkNfjVN=B&Z`y_kvj@GX)A`4~_zyl4~%VWv^ss1(WbtO83ASd}I|Xr|a^3@?n@+rh$%Hf0}hslx_d&L&|(bs153Ca<(KH zZlhGJPgjw6LbDGi#ecHGJe&pLN0ra0O;HVs60qp4>(2UH{(AiU%ZD2r1aI*l_8{?w z_8V;hAFu>yl{GPW6cvH9RYaFG-Cz@Ik{=HH}Hv9|d7G#Wt%pn(c1Qh3!WR9gy7yHnw(*-i< zVt&3Rt3<^iktiG?+zGKCM>Yv0W4mSf9=-S<0#eU+IhF7&i5%yj6>;Xg2G7e8c@` zj9c&oiD6jof7SZqD5BjO7uUXmbDvj(p39mmvl#&q z^LlZPlQ9sojHoav-e$F>@|2Oq|JklLLjG`WqNsJbTOMw(TF-D%s~6|-c=^bKK=}e&!GLsbv>&})e_J1*M>aNz zm+gN&8DRmt-STnqdAXJwli{ENZ!o1lEW|o`iNi9(8Ng3I+yBhd4{Te)i~Zx~N>w$9 zB|MT(=wH6+E2=6%PiP&N8>#wh>loLVHQ24{*aUUAcjj_H((qj6oWy~$1CqcH2578E zIqYD5%Gyygel4KE$cbn6R_Ul@KZj&A## zVE;e(IiSc^zuK6MSnU;J6?8hVD2GYWaKLcIBRu5OS68^JdvhDs6B4Cco228C2?@(J zc&LAOdIu9eJ_DKo4H-amQiJ|7A1i2Q#?yV{5w=fW0q*<@F+}ln&qAMN@-{6dsVR>#cg{gVbH9& zHJWzdou$2mJEl%sUSIPD^sCh!gpGFA9=AN$xmCKkmL0q=>ErVH?drzH2WUp@VwNCE z(UR}**|LI!Z7JSXdmMR=xIBz}XgEAH5y*J6WaOK`4GfWfV;IL#=|UFc#NLDkMN{d! zqtDTo$@9up?fP4(p>dx9`wF{}@$8o~AIq4?VC4rmlke(8W`dXLcP&taKXHx-_wCl)k;rE!yzmNASW(Q`S@au<0pLJ&f7eD zdSAbsaDZWtpJ1Gv2alieV+6&{EesaA2B3b{3b_vrUt+cFyCx9SZrCs}Ti^>!Xem7P z^aKtCu3xt9(0N!a;l7-6HnPx!FFb_~N_5HQP92a)l-lock2_xs)b4wCA@a}WGj53& z-Mr?`7&n3tsqvOq9lHY`ZLK`ijf7EtlO?xv`3nwx`rQHSg~Cuf8EHTTS8dK5y~<4_ zYLBit5{)d@9HZQqdHAFVo~a^iTf+M^STL|J0W))oGX)k4Mh^_7Sg|YW_GclWNh2S0 zT;CQons1|ELB@W48a_4}^H%%53yH#x4^g@mKboxHES9gJdCrrt32`L>8sA*&4zWsP zBhM50B00bgzj~o(w(u?to2525Pke6(!3f-i2Cn!aPY17zpDcg8)VT;BSO7Q29nI5E zj}d;O+y*OCEDH3rF5U|@h;!s`?g{X-PJ0sUyABWS&MM=mhcme@&qtU`379lv=a~hQ zJXK7hV7;%-q#5m_0YNiRjK08U!x|d`3(MXGQ369ioOBH#F;Lor_nS9i6SU?0g-5Gc zjFdl5Cg=Mzekk1rAFK(R?0Rv$@Iw;Y6?jpzp7TzF@bwrTRpJZPtNU_3uXbV={2dl0 z2r$rw#KMR20x=3vpg6%4l& z9GKfMl6{Gj-4JqO)D^vJ>}dfGeHfY5m*EGXJH~DpH+wat6B&o~`AZyy7RJ2yR5MBC_#x+11Jwnlr`v8jo<0;MzM73^I{B zu|7z39%7JT^bn0xYy>*~O5O%t!1Ky#syvishdT*iF@x)#V)qr|W1zr&JRv$KlBU$EK0dk9g7_KsYL;&uqej}!~w4;VDmmik3=S7=IN=Ni^&jDRDkwj$xmO(0mM3PiW0)@#aR6> zH&~zPhI=)M?2*NAhgaU7&EH@{3tRc!1zxi`#$(D(F}{0s50MNh{NsXk3Dnah5$8wJ z8nF2QC;IB6p-=K2;xcitCl$c;bvkT4uP|Ku9o`}c^PTXq;KkKdNGY00bx0x!1BB!? zn73F#dw2Lz2kz3fQ;}GPvw<De&fP;{2!qEo}4>@e5o4ewD6G9cK$dIvdMBp>>Z>tXgDXYCZh}cK`dD~k%TkFn z5F!(28d+U&1R>1b^#zmg(3osJnV!DHd~(6SQUlJ&nerjkiQ?u_IoyqAERo~T1IMT^ zi9cQNCNG%@c^}Q?S=H=W@OV(p3SWX2AbND*6Oec(f$th2H|TFYxxkN1Qd6YH3(kNn9~?r_llj0h$g4@GGxzro7$3T~X{?9Oa5)$**cF^-cmo zxslk(kSPUlc|L_- z>lnN5+i_Gz8fv~eL5M+?RX3h4yQu^2THf2VZ%&a1VF;Dp8G&)q2{L!meoA$t4tDJT zYGhm&b5@DL(g)y|{_`LszN`+IB%!>}a1z9L*xWxH~d5t#h5vQo3wje(4a_lENXz*A#u@ys~)e!nY%!AIvX(&QB9ej}~Wa1G87qTPR za+2tFooME6kvN40k_gP3=7d=?_<{oN#iT9`Z1^zcO)sRUZE>!qBAVF zV;tcS(J_6Gqi;Je;QMg*)(xLd8qujbvWcCu1ZU-fzo0>va114`o;5!a>^^VkZ|JD8 z^KxbE|46_hMPqktAM%Qw2+gx~xvQ>u?(U0jP933pLgR4zXCu*BblqXqW*r_UsMG6; z+6*uQw)LnJ)WHEqor5n;dWghzq$%3imR=XA60JqZDTuW7NBzT!((t3;& z?~uYAhr{-@a$;JT&MGXZXo}cMUW_B7xLG*k0AutlI9q#Z=6pF_$b`%q;!JT4$1?Y? zLK{(T3S3F&6}V0pVM3lXxsx0T<7RXt)z^|!9jJO`Qyz@$Hn(5FJz#F$y+EYFO+$w~ z0Bwv2=n@~kL>H5P*Rt~gjg@^nmRW=|dh0g0pb~frfYyz!-Nn2p;&3QuYMekfE-fVe zT^??xFU1j=43OBxUvO27UsssSUr(2dxddf^17`b{1jZvysIPDd$E0sQ1lZ{HSif%` z61VUvnwLc#5`@DyYr*JZ{Zbl0d`dEOq(>7XY+LiCfAK(SlJeCz=e>L$008A9vz-5G zI8JBd&q?>JNFP-;R69wzqha5RxZ4%4|0%cLQ{X7Du|@<;dVe82!C!@6By?|MBQXAz zuz|5gI%tPZY;!CPcci~c114|J5sKv@k)yd2Ab%5M(d+Pth~+XnVlCP~9%EfC;8E)A=fk{nWJsOjFDgyp$_VJbfk+1rsw&su0=7 zg9YIkoV&(cnqF6CcZXN@==b%EM{i#$#YGJQL^g%r8$jS=H-d=~V8C?16}HddG;Em^ zbW4Pk0JFwWZu2zfbdc3qBRM2e!xUpW=bT5}H^!0k4i0>I$!v%-3R@;Q5DaBZSO;@4 zOOBB!Y4b$Kc>xZ9t{|CM zIXICsYvJzuo|F6Sn-9uO^=%?HcQ%RGojGz#$j;_&>}7YAO@+tk%*@4LLlkqTNJ4in znXEy-VX*zp#S|H77XmY~pTMVLHzRWC+~^MyVqa!C1htqt+{xhXeOw;I!#m)8rQziU5ipO!xFFePe>;^km83jkZ7z72>FA!1z z=}+uK!mDnHFLdyI;EYgK=0ZT{VOX8_H|&F$wYl9hNG)LTNB+jmh-uB-FcIQWWHSSR zTfc)jhu7biV0eWYDE0>FjLeL9j4mq3!cZ}_wV%-onJsZMh%3UhpD`DLp$m*I1~krM z-%MV(K@j+jTAWTT23GFS3qpbG0iVyan>FlRo3x=y<{$jBYz zlW8me3vs|`dHm8fhKpRhM&mJAo+BRD#TftNP-a7LQZ_fHDqPQU5O8xtjwTrB0V2ok zVRI~BBAFtiM_)OHGkkn@R=s6W5Q*QdRr*4SEOL$dF`IyvLW4H|df0#?Lz@~WI`D0UKr!9_MfBGrX_ zBGUIujI40YrneQ>AIAPYba4*62d&7Mzy%|43GA*#Xx$-?XtKT4H85*MN>1284 z$LjCg0y3x-5$TJP0K(Jg z0_7O%nU}hJ#e+<+w#cjpNBN6Rk~`LCscfg)17tlh(72f**0nHvVZg%P0lqmv6*0qw zAuD_prrpLDOvlPG++3y7)7@aimwR~3^~;-Rsjf9jrQ@(*D0N%kHuoI-3$m6j@(rJK z(quXMAx0e;I4_pdJ$S(B%`j7va*W6)qApBhR?riY*Vv3krI_ZcGdwklhKhb}=j!7K z?_8AjQ_tdxt?uG8XCVnAmC-#);)l@GU-Z1_lQ{cGf5{iCB;6;gR~+xqcmxuh{9xEX z#(E^V+bHi_{ZLf+NE8I+OHJ~xYcVb~BH;!}Yt%ho<|87;B$ljo-$U}9*qJQ)o>BBI zBP>pPhV&Cf`gtO3qyg^y{1=O=Ovq;@=;o`lQ}`!M)QbOMCe+PCM^sr$!mzQyU};st@~pR_wc&%{@Ft-iU-26BBC*%(gnrBiT7 zkX9gW!cX-l_pn^0*HCD5Oe`S0TdCQn{|Y4Q_ZHa7 zm}9>YVe8ipQLD~>!ni9tKT!4|s-53SKMRNAZFA~2k|Lo5A=0k7mYA5F3E z7ha3`zTlx5zqze3$bumBOP z3}x8B;J44nkvWK!E_`xO!cYv8E5uKLN)&=l^?-+Cpj`I3bZ2eniYzm_0a6!CeBg(N zA?7ePzM*nioyC%89(jrk_09*ZJb7x2JNJh%Pd2ODa2dVG?Kf4!NrE?JM#bS_5)bk`$ zw88De&IN@(G_BLU@gcUiDW)66&@C+l>^S#9(t&!UlRUIlrXae{n9&{aYZa`pe^6dH z^IFmYNCFny9C2t1rgrJ$cegQM#m%~Oz4*uVRGc${?dsTlARlY6hs23+ZmTD-gux2H zzmM~~-Fle&L9_V`iIWW$^(5qMmxA^0TqoygzRHG0g8}5lkp+^g}_YCU{ z`{z^UGainXuNe7bhr3IQt~v(Pq6;^*cgm2*d$cg^85QrvEUdlz*vN#D5$`H!PN0}C zM#@50)v1QF>Jtgi>5;hQ!@v`k4gx%+X*^^?rk_+1!+o7%JFBdKj3^4l)-AVqEBht7 zJMyxPysD$`vFPeLb~&eA3P{AhFAIWPcEJae?rvfa=P6wOb+64}^ABx{rE-loxf>`c zI66kcp;8DV6`U*Vx6BL2lZ&zt>ndd-;;yoAiqNvC^FPn+oq?#~N3RfR6`bd@SCzu` zi?R@V(96R065{TIh|UG(1*)Np3x&{7QAiEtg`%OnP&AYmiiYw+(NJC}8p;brLwTWS zs3@d{@r_9!EK-jvi?w5AiE^wg(T$ZQsx`CSXrVL zR~Bo<$`Ykm8FeD>zrvZCA66Gm=jyf#Y~~QXZLCLtHh>Gd-ONR8Z|0)@H*?VEGm=RxoH?3xLv0d1IhA){|3gk|u zP_0!bz)V#F?NcL&VQK`iNR1$-s1d{lHG(L=Mi8yn2%_#PfqJeHM8P#ep;>N3jjBki z%ZR0>ONLD21w*d+oT0El&QMq)XDBR^GZdD|843&K427j~hQeY6LvFd8p|D`ikXzCg zj$5`Y$%QqmEU7(fESW_sEV)f9EV)%HEV*4PEV*SXEV*qfEV*?nEV+GaESZHXEV+#< zEV-3^!YHz{ys6KuMPX6L4MJ+iK*+4;3AxQYA-9w#TKfTIj@n<6@H%88%)O(vs<+rJ{K$}W4w5ha3n@WT9 zP+^rem8NM^Bl~E^@=)JhF^RY=wnofKkd#NdPs$^$C*_g8 zlk!N@NqMB>q&(7YS|0S8lt&s(%2k)##%560*SE3S$5pVR)bv@WxYMt)noOrP8P#fy zqI#`SRI@dT>b6Ev?bax&-x@_VT&0MPYZTRTjZ*5Fi#XUtlB!t?D_Zu1T*;0gxQ*{@ zgi^&uC^c+^Qo%+j^=pJuy+$atYlKp{j!@{<2&HO`P-s>)>bCGJ0W(j9yqIqZfwB=!JbUdSRxVo?9!U7e>qIb=xu7YilUV??++aI}jVz zOBibAiy5l+ix}z#j2P+`j2P-Bj2P-Rj2P-hj2P-xj2P->j2WtSj2P;Mj2LK1Ir)Nz z>CG2B*$w~4YNMs?xe~@el&3IaKXK0cz2trmZSf9E$yohNc=!@Gw+2{s2Xn_ej7zW* zx35_rIB}Tda=BRAlk{)`J;OK&qhXxH(=bi~YZxaHHjI-{8^%e@4dW#E266(0!#D}Y zVVpTGoi!bNdZ3oFG!ay=)!~z~HL#PhwSbecwNR6>wIGwRweXU$wZM|GwUCmrwP2F7 zH87H~wE&W_mFVC$S;s`5mB0+n2YV{8!{T7heXKD8Q&Itzg4{2ifg_X7L6MPjFl6K$ z1Q|Iuen!rXo{@87XXM<-IXN?KM$V0zk%KYaJ|JgE=PZiy$WAqyGg5`-EmYEic}iNa zO-T!eDQUqfB`uhwqy>AFv|x-1&0C_R1v8YiV1q{sr+pdvW$Ti>-iX&Dx&9S(t|6?E zr3SJ);|*i?_8i0>OgxA^SbY$C0Ky>lfQ>=y0WO2s1A+#z2T%=T_c$BG9-udfJ)jY; z!Zsy7y5QTv5&CMR2yG3rLsy;b(NrY|^i;_KEmd+rN0l7VP$dWJSINQJRdTRyo$Rey zB?s$O$-!D~zos5WYU6BL7+I(wINRg|Z;986jvqBLrRN z1h3_c5cHc7BF!e-gj4-iNU@gdWUuQgIcU5_j`Ut5N7}EEBOBDnktJ&6$R0IvWR)5@ zvQ3p7EL0;$cB+wSExs=@8RNv`_>8hzp;)LvSXZl&bZk|E_NWqQi7J6Ms1m6CDuH^h z5~%Sifx4~}pyeuo`mGW~vrsG}5bh)2d|UByJT%kHtRqSWiWMp2blD5mj} z;uKy|oW4tnQ+G*m+Ab+h*(Js4x}-Q&S16|GlHwFyQbf-@)(4|2-&nvY#^FM>nik|7 z)M3mas*4LmV{w5fDlQQH#08?3xInZK7l;y)0zP|OAdegu zyE@7rcXgCM@9HRj;MGz7#H*wHkyo;zK5b$C(5s{K?5S5r`D3q+_}uau9nT_^SEuYe z66;X@?Ui9%qNhRiiT(z1La)O(sqbN&w8Jn?+GiLi?KX^)_8i7ZY!Bn4{ReUa7sEJ- zmtmYGj^Isrf($?KgSRyC`J1$&oLoUpg+;zz!P*S0B};*of~5pW!BV25U@2iyu#|Wx zSV|xiEF}U8meTYkOJVbZr8IWIQd-&E#J--+s^3h{EL~%%Si8ZNTfELzSiQU>TEE#5z`iE zV5ce2LQzYgiKmu86Im^RCdOI}_h7Wj-viMqe-A>d{5=4z^7r7g z%HIReI)59TRsJ4uR{0g1atc*=@!uS$FEEs)Tn44QQ1Fr!Dk!o-wR~2n=FJM#hFPH+ zDl1fLWQD5oyioL-6{_;G!bn4$?xS}K1AoVQv1|6Oh#s^d^hgnM9qB->qsN!)=*i_e zdT6#7;N7d`_lyV(Cpj=1KrZ24ig-2}2vaoxOygYv93e7)%Nej9tXpvqDTBM_b z7U`>?MY=0!ksb?Lq|<^H>9?c>T^F=S?*%Qgft@O22-2%q-@e3a*_8y{T1li)D~a@I zC6V^5B+{9cM4GaaNIzB*X~mTUU06w^0W0Bi^TTmAw>o-&OeV%b9!gs;7ELrs>!uWR z(~zQ)8YF6~L896kBCF@{zQyKAe81hPbi7XV@j&= zh>|HhqGbAxD4Dt=N~Z0Ik|{f)WV((hnW|$-s_BT5DLSI4o?lPyPp}jH0S>+457VZ< zpaR{6=*%uuO?IK0u?tm(UFh@Mg+8)f=u_H-KA5|ZXR!-yzFkO*`SCS5_QfVk{!l{T zqfOK2>ZRjR`{;c79=a%`hb}tmp^Msj=%UFUx+u4YF8c1FiwXMZv`P54W(^t8_|5xjeUf)>(P*a@jW%l0XrC61wrSC5mllmSY0+qpCJnY|(P)Pj zO>BU}LZ>*4%N8&gW8uR`Sj525Lwb-=JNXt|sX&H6mPU5u`l*U7HG*gozdg`OD+qH`B+Oot^n@WAQsnl$n zN*%YU)OMRny|<~fKo1qRXj5sGHdX8+2gw~FZ;0dT_{{^Hi-ujbV>@B4B&c;7Qqq~# zE7v5^ubV_MO{-e5Op7RnX%WRPEuxsEMHH*Fh+>o`kv3@&#Uw3au!zlzo=@=t#>JXj zgz%1qR$3!PR%wvYY2>bTvbRi?9Bfl12kTVH!9G=Tuuzp8Y*Zx&D^$(_&Xeefq5Vb6M-Q>j0FZ^F%lRC$4FopA|ruepo|2D;W82!2F*xd7&;?? zVE~N<24OT37zWcwphT*L_{9;}sh4bURj}FS%t6`YEYR5EEMeHbJED_h@ zETPuoEHT#NEWy>}EKt?rEaBARtl)^>@s7`+$^ev+Yse{C3dj^JC025l3P^I63OaI@ z3M_J#3LbKn3J`LZ3IcMLis=iM(&jl!#n?GZ!^-ZZ*({Peptz&8>>JUm#trnkWj(!N zR!?u()YBUV_4I}{J-uN{PjA@K(;G$%^tuH-yIY}iPN4Ff5$VI3tlOk>2lU6j}`iV{nUJX$cS#Iniq!SsdMBW>sq>X6Y48;|sq>Xcsq^4mE*El z$60xUKj=5%kF=YVN4ibQBh4n|kzSMXNUKSCq|>B4(r8*9^qG`L+DyuWF1g21f@`b- z=r{c2TW#vt>cmL9O$Mjt7K7J!gCQus!4NFaUebAUyromsXuMYSN65ORy;#Ka;QF||la%q)@;GmE6e%pxf&Np%gaBsi{#iYG##`nwur1=5|S`xnWXj zZkd#tno|GT5Uxd#*{4mXO$GIa@%)=eqcq#;Fv zG)S~YgG5s_NVG$PL?bjvv_OMI^|wgSeS<{BH%O*+cWf2Eh>PjX@nXqu-^!<(A=sum z8}xFBDf&3nB0U^tm>v$ZPY;KgsfWX?)x%*%>)|ll^>CO8`#98+Jsf7x9*)?qG@}K6 z-{Oe&#S`1O#S>e(#S`1P#S>e)#S`1Q#S>e* z$rIVT#S>e+#bY+tSLh`W)T7();=^=9_o7gK>9Uzihdb3Dj{f#0eA~uSYDA_1dB4?vWiKdMrCBim}l&IS% zQX+A)NP*6cA|+xsij*j}0noe+DnPQ!eUdkIK|P!m?Dli#uqV*_)A$TEkf?4~_y`IY)`7 zjH7~~jH7~^jH7~;jH7~&jH7~yjH7~sjH6=voTIdN#!<0x#!=c;eMd+qahHl4E3=WY zkF{d)4eYu3``HUD^s<-0=w&Za(#u{#rkB0MPcM53re5|ES-tEf%=+02%=NOD0PJNS zf<`Rsn4^k6)}htn2sN22mN%(N4Z>(kax+{Tk^_)immC7(y5tZ%*CmHQyDm8d<#ov+ zfUipq!G2wGNH=Im4(Jhe$swJiE;*o|@Qk3z_+ZYt&?dJX^A70~8UMzfkn(TFf5JZi z^$Gs~tS9^f5T5W4z;(hu0L=;i01PMm1CX2YZ^moFKLDi(Ke2gujPERReRr{RM`YB> z1Zx#ul&|ajz)p>y7^#&L3$=3EyjD)z*2-zaS~+c1E2mAW<d$~A7@_XFjoP-ulb=ymaj{E4;bq`&1-a{AN_t3=#J#?`}4_$21Ll@ih(8Wf5 zblR$iE;j3-*KB7W^;Vm$>Nc$KRBc)3D{WfktJt>2SF>@AuV(8SU(MzsN?rC4$9LF;|QaRtRNf8M)JKFipB*q`waJtDAU; zjSW14iw!)2i48o0hYdV}g$+D{gAF`_fek!j|4lr!{RSSf`v#uU=69BpG0uvfj%Kj> z-!71shKEmTsc=;6Ugv^m*j!g(`&wP4^=n+E{cBt$0%}|(25MX-3Tj*>4r*K_5~^GU z7HV828fsi69_~(8qZjznR%SR?T(Nva%FP`}g^fL_G^{6;*7T&(gq~E|&67%Fc~WU1 zPb$q4NQEstsWgBmm0G_yuPNP1D>@I9T;-lpXxvdsg*!^AZ$~NB?I@+T9i>#Zqm;UK zlv356QfS&yN<}+Lsb^bkQ>T@Pw1iyREFqUB zOUR|g5^`y+#zZafbLUB!+}RtrkVv?jD;;5jHOlUj1~LV z87r2qGgfR~XRKJi&RD@gow0(5I%5SJ4aO2Bb;b%_>Wnqy7)Ubz%nuxMQ@(!9E(cPe zm5}hX3JM&pW_U-d+1k-+=5@51MIEhXL`SRH&CzP6^0bO|9Ia*$M=NdcWOB+>E+zxU zli8x6SNd#?C$~+7r?64UQ`)NJDQ#Brl(s8*N*k6ur7cUI(xxR(Y1;}Nz=FCyhC+)p%PltMWEbR^x4ftj62I*ptcG0&8F!F;?MgVyw_*BO)3qB z7K9u_6F0`t28%JY5n>E&SQtYa3&zj}fHAbKZVYWxJBFrxjiGH+V<;{8Y(D+l#e^x6 zY~mCM6$>WB+h+@GOQEb;Diq%>~ zu~&;ImTD2jMol8E(;|vpTExsE+_fo(0byTQ5DBHZEfPv%h?tUEA);g^h$xx~eOqGW21D4EV9N~Z9bl4?7mWU7uR)blf0;eEQmx4B>Ai~zoZtf$Mg2%_yC;<{O- ze4nc+NwqhL)O?djNoAlSo6fiLgbJNOLrau|@8^T`xz+>;2Vga`s|& z`EarxVeko0)1W8zY4Zd#HF?lOTtiztvC&#QvF%zsu?br|u_aqPu|Zorv0Ym{v3Z+3 zk(FCKv9Vh`vCZ#K=VSS9ksj9N^`Dg|HZEzwwgoM+X--=v)vbk@o~1BXuoUK+mBL)5Qkd&f3Ueh& zVXi$X%+;oanZBejSCkay8gj24C0cTYg{C4xs;xlCH0BAp);uBCoG0Yk^MqW3o{($N z6LL*@Laxm~$TaE+xmG=)(5$|dm?+l2bLbp00qhD3ZAXMu+kud2+Y@qadqS>lPsp|H z3AwgCA=kDikN_Rs|>LP zstmCustmD3stmDZstmD(stmEEstmEk>I{+PstmCOs|*!OYR{p+^1a7I1EpregjyId zrk2)=s1?&i)Qa6AYQ<;~wPLY|S}|8dt=KA}RtyzWODjdxiisks+UM1L@x~_5ZeEcW znMEqaG(}k~HYkc!`9-m6y(m_77saaQqF5DN6su;7VpVBbEV?X;Rf$D$(B53n!-%yP z7e(63ik#Qi+(H zoWUd`=h#Te1t3y#fr6A=m_8*J_D;!#kyCPE*_2$EGb86VOv#1eQgUG}7r%3cWTAa( zvkF6LxjIv3zbaF1#Trv#%NkQ*(Hc`>*BVn{-5OJ2;~G<8=^9gE?Aw9WnMNh7q(39)- z^W?hmJh^T;Pp+FSkgGQHy#Asxue7+#X(}r7>WRukCo2>@x>#Y68;8Y&1;1*sYN$nmq8cRXt43y6Ysbb(aLjGWf}qNA=vDoKNOBf#7&L zL8^e#y)3~Ro7l`U8`zwc`q{k2`q{ko`q{iC``Nrz``NsO``Ns;``NtZH?TPr^s{+H z^s^Bi&qqrh-gIOqm2d;fUq-WwNs}N*Xi0&%?vaGmo07D3lSm6UiL`5zNNYBUv|*D- z%QcC#SDOecHHoxMlh|hwx5U$wb_~51ikRA#3QRo~^Gtn~^Gtmf^h|x0^h|vg^-O)1 z^-O&h_Dp@24op23_e_13_e^~VkS`23&9C1dK7QGTm*Z#~-Z*-!t{i<4qa`YM4IC`wB9DSx$j>N7P=d-CS&Ds}DaNt`oYlWEh=sH7W)G9-4 z(;7o!(i%fz(HcWy&>BNx&l*Ew&Kg5v%^E{u%ql}{%Nj#s${Is#NiN04pCL^F>Tn2?>FuMz&@OAy=$?M+sa5~Eeg!E7~hWVi}BHeH1r8?Qo+%~v7E2CR@{ z6IRHv5i8`_j5Ts($O<_&WrZ9Y^Kf!D!Wn7Df0-;-(?y8Yvhu`EB`p}KphXtSX|Z{7 zT5Ow~78@p~#a78_u}N}TY>%848>65_mdI(b8FE^W4P2n&yGaiV_{|JF|RW8*j5>O466)1R#k=`lNv+Yp32Z;Ol3$cd3p9?F+1d` zqDy?AL(@MOJZ#dit+Gl&g|>-WI7r94B%xxy!J6Uf8p`d$1HfPao(R!W&wgx6+%dm zS0M(#mBa{V1u=$KL5xvV5MwYE#27;bF@{b-j1f~1W1y782prV#-^ja*%cn0# z70HPBczN${7o*wJMOnpz$!b-Xca$Ds_fU@(b9=CR^A)`5!I`SleUj_J#t;L6Yai}lOJ za@9S4Ge=0=9UL#_&z7_9!SopRoA9Sult1mv0weApA5YHLqxtcqyUXS+&S|mW1kf5B zM!n=WhQ;g{EwD21U^G9y7@bbKr=!zV=i6y7ZE@|uW?Jqwt|yZd00(zj;WoC!aEYVU zqf1ITc6n9*;dFjyHbra95go3VNJ(e%7{hQ_^7Oy>feX6@t8El_vM_q%z*@)K+;DC# zCAnp&8%~)Ga-`!1N2K}&hcjP;Y*uYhGj%q&+U95o)NINbc2F~LHeWg9oQy7JYd5>r za{*x}EyZ`S_04i$GCF<*@c8fb0HgDDcd&Xm;t7?L=hN{7HsoUU3f)3hrf_FLI#Xi0 z5RtG0-_yl%3YQ{nn$5fy!dB`jn;91!u1D(&oT82d0NQX4|Asy??PvzR2-YF9zz6jL(uYB;RSPrUZGg3%vLU)-Inj+fK(HK+9@ z)jQ3Sq+ZpPKf@a4vQeF*6_q)HR%IC@ZDOF;*4Bs;R~8j(CHkOd+NX=fY!z2j zQhjA5Nh^FdM{aI$I)}3#ms%{TzQU5E6+WNNpFDlcj({&p$Bjz}zM7=eo{>@WXy$q| zw_VMm4iJ2^<9PmZv_3eou{+{4*D1Z$dXE!c^ZO6lU!IJnD}7J>@%+x^F}(5NtLeF8 zJVxIWMtx^O!!b{k;dM4!ya^$kHEPv894%j=JF3L<(d^a1`~*P~Vhwadf)>yCbhJF3 zz^^S9>+arc!YDzU<-NB^nXV)c2QT#+-B`ZF%?8K!#=YQ`;D+I%B~H9Qn$CbRuLHI( zxvP*VdXxcf;LowtvQ9xmn!?2lhhaU_uK zlfRD)u*I?uFbaX`(PaJk7dD{4_dUjE=C$hYCOG(Zj%oARIib_tMm)?BM4t>DPc}~I zDh{8E6K%v|E~e-Z4@=m$oTzI(<^Un%2ULg{X|-7HFEJLb5$++5>>f?tH~>AKjonY@ z>>?`1G&bhj<>UlIr(QUI=f#)bEwIkS0n7rXvDL(;@-ZGG4xP?NGZ_>mK4fd1l@XS? z8llJO{9t}OyI3JArCLXC!zCW~8%zdX zPG&O=I(k}wq4i|?7InE9E_$VfgwACK8r_?MG}EW61T?lnVZGH!SeNbf~*)QPZv)n z$aNsrvFh0SYzj@z+c*w>j_z($-+`nWAzIL4453_mZClPymSf3EWogmDe9iT9no(}` zXkngZ|22k50bnk`2NW3yHflqUSES z%*}1&^NK_AG`gWL?kl8qV>O8K00WazbmWB`9TpesC>%tOot|JW?;R_lYoYNNA-+*# z=K=Qw-!mCoJkNj>Q8QF;`H( zetTjMsIwqH(o(~m$V`?t6y`nEM~j*cMF%*d!Mwt)JJ=9lO>$4TOdlFe0{u{6ZVtz^`7Jg?d5ngMA@%QKifHP6e>OU;Ga2U6 znRuJs1x45zfS(eJ`D6-WkOvE-``u6*=8UT&c`-pgof?%^)=e;l$6(#b)EZCzXrZZN z<}JA5bbGR&ZA$Rxr|>menW9Swae$FI9ABp|*EyJBBAvcf`TXF%F9D8{L`dK@3a8k5 z*vH&}m0h|3)v3yu)oX>qm0L5yW4;U%#%C+2)l%t4u9*mzRu&41M|4a6yhx+Ln1cOs zvOetBZCyt|2I_n=zKv~jNHre->j!h(*0cE`FpOx=r?c5@M4hjk4lzmle!SMsgs+}* z`Zl|~KRsXhCs}p&tkeerY*|^zCJIg)bt>}H6Qp3#iswwMtfUuw|M0=nC;N{M?>~O> zaR2GS<3~sPnAVwK#1Ia5Jb)d|_~FRcNU4EvVIqkQxU|9Iu&QlFVPFGPuhn7{=dAdS(*? z`T$mqs`p|&?kQ~*xyfg<6}K!5EEQIjkDvLc3$ut-?e5|xrs)2R&o8*xQ>oF7uJ_*Z zuu5xU?bePF1J3m9;;fH#z=L=|xq2iRd9?7)R%?P6lDPq13O~N!G{a4-N_BYwuPOAp z-ly~uSODAR2GS$85wJ#M9sqKKHs|B4e zvm{BV1RxUlBqa)yq$u1s>a=?Is6ZOR$9Qxh`-u4SJFZo8wMeyw&M3OQu z>r4I3kVn8<1X#s!QGz|R*6&nscX2HHqdMASe?E440G>`KZ<*2;_b6s(a3*H6O$kw> zq7vVv`_tuW&1@aR9=OWavT%i=TqC!6|WcKCLExSpKzW=}B#^9B2Fr`%`24H&1Gq(L!W z<ULQ{ z_c#2UmA2cpUQW(rZq4_2W;bv~)o6b{U%)lNo~s+OB}IN~kh%43o*G1gAaVXyGShBt zNOq-ar__}PO?2VCdFH#3?!dYbUYQ0kb&^oAQB8AC!5wb<01Jd}?)8+*)dOouSNlSJ zTZtP2d>;zcINZs)EgwFCrfOVkgQI?+ zD=yJQ);kwu*f=!UP|D;ht_+vn<72(smiRG@A1@cH#mV}{^T~@F-=Bpk>x~0kse_X2 zmcPa7hW}~4|Mvjs#ccA&?%|;<)LC-r*&16suqRJ9xp^V1WnxR|e9V@?HKMb02Udi1 zo^G{M4tMFuPSRHOO$e@D*0t;s+xKSYzP{zFG4=;M#mdSIQ48m;eRRL{X7a*sYYwh@ zdpPF=;9uaDkx!9lV!CSoe2V=XzCd?Fa(uCoMPo%PG%>1$0Y&_S@FU4LvAFL z7)p%qF0E$2NbWcHT+%==x->{;+1SxYBYIR+3%P!v8v`TGP^q&=M0EHAeC8``@*+<7 zZ%7(?NU|m){(toZeZ)mVrdk#yk6zmY zYl>31n4lV06z){%+T(NFDF9$ax0~NgXKm$jEvG)NNnr!0tI-%ab+DhP0ef!F#Au7T zxmg-gkjwNZik6U9>v~^OrWuqbeKm4@qPLy(YE4LKncgVVe&`}S3kfqZgS%h^O zlp*fBmv~TY$TLk!nXD~%Nqt%&I7v3Da~JNezQCdJ0O;U2f9boI>jy63d(6H6emTnD z(LJE21b$y6?Q!+_Cb$A(8T}z#+7`F6ByNCa)1A9;!j?}C<5os+{9I!wp4ikH9w36G z52sk>wCSGR6T8epB?H_UwsTzQrbP2Nm?u@-8*U`q=_{lIM?((`8ynoRiw3TkNxYse z7jtg<{L2V0Twt-wG+m)QUyiUMfE`c>s33q{bq)uS3$hsvDvAmpzu+E%sKB)7w^>9a z?zy^TL!vVGC{fYV(dt!nX*P{Yh$r7Gqw+|N8smn?;RFwZiGMuFh97Y67-A?mEP0~< zhFvb+Vuug5pL7qA=boOAmTO^<_Xb4Wp$?}a4y#To3~pWl8c;K*jIhIe@u9}TR(S6< zbPl9S9Ci5gkgKW2h?OZe)!*u%HrP-f1Qz$s(fQigeo&OZ=D0IiyOl`PdIHf(QJB$I zh)51V$n1>!X)Gnnt_V`(1yv*ZzV3RABw3vQk|6Vp!}9+-JvRv~rTv#IPPuDw@&XIuNLUAwY(ti^GTF!K==poIQ0I0Xm7vOWSj2o%8R-i>1ggF zJikF!x~B!G^yBQUcIk$ZsK~X9@$g#q)_ZmMaJ`Qvz88S^%#r179IA**;9P>poRB(7 zw!I?irQC2H94C}Y2BA_sGvKF#WC~M$^W{9{w^lapJNvivcHI*@e0+N$t>%@ZOk@vbKzJ~}n}r=5Vl$ZE z3m52kw*c`OGB=3sF&<8qMA7;AUA(k5okzwoZ((m}xX_0_D`INh!?E97vZxWRFR^?1ID)=liL-=kZ0O>=3Oz&EXC7W!kcZ>@$w-+v z?Q{n=TFJHty#I_hA+)1dkMNAXhd4j)CTi*m55_hLL;8%V_iz<8#VH88`u#q5H_bPt zMGx+#jmZ7IMBoKOx@7rMfB9E8LeLX{&P68o<{C93se9%GW;6R~Pvl)-khRq&ozxkd zGpb#>sR)W?0`B~$4heBDF#+$bsT~gmb7sbqkkEJIF(>Lb#vOs4D&pHXuohpMAOKL} zorDR<%jvQ4t=!(uisG8^lc?*$rGsi1!z_7a1C-`&?7|Ia>y@LrS7q(mKZtqV=!smD zqnv^QE$~E82}4g*=xcX~b-u$itHW0!uzu-0t@tMTZdY)Ju6vQF6}gDX=q##0lcG_9 zHNmYf39mkfmrI;3xf^Ubb=6qsb(i1UnnKiq-y^uf+8mDx z(0A%&&BER<`s06p_4%z^`=8yu{|R#WWsn8GSmxmVZlUR}j|bsg{3mAqHi@?Krddv!hU)fK&0*YsXp)q8ba?;T@IsOY4w z?Y+9X_v-q-S=aYX>YM8pZhgZov0FIRv)s|ysG^hkc)*q}>Bc8SAY-z4XJs1*Fqp}g zwz#rxsgbZeQz0HcVNl>!{Jn=jk=rs5FWhsTTKH>3KaQ}%i~*6B?AgGO>_*$Lj1-C> z$>nhuMB>t1Ra8PJ&aDVerK0ER`Vd=c1dMgEn?hFbZBvBH*SbsG1Mk?$0z$O>Bw7y> zb8BHJ@Qvs8V(bEnMKLb;Rwjh{UCg!^osn?1-2=M8?BK+D)9 zmlAM9s9JYDO$vd}DgrmLRaa)OPbQerVsEDd3QZ{SMbXkdG!?I3>6!hEidKIdA06GS zJkw34`?Ke~RcXboTi7n)_F@3gY5KUR;_FJSWf47ccJD9CA}A5Nl#X;}?Xv-e>L6)B z^Mnuk8T^HQnH-}zUwc7dE%o>na9@=u2hSXf@4>Qp+iq3>@Xx)K;sId>m-VErBtOXZ zq5h2I?X-8R`#qqDgfL#a!|nok9H2%vIQTi6kGm@k6~CTo_7)1iTVRnBtza-L@6ty3 zP&W;gW<5QW9#km#R%bcN)#smupB24~%2s_<7ICwYy#1wNf5;J(tS22a#gY=n-oR@!p2(qnE_diX79CfwOigsc;0 zc@*?oSXL$zk~YFcG?8;XylnSP?Rj`tff)G8XEh+NNeOOw$g|PEKg2Nt$QUamr9m|W zpv&iKy7@w$$!2%hKEu1B37BkgpKrY{eTi4lvG%}yIY_SRon>mvbeXR8NPgId57sbR z!KW39rtDQrv-f%Tmnrs9;#d~h8hQ$M+IEAQ+Nmy3dY@w{_VVtP;iOV@lg{Y((Wmf4 zVgJm+T(x^pZC-Y>;!1A9RDc!s^dZrV;}{I6s8QVJ;}gTe9%Sh#bn?k9E^_Z0_P=6# z2-O9Kc!VrATNM79Z@7s*RW!AN7WAo{ZLi`lFKZJ&LLf5K$AckJGf^HTdE`rg^djM zA_+s>oOtgwKC2?JuFJIXgdJBEcCNT%MCe+YJ4BtVu3I_jkSunhz~kQ>uj8_Gi(UHI z(y_T6#;fbT}r zC6w%{kA>mT-MK2!{#Gc5o2Bs2K?oyJ17At*Zex?j1V=+27>lNXx;ymL=WYfj_Xx&O zBDR6Z5kbrp12@Da<8QmP$8oy~=`#Uoyl*FNb=753Xq+xI{Kx0MCRcI`CsLvd;`q3W zv(7yiTmm1+tjGYf1OS3I0BAb)o-<64^t4QQE~DZ^FF_D)_-*K9?glAuL{FTL@0lq)dksp?GRt zQDb)?OBUJEi(mN)%~y}lt+193_Y(kmO;@i&i#dDlVSS2oJ(&E!gUi}82W&$MS6$q$ zH*VCx6C0Q78}2$dicU0cy8$E;53p8;Wbeel8_6{`>Y;(Mx1FvEoy8`^xg3k7!Q3^4 zv5?2$$i&?(lmqh@c&o$d*%~72bJ1^R!l84%8)EVH0WBbbN`6bPL)}Y_=hqR!yG5j8 z`#q%k7uhA=9f%XdBS706k<4V@`(%M30|OQgO@ga*8QboAvElc@J5n552;}vQj0rs%yNDTnPGp<6F7{XplQ#s|g>ImPhX{p5IUL5q zogd#~V=bHz?iN;Ex&xmsN4Yn5kaghXGv5fsILARw+zlnsA|PsVJtMB&|L*t{*uac~ z*leKpS@Wnq*+L(6ak|)u%uO2Z&E}xxhj{JwEwUCJ+aD456-XwOb66pV)jJ76c$SIHAg7QFUMoN z!EtK-68;5;JMz8A3|}|wJi!CF=IK~5U{PJYsf2a(vJPE3<)CdoJn;y}Z{2k8L1vtwRZcZ{9TtWI)m#pLoKE{e~8H>`3Mg293?DR4o_f80_jHT7Et23{0~+k!D3PpEUM820whKi2!EgetSJY2 z=(4(zK>BUG=Kv7k`xb6@hW?zc7IPAGVvgaHBQ{BRUjcU0{2CZAg1nmG%?^ph!v$>w zDz_DI#%YHyb4<;LbB`D|s;_R?xggxz$3acK5vz(uI7X6m7r2R(+8#{&*2A15bD%6} zBfij<<#|iFwrCA?k6wubbs>&57dy7`nhEyadIqY1Z|`-_4{v_<*;i~X=L2*GCS(0s zBNN7ZSO{3TC72}2(oL9cQ82Hyu;m(6nZsoL`AeK$N9r4Nk@n`EK4=?rN9$|5d4G8G94F# z+WaP>Z5nbqx7E-wIt14*uWif9Z7A1?mcGy(f`FiFD?_J|&EY_m?{pEn$Bf() zp(B7K2`0GU)DU=GGO_~jA32af5Lr&_{dq*);kWxYzxYyzfw5g_pE9)jc7gMYy_*py zuIijx3jFerPR41@!O;@fR^;|6!9(&*jP83zDvSrbHr=qqGMJr%5b04wgDY>g=vm`p zsgZMZ)&e`pDGd)t=iN7VSQL`Ftk$k{1`|m7%Lhu_l7VPIQoyXkB@FEj`VxRS?6g&P zC&!rPisy0d5^Jy_;T$K^IlKOd3**lHa4ZS)gc}$Iu$JEOq4bteYVKT7hs?6VWXXJ! zB##qBXR}WF0Y68O^WnXweX$H~^UIRp1}J!klIXt;6G+*;sS|&G1p4Xfl|?f+kLR4Z zFC2GC=cDNp(V)DOa*Xv~KN9Y*F6YPIpB%^~93t0?@DFq6!5hn{iOKYegBSv@r{M@9 zdEc8ivmG^|(H6%5IQmWAUk+67okP*1$psckFb%MouKO}h;PA7?_-(C{ z41Y1kVN-#iO`R;|hyeKSu-7Ea44x21A`aa|?GlH*E)bEyTLa!NP z{|t|!(2RDMoln-?y__RmaU5>s=Jogj5)VH}YeiQj-~zW+2VWKSgL^z?dwCm2ck7mX z-zH^7Jiv}==W&IqSTP8i9SFs;Cn#JZ&K%+7Eo9h3*_{W6e7@Nf+c-lh`!Kx-cXWhO z4Ml7qyo4(}M|%RU zD6;L!GN%fqVAKUZB$h)dpH$O(QJJM@zMYOS(+}k?YSj3{!Nir`_-?X1<7uBTke_gd zvQUk6F4|1fl(s~02rI1JSXVB~=msw>*EJO_VRFU$0ZmzB!t{b0@-AiZ1x8ZN!^z~8 z?g-?^9@Yq(bZx;}fl=Vkn43A8HXadw2tH~^U=)S#V6aUrN{KPn@xWYm$* zDd+&hX~vhId-ZVE*Clbh1y7IUY@);Ur5tq*8{zg0dK-Rq5m7%UzgVNK-Vjlb>w8v6-ko0b_Ezhlb8}6GbwJ zl!ilgJy)wlS~~679G93v0>*Vh!MwlgT5&m}3o00qj##JYFN^9?iKz%#r18JPrF%%Q$@P7?z~%ooAq@6_?^g|gOBp}g%Y^C zpWj3IQc9nK{uWZ*PeNJu^AEeCUB|yh`~JN<>@KCX6Ws9zuGXk$ingA1f9?JS|LMKX zaObr=52ZrQ?rtCBN&oNe1N{Bp-DlnZ;NOq=)IWCg?!8aD?!8aCAN=G5HO+9xN!R_D z_qyP(PalfTR(9vdc=k1ZFVK#^2tT>PUAy=G&~-oBhn~ieJpk8h;hN%lqwH^X1x{*qO7S{N00=;$b-RQcXWi{Qp3mP?i0UhHNx8&(aTLEpfZ_sb|ad@61 ze+NYX(iD&&T4(sL`{gLiFDKMC~V7c>gKNV;pur{Au@ppl9)K{`nRD`vCuSzx*Jp-Enlc+%NKy`x)A& zeT)=J^S?Rn&wjVY@9qcPo~xY_u}xNT-`|681Z&SA@kc4?K5;dXwzN`L=KdvZ3|jY- z=c0Ssulw;M)OE&RAN{`j)Hvd{71Dp#u-|X$@>=TJ)cr!Q^Y+J2Ob@{}oy!yIGsi9J ze(c&re-!(zLtMSavpj_5#(btfJV(hA`c)hq0Y`Bk-5`yf?nLSuwb9$92ufvBPPN%% zSQYE^Qirp8_ul`7R0f@@>B5{6L7ON6Vw22^3#230ev96-VYC0_Ori$x`dpL5#3Lo;@4W*-~F7LI>)`~ z=T6aPjD>f?SgB9T@pRGsr`#2%_@Q-6^8`*CxdywB?}JNqG={_?8m-gXx=(W*@o6}| zgjU45?S2v8-)s6q`rdEwObh6eh}FALQ;Z5O;ZH(+-7j;^d;jvIJ^1L)Fb<`yB)U$d zE+Xm}zqO3MQFIso0%2e02)&DEq)(heOY8+Wd;)yLn6reAYTf(i zpoeG5H&i9nI#C8JT-aF{kv?jeBFm!kwHD`5xsL`cs7yEm9m+rTYraDx9wx`1T;|I_J zqt5Qb=VYXv2URc^VY=rUE*m6)rxtRn+YAkM-U}*)V=N=;BVJ|TX*Vy@=wt7 z3R<6IUJ=*yP&^|&g+@nt4e0C7WBug1i(5j=YYcxTs3@O|*5TMk|I%M?e9yW~h&#A4pT}O!c7+*S_eEdruKh)u zlCg^Sc>;hL~Df^Rm_Lf74XHl7aI9$Q$4X{ zBqyGM_UV4gr#?fwS!4Ipd(erCv$~&AZ;WMi_R;fN|J~ifufKNPC#h80JM$jd{kD%g?QPvp_1=2t?q_Kq=zgN}4w-xW_}6%n zarbM?+Yuc3X$>^dJr$uIas>3k2_96X24 zDbwe5qeqB%VF#eS#;aD>h<^;EUqJuLr}uOjE>~yw#s9K74(A#F()|fm`2V^;zy|+> zzyH1aKk7QzV-U*Wz%7wl;%^s^j)8e{#W6xO zzJ5#zJ_|iTif3La4wq7*3!1mpQm!q0(ET2@#s2kVJchb&3xJ4>Oe3YW)bMC#Y$Oe~BCAqsBkL-9Es*)E{Wx>KG+yyKZzp_-G&g zP4h-Bdds=!E4oj-a) z4!!&fJPG3_qUKzlDe42CxVcKI1Nt=PUf9nl`wVi3s(xGN*4`HPZg}{IDQeUD_kk1k zEbX1zJ2;0A8IV-E`*V7o)s*1O;K)s;pn-Orf&j4bxcw62&E z=S&kT|B$xqKBWH3@inU9!&K*7L7~=Tj&zP#;V{3R-2Yc<`PgS_mM*IKH+?*`ujIBq z?bBR{7yan`Xdw&pKJ!Ac{koqip8hv7M*P+N$o_rb z;ml{Y-(R7IW5J#w@R!G^HDqjwN~8P(^CF6nP|BKC7&E({F;~b@M^`U+{ThghuTNq+ za_xMk!2JSG=dXXxJpEg&f4HGc9Xw3u6v&kY#rNk^0E_C~aA_Uxwc)SNHcV3hpDAa8cl? ztf*K9^B=>k*Xc=-pl$5FVor2{taSVw2YA}BMoR6Z+Ql8e&UF;$R&#E)+~+cH^-IRB z^ZLG(=U!9YI6n2&rpfovV?WOC1LW;@MZQTlWtuu}eLJ*!_wq&R1`^J{aROuK*ym4fwU+nIMF{NCe z;9S>L3h1vgJr*IJ&X3?_zt-qg1un9Pn2Kt z!fDH$uX9;fSnqlNUAL)A*Oi%9!(9D$F3ZE{oq4G@@*m>~w`Yodk2CI1p=Yk1d;HPSBzm=v-I*6xlYl(<{Vrcad+qiTvkh-RIp` z_^*2hwSU%qDgS-lb^o2WeYmcU#Y?L%9b>l59T^;RS9l`bB{9a+zQOfBgL)24hZzWY ze(>Aj_wN4P@v22*tDNEr>!1-~I9_?h@yeJ~3=fp!bsd2Y*k0 z7Uucv(Hw7?rRd%#5;>o{k3Sk;GCR_}$IJ+J1pk6}auKoX)7_t0V~D)c(LB8y{YAh5 z{a&_{=imD3+9aLcE6p)ePrA>+b|a*AK6@jyKw6q-N*HE=IS*baU)5alW3uB{O;bLSMc`!w`eWfOl}!-F^>&<>WY13 z9|uoxBl04wnFw(`3HbUE_l99s^Al=C_sev@OR>eZkaRAoS=qm!1YhPHgN`o zY~lvWYVwWD}<$1S?cIXS#A@9%K+7&@q5du(6TPkg<`^aIulkP_dEEFtL%( z5V4WZ@UW53&@g~cu&|NOkg$=@aPa4l-QZY`Lt5ZLw%a4`eaW@e1w36-U4pr5smpMA zEp-`ouca=-_qEhzec)Q^vVL+cby;7!mb$FJT~l4sC$FV0>$lfZm-X#W(=op_OY{AA zD}MCRE%>D$4(7MMHkjY~(_nt<{KQd=KU~3~#|NI33JySRBl6c*Be} zWH?gn=^ksYgvIZ#dF1UV2@LKeDQMh5(r~$hq#<+%NyF+6l7`wHBn`hiNE(uNkTgv1 zBq`|LLDF!(gQOw;mLh&ge#>KhYZ;(?OBuoURx*a{tz-<-Tge!jw~{d&ZzW?0-b%)> zyOoTgc1sz->sB&`)U9L;qkkHX(KxT_@p>hBVD}30g5#a#4bwZz8@_jzH>~e0Z@AxC z-ul4K^41@Ambbof1$pTwJIh<2*;(HD56-oy<>#4)PjlqH$80HqKC+dB^olJctRHM4 zVaVS?!mz%DgrR#23B&Ui5{BR{Bn-1#NeD`}kT6_sAz?_|ZzGX8oA!#3N9Gl408+11 zgCO?`H5ihwP=g`+3N;wguTX;_{|Yr&Pq;!2)-$e9gY}dv)gV3R3N=_yxl$i754&#kUf^}H+AV!iK* zwO9|laxKyeuUL!q#4FaaDdNiaFZiC<982pGeGykpqCes)B{oG|rNpL)tCZLjag`FA zBCb+mQ^ZwDY>K!_iA@n#O`<>IDkU~WT&08|@ppIO`NR7PX-<>0@@*=Q$(?0^&K+e1 zr#s0SVt0}??CvCMDBeld@Vt|(A$uoT!}w0JhV~t01@}A2S`XMs*80MyX>kC1V)eO2%-wm5gC=D;dM%Rx*aktz-7&5nzaFOcp=Wvng za0wTw4wrC|>Tn4cscs{ok?L>>7pV@HFeLt|EfG`l_c}c;uOttYUO`?kyR-ZlJrH}1 zca}Fi?<{ZV-dWzTzO%d`e`k5?2RqAKuegG|^pTzAt;g&vZ~bRK?j_|sPkp@GAsN4i z<9pB*Yd{~mQVr6Ju26&Zqbt;4J?RQHSYNtA4c42kP=ocSE7V{;>IyYjpSn^F(yOje zgY~N`)L=d9u=TDg-|^qPIn<~pT6UTdA!L$9??>$BHd zr}g4%t<(DTwbp4p{aWj^zJINCnm4%SI>kR+Yn|pXuC-3{ArFW8klbT%kA^#i!F!Ku ztPOtS+G-PzaZRY7-A|O|@Cy zzoy!(*Edgf?|ZY`_wp-9qgU@NExmY0Y3sE+N?R}8QQCUtj?&f(ca*kXx1+T6vK^(Z zSM4koq${TQB)49vkyhv-0t96*7@K$)RWLASZodYdPx;TgzEL*jmndz}9kx z{jKE;^;^ps-nW)Br0*an7~fjX(7m;s;rg#JuKDIrj%9cL0cqT!o1TH7XTj&^t@zR5 z9oCLM^3H0Pp7f4txBm8yYPVkbj%v5Q`;Kb29{!GMH$U)>YBz83j%qib^3G}(&-0FI zH~;jGYOlqx8n^P~BA%_I*<_xH!!uBLGNkip-b?)<_JP|;=P~Ry(q#-gT)Gy+4wtUQ zu*0QmG3;>ZS`0f}x)#F@m#)RI!=-C6>^9P63_D!97Q+sgwqCLqusb1>){g4Uk{gX{(88C z^Vh>AoWC9};r#V*3Foi3kx+j#5Z@KrM%<0j%gA@?CwUnT8ODRL< zmQse!Eu{>hTS^&9x0EuZZYgDG-BQXByS0>{c1tNk?v_%9-dhg6dNPil*2%MAd(QOS zS_a77QbusQm5iZwD;dM=Rx*aztz-a2U3FI1EWW9EO`74ns)~hhd_J!w}HJVfOCj5bO4Em@Rua%yPeK zSuR=y+QrmY3z+Ll~ z_@4J$qo zduR2VpL=KZo0ofs^^1>tXZ4$hduR2VfBP_v&h{~5JH;*{ej$~9i{EkE#NmC}Ku&n2 zL7d`W25_3E7{F)4bhBy?U2NH;n+2P6v)fmx zec~_bh3}HadtL1-dppU&o;%2iowt@V`)@60xY%0G@Upd>;b?0)!`IewhP$oh439g= z2~M|`GyHBXXSn`VitBj4p!Yo+d4SgqJc7+$9>ZZTk72Ht$MDt5V_53tG2Ha>7)E+| z3=bQ4#P+>BX6IfWv*{Q79-N+P9)AJ-NWRI=cT~8}Ji#}km&iJMEAJo$yKgNeNZ3-! zu(73-p=C=c!_SsdhNvy23}ahL8S1u_G8}F#CCJ=T%CNenl%e-l9lgv7q-!S8+Q`;2 zK<<_@g4?ZR47FRy7-qMUF~n{qV|d+4#?ZQzjA3;v8AIxpGJ?~sWDKQS$rwg|NsPXR z5512++|}v5XoCX0RVjk13dOKfp%_9c6vIJt8YM+s9k<(@wuMTv->|$OF7? z;1O*0@)!<#c?@&CJch4c9>Y>EkKv}5$1u{%V|duWBew75F+2D2m`y*@Se{?D`%gqq z$C5j**mV#eY&(EY?7NZAY`l@r?7WfBY`u}s?7flCY`&4t?7orDY(Icc?7xxEFtCx& zaPYgw*!^;Xo_&lzBjh5|=q}DhJp!HUEI!WOSr&NNQC2XtldR!tCt1VVPO^r-on#G@ zJINYOcak;i?j&n?-ceRCzLTusekWP$3tz_(+fk=GzxWLvX6o+Z&$;XmJw`uR<0_hQ z?kI_Vu#=?pf*mBS59}amJzxh(!~YJFhW;HS4f{Jt{{QT~U5s4El_nTjWRWb9EUJns zQ4~or^+Qv%#3GADilk&pERmF4BT}MNq$Rg0)5*##vPSji%q)?#Elg!qNp{c5yY#MS zrhB&=iv?`J1B|dT;Kp`i-M}m`4{o3v@Iw#a0c^kn^n)LIvE9HtOk-z({k{_??qA-_ z%KMX~T20HWyfrQcjCl}xN%kUQ&6hjr=k?CPeG|VpMp{~{!~!o!>bT)V?3yv z8_@nm{JK+PDnNi||1(fS^HWoc_NSy)1f--^6r`k9B&4KPG^C_fM5LrvRHUR=#B_>-#B_>)#B{3riRo19 z6Vs`_C#F+PPeCU-o|sOxJ29Q=_2u?@%~QfE@c0+-Ya_yZU+ra#2Rxf58VB~Yz6)G| ziQKsgVshrHc*>cpqAh2xip89{DpGUisyNP>tD-z-u4)5v=PDK=XRc~La^|YmWip#} znFxFVwHAp=c($hNWx(=uxeT#GT`fbcQdi4R8`ae^)M9nD47FcfEkmtYSIbb_*3~l9 z(sj8Ev3p%DL#<#}%TSy6wRSeq%^zOD9GiQ_&Pv3IJH?3LIYel0V>O;9uirofiYxS? z7*yAI!v=PVx7fa}@K&4G72ay=y24v+TvvFjZR-kewP{`9t+uQyyw!$viMQCUuJBfy z)fL`qt4ev;D*GN`3yam|5@5HwT7p=wE|#D+tcxY6CF^1dYR|e@f?BmMmY}w+izTRq z>tYFN=ek;gSi3Hkpf<0IC8*__XlMDtt0dgHH3yGu2EX-;piOgKYGGyeUwjd)HMSn&viua6M6yX`UD5f)VQ50w7 zqPWe>MUa}2i()e)7e(VsouQGt-DYIJ!!)h}ILt&31ZJQY?4_nx)TO3ZyrrgBq@|`; zjHRYmbfu5j@P0^PVH$`Ml+!UobaZ}{x#!b+i6E{V8PTUmr zZFfS%p?;#1T?$>`3M-I1SFr{;b5*O5Ggq|^IdfGjkuz7d7CCcOtC2HTwH`ThRV$J^ zSFt8Jb5*O7Ggq}PueI|j!q2$QZzDt#n2*llzY=0ZxBbXaZoFVga^fZSBNtw3HFDvl zwjvi^Y9VsrrFJ0~UTO_;;iWbp7ha0|oOlV|bK#{Z&xMy_`;9Pu!|w=8AhyeQY^U+2 zCHL^m;ht~RxH&}3nRU7H1IBaYCpgcIpJF{Xev0?p_$lUds6VixoC!$gPO+=$Qx<9O=XA#w6&)Gd&{`YUpq0aJodYmXfbT~nN z(cg~qtL}E3U-h=*{Hn7Z=U090IKS#@$N5!HJI=2aDdj<}w{~0(a zA~JALjAY^3BB5UL#P*Vd^!t)ba{NPD1{(OY1aLFh<45p+6_EJ#_=2B29 z)>2R^#!^r!wo*_krczKUmQqkEhEh-}c2ZFaW>QcpR#H$ZMp{q?$D|6@`7A0zZ{Vb) zY_0L_%WS9d?I_!8d^^ha8sCnxy~ekrY_IX{DBEj%JIeMN-;T1q#<2Y(Niwm8F>6wZ1CLvWaJ?j$;3kulZl7oCle1vQzjmYtxP-=X_4BTm%7Mb>{ZwJs~zhaf3W42fJ*jk;WlTCJ{^BG#?TrKlC`aw%#pzt+xLhG#|LoYs=s{XrLG z&Q#OclQ&=L8gE#}F7Xz7*cIMt1-rsqZC_V-tHtXIZ?$t>;jPxKE4mHfrm>vet4Il$i%?PevYMV>0njTat-~+Kfy*)HY<|p*A2B z55;;W9*XHqJQTYbc?d=`@lY&g;-Q#(H$3Wx_pSt=#qD2lr#3`qTAqi{Mee|9*SHIU zyTo0w-6igd`Yv%-d(b8BYAL$JU2RC0xT|&P5_h#bUE?kmsY~3|R&|NHTCrNtQdJ`R zvV+fBSuy)IM0h^LGvaxE*%Tr}{Ej=n*AqCC>8_Uvi+7jH6gzlV%T()lSIbnJc~{F+ zOL|w!RQq~Y%T%j-SIbmed{@g<3w@W%6uW&_%T#NASIblz-*)wFZ5z*%#cGKB^IZeR z{G04~SNX$scagu?>8|lto7^@2YJa=NUu|vI_^Vy*8h^E+UE{CzvTOX+Hg=J}*uk#x zSDV*0{%YS2O<-k`UnH7?rQ&yqxW-@A-HI;m6*%W0QGVE;1o_3Dbev!9L&y0Q?;Yn? z{C1pQ@z`;G#aGAq6)zpe_&M}C6$-1sT}bK|G>;ML&S4>oq_?@qUGI=c5Bc`l`$vHEXFdTS=R z@B#jF;3K%t%t!H_nUCT;GatowW1*w9 zdPa7LQe)>ZW=~_3o>8oR6XUquKN|3XbK?cP=EO@dn+q?+Z7#eNySeaE{N}<-F`Nr8 z#c?jY6wA5rQatCxOE8@aFU55(ycFBV+F_gfzqyC;nyljmP@IAacuh5ws7Oeq`k$CeG(91e>U2UX)!xx|+ItQoQjG2ib5shkWA{y1#L^P_ciD*<;6Va%qCZrKPO+=$wnutbq6j2lZT#V=}hwEzo zyEMU^-s~n5J#;q%z36djde!OF^s3*f=~dTL)2rU6rdJH4rdK?qrdMoapckB^rdQ0Q zrdRx&3Zf#49~U2BY#?^Nk%1c6Nlh)tNlC4^NlC4!NlC4kNlC4UNlC4ENlC3}NlC3( zNlh(CNlC3ZNlC3Jd9yuA=FJ88LUpW{-hfZfwbF3@n75;I<_I+9%25!OBS%GDjvN(< zIdW8V=EzYInagH1n**S7lwCBoEEI^JN)hgu3Q7y&sF!ICiFGNpd4(y&tMF-TU zpc8B-rc(qbrc<0Irc*Q~rc=x%rc-1krc?YRrc;!ppc5=4rc;eiOs9I?Hrh@9@VZ2^ z>?udwml4K+H{Z&I5A;3oYnqxg|k?pE^t;`)CJCJ zk=_nkopu%}yehe-J?aWySfeiR6PJ72Xzx${*UlsjLwK)Lf(`;$9g zwLV?oE4C+hzG``L=bO~-Fak1*m1z4;N`7`H3Ew1kr*po^>`teAliHn5`6jhHo$^g; zcRJ;p)b4c3H>us}ly6eI(<$GicBgZ`$?Q(2e3ROpPWh_c8SB*FA>Y`Q`HUQ3OEPf~ ztC4|&+J_7r)DmRipqS6VLD8LogW@;?2SscK4vNi890Y|KI4IsSa8P8OY=^ANSi7GV zJXP?$>=H)w!1+2UDS@O^l!BlXl!}}bl!}-Xl!}xTl!}lPl!}ZLl!}NHl!}B@l!Aa1 zl&bkDC{^Rz?tBSqd=MF4>^}oFG(R=9Xn#s-MLvD zimHrU6kQp)D9SQ&QM6^`qNvNvMbMX#i=r?i7vD?c*DEI@CxF(;MhrFufX zayjX*Rb!k?LutLVHC>TV($)!KwKqOXZ)R8tetsE&?sw&33RyM$=BJ&%?#Z+?f^edY8V?s$G}6*J$Y z6rLxQl9F~jO_|n?C#Q6FJUOMama9URtGs%mpaNR z+S5T!)t3%(s)jUq@y_x^zc%4%Nk~_&klPxi)$i&AGO2$5XV^?Kq0| zy4_5%mToswtfkw{6l>{rGsRlE-Au8TZZ}h`rQ6LEYw30zMSIuT3VhDK#!Ag6i+iVa)730;vkxvfrDyt1`ev(891n>XW*ckpMirSAp-|R zMg|UwluR51IT<)8k}_~mWWCTHS&TfouM;%+ys2q{rj)dTqg1qtpj5Pqom8}nnpCuk zmsGThlvK2ekyNybj+C^5i&V6Vh*Y$Sg@eSxd-ykRZbsHh{V2RQq9n)-eeXE8Xn2RY zRi``5tyo~V)T!*<;$2!a%r&V)^gDt>ou^#qX zTGc*xRIA$MZq};Sx#P5|b?!K=YMnbyt6Jxd)2i0FPT=+l}bKoO7nwgJkZ)QHK*O~dK z#%JcESjfyrQIeUD;wLj7MOF@c1aq1BC>k^KQJhW$aTmquB4(&nV3nuQbN%uyvxsZ= zg)r{OdKFLWhO3@|z4N*A1b%boDOk>xr{X$So{I5Yc`Dv><*C@um8aT)TzRTZ$d#ws zhn#tet;m(9+KpU!stq~S+J^Wxf_b=$=&VM?ym?9nYS@L;)M5crQY+R|QY)%cQY(H_ zQY%tZQY$7?QY-pWQY+39UDZX>zBv{XZlj1%HPHF?*=p4bp6(oNY zjd2H9>9G@9)k5uJQUxVcqpbb@lYIR;-T2h#6$6#iHBk|6A#5@ zMjnF2Ogt2SnRu){TlLXszw6+SyB^xw%mLP-Za-UXPrcS`wH@`^v(zdb?&m)zCHbsV=UWPql8%e5zk-=2K1TAfM>Wn)y^a*374RaO^VPVDhbD z4KWO^-Ej|Fwe|Zl!DB&1SpN=T);m5@p`Eg_ZaT|z3=!h}?+lZmNB zLlaV|z9ytn?Y-b@Z*&)?eVcU(y6sk=&qA}Q-Fz>Ie<$&b{qEVi9JxW;bK@o`$cdYx zBPVW(nw+>PnsVZ%D9eePqAw?Iipre0DOz*mCMeE{o1!}>Zi@PoZ)1(A>a9~>#hO#a zOd)L%tJrn#6p^nWh3^vtR<2W00?nx?1-U6G6{RUC6_F_@6@4iv6=^9b6;&xH6+tN| z6)mYK1sN$Q6$L3MRpZB#YJBsX4E&m54eL4iZ~`K7;Uoyjfs-O82TqEh95^YWa^R#0 z%Yl<3E(cDEz#KR!B6Hy+2+e_$A~pw3ir_Ozad!#cDR*TBMz72~fZB{a1h1KRC{i== zP>g2cq3F!SLvfjjhaxf)55;089*V+@JOqE4cqsBR@lec-HDhiOGr$=j%YS!|Zy0e_ z$TN>_o7Zp^_*Pm*4!~O`4uZA}929FAI4IIGa8R6O;GihWz(FyVfrBC}0|&)dCJutG z3>*|&8920>fw_0{&H_!;8Gakr?i-%QUEsu(CLg#n%D5R=CzSCsu(c^$&A`^CRK%oc zXR8_5+GQ$2Qk2Os9xROs7~$Os6PGOsDurOsB|5K_{3Dl;F&S!O!`P$2&i! z*BlqU( z1`dkM3>*}buQiXIx8;Opp2|gxROP##RgF;u_lfX*Xnu>r!&q*-fVP}?3GQ;?r3lQ0 zmtrv&UW&?Gcqu+};iX8;g_mMB7ha0qoOlV2bK#|k&V`p^yJZrbZZ6~e zlXE!L;41#UgWq!~W5SH%3@!d%Mf`URsb4l`^B2u9P&b4B%-%1`c_};@l(xx6x@!LO z{ZYQ>!0C1HB)_-tdjY?fu%rGfpsk^G+{HG5_}L;#Ah&1??G@0xh4bPppKzU-HV*Er z`&+2(8C;En!#ULXAUM`xGf$YpX8%nXGY~!{2GSfGgJiBFH@~3DNZwWOVN8!F)$#6S zq_Yj&e>a1xH;|t)vyt)JILWt}!v*BFD?^r=f9vKZ{;~{na&3xp$j3Tem3`{`28Z-u z2b8a(eigKUeN=@+Zb)m)YipMoPs#S4lP}G(uXqZE8C{##d6drYhj`_mOHLD0atBSNiG)HgId;ra5EZOdjF*`VN)PVnRe)IUGw8^5dZmvMH z_?81(aMT>bz1hiq0N3K4?5Ts6vxQph)7G59b*Pyu%ZlfSCj-P(@S8E_AW=?<@e4DQ z`EA$(>go;j!<=06Y&Dhr#;cI;RmgNdvq+537w!utDPGCz-c_?*UY%{#7| z(TeceJi~tEx2I;nXTh^H)Q0e$eOQxi)XQ1?R}$%1YtOcqq^5T5StPNYwOW7#sU6qR zQus9kGg2z;naU70pI4B;PSzq^o_$Ba?6=+md+Vmi>%GX=e>C3^TRw$<{IzWZ-_6%? z&yi{Yn%|tmJh>Kkw0fwyZbEiF2wHwm4*g=^?ojWVwz^?7p${}HJ? z9guxT%Ro&fcIR<754qKm#y*=x-UZ}wnr_QDWR94_W(5BZ;ooumy?bSjNSP<@dYQJh zcdwmCq@B;*)pm|bnUwI|Yo{&qSgSG**i}o;DQQXSGOmSj%~%o;cHnjBy*($7*2;em zf%HuFr7R&7V4Vukh6Fbua>Rk$w!Mn$c~DZS7}Gbuj4OKnu*QA#z0bo# zHQOnL>&VBJSl`oZ4e7@Yj(;3`ZA&@YPa(hAP8-g7k^Sl%M{6_mZCJn$pkd#JJ^TQF zkI*yP6Het*rhnUv^69?#r_9CY--c&|T#wV%Qm$;x4E$P_ZM^{Pfqjd}v1YF02Thv) z3hQoR$IC2wg75Os2c(Saz$s->MPDC;{3*YZc>(_7HKejWOrx)K$`lq^u5Ig9^MLlf z^8^pV!z2n9Ku1YCy?P$J;!7ZsIPRY^Pk<|>{e&T3{~ZMOTp}5F27c;|IOxclur}K0m5!{_MV|#)|g3;?x4wK&#=z6 zKN%J=+sAOnaFTU)S!1*7404f{8porbZD4?A-3+{e(wJUF_+(a6{8nnpfmZT3C!oHF zGT29TQ`pxkr_b{{rqK634C65VnLdXQv!AVI&1pbbpE6!>kGj6-n|03^bI5AG|IvwP zJ}VW6Li6}q`OQw7!%+l=xor_%JHk1=q3oQmFO&z{;G78db=WN#lV0{6csd!J+TIZa}HWFq<`YfIOR2%JOhZ0*^D zbU>v3&jg(ZI*z{0rIigjqGpt6ayk>#CVz@m(a=pDB@)qQyA2O;GG;l$o!Ju?%<)5? zQ;;N`2I`!fy4epTi3UQyW7GH1!QnIeYCh3vgBc+gZ{CeV-I+>89e%TU4z1B?^Z`nQ z_QSdil+f+?T1MLFVR$1$#L)ILVcATe)pYxcke44)rCY-^s~1IR#ysw9m<~PjB<%$C z*19~@ifG+v^XSG>dz@<&o9~RT4a2d)yD`xm9uLm8*JARR7os-D+I{Ayt}^^{twmtZ z9P5OZlb_J1;SO~|bqVEanDBTsKNrv}j@s4^f$MNMBudv*vuD<#2-a#GwXo9*N~+m% z5yvsX&5^a}nrpQdMW9xvXcy_lFi^(HXjuY}l;s5OU7)o^-Hvxg-BiPo&gcU5Jkv}; zpNJ@`d9J*(BrR$i)7I8Jn(7?W*4EU4nvdF=6Cbqq9RK`qW{a90Y)8{nv;FOI2Ws*{ zut&qFrB`Kx;4Nx&tP|R%njPwdDp*4zj@Fm9>^iQe&9_7+gM3=muoK$m8g@bzsNskm z*WZF4^aku)O{8W+8ZGMeOlyj!+U#qcH&BOl@ONdlpOt9A0p-9!#V_V|tA5{@T{n@BWmZtyJPd2# znXVMp@tiW@FP$T^-#ST_Pf4D-sjnX!g+zy}lgfHfib}ot47e;x?ak)5p#zjTC)jV` zm%UQO>Nuv(PloTEVPLx%73MHtwq6qYIZN?)@Gdrg--LW)32RDylr4h?X5Z78(p|@Y zti^R{r*$NKjX9wEBf`C>>sOep+Ueq3ptq*Xnh&#`Pkg7IQ!3Lp3^{U@-|X?-NbU`= zt32#Car}2Wjh{j7h&TI{h!SF@es0s}$~N1=WtqJ;7RjlYjUiL6t`E>R&#<-rdL!#j znX*qUWuU*ePGHBz7;t&aJZH@H2^j5Hpe=7B?Jb}>e)4Olxy2Y4tq3inm0*D33S9KF z(AF!!{ZagV1*x140WUkqA^&z4wxgC<)R}#L-yKBli7glYcolV}Yg#jfZ5Aia-kDv_ z;xII1W#CPAHrwzMCrM_9wL`=bTlp5a3vv%Q9Uv|DRA~7>Rff_Bhw4xLQIzoz!y^Xmf1{(?DBn z1fSy+v>ayu`vE4VL zseAoe*3>#*MCrEH(9X?n{KxeG`-H8uL0;4v=wj`sum_5Do5L!0P3r7cunz*m(NNhz zA!~( zt$+ADa$GTm&7`&enQe}b4f2?0qP7goeDyEF(=@#jWa@J%rk7V{ z``66Z@s$4e)A*$i&L{RSMyIVVKYPLKU~XR7{JgRh^vKP|VVM<}T|p#7u-IEfUo$7i zdXDD7F~w|UI#U+dQ+t9M%wF;BGoB*U6|~|!UxIs-D0MFox`S(aVT(42QKE2&3m#+M zl+0S}^CErP>84##@C>XsOsizpL8GuD?5EI2=K7;~lKsFD+U}|0h{-3xd60RWHk>w$ zDDu6gn)r6&I}O=A1D7EMJJzvI++P*U&z#ie@DvUy`@(e?Lv3avwhy@;=ZNTh?&k0k z`Gh&2fn2TMMEk@t>MXx^zu9jJ8~0)4Lil^g_ZR5F)0V>m?6IM8zi)!*ulie8ZwJNf@ zKY@Oyt{d!h4$217-b@?MnHl@XA%*pzhrm=jxA8U8`v0iYX6)% z!85+;M~Rd|`Kc}R}X%bb&gL2J>B# zp0d$}W=l6NJ%i=KuO_FVe8YYe+Z$eVMURZ9;rudskV{>@)WGRN8ctS^sF6g!(*tmO zHw`zthqhUc&T83rtzf<0PQ#rZxV1*sC+FA|4CkWOFD2(|$6?wG>>v`|3(j$9<-QCZ zsmWr7zvvRNe6OL5-j$*q(F|lJnp~yVuOa?05BYl64~kV;q4v z!Cse-I>RRxeO9s@BjP+owun9&V-B_@cFBkHxjOnC9_&8q!z6gxc@Vt^yYrM-bGxw3 zF=re3-VR$*1vXt2)$d)~C$6Su2dR7%y#hKr3P#gi9o-Id7bBv@r#H>onv-EFdJxto zgySO3o6F4OVajkni=TZUoFj~P>xoXIEr`D!qW;K7Q=0@!A$5?zvSTTI$UzIo^UHuXk2*B z@o80()j!rcOxeuays;LqpoVq8@7!Aanl-Mup5}n#(wsZoA|DfL<#gDMXhWbRH>;n9 zo?eeg4r7@obzev0bGo?$XHj)n=$Odo3wDh4-&JV#Se@0S{QX%H=2fpX(+0;odXvtl zvEGyQdnn~|s0;0d^*GU{n0hqxx(zR-ES51;kBR1baP0A;EY=3omQhYoFYXM+Gdmt~ z=EX4#^*PzhlVvmY7G}h(_jTmHAwCQv(`*(0nXx#^;CM$V%)v|W{$_gX&&1f@ zCtKyEtzASb(HE~a=V@+nO?~?|+Ig%qK8zWj(~%rjHbxpOJ*c~399S>RZS9HEq$oYT zLF%qKuhmlO0%Qs6I!<0Z7y z#UoR2jKKnHKOOSd>Hh?J@IY9e&2w6(!v?P&a0Q8|45N9iF%??+LU<$zzd}GebJd%Z zTQmeTJVG0u1%#y(?IW{4T&}gzQ9A+;`#@*;XvM=hDZROfJZuZNb(P*Oy%Ku3kV0g0 zEA@`Ppp6jQmQY9OxzS=X{`npvuzble2(&?WwMrUgfLHgVIaEx-HJd@Qlpt~<6rFH$!I0t5cW9jG_Npx#wn<+kY@t`*M7nV^lp%Pez(bivjB_#IqVF?(^Nh}$Gyh89;rO)rHQ zvhQ&Mvm>3nLP33ZGi$S#wX$pVR%eLmuR!AVi&^HF+h^g<>7Z5RstMane}ttspXW=z z+0Cw40{fh!xI>@q0UL!F2M=;%{iAgB*zG!LTqF$R@{{c--iD9)3ZhZbJnU<(=-L&V zsFh@d(ClkZV@Dgahq>OvKf4+kwpiwcc29jxp=D~4;DIo0(ANrUm6cfyUJ_#!*1}LT zY;WE`U(EnLOt)(fSZNz~djg{xNL>r}8(Tyl+S*O`=Bz`o8I7KC<(`B`N)gDcSJ;;8T_4dMdbEJV z8Lgv7q5Id4VNVJ2?YbcxWBVH~Nw5XfQ5V5xKXKK)Sv$}}SeKs@eR_{}7w0YUj-I`& zQwH{b5&dQ}SD)By4(JU>}wkJSAMw% z^kw*XucDt{$2C`QdHxYkWjcp@o_56Zn#`dv4Rf4rmw)?Hut#BU-0X5MxixzjX4e$z zY-6y|_@>!tcQ12SM~j!_BCq?ghw0In?cCqTx4XG-kNZ%tZ*N`YiNdz0u@{$Ve1^2m zem=ADX}e2z-P72$R5V|}f7~bQ_?UI&Cknk^V%mVS7S2wa9j)reF(IbaTj?=7y)-+B zr)E23Hd?OSBdS->4!$fH!*QE^!cl_b8`s1mDcD;}t>7kmv(G)HZ#oHz*jnQCE%xLr z?274~fR32no6u#m*K#TgE`l%i*`29oBXgCY|KWU0XAR6&B6tB;_D|#2YzPqBiCx8yS zUn1J#R(1ReBuD9XRSK4;Dsv{xcE zaCwP8$gz($&PrvH_VHNQ*7+|xb1!7r?>q%>L|?GKN-c1^pBM?XFFME5i~`+<4WEVQ z<)^R?P3$+@nL-|H)K zPAlSdO7-xd^9S6x8lRiJb`16?tZ(soRrVd{g+xY@)Iwy~TLc=+PM(BlR{`8wik@~T zZ%A=g#Q4=1Y=B)$rr*t(v*tR+7SM0tydrvPTT7}vq@vdlj$SKIIi1m!tZ4^GK|AE) zx6QUwb8cme7Vu9_b=(`Jb2AxzE~h?*X9x5zDGSZ9gZwye=dO81x`Ui)Q|Zyt_SNyr zU84G2T0zdVkearWwF#G|Po&l!Z^>t)Eab>_Bh7b)wfPdf?Hh>ca+eEd0Q7*#m9^pQ zi1}TN#?EW2GG9|3PqG$1_ppW4JlmLuJ(RuNDaS)v#o46)_LOoLeQ-7AxITegd?Qc# za4c<`M;(XlYz@8GK)>R&D!YG}60_@itXZ%|=R}(9*+`9~zUDo3BMnq^$?lGqT~?U6h+=!kXtwwT5~OuawDkP#&QGNC8fpc!!yiDtqSf=iyoh( zbr#G;^D@4r&qdd-yF1Bk_f=9-{+=<%jV0Ta=lJK8!|SNoO_6jHeFnsM`m>*9@Vznn zh9xb%i|o>S@H7?Yq7`L@_n(U`&=$yBq;@5P7D7|;UR@DB3+IoFqLGg$6VagHezlNg zk*9ubUI8s_wDJ|ZL!G5Pb~0rBr%+ZnTHxyj>oOju5sP~X6l+>s(IU<-a^#y9mw-M>>SUJW`xC_H+=zjJWHBq zVROF$M;>~Biy}?#QMd2uac-#l(O&I@6W_fm$BcG?*!0_-@Zoy{lmO3Tb{5*Tj4?TD zb8cb$b5SQV(Fxz+JA-2=A#8uFUiy2$%^OL1vZpwfTWuVZYkyofZ)W4?Mpipo$$EWa zj^PM(vkXi}{XC8ym=sOpn8~@Ad+W|=jv4E$t+WFiH!O!T^x4^ab0I4ST64y>cot!& zed*UH+Ly#JkNU9VY^Qzc^J84c-%&e;HV64(Hu}|0+Up~-x$na3dw%@BGb`Y)A<=K~ zZ8YC!VGq-;bk=Ih-CxP$jttIy$s?xKmpf^7l%pLD_zpIEB!=s`PPnewBh#I6iCU;= zeQZ>Svt-J+x!=(DrF13vyG1EuB9XHTYke+=_IvL$Po^uOS*PrZLTq2Yo30G{iJZ%< z(SnpR^2dlZOX$d&HjhYQNz*nun`TmKP?#gNh6tF&Fa7^m4NBR+%_Z5nLu?tj$S2qm zlS{3G9GmNHD(SQ`ESdIY0KpykA8Y9jOW~U&w3dkUrNmRX=B@hET+Kr+t+=O`i1Tu2 zHQhS~+)*I=FFG16nx!1aa{{vwmH^)%rDY*5{qfjZ-Jr+M=bXnu7Abp@9JoHchGk4C z6-M6qW|Un!v%6tyvf3=#mr^RheTTkuxQ=h%x^-3zYhE3BJk7o7R;0MT?ds06MM~mR z$y(fOI^ABIGkb(PFd2!brRS`qGf%aBI|6?1nNI2+_T!z`?eyHdl$?j%3qdJj|3L2{ z#K_FL2k`GS?!II2ce6ddm7|;4Ifs0+qT7tcP+y+Me>kn+$*ZFC{sE@0PJvlV=?ePI zo>FE{GIQKsL@Ljr;Avs*6tk!GtTNlXz6V}I4S8O|0pvV@|6cMaeh;gu&AH9GKCC{k zT|1m^C0`P$Nvr6Y*d0@BAJ^Z>+0FqZ1#8Bf3;BB`B^6&BVNAyE->{Z}Cz08Axa>Xi z74~z#3H>(ihG~;;M>9VuM|L5E=_MT}YcVQ4CB0(yMeBGFwX5PBk6^CC&}rSBa0=%I z*u4ulH()cPWRwD;-5U=<;y9;bfRTOrn$!T!#ra(p);@rVJ^W&b>sP<;SvzRSMm-y2 z8*|xfN#7crp_>mbNyLcJRvs~HXPT6hAC2@myS)`fXb^1;^U!+Qli?R}M>^J$k#I($ z_#5@z{Uh4VKr}jNwl}xOTu*$cJSoWQ=oWT zfajoUY3$UGp_>Z$G`-QLOiE%&4PSLyAP#W zZr{-ogIwGpgsxqV+Lro~w1x-Xxchy$`oI)UTKPPMJ78Nw_O^+0W{mf<x?Ma7*3W2+tDubZc0{@V$LWibr9PL*!tU54ur4Wy>&*9op#V)8yMNsdTrT zSuwSwrMKqAC;oAA_J|U{4$jSixqGjBWxj?w_(#>DtffCmRv1?c!mB{1aOx`8uiZIr z%y9<7WMq;e<@ZV0v5ejjS~I%d-ujF-avVX_ehR-Q?33yEP}FxAKRcJDIkZUG^~i6gGDLu6R9=wLO3LO(~>NG($n9+$+o2*v;#|j zf`kohTEA`h?ta@Ssq*0A-4AH(ap{`%pV+M3Y?i%(K!ytan?4D7`}NDA^x6Ak_mo&pu20m**JinMV9+3<9*ae?h_yoqE~cMr3nihKH@D!9YnJ=6pC zNn-%N=!M3BD`5a7;K5b`X)F61+B3L%2iY2zkuqRY0xd&INk9X;$pWUSfk!lh9s~wH2kX zS5BQK54OJ{C0&9pupu?-{ zE&b|xGF@G7%UE4c1VGA?Ko%}vXPQSJ!#lbCj0!7(kQQ_>+rtP9;YQ{ig zpbW_AewU#BG!v}M7B&ft*ppk(tEj*hsO*qu)NliNC@)2T-r4L$K&yY{xu3Ef&qF<2Xr8WLsAcRWc5sLFEdi;z%;K|B2HdzQXK32)9HEC>tK6^wW~h^;Bbv%O_$2TV!X?Z;@=2K8g5CCVDU2 z(V(TDv7T~aTicH$tTS>$na*NTD+|uEH%jW!BX%6|VQdLlorB%mn zRrv5$p7*m?msWvdRO|0S421WuM~SW?%VjBlxojCNmzmSuQB$P?qO!|lDCkd-4}*(X z0F=-euz(mq4{@Ng_Fl#pvHWV98d^Qbk%Hd@5rJU&O;WJ?K6TGOW%6Y&Sz*1p9vHwi zDlY{69C85nvBBkYs1NEfMnRC(81;9|Hovixtz4c)0Xr!N+>3O6!oMruke2=$bm+R} zS(*gXv)ybjAZ7Xch3y=1`CD*2s0^q9>MkO)egBbY5Zlnle3x-S0#;@DQ_H*IvMm1? z9e^k6FubqBGb+$G^w(MSD?O^oS1gkTu`V*=1tosV<+!B}>Mlp!njz?Qqt6X6B~bKH z)f*pkfN{4V5##i+lH3|>+|kQB&D2Pug>}?yk5J!1Z?Q^uq$cbY-hBft*UK^xCT`Mh z3cGPddjHB`L$s`MOKwrLt2mxmWb)!cVd|IaFx%*}BZg}VvZ@R}c7nL$7~hF9u}|2< z5EC{~ncR_@%HQQrg%8^!iR@uffIh1Lee#%XRNA^|*&Qc{S#*~4!(d}}XmIsMkj?Di z@+Xj_ca8ed5Ag1LwBNi02q(beCjl1j+Ocr=9t)R%9?|YVg{}m+{ks4+ckQ^jdykt- zK<7Cz&x%xL(RdD-o~^Xq%EmkCd#!=c!?M^)Zz=dVaEGQ#0dZcN z9xNf{WX5k#5e8F1^mzv!SJ*E%T0qH{mNGIqhE2_B0hJiXE%~LJApyt=IE3jkJ`u(x(@Y%}2jd*?jaXR>$UA{w0`0 z_((u6F8>m_P??pjQW=^M)DD&vpmOZDKd{cLtxE$BblmbU+1VfcO5H8@5Gmv>rP`L* zjLW}-Y#`8q<-de>-RXP4T_F?v3YliEoVCB0#K=8Nz5IUkYkfX|U$8+3p_0sG&C4=o zF?Z0AUd%;t1&$y677wlbm+y!^E#E;;tza(5pPk~z2q1n7C4j~cHvV{U`6LHEP1(eM1cEBnA5Zp7d+i{a?&T{Qj>DWXHT7+5)27+BduAY3l=1MT!xR=&zv z`QXaCyU`f`2BrH?`#HO{&-!;ex>E1p2ZLj~`-RTC3DNEL>2~jC)qwitr&yys&}uu7 z;8*>^MxXO1`2^_1BN*{q@$~`M) zomB3EG-5mZm|>+sO*fd_a7WtW7XVG=-ly2=Y7#~&qf%=LB)YUw@OEJLqgr8Rl%{|!o11HNrK=FPmY792piFayB5Ic%!TXr;F$)5;H^y!4NyCUU{i@Q-E6 z>P^{Yn()WaB!nkqZiB7`Fjv>xK~N1HM?sl=VeSKPx~Cum%rW-p91ag;XvYsnQbs`# zPUH9myoyi2p@#7@=(Hc*f;nZG%fC|_wER1@3M)Udmil*M5q_u6?DFphSAHZg;)ST` zkH8YMbeSkY4BKVc71Z+v4>3}2H>eZ*2is@r+)Dr9@d?eQzsrAzdzk18_`xi0yMYpa z_sf`)J!V8cD2U6gc%hhg)29ZAzX?SCKPgIE7pr_$o}1n`g>M?FZtwB zme$+pj&Km)FZdKK)QK#rOl6jAVgUX>)``r?;Wv2nU02e1n+Q$NM4^<&=$MuNh^{Jg zxO%hT`ln0`icbEVCb29Sx0ep%_72a~8X_o(irP-+gl+kR{{WRlMxT<32&QPv>d!?h zm&+U`Qp=$-==_*+o^o20%T~ z0Be3xHyde_3+M?oxh!7Fck8}Gf?Q?MSzd^jhhOapX}?5r*)R4E4VD^6S7{Q5#W2k_ z>0_S?{l&0epl(ZlhBVUSLg;Y;O#luhlm{F_*bef(ywMp_yeDwZLI8U|hnbJPC-mui-d*Q&jGOhsVtko*e{ zNA?cJ9xg>B&SOGpJ#mTs zu7*KwY_wBA+c=EwL5YCKs%N-eyQMQ)2xq$t?qPz5g&>?3EL>aX3b(gthAli;X2wqIvjNP_ch-7cwYZf|Y_Y!lUQ(0OqSPYeOx#Kpp>@3HU$FvAZs+F1UL<`W3ao z-S<-y!zr$x!>K)vQ#cvazYY}Zu3h!)nw72IZzZ1%Ww~oNT6p&o2cfI;-CuAp{O&Ix zt<^qjo#DvRgTW%F{GgfaUAd-A-?8&Xqz$ePka%@KEC+riCoDZpeD3SuQu$-faXB7qj#-pC`QAmMlNl#lF{sEv2$xlsDh@wB(#V3mDk z69xuxy^$i_=E}E)*iymTnk2n-iswBQ%Wdy;`0{dkRR=?lk5Y9khke#0itYl$!ReN1{vd*jQeJL$W-#8|PeGjTS z1O&iyWyr`beo0J47Qd)%ekiHO4+$aACv(5XH=zfoD0`K;yH+HfMzZK1FQ_JriZL(K zA>jfLCJ_a;|0hs-czU}BzxNRP7~LifeTH$yFU%^6 z1ZjGE2EJFcZeTyI@Z@_>SyRS%Q{xAyh1jnjh;P~Wft7*SI;4X_dI-O$GJ42fATKUB z87EK35`VQPvA|>j2j*_I3u|&$>|^ZzXq4^cGNM%9u)-81`GMr>9@dmJk*cBuNHUSQc$QvewYFO~*)vA&_I zcwTj|F>rvJbMD{@M2OZl!(q{@G_1C5NuVKW0=zux(YSV5kH+1hl~!pRzqL4M{MIf- z{gzQ>x+_7ei7f117Yl2gx1fef^8(KK;3@+{pc6+MUOEzJ=2kaRI0!tfa?gz0U9!4K z!!5WFJI6tDQ%@*|maw|X+p&U6!cKos2i~CuvSuX_$p4BV2-WwCfpnN=M=Ojw_Euu# zQluupVxx}kM#|6|8>%D^w&TyWK{6zqS3BQ0kMO9r2XRv33}UcCTs|@Iy&LNP$u&C? z!ljDXXa<%U2wt0r%((o%+IU*e8$IA>w{m$|q$9mM@S>^zDw!g`*h3b%`oC$4+C~g+ zeg35e*lEsBOAm-0J}s8d&&kKK{4N)0FUC7NMf;%$hLg1)29r;F$sQ{S%1b%Oh>4hB z86dRnUj1*ZC5@~!!@C8TKEZivPe z>cPrOaG>-OBGpt(d5>j!F1v~M$P>7QsNU|`aRcbHPhc$I zyVzPub-`s@(w4AFIJE=c%Ai${H>AJ3zmw5a;K!j&I*l@eygG>?`5DJ7=B6=S(n;>P)q ztVH*yDb|<$sq;NTJ;!O8*XPW^>24^8MC?K-n5ii-zICPHyOd;|h;w@}T{z3Oi$ZBtyLto&29jnht2 zA);NnB_ys;C%6!S2riVesX567Trhq5BQ+MkWwbcps(!eeazwR=R(Gv~rhhNno;mCE zr)%EBfQ#s*sX_AY?QrJZif7^7fhqKj^BcN|qHF3aF8vk#9i}Y(6;*fXudrg(3k!f* zW-t22ns5HyL%+#h=zu6wAU8}l`qc1CsvYFz5(WSMCfWfiI?cg{Mfs6h^s&2x_g>cp z6SwnijYvkFRfLGOX?>65g0qOwL-;>Pc?z7Y`{XWB15tf<{}}b8HKp}gJVxN$*l^k76qq*<#?w$>UQ&VGwNqg4ep$nr$l2ij3=Sxth|bWo z$wrOFqUzeR1T~jGfo{M|E`Q=CVz>}pT>hy(`6+dB8PQRhu}Lztm;~~^#OhyIwPp_4 zyR`ZjBv}0m+;Uf$M15G1pXwf9HiwG0{E0W*xCRKwA`Nz3>+&eb+^q$fy8}}T2DBfW zG%%~9BqX4wv1J4pALBv?_HT0EG_QW*UHv=n>QnFPXWrGHdRIU9u4K7HxX2QUT>Zj( z`j_4nW8Aj1zxJ+v|~ zwep8Z;PikcGFH!DynH4r2zU<;!vH)j*~QOrW23-3&co~-GQ%bK@T8P^O8$qhF~o@ZkmfKw?(d+-Y}g4A@_oak<9|z5HTNqKH^l65O6cyA<+e z5hgd16ttzX;Y58rxkka>rVJVkx?1wUPGq^wNE2lCD34$;C7(|@L=)7+PeN3FCV+#$ zElaJjM`63&(}ML~!S4xgsw=OJVG9gjL%`Hu6y2^VQkB1JL4pD8O?_n102xx~%fF#$ zmwzLrZATAsxyG*O6d0?0v%p25-@jvphmfascz`lqy0zb}<2h3`)6ZiSXYrpsQsoQo?2xN?Ka?lg z-olHtJdnViJYv5}Q#OS{zdiMUXB+VS-3>f#<(heb=Lgu+12)t^Kk4}MWmBGtAjOfu zk`==zE%1Hb@L|V~@S8!`k<8-_?Rj@xc zl?hMy;<6tY@+IMpR2mFs;c*icWg=XAz$8!zZ@=h&BwPr1n*`mli)3oTmnEXu699(o?{>>ff7^1CcNiH|)rjNj&yc`3J9 zMN+(Xt(CUOV=aob_*+M$P~q42^3MkRdD@58Sty);Ko=D`2zJ3q!E6bp6@X^`=VPdT z5bwC#FH~(c59oQZ&eHHqBc6oppY3nI9>i~d@gysr>vs^k&Ld+=<^`m>qjXN0C-m&8 zCk*-W-?xFA59Dg;)9#nb+PHJx!?j{9Y^~0q4ZdCFc}@1bY_{*)W>jm-x*z5#K0MNq zhJwwtf;a>QJP?nDP{xS@V#yIKeUDvAD!xF&;eAqr-9J>FBWpJa1+K<#R~m z2i@IS5>K?s$RUHLlgyfyS^RThd|g>DhFf$JiU>72>+2#*<>2U!t7;yV#vq>>3U;5z z;84qxhK{h>jx5M9I8>1x#mhn}W_QpiSLC=X|6xBzNyo2j2KiY+64KbS-zLrOVc`<| zngzjAhD2rp(1>lTE8@)**ddKzQ)b^OOpyAEzdO87G(2UOs~;urY4+gE`z(q<0StR8hGY`Y${ zcI%K@nj((TvNq;`d8*YT9xY;_jZA(~2T?@ui;=HFX?T8i%~tQ}vr>T)U=}`nhrsv0 z8vY&R-ek-d*j(RcQfK*9&8UGcnA3DW=WyOw*daLxU5z><2gSC}A}>3N9|m|IE=ZvO zTV96LzDR%sKb63{FQamGR3!LGE>;o$^WD2hzZJ|AYn!?Ut>??ng<4n&&qsr;2p^nO z;I0e|n!EAM$(8e!+xNFPIZSPxANR zKVDSd@eB@b8H1aD4)<0&_{&c_TBUi2-v{KkD=(l{{F>!C+&%CLF3!tk!ECyO8eK=x zR)iu)v4;N|m#FZU!QXxoRBw*R(M7@GR&2`G-7wG{Z*;>|XvbCasQVUD^EZMXbdNXy zQf!a7gs2(s4}UcA;qa{^?^nVBFug~HPYjP5)3{MEjcczjzFMgdU#isV!{c-H%JpigK0QD8 zZ1LjawVCP3*DAM99X@>I=wbfv=n45bY3~jne(vxQNzmuVjvO03Ix;%KoRZEf{ySbC znJOJSK6$iMDW9A=TpB%6IdQa9Iy_aXOqCCpD<>-_rpnKiDw8LsP8^@Qc6jvobI(nl z7(F_9^5n@QBb94MMz2+lpDdM6mXDPuCr8Rl$a{Tzn;S*Dn&mBH~{P@wSBj7)JH(EY^6sc23%9UeNBgZSp zDy6BzDC;Di9C>bJ@ACB}XU0a3A0B=Ftx9cvu{v3qs8^9%o47H*I8%PPGOb|%vY>ok--Sx8R=QppFZb0d_0q!hdzIUl`gh(%}x9wW_ zduD!iwlr5BcA09u1**)_zs$R0J(a1HHCp;%-gVhBxmc^u&sy=Otd*71rh*}2jll)M z3Ezi9@TH!I->#MxUZ0;kcWbh;K;T@uQJue8E68Z_Y5~UQ^>deAsg`CdH|MMGJ^PhP zwT4Wm-ak5g807sAd5_Lc*A`|JjD8z~iY>EFau=Y-glk%&!!sw~Ve&XwPu zua-Uga;8*kXpfa%>X2n8 zR&+PMV4c$*jGJ4>W@hGZURa!|PcO_=kkB_iccW6Bu2;$hWLrY+wX*qq<%*TDL*KX# zjTcID@Re|!b0lZn?b82RFoTv{{wj+Nlk;;^)7J;i$d4wTt<)yrjcwX^p>%7kdcDS` zUFv`2GCi}2Mfh3O+s>!saQ;w;STnf%=Wdl|A%I~1p-_I!P>ZYdu7c@*tMsk;>Z_Hx z3M4;Y-8eElGJJR#U%HOV-+;>!)5wj}bG173bmzGJ4K%YlEly3Zw03%qtwm!yZ~LQR zmO&9+>fbH5l%^IFl)_^%l!2^mA5=om993fQ4E*$lK&Nlj_vJ>s%Z=P}uvrHn_7|IUi z8bcY9YXjqPjCygZGPW>{ndGhschz1L0;mYQ+A2#i$~dZ7PF-T)xjUA2ZJ6$fr3(op zfFIP z$=C`-x~FCJb_6(t>ZUDHfkC&V^)#4|L`j;3y7d3X)_Fs@GE=En9yu#N&Gio2G0x|F z8_pof8Ww3!h-(53+u2OGwt79Z7+EW03viqls?()9zQnZ`GgCX-sgP>JU#`q7=ndTI|0hu(^_!-1RuC53GJ zH;&EJ*@K0be95jcI_?N?3+qQ)#P5od1#05J5~o^GvXD#znD~$J?Vp*h)gL~OKh3(; z7Nk1@G)Y<+CF`)2QL;Z|ZhVV@YW>1u?Z%;t_|x3z7tU6uO7K>#Zx*OY(4>UNEio|dot#~P!lbSN#!ic4 zhO8Y?vbD|0BTX=I&cWnhZ`_J44nCv=IK}+e(()(T(2VFc5S`}mrnh)gR!}X zG&cfyjDc56lXdue_y|#K?g7nhb>Wq1%wJ;NZEU!S8SW#~fl8fy9%OJRmTX68Rxnn( zSgPNMQu_AGzk*LDkxS^NpXgs93K>(!N3@%*YY|Y&AQMc}Vo3Xy^!52OrO6vkGACm5 zx)>&6pCtr=E|xG@%~WPCR_DdzrjtD!OZc!BqDEq(^nT^SeAyNDd@Riq;b{RPO$&?l zv(wd3>sr(^OsYZL5TQu)wb+99hp920m{}}Wnwf;u`c|~xT69#H_r)p(`k4wMd&cbW zDl@KPdTWuIdeln~PIPyN8NF5c=HfJGEOWKW9KMGp%{v}Txi?HXp;(7x5g?kcI^YQh z-|ZN`0PR5J`t9j*1&(x`D<47S@-;Mab-G|iCvMm3mDyouHfqBRf7gbwfHFUKedhM1 z+Y6Pld-(F=^h~)@-H3pDU}C)VaVVyOuAII;_Y^!QEM?Jif(kBFs`cqglkdb{+WOD1 zt}s0{Jt^VIu?v;@jrnr%Cf99><@sW@GBty?!{8OM{76P>uS}>W3hbpgr`dMRGUG3E|zp}m~_nwh)_<(vU)ts3ffkUuSrMb7ZH=d(o(s( zA>*;7kAzE~pwc)V<8xGbb=p=i z50ood-zrz;CNWyJkX4xJ%NXM0ks|f&gQq+terXGbCd^HR-=czBvE)6A-3FKil#J<= zA(y#ZFzkqhx=Afq^cPGYhFN$!ho2 zg27I^>H&KZT^wmKqK2#X`n=9iRdCLmYGFB}#BVLmjm?#2Zr7$AAp%S>L-D>ap%!)y zHt)7`7T`1~ZeNOJu|LdWd`|q~w_IF-Qc3N zb@j^gm0A%Va=ilY?%H&{I6p-vPiHN|9q9rhvu9?e;a#iE3fejW@yoFd-yN99H`(0a zU_&=3!9deW|NUslrRmwqJf=+kdW}~3#n_5|Ay84B;Jv&!1<1nHZ|2cLWT9X{Q**Wa zBeCU&0}10X4pNJHhsu1bf-MTuoX0eo+ZJPDh83?Vl0!(qp@U~GhO3v48;kI9UvNy~-TUHjwh)0HsN^`f1m1=dqTD(~)!h^z2D@eKswUEf?+;xs~;_szZlh1OK zdKsoSOaD3~bj-jpE**f-v32P4^?gR$0>08Zse1&RTgpW{~ zD~`XpSgGD-Oj9C2*xUx#y-%Kq3DsnSFXWDp(RBTWAQ$4FEeet7?v0%5hIlRlt2QJx zU8BvIwA+fUEP{Wfe47xx>@ut%)22PrSvutDvEq$V?MAVN&9W-X|XD5-^ ztu^i2x}{(?F4#qmrU4DB6dQFthMV{Gf7@*szGu9=(ew8nMA~?Hv&&+P6+0P>Eh3~? zt=T=tk^$3$&F4$Cx*eX&d`&?A=T|kSJuMg!?f<+BzliQ#vu)63Np4 z5i%8UlM%g_0rME5_tj}GBw+;etT#qbyiZ|Yui$c}{~ATl=#x>u>T8}~wOFf&alTxg zDPng8hs`?HEUp)?p};a`fY?m|lRkI-S!_)%PS?3l`8^K1)yhJ(!i5tg7E2fr=zk$! z+R-#0IsYRASShBDQ?AfFEOYlsEG}|1Ax9$G%%w5Nxj~%f16d;AE3&oL_%-oCe05;$h0SSfepIgPC2uB8#wyg^taY zFVEe?J4sk#gu$A}OBFC&3k#Sk&SHP-XGB7otMR26Atqoz4Lnj+i3!BcVH|X zO)>d=(i=SO6f_Vs#kg4KyK(n+5cJ!9|0<{j_oZ=P8lGKP`tR|UssD1st26W0N;A{n zlHD%cUOamE#PhFnr5}mTuf8;Yc>$YTCb2Nt92^?2d!I8HX18dm5XV1NS`PKN2!dk5VE>dtm^j;ZrN8Ff8FR+3& zrs?-FYM${ojeQOPu=IaoA;j;ti3(n%#LmaVXU6cJ{81Z8JX@KZDOK^lQK|gK+|2C@ zi*-h+yu~-huwr70*TjKFuT9U+T8iPKdv^A-EmC zI5&TAeu0582~$@kN;Wr-NfEjLf3X9l{P`ahFwSR@ z%BA~#(CL-8nsfgG0g(Gb$WaKLs^C4yhkmxJsNTiW@28zA#!PH=^YB0<%@JYMFZj_+JJj}4eOb%skt)nSlr^H(V3bcbiKJaOd6 z@#p;wnf}W>?krrlT+!@+Ke2Y9H#q~ibn%gM-0`dj=;Cc;Ic|vS9go>f=q{-?Ecf0m z&Z>x`WHe$APMDjX!i(2e-8+Fy)pxJU1Ni2IbN-obOl)7=jq08C-Y*=-i}CVme|aOs zGF`@b-Pmr5i3(#&#(b-5vMPGAauv9sd(1CP<8-yT>+lcw%91gg3uc4WSX;S@JupG0 z?7gxv6+2`f#CGkt*QZ>o?qNgjVbP(!BA?*$>M zS-}QNe3~f62H8EG&&i%n??eV`6oPv?o6W+$cV=gb*rF+x=Udod#mMPoiK}BbundWS zDwalT_n=5xUc4#D7aXP4?6GOAjNdB}>?xQ>)Lx&roiQe_4Dlo}V;)7Mq>M1e%=`l1 zQ1Z=)+pB&L73)E4h(`~*vm>posM+H;#a7s`53R7Ci2{|WK=JtQFb+(&=Mx8V@;zs_ z@4y(oLR~NMP?E*G$1KFQ83?M$i(Bx7x#8g!^wo`%qOCY8WsCP(I9{oRj`ut;UVD9U zW`-;FJWI9rdKHIJ?y|#|Gp~Lh;m{0xB`NfF`6I3J8gsk3m3z2z;xG@Bet)`h^YR?y zTK?AR$U?^ZrJ2P_bT73;@uBWK{eo1z~s#GZ)oNWpB69+V*OgMVJT05J3qBCsd(<1kt4 z6?(gB?j^dsunR2x1KyDdP3B~uGbeg1WK$c&nsmPxyI;b+!HYoEKbGSjj+?QQkk9o5 zFmhOkzX;`F!@IzV+TK|w(Gj=%s!5NXtNr)P>F`C9=!)Tv{PvK4DYA!rzIbhBeiAQM zVJ|}E7WPBnh$XDCad+snyvtmdm$IfxkX+fXx7VkV6u$Fw-K(o>F%nDpc9Y%zH!EM; z!0O}u*P8+_H$=ME(*o}zlA(LIIBcZ#dGp+(dC0 zh6z6WWut|nw_Oo0Gn z%!+st0bYYDVw-ZU_8F)`oMcj_VbIUdN*;XqHUb<|d;t}q4V>fP&U}FEv7x%immLu~ zvqwdwc*Q+s8}B{YU9ha5k0S1US9Pxj@5MkZ%&y*)RJs(qkN5VDcp1rquJ#C$vCAM1> zLf$4%<#a)O|J2`9>K^br;JkDvMi)}!=ZqVNr=tTgBb;!7@ZM8sDCVSv@S2(_U1u;9 zVbRZCjXDwIhk1(eM8FLBg5x<=&WC}}N_9Fq+%n-ES~a|MzZ#!CX>_1I_dd&A@Z#=$ zBRi-;v)QR&91g#W$?386A>G}gxoaO?#M@N7RG>! zSa>eK-!b)=Vp6*j`yq9EqNj7n<-m+$8G9Ab2W7lUEl2p>lR_h(Fb`OX#Aj|DZaQ=8 z*xQx;P`7&ZHXDkg0kE1p&94sGL7(SmO;_mO({!g^e z&*d=W88gT9S^ZdXv5te#u|&7@zvOBBUk^-xsucv!=ZbUK z>!V9tdR*`PB#!jJX&F4-8GG1#dx$#MQvvf9V_=qe&mlc@I z&tPz`7U$<;Cm%bx$aF-H?7{a_T5US(8ENt)&W_{QyK>nEW8g@5aogIndQLiz=M;D; zRBA8Z_V>)aQN_2y{_*x?7A@!sEsrE>b&?c&c@&2x@FPysQ@78+ykg04d~Rw!ytQyh zKlf%2aK*RY&J%gjt%h%1lI;N!apQ^@T*Yc-7CS!OIf-xZ`!#wjzI?wUunSQFa+9_K zI9WkYkL`*?y2q4@rFTcxkGL5Tmu8VX)^?q1`BGh5g+t4lrIIvN;pXXY`- z#T(mRuu)F!y;-W_j7~l&!;rm?N%UN#g`d3|MJxDG4&GApocJ)&rt^P68iEWso`eV9 zQzs@hyudiO{q)3VUQk(AWisyXSo$}4s`fu~s$G^3p>sx!CClpkHN0btc-xIiK-F<* zHN8qk;fMlsnf+{~D*WB^*dm%Mx7a`uSS!mbytT)P2XawofSPI@{fw0}|%sr@R>Co}YHLTQqB9HGFRj-`r~U?cS7E zsW5fNLfsAQ#Kc?NcyB~UM|-#tZKn7A9IWL@IkNxZbA9T3qTaZl`M%-zPSn0v%CPjK z;lk3Y3!vzM(8I6{vwZ0uQ?_AuO0=IgR>#L`t}WIp&lYE~K3tudzV>WUKU#I_=S+{CMfuQPL3X}96oV!SK(66 z!xMTEgzThzMZOh|Fkqo)L;t1?Lxa6NgMB^gHVlsSY}kaKM|w8&^h@&4z`35m2M5Rd zH_Gj9`7IR&i%M1ONA@l2Hw-TSdINHl3jMfV`Uw)>Vd5j}Hw-L&jH|{^c>5##S@{M@ zmplO{d%>s$(69OX>j?LrZ!G%%lms= z%0T~vu5@8P*f_mz!@!qOiAGuT6bHAce70-~%W=~~wk|4G)X6{)>o)A6Fd9E5J%lmX z*ra7NKDG&~uE~upQlZ8NBBiCj)Do9|=IXcfGwIPc3xlic-NRM)uyL9K8%J47f5jRi z*U~EPmd~-8%WoF6_)n4Rn5)e435p&)HaLci(tD_4QJFV(qWaqn{uR(|jfa@LJk2JM zuT*>aGHwf6UZafGu5&k6+=XZbs?+c!f2GIC;ELADiMMgfdvr@5)m1wh9}hPAy#6lr z;BJsBa)=Ren;3(&bMEsz zko&$b%-r{NuKPM)*SWsV$9+#)-`z_yB+(I(WQ~oOw6>u==dR;(W{6hK43pL_Z>#^( z_EK&=zLX5nDkZ}p#@V23=+t1|o>bHNoqo6l6-oo?QiHC6S1p~4q{oWy^Zw}7g_eSwk*pRJFzU|du@s4YGX${Tj{XCG!W5x+3Jz4Z%VOS8*A#OOC~Gw;&dxKRwgLH)wE zF|dMaH|EmD;jFLCf|_!1g0*ZJ%v!Xj%|dX5Jd`i2X6$Sh3^I0gXiygDvR{%RODQk) zpc-|$Dq;}p3(PFTeo^+;@6+W0<#{A)O8sa?do&yH`q8Yu#*V~vmLZz^6fFVzAPh zL1$F3Ok{mQ+aq6o9KJh@y_A==teIgbF$zs%M>IF1*98#iinew$>Q`OAI9fM3>6kCn0X)k~}k*>yMNtX;+L%~(;QT%RE zLw>%Oo(7teHZG6|nLvr-qxU3;kNDFp24SqN3!~HykW(ru_oz`~Co-}8m7Nd_?ztsC; zQk30mbY)_r-bArEV&fArzUHHbjK;|)Te{-Y)mVm4lQs0CQOT6Jz8jO3*4Fzj_`)Ag z(gvZ(y4bA@0s41wwRNG{jV8~faabD$5c`Y8oUg%@tT&YtqgGoF0~QS(;SQL$jWX^l+C+&VUJ1Z$qX`?ya{Wf9A;^ z7r!%SRJ#lV?87iYON_Mh2(*)y<>v`ssgGV zYXcu@tFu8SnA_;{!>0CN@zOxSq@cDkz;z;$%PFU69#O`7r$nYXQ{7uHXfTBFY5l2u z6*FSCsH5K+NSDnx!RNMz*U4SVuC$QJixkq_MDg{bx~x@eG=`6)4Pl$jto-Ja@+ZNU z2Y0;rB&dm?81MF{W+6ArN?3HY0LDY{v^l8Sk|8;kQY z(QfRhCZ$d_b5n-BAYvS`gmz@(3!KFilA|RwWUQ%1`cl;-0w!ZweL?K@^Tr384|?Zi(_M2LNf<9p z)C|G36=r57WsOsAhDw9tD{MownvW?W?$V`_ zwHJx2FYpG*Djn`gDwWJbkQ-WIcoycLo@#O~n}141Ap#+qOc1SJNeFE=M`!{Su=II& zDw$$3Yg#)pHsTmk)1FMz2(KF^Q|j|0<|f)?bGYC7*pkhNU{s6DJcbUKRk zd`7T0B2UXY@xp=4jXS_lN%07qCvSK+kX~TyPvUb-eK`_(d&! zj|(q68=1<*8}nLCiVf)!bq@Vh6B|!+sjD{)^M~Q8S-PCA%NbniI|Z%QMLex<7j&I2vbeQC*n{aTJl) zHJfEKC;l(6sc2i${M((a*`v;bo4an*6Jp3dhoEcF)x*SW&`9W7L4CXLegDzcK1WK2Js|Z&q4ko#3wLD19rPv`- z+>K^xrxJr+_4uSrKkLu^or8vgDb>O+!w$#JwwqqAxytY-rUNDEqaD-ZSV1rnOTFWS0zq zGRnOO-0PDM-Lg59W9Cisz+=c{N4kZ-u_MTDlo2>8g2)-W}E*3^mB zKNw3j8G$%!(pKw2${90>TV$Okqt|(Uc3~#zi7|rtdIw!23Dh-+V8WZ5l%Nb>1l5>k zs5mM8(0!^!H+s+3km)9Olx*r?nN~>Gn2#86lr_|}*^I(wn%jZ+FsVmeD2e51uQEgX zta(HGB4b!cjf}zZ0y83AgHmAHqQN6ak`6UHXrY;!1(*aLm>rM-fN76fKT-9d@Uv9BxIIbBkPt4)n_0JdMR{J5+}r6LJn#a|-p4A8Rh`AMyDpgz=CUj00u8Nz_N@qz7OdKq* z2FJn%y9VqfVEkR$9U7I(xRcg;C9B9zZx!*xeHw+lsibe{HawTOm+Kj5vjkm;5WOG} zMr)&Z?hz;0rK6boE;=ag%%{*?I^Vin-+}vtl`?CflO76Al4?)2HF`nXiNz$O;?q*q z&hFn*QvLmSa*gED3YCKk$#ea<$dnDz-b9Q|Agl~n)0xotF*qz&Y0Non#SD6RxDxb4 zC&zJRkeH!eRKS4mN@qgPU`-QOSf0$mL?S^hK)D3D%u6StYD{K0*of3XaqCoX)vB6V ztXh{;Q-%+nG@f@^=is7^qC66fkdaGk^CTu_JX;Lz5Lqo}WVL0UYkuM*&ewAGcw3~e z?=i`h-fxP?ns@m-4PgsoG8&Nh_ypOm;gDr73~VXnPNpcHa;Ld(LRKGzD0U4RQoFR3 z_0ovEyoL0Egi7AnLT;Q8?REze!b5gVFNF&T*rm}h)mBF2#ujn|C0Q2jwaRN~*P3Ml zJhHcAK-U?8mfyr5Ba#AsIX?ubmww|meVfAQ*miDqVwu8=SCzop#?zCa7KO2rkklOV- z88VDzxV(+Ts#0rwRk!|5&J1HY*@9_i2!sOS4RN?Pv_^IVnplyYV;U=N-YI#nut+L= zKN?*jzBW|-ko46X)7lu08MbwJ<>CB+#0xQQZo+KE2T~yt8_AD`ZZH+tYKmUF86(_< zYO)0kls1kKw9&?GJ!yS0DvQP@6XYINK(X%_xT>o0i7Rq)$kpmZQ}9+A+pEA&>UOI| zH#irDHjlZJpGoUet5^~*1ABy-DEn?UK*<5J2A@is*C`y;U4SoNY)} ztR~T6(|L}Wl2St^Gj8ovVOTS6D$g2Z5UG|-*AE+swM#KE+uv%w=i{UBk^Fm15LkuY zQ?@1~m5I*#k(*g$mmeVzl3y{`@{*pMk5}8KLTm~u#ahL5*V|+9>Gg(z3)9^MFrFhk zl~gkZ^@E;8^@ApS0RtyS9;8wIJXgDI(_C_--FOs;{dUw>i8N^4$b%8pCzRL@4hCg= z^OU1aetxXCBM+*#^@Bq7#AZ>*EwnTQm%BP#f$`ae%mBf5plXIoTDZh4J5MK5(sbt+ zKEw!CXQn%|Sbio=7~W@<9ufrADHgMwi( z(LNsQUE%;Leo#P+vg>j1rPGm z*kQ6z-$+nsRiP#d>8J!IX1NIpk!?+KRvN_orhZt;(Ci{R&a4X&(73Zwp|0!Ph`6Ax z3s%?b%R)l7xWd11uPPkn%$}cMbFb2M`Ig*(w1SMAkees{V0mE^nLj+oJMJrJkgH|m;z^%VtOU30{PBw~dZf`&zA!svV zJg*+J&0D6!1N4#x(iz=or%yF@oXAKTy{WXFKGN|dqQ7L8c__Gf>>~^7q~uW5_KMI} zvvf#BEF8K)*V6WC`h(Qb$#Y$F73 zu7gpASm7IOO`yjrb*V27j!etKNA*M15k4ta@bDRJu)GFc5KViMJuOIdJIVw^U3R-LHO(?&o<1Nx3T-3Q+ydkkcwlU`#jk}KgROd$L373sveP$_>~q@NC{Jh z6Q^OKd4OjCJQIv9q|E*sPt792^5zs=poRm8l}lq%$dARY?d>uTVrn|w$thyRy*V*j zNFK_7`&AcN82;Pgd4}H2cvp0IGKf|EM&Ft3T6rnj(3`O%9Jl$`h_uLetA zSMrdfFNz3In~f}+u>1=SzrWiHsM_1u2uf|Sg@XpWMarvA9Mh$BR`wc;PI=1dMU$V$ zBf0W}rlA=w4ts`Bq-Q*8Y^<_R7GbveC$*A(;J`(1r&6t{O<2Leb<6TMnyn(5Sj)B| z5E96RlY-El2HOr;cwdGYk+HP4J#CE99Tfy6%Pb14rXUkWXbUe%CBxW4gZVsm+}LGd zdl{ZBVO&%wn`KSyjG%r?(%N#{8L5F@o?volD**8V#UFpDQ*9Bil^2`5r|TPHYoX_T>a-?$G|O!Dy77!SjYC#*|?P)FH^mgW<} z_-LOOdNf}&b)|bjV^?bDP4x2J%zTuC%=efE)erh27Wf6d2Gb_w=W!^s&ck49tALXn z!E-aLe%m4ivYf=+Hwnp?F6k2jv$jmf0#D-h|B0sSUK=}36X@ewLTvtz%1?} z`yyY`2`Bsl<`Ua))>Pf(@H+I!vmUB|^Lrw-wPCVgbgQkkjjMv;xXLe7GGm6je#LZ| z77=7~uc9YfK4C)Um-xdJSjT%YMzq7?EG40Wc-iz9S<$pp9oC@g2|T6}uW4;~ax=xo zFcd<=X@U+OcsT)P71f^T#4u|Ytp60iD#*efXeTVE6U5v^U~;=QuBfl}5~!tN-*--6 zg%PvCq>o8O!pxRDpk|&1X=Hu3Y{x>l3(N?qBL43I>K4BN&0yB^zTWLeZ%Dar0 z5SQ~*`Xy&g;kwIEnbEa$(|Pm-KE?VaN^YgpcY3B9%WOu$ zRC$kA<>n1gqTxIJh8yy#BS0BlBaE*MiwARTQQpV z0@K}OhwnJv#B-DcpLoovZq6c$Kc5zy1cw#vVAB|^r96GgWUIq8E9Fd@ znH{2^t}Mek?jt_`&iO`SLE9$tk5Nhyv1;ejajVE9VU@L`jW6ojQDaRzUkn*o<_B3E z>{YCG%z1nk1!&zdD4Qm06!v?DXM?goW3{NVaD?$(QXHL)-g4tGD+ii;>aHXOdj2cGRpgt9uttQn8?BNXE zj`-uxP5EY4LDK|BVd-9j*J zOryBKi54SO#hfg#@f8`3U%`uD+xcE1TSI59()xNei@;?_F^f)zicQfepJrIfMw;-{ zf)I@|K~)FGBy5mEX;{8Od)jAAII!ml@R3Q@h?yRtW>cnw~%BaAn$0kN zgd7Tb9!VRUtW(Wq?9`xm(@yAxqLqPdeAp5wraSN4h@{M-K&&M!U(a-hR8eqNm2cJ~ zSSwf5jQmBO^w6Rc(TE1|;_qbh69$@Xir;!q2^p&`$wH}!*5Zi{t#HYU4MPWzO*2om zy^4<{sumO9Qf3EH(T}a3_~1jakYXpharL6DV0@L?x|$*sUr5d*k&cU66W$`?Md267 zKGZs7@*vd2L9M8*fy(qmfE_C_RB!WcQPuA?8zR(tiC&;IskEKV+8Vlm%KL?CwiSJI z?9cV$ie>=3X7L@*SF#`2h_kqBWq_T0*UkxTs`j zFmQq@AqY~3uU=$8o!la%4Od$%0SH(kLqUj&ba#+FovfJY?nFaM*i`s24>@lmMVYBR zpx=22riZNlK_Hn3LS<)Js-!8w?=G0AdQ~*Xx;XN!k{_f%kT+J`{j9XU*JQGJfE%*9*9x*S6-?0SnpUB>X)5ovXqF0&KNe)}hI1L( zqySOe?MvKE%$GI~p}qHau!_T*cskY&gi`l+XkWZiF^pO_t!i7>+_i@<%=~KU3=Iu) zR*tv&!x{CvS-$7pNjB%X%1ExDv~DpUz%J5Xk0-3Jfa5JW>aqB9_@cD=utCj-)8Q)! z_NxAt<3_E#apP+@-L+PrW~(eVtj|%?2Z~mCE^i46#wzp-DjMl|Oh+*^fXc`ZMe~*=C34h$cmpNsxT3_zr|CGSzqLun z(yZsJ=w$1LuQnwN{WL+a9*gIy$kzcYFY%b+d6dGGIUSzoiD~0JYpPm|kS^5;5tJyYLfBP7^0D^X0f?1)a9ePa)2MfShG3!Ga z;mQo-DxX^-b|x$NqQz% z?3lrWem=yzKFogn?DJto`FJgVzKCTNIo1w;resO5c}r~8lR3$VH8X)tTSD2qw8ND% zId)?D{9Kinusl!oR)M;x*N8M4ELc2l{TOQcO zN2~+lv`&ocY^9lrba-LR){#N=ohG#`p+UU5b(+79SK2^1oWAu{?lX!#)-D#`4_dZ^ zehrd0q9!@XL3p!Zq0Ll16X;wg!puvQ>Um>nFCD}iZ*UY32to=3C`$M zq0t zvzByalvP+)zt6H6#A+pMw-qnsi${s)c($Gct3w|%Out5aSe`JnB+)xTDvTj zaXFl<3*4_ObO?cu6;j{ZVw3`qP+t~!nBLFV$~T@~mGjoD@^SM!I5-0N7ObrH;4!He z$w~B2m2Vwh#nTnu^bX6{!yLjB7&gB@9bOZcg21XwOlkp{?KP=m9eJ9fB@LE8!fiW= z*ot&omBY7kOrkyoOfH=vaE4?cosrI%M7|Kz>Sh_ULV1rRm-0O^=x5_-(mD1jWqPI} z@oGABFRmgZJ(KeEeXntx2Bs2#aL?ZT!P4mMCtfDZmmR*UcZBz=*dtc(l?;IG9qA== zMS0{i@R=@2*Yu7|sZM1fV_omaG=)#-{?c@*2i9~tr0hyri*-8Hq8EN9C6n^;UY zL&NFP_EQS=hZQZUV%DTutZL_2SBSz5cg#t1!&IjAWIU$+wv5wrosM1`&Q9m&dO~8~JM@%lbjV?k zy^z%(I{BQ1-N$rN0zUM@1h`071+~+HAwW0nVvd)x!(ky(6{32E*8 zl6YLZaQgo+Hh!SsH&trGrB?>pKq5kfC+iB$GhtT0%u?4bF}7-|rr0EM@8q6DD>b{5 z=`EaBoaqqitqKhV66sFzO?pwvpg5CqFZ0FOna36Xmol|1U?(fm+A8=#PjCb4Urdy3 z%;b<^k^kfb)<1x#UXCLuvO9SM1m2IsZr1Ln+uRQIj@W6jn)SAJNIf?eVB=V%;y{q+ z0{z}A+e6#ssS}#Q(T=qC0s@`Zo3O2jsZX()N&OTJMtzPF;P57F z2vjhb>Q{Z5z8An~qDl2LHHu#8^Yk{~r!$Z%OzgdR@~5Ebx|tE#RC9i&88+f5DH@;` zLa9-vi=e$WpNf!)hrN*^>M)aG95eL@d@}iaFaLU| zly<4rvC4<0`X21G7ZYcDPl-Z#KEKS3gT?DQX%;uYt2&Sv;A^xVV!%LuTuQY8&H7i@ z1|@d2zr&#zb)6xhchYf9FHi~}xUB$h?YdE-U*BR&{x>oe-;&mE&;ZzppFD4u!~cSF z1AgUnXc%GW?BN^lUQ3Wm?_&_67gcL3eXp*y^-^cOF1TKusbHvCZ@{dZ2J2fflX5IT z2y2@0IJQ-%q18A;tqCP94a-wT-<{^?!}^6Sr*h z2kOR`!@t`40luT*dx44Zd|yAgCyyMl9Zu77(+L=KwH|mmF#}#-OdvxE+o*SOuziFh zs8O&REEh3K({uhAr@&ssg1*#h^42x$aLc+XJekhhcrwX+v(=_Jp<8Cus`>n6x<2#- zT%V$fH4W+=d4qL1%!!nB8t6Fuxk-b0@mdoYnkYbly=()FJ0BrIABOHlNCt}D@@8w7 zb)r4RV%v=0i`DfaiZW0XYZ|hhy^Y0EA*PMzmr;Ypx%N)9Vwr~NmTiF3o!*g)ac$@e zZsZQKqMNZ(n7y{h<59}=3u zVshZMf)vYrj0!dhD}cS0X|{p5pW|>|kfe54zUl7R0LgnE6HJ_g+Ac-%@)V~F=BpXA z+(17a(m{oRV(-WbT8~J9M`Vm4Kkgk_)jP7r`1fPU5{^~O_b;~hj;!+$*2sE)-nZF7 zbBI<@6hOY?MAb6Bz8h(?dmwE*(=xAniBi+%DRN3nTiD%Ys*wwqlJqv`P!1#3@84A_ zdR;@AP#2B=dOO%`Dy=?xb@i(JKdI{!^Xb?C>`AZ%l%vt9ZP(~v*Xfpmxb3{NZ+@w_ zxibd1usa`(hyj=p`aRumO02_a->uhh)8-pA7q)3IL!}nO*$N*L4^xsGVJlSU>ef4Q zB_6q=g;YFJuAh)u4cUT}NnV;4Y8e%0!3rzpfL(gOs1{rxs25eTs+Flc57rkaWI}yIHCoQ2Gk3z4Zh5yL(N17_9?h6mGbNN55CF zhVTF)+Qs=Dj3)mS{<$KSQLXYmhET#9ECWfN2Com4%ytviF(uA3xC41OjClzSn3aue z7GE4Yz|s@%Ea|4kxn29Bf3NRufqM%mi{6t9)U1A3ki8yVl~t*|Agf}i7f47wSW34k zZSx5mXmdMsew9RdxTs95l-C(PRODexK1v?b!eLv}AD*IO8auVuqNQiE`f&4ZS%vzl zVC;s?dz;U;S|77ecX%L-wtTtp!!u+FRi*uSQmiL(&<|6YD-PXD6N#KXx5Z_v81 z(sK%she6_5TR6j32_CkYN-^o@Otc=G6JR6C2AnCF{3w;r@6wvY3ZRA9Gj+N4g0*`E zyD;Z>suea0GUvU+7h>8gJyvV)$hco=FvVarHTO{u`HG!!E(dLAPp4=7dJYL5y5RTkv?*R3VZ0Q}@M)<~qz+yZ$4NRBjXYn<* zS$r`Epdz5FJPTXOu2zu*@5OTE`(<30>oV1_Qz1@(dihKlNWM?TwZ21FzDTD0gmWw% zk*2+t)>p@rJjAuPIcAmUJ7dbYQdbV6b8Q@%mqy4lOSp{@w?Td3&nSdinwu=WH8!AG zFvP8PJ*tlI6#-h`m|>Wkb>)Kqx_0^Y3{ZZDA!Z4Yy_K#Ctn-@-WS_4xnq4Z7_f!RY zRxc3D+pWT!Zcp#C>jPHcBf@h=fotRL*hEIO0>T>6DA>at%EWtD5Y`v!I%-8Wd=u4- zM$H^Dx8nfK3f{!^4Xk`9`pOs|&;xLN{v*Rmj9U-r-5@GGi>bhAO*fi8tLa%*SUAqnQ zs9i77wRVHyFVGcBs=Ouw!&i5!n${kfav!vMZb1J`o!EL(uAO#0Xssn+#Y92SX&tk@ z8a?J5p)Zb&3jxxO3BDh;KVdXB1Z?O+&(yojrs821l*`Nn>FqR=(zUl;3`n(la&6ji4Q_2&NDVo11+|rQ>gHfBtWoQ?^%7&~|)&lN~>E%v6V_w2) z*qnj1cBcKzNo&l8d4j;T4+J?hZs+>k=c^ebUDlXWPAYpI)q1tgYYW71n3B8GMf1wt zUd*|o6y1h4j%L&}7ZO|7_Ia8NjXqM$2N87;md004F0-54LD5nee^p4vma)cCf45}W zDR6v*GiKRtw8G0Fd#(JfAT&ywu6le{*&p?! zRvl`!S8wpE2Aogq5wQW_0fU2>mdduI+sRE~DMxfBB7R~yGjd)wX*%QK{Ap{ zd>$~qdTuxMc~Z=zBO=Vh$aJE&T)=3vQA!O+srS6!(NzWySB-qKq}Pb5oI7EOSEZg@ zmbRF1%_CMYg4#;0b7%msw~r1=5m%<=PBSMr`K~DMiDhcjf8UEP&%>^?O9Mm`Dsz!$ z{t%fb!PQ|PHR(Z`bn2c|cK5|tx8h}^8*lr_b~^0{0(XqZ6?JR?2hwDJUGTVr5f zZAojp(d*qWdYd60sZ{Kh6=5n87;bO{>7lor{Vi)T2+^5A!|O|Yt6EpfZTsDI@CE8eFR| z@qON}ufs%NJ@%n%a=A+bqf%Q}6MnU)qCv+wR;uL!J9hl{N+Frl;4wdxAPPiCSZ9{l z-sJnDI@k`Q{q-e0H?y~`8;f&wdbJ;Tn%AKc5u~AgsvS9B>0QLoK2X>()6RW8avn$I zl^PrPuC(lgl(Zj?s8QrRt=yB2oSrr(>!)#~H8@gDW~m%5$9-`bx^a#fd_UGBXM}O~ zO@?bW`nLUp@l5M?(EvUZ3LnyudFjv)mOLHdj~|s;reu5{(}UW4ik?-5tu#NZn~>Qv zzOjRq|BW^xDt(Dqn%15ohXIUCP7)HgRwx6Z(FlnvO@WnuIZA3-S~iC--_Di{Y46^< zVKED7UUjN{5^}ya!CvMfD?|tBpT@Zo3;B=jq$5jcsFk@*CJ1`!!l-M5l{-JG)`V;s zF0ul*gYs&Zr(EF7Kn2#5V8{|Eux5Ft{uohh@$f6IHJ+R8gn?b*nSogOG%h~7^4Q%daW7f+4TauE|*K4Z{Vfs`pD(zE8@+JOKP!l5D_yY z<*x(W7z$!4kF4(!S^W)0yjoIQ89;dIZ?ISkJ)rAM{(M{{A(IByFXJb$WbGLuUxm{^ zCd~_R1~VNZLcu!9rcCBb+$!S*nwog|!^O~6sZClR zPV3_qH-x5Mp#~4#J;D5vvsH`|t|{yG2JCk_veB!jHN~up#dpd{h%fG3ZPy{YHtjlQ z*Xu0syMcWp_KeaILWp_7`|HSNgO?fQO$P6jTg;yl{Jg!Zh`pfp6-X+_ka>3znd0qB zep^(;6~|DK7S$%GHd*0YyCvyCb()*Rdd~jhhTI7RA+@!>dz`%$8mpus!4g#Pg5GQC z!a?$HcSuVZr}clUh2|}aKMW!r`bFYSv6Ugj@-!dOVx3LgtfMop}S|zRDYd?3Q&t{j*yxdB_ zk6j&LqdKi0QUt(u#SO^O?N)~$vpv9Qz16qRuD5w1IdUrDh_~{hn8QDl`X#lmN4Ww-pCTBD^M}eAy9&Rg|V| zwRI|ruTIL+h!Xp)B1FIsoANfA%Io#pU#DgS$3HLw#o(KrR0$_VX=803U9)1tl!q`; z+mZ|uOx7NZmD)yT-Mr7P2d$lZU_PXe`O)0hB^QGgdO>1k;xh686Sj%IGulu&pJXz_ z+ORu*{n2WuA57~9F>G{t{ zyKpYE@x+VCdZrG1a&52*`DFItw(LGhGb#UPBze!$1J?T|)5cPb)lzLT5LwxDsxgnj zOh=xqCND3iX?-#lqj4dM#56aX@MbgLHrxbSxs*A+d?!xK9A15LG!7@^2rayVqer7dEM@^ZI#U7h_oi{|sB5fd# zLmgAHs+9AX$#=d{h*_DcR?$7L-?VJBE10L5(#TQff4(Ex-S~`pb@bv=^s>(?tvEVNvZ%HSQFS$>3x6x+6qTH3f0`0R2o5fJ;F)^PSUS*90(&)MM1voH)w zKA)hFii+dUvb47BbTjS8{b^3+iN!^rn#g^OPR`mS=bnU!T+@Hby(tbSFAm9+4&5$O zF*-GE1%70!$oxv8_1{e9CLahi*soC5_}sSM1|U(%H8l(if5|CBYtSk0B&(xKXbi%jlJfZfRbk>P!v0{jvlwz92yAX><4syC z<)xKd>EO}VU{aYW2KQDcS)(~D6u<#l2iexWt$^+qnhHp z)oN0T@X@gTo{pYTsbF&{l9pKBljO^?ycsgYl#|8gc|ImwA$p9_O7VGgLG`6&&e@87 zlYYX74~g8<`h(V9&8nmu^#}Pe33xD@6x-a-Q081l7nxAha`G`gmgcOC3=F&aA{$*| zIhk=zLXY+;qFrjrGg0z(jhegG1S{%(}EKkJrA5QWqHDkE*OrL>FA~D=;g^YHOkMG zZTPe<y|6_X~3hsI=GFtFeBeIHN|<|Z~5WY!pciHdxF_()n{u#M@) zv+z(>5hjaQb}|w(vib^HoSqrRS~t^L-A3O|jBPfe0h7Yk(85y$CHtz{>->fe+j3B} z{+z6mw#wS{%xE-?f-(b?eMU`fA0q~5Q$?{AjIGUQkhe4c+|DP`#I(j3KP=m+aoW@| zgRR}AiAfAn3q5RI4=?=#t;2mRikhy%?xUb40NC;75q-w(gBiD$pV?EYqMPWn< zdNekfAFE(8p^J82n0jby2egzde77vy0I@~yR&cV2PAL+JcN!U5$f;30DSuw=OuEVp zp=jb)+hP`{vDL=(4%wpyJ7|O!)=t*Q*7Ag7NRZK|yrLzRPx||rAF#hQFCD&_Kj%n+ zsNeP6a5Gv0$%3WFLwn-gB%75wvsqfQQ@woNRAbI3%0Sy4fXHDHKYe0?)Jos{XE<#( zW^04ib;WJ05;7xrTzh#~g!*!CGqoB=Sri^bX^E}2>P2ikz4lXh*f=7q*pW8RVF&AY zavO!Ib|vCs0!cnuL~h!CYh@Qn%q6Ryu`6M!B2<^8&`^_O?MhGW^Yp`lb6^J8et)_)?)|)iLz~JMj$(2UdflKnPng5 zYHO3_X9bp!P^K9@{w7|C31}35azNrY%ecjxTU}Zf%n~!1vyPoUkca1-Ob@G|5M|E? zfU#Od>S6V;Tx3UqZA6Bq!luEMY4a*}F=^@=KV$KFPPa_WoeV2C9GhW zyqcVHIj79TjE;oQa@F_v)RJ(cEIJ?T`evwLoEiE0o)lVd?IHkjk0_W$V)VH(yoHgK>_UYu}0Y5ZNc6O@CV(k+De`n8n8Ok*CY12ssl4C2i zd*J;-6^mo#(pNqj76sN)Ju)5C>7Vh~da~BUN^$%G2J?!RN5PS9Xsz01!b4EN1N=V* z5ql%c7=+q{;cAOIn`czaXHkDepH9Bz=6Cj*^L}S z?R{&Z^N*O#d7d=W$65 zB)$26(AW^EX4~zt%5M@boUmdNVmR~pV>cVT!>%^N7`s&iRpaZOrjis>yH0Z?uTB;W zjqSl*Fte4w5=!}K9Rap8-CyQ)BOdHkiS;Y&I%LK=6CpkV$C(d?W+>}rA7U_eTUx(7 zR1$99N!>wR*1EgwdJk=}d{rMiZ17Q$jS2)hC8mmRGbVfXhtZ?XSI7kH_wsn2q4g&& z311ah2O)sszN}$ZqfPqjm*No9u?Ir0EjnVKV()Bg&2vZy9vCqTf6AN!TDb6CCD}Cj9%e|Vg zs5rZ4GE4Wy9{;~~?U=eeM^pmL^M`{t%8`ye896~8yb97b-pbGfxv`1h*)!-7e62Mx z^wTez*B&w!JZsnIne-x}6Q8ActWczwOVp~V2 zusEBK-Sk4|uPS%E&_TKU=OnefumU~pM9b}K3+Mpxj!H8)1MUq2WluD8YakD+%^bDX z;vdU@IzeG!1zp|3pzmapR!?lPQqFUdRtFzsU6N~_9C2};4GNpPS&*(UUE2fLG=f!J zt?Y(Di2F>YDrGHz60-@*N~%tN)jIw_hR7DRZMV<9aMzwR4rtasUST{e#n8ns3@LE( z`-J2)K6Padga|(!?=iO^w~y-Ma<1kK~8lMfSG-|e-^cCiAuS38}}dtv@Mev$<`xj;vZL>cUOSo2kh64Su8zKGFssZG#_ggP&}JpKgPnZG)e0!NY#%-grWk5!=S+BU*4* z8{A{C`8mGF$6JKrET5@sQTMmOgKhB4Hh7l7+9YMeK*srn0bUZ&p2JcvYU9gNet5_X z)E~*QqY0d*XCX*}!?aD3!DfW!a23AYB$>K^wM6Y2Hy`5f3o^~N!Z5bYM`vciEkOlq zPvC}b0L^|2b4HKhE0-Lj$r4yOCa_PSZ~pY#(A|8{V)#6*u~wL8B4#SU-k2D=y_Y=% zKU>mjf^~@)P>($k9}gP3UA}L)Ll2Q)M0a?lofpDO4R6NU`tBgVnw&Kc`Blf^4*I1P z9GT&c<6}H?{$8fE(ncAv>q9&`&hiL9&^h_f#%n1{2&1cf) zbEeD7t%%j>@D+3)b$qOMm`@J#uE$z6+xQ0thUl60^D2356&l+x^fb*J8LzOQ4zCpf#c|RbyqN(TZEXjawjs+6r6$LhZjQp(E4P94f@G)!#wb!WD$O)vZ#C_%3|0o z?=cyN@r#$I<9zXIiaN(%6xI!1qHMOXZh{n=Vqo-~4nkT-<*@&d~?UhUA10D1kXt(Iuv4cYACV{bkz<0J)duavn+WLeWw`x>*; z+Oq>Hh>?hYHwuQg5^A^Ka~W?k#(n~BImb8yE` zg{-Ran_zKX-V>3%NJfb_%n&y5g11VsHdrz`$BVdZfebwX3;c`p1X(nj^C1LfGWVqQ zA(^fz=~V@#^&J^LsKS-7fRFZh9QKHKLom@Cft?^d4{wVnl%nqjlued(*GiXaVN+PN zLR<`ja#;P&FqZ|)iily$+A`nh@5rU4>uNG(*h~AA!Nz?k!(M{t2`5@ap{XWsnv%XG z_GTif6-CjjZbva)= zR76OEfx`DwU>FW4Yp!0_6P158wDQ#tUi$WF^Bx(m?0MZgukAh5`I^d^|FGcgyI=L@ z`R=cFeSFcu+3#8U;qq-OKXc7jPIdfC_%g#g{{1U2?|2D`U*%ru`kc!F?&9*E#JTU6 zojWqkxt~pR?k~W7w$r(9@cgO?o=&u<^ICTo_v!o=K-|K!8@fIG>q3jJhvyZ3pXB$m z{Jx#?`Sw2l-g0hYwL0Ea|K(@%?l=5)y5i^Z?nHh&-PdP3_iBUQ8gM1I3g`od`$)i5 z-DiOQ)^J}7xK4L(gdUC13lW;U%hyqIF9Uidza@7LSGAzzO0P`<^p=Rb2uSso+`ECU z;J4)J5w|HqpNP;-AYb0+BlLIi>DK}-b$0_jz;D(4oNIDR-lgtjt{yiVNVt-l$5rK3 z-QtLQW5AW%TOxFEg#Hl7w|OBb`TNuV$A+v&a$p+i7F;3&VxOMBm~S&o-R=oJAfxu0L++uZ417jP%K^MKyYZ=bt@tJ?BKw-LyX%oxy({8rsQ zuAT>X0g02--M4@a@jKl;7NMU+=;;8J+;4%DqvWRS&iFS2=#~7I+=UVMjtE^2YWmr2SUt)aI(Y9q0hRRadz@gC2{xUqmSX zw^@!80#tG{fc%)B2ITv3;sUSdRrgA8D!Js|2qc-Tx(gz16_C&SAMebd4}p_hm0T@C z*8zEX-Wefjvd4WZ;vSCB6F_Rq$?jJ`xi6}`0j{qw=)3^+xyyijo8J%Q+j4D$J_7V9 zeoOAapJbNn3-ReUfPCKjBXl@C?Q{PLm8NCD*ejt8L4B6E{GrRYuuLhlSvpIZ&|=lu4$>jG4AUqbG^W%v|0N$06<570OHJ=Oi3YksewIb1z- zc7)E4(7S*>$ZyGQH2%1FpU;j|FJ1`x%gO%yoq?Wl$B! z=a>rQE%o#Wofe?E?o~i<;&-llcZ9Br(1(G1TW$jK+_*VHp9PYX^ts!ByzSf%ioy~@24XstaRk54}q zp}z%^{FU5Y5qCI1XSx3d`X#?-xi_Qx9-8o#44NIG#Q`e0w*mPUtc=hV0qS#q2_#F{ z=Z>D81n9#NcSD4>N9YzHX%FoK@+IF3D)Kq~n&?n6M{lZ*qY-q*P8Kwspy2>y!LF5be{$CQRG(wG}nC-NZQ!vz5}EYJj?wD zkYuRjo(58sQO;$pD!J~jd5+I@Cj!YH%ynl1d5u3eK-1kjfLg5=-D&P!;Jgj{^8l6H z5RiH_*WCc*%i9^Cs{1mK=g>C;^hNgokp5NOe*|2~Jq6@P`nLf>uD+h(W(TP1ULA37 z3($0TIgnamEPf7HgwpM&!~ z|3##8cVyPF3M9#%>z=tPtM{bvbgr8dq1Qy{4FT$NZv~S6&UGu}(+@=G+DLaj&|mY5 zH;A}z0C@>}FhW0#Q2x$LdrpYZ+z9=CgjPoA+6Zk0lK#HReGbT5!mk4P+U}0fcO&#@ zgnkmC-var$#z}vlck}oyxr?~2HfWTq*Y3Xo@_zp-Kt9L!0#tQRM_l2av4eYurj8Z%Z};d9N`X zpg#98AX(P4-3}mmjXt+0;7aZrKx+M2?%P1#6F(H7x$eJ#ytVxWkV?6>*l|zRf~i2h z&9i|t9%s9=f&8exJwPS59!TS%e_eHd*Gfjt)p9*<3K*??EHq3G%;$)=g<0$heAP{u zmj{}~uGhVkXFcw=udm4>f(VcEI7e`FZ z%{R;;xT>DcbWQg;xNycVHnjZ$t?|~7`Lia=WytU`TNOQ!!kjoAB zxc3B_X}P2BH=Y}T=F-AZK6$5-Z;Ivomn$XPDe((|#<~A?6B3P==8v8~|LtZOW?#(p zggei2eLLp*iMzmPjs(n+zlA=K3k?iNd7ASudV+Yb@9FHq4*Gh90+|4YMwx z8?K94r}{q5>f!q=*LjiVX)qeE_eRV!ZoSpDF=BqV>*o>kYj?(t!44fCru%qqi781wCM?$q3080Mrl%vDy`oPb#f%?Azh`iPm8yVfw5 zMa*frreUs#nA3CPhPftU&dhzxM)@NVb8+sl(QJ#D6}hJj^TmjHckWrsbx*{s%KgUr z{)2#7?EWlQm|&6_Y2KGBPmr{9j%S?zi(Ic^UJ@}Ib5jg6FJeBJn>ImGcTT`KcTMgk zM)R(KS(dvdH)xnf%yms}rsdiab2V}=H=5m%rja|{X!b{%k=z+Z^PNaDl3QSyQ-g(E z=tgs|H_X3BnvdlE#L9U*mh-XP``Lx)apk|tdit^42aFphw_&cFAWeB)#C$xr(P%De z!+gkSR<&WSHq21OY|V`uW^)_nuMG3Kh}o9gWSFnCVQ!d^qxSs~^Xc48){931293>q z+IsO+q}iU^PA(#m5Bq+NyU*l4XSsR;hP{=5nbC&%f^lq4z$}Djw_%n>%;$4ovbvT> z%olQBwKl&yV)o_kGMcL+=8oLmR?c4q%(?l0$^DDv+8JrSmHW-5z~{b?b7JX zd}lT~CnQ&Q$=2-)G}ChZ!a(!AfEjoF$+cGd;{oH`O9Cc$y)Q?Yn$b*Z!!!&ts}0jM z%#t?DuwmZThPlo#?`^|eZ;EY}ZXu9K7P zmg~O)=IZ3+WQSo2AMyRVI+>aLt?_U=m^1iY=w6omeOGQqk6RLG7P{HV!4S)_eGk^lbem^FC)z=FyhZg0>-)3$u9P2 zd)y}@&FbV%%XLe{{AqH?a@`SgtxcXXn(s%NwaL$|oS#R`70Gi(Q`nS^^2Q{siqBI6 zW}*9V@@m7J6=|+ZUTc_h1LpbU#^jF-^Y)l)OY&aBtc#dWCRZ5d%Ef-4d7-;GsTt-k zBhBvQ(^c{3qX9GSzL@N+YLq`6Y5oq3tnuA#n9mu_cOvG?$!^0OiI}e>dkyo;h`BBK zieV}@WaEBY^3|$1KRIBW`+D*xZ#QbCOpq1PR820Lu!$$LoNb`;4zl>&Yr1@UbGf^%4r#8%#i5jo($6Vh_PT&MU zk9#U$4&;B3oNBoWAM-tR?r<{OFw@&Gr&+F72h4%|;biVaasEwU{+Qo|?vZ4^Vb-=` zUS+j6LayhNN0SAH`BbENJh|L3Ux=7zlWVPfZdH5f3Wxz);<(}wvw%e5%xD(AmyH0MN`a{ex(c~7LN z-!NZ`m^A+bD`$ViOv?YeVZIwM zcjTw%U$9(9Bh8F_iNCkO-~VLI$a7;xzHFG^e^aJkC+DXdW>Tb?nSUu?#OCjI1kAWQ zH9yN}u6Tb|&g_7Bd7ybJKPUf64ulYW#d7B6U)3Wy)QXjAcW!R}HCE2s+h|^Em=ytY zS>YA=C06@~0%qKumH&OC8H>4I9WbAWm}U8TkL2*xmu4+|Z~j5c_1Q@CXZfRs`C`Oe zmGABqf9{N!dj8a2wfSJcFqh7c7|mmm<_q}`8^+z3)wMgn*)S6W=IZ3G{P%msjngB| zxAVU?nkU|!<@(qBZ>_GyfyTK*`L0y*^9M24q5MR{ygOnZ&YzH~&FceZQSxa16{+gF zI@0_!|3<@nIAG=#exAS3a(ymf?nr)~zsN9O&dMp23s)QFA0tgip>CM(M$Cl5^@jOT zz$|pr3SZ>VUyu7u#LOuCt6_TnSJu;)7oIlE=@IkV!pkP97fT~%N#Sh6Xh!cNhh>E~ zOj5~z7-`;ExXUo_kC?X@H3;i zrw#LS!+fU=^Xw$a#=`;Q+{K05WNFId0W%J!d$QX6Ld4^Dw;V%r+c~e%`jfHCrGZ-CP8|GTWygy(&`17>4!u=odt(P;v4eq(V)@y&*LQ>2+uyudL3`axg2Xx`fA27BB^ zfo586M)Bf4H>1bBFJP{N<{ji>P(!Y9_tN4!4KoyI#@#GoXyHc#W_9uOfY}u=3*DUJ zdyM955woCJ>(h9BCt?;AZ#0@eKg;)Gd2vzk6Nb6vuAKEtm@S5>{e#Es0khRG*UxX| z`fJ1ddnoxfXl^zh{xo1z&fgf#kAC0hx~sUjxWh14wP8MMn16enr@0iGU55EpZ1dvc z=M8hhHqX^di%X09472qYzJ-?-&oBN{U!I=Mg+~7ty7Pkk9wtC8l9ix2eW8NqJ_%pJ+) z#qSyBJCWvH#UCD5&U=cF9oOd7#UC5yzhkaHE&jJ*ej73GEk0qGDSw@D<9)?v3^O}o zt|CMc_>$u=A1}^04zs!VQp2=LzOi`9ahR>eImhMNS$x%T zG@mOjI1aP7xb!&8*NbmD4s%cOZO38$rT9mN$$dJ=M&VF#m0>1j7!E$%hUoe@(k z-ENrgM9k#UKN{x0BBrnO&xZMJ#7ryQYnWCGPbz)OFq3c2^t8Wp-~?HT84)vB`rdK5 zW|R(_JTHheGfV$wH17zQ$;q66c~=|eF{5ee&YaTYhG}tQZs{k6Y3a@@N>3VQU99Vj z($5UjvTSFTo;FOY3O3xeS;}Nr{^c&;GXCr2D z$xW4QX|;Jtsce|Pk2Fh59foPypWiEW8Kxy;uP;>%(;ACqr5?ky+Vh4|I#pwFe=PZp zrO8HfIAYE%on)96fBv8}V3?;O&H1I57^Y);Hp-WjUS^n93;(Dz+b{!>=Cabu4b$TD zJ4>}%2$M~dWN0-B zlcz~C36n77HVKP#4`C7}$q*LB-2U&+_wznK=fdy){eQpX|92dY<9)o&@8fxW&&TVk zGOf(mSU)evniR(RdpX@?TrA7W1e1$n1HF`)Oo$Eka)-&p*x_CtHTg#@+sjKP#jzn? z{$+AiY^awnO-f?J{675EWJ>H*Z!Eb3#1bOLP?E#`pCS^>t!=e=ql2t&`#h zcv)gn5Fg+rn%BAU!Cv05GUvq)_p;VxZ2Sl>(OL9@_>o?sCxvnGqr61t@C)Nddx@^X z7sZe9@`JTp6hGEW(nH}{>mTuxyhQ8pn)qlh(R#i%e!7=v%-6@y=_%J``L0C%Te)3% z{5)SKI+NTGKi^BV52wX1_(Pc+;}?4AW#gG1|GSrdCNtxgdl_z08K2}OItK5EU+d*O zD>FBKotH@_cgD-SMCZf1;#0ju&uDkYZ}1Y$qB?$~muMFE#BcHv?Xh|Bo4rJLAos>^ z@p6;(eqa1HFV!aZ$1A-oHhC~!)e|pjT?6IxPBrm4em}3XGNF7!$>}^lewQy3o3EdR z$lbo>XcqJ1)xOMbEOSu8g7`cy2U2$Iv^f5hmm!o-6YJtH^prpCI*M{urzP>1eaq1? zxFr6nmuPQ16MxN1bk=${{<@b!wsV+!KK`bc$tEwxS9+;{=(**k_}gBhmS2g#??<@M zYP}l&z?X?W<>m2@ygYAZUXOq5z?EB?WvRx}W0u}PIu@-2Zi87@`~+G02;2ck z$NO}Qr2^YJ%~GAEXarF!QOjX(X>KiQWN<+sU3zSt#oy=ci;D^0M_EuRHRM1m6lh!nl3AeUMSIu--n^BK7yF2`SsBcBJ7xrQ;U|r}&g>sTiri zTQyRlPYp=APg{{LfdwuF-)!rs~ z_j6z48En;KW-Zb;zU62ggr9!kV)!;zuU2~^$&8|XpXQE+=~;9*w+1fX627!vwMwgy$NzM^>q8$E=FY69;msq;Z(!G`*vGl5?k1e%X+HN-; z%K*10(=Kj*O9L$BF%7hGY3@RpT+au(iQL-piaiykD?8k$fqMuhSCUNkBGQrAR~={m zHcUR%(WOn6)-nZ7t~kT|-u;4gySgZS56kmggC5}#x(Cv!u&!=jq;rt+N(UjyzRz?g zFa<8U8yO3`807*N-j{TBB`}$X9zD@o(`KAz*Q3MO+k$`_ZZUk zzV3gj5x7@SZkiv}he*0|zPHo`e}9e6Xg{QzQLf`YJp?9oN0$y_$}2sRHS$VNv{Y#6 zS|mA=ySlkZawhC}J(4rm?f>Oi?q@53dyXm1y^D00uOWZaj*fm4lHLmhy+YfAN%ynX zkb4HLk&Uf2zLhhqMznRZS$Jm6bQQ42P$Sdb$JFs^`mEJ>$I?biKUk78tDe#S+(+P| z&tv=D!;$V~=_pHQS}I1;qhg+==(Bv)%B{5&?#aNln{`hOd&@w2!ta+{r00CP4Cxgl zJ?l04Bv;t?e3BLWrBAYAzxPSb_X%G}4Y`_j@o6Sn>FbkR16KPp>oc$nZ=WCy_9?!H zb2&a8f;7e_xg#6v(`m5Fe3HAbYmqYDbud|1dX2xyZ0?))W}=V%eOiii1X8A3ha@Wkk9PRWoOE49Yp6TyWRx3K z+MioTl@7CXilwoZF1K_&Q>L4Rq;p${G{%oWo~_PD(&qrZ>-#6RYI?`gCzg8Q@2k?S z$1-KQvymoPd-@5=rz)RsrkjARmt$+13y-ZdcO~q~KiCbhGT(|^-=-twl~%%ZZ#;^m zclFZVENl&2J(8~2kCEhR8n`wjsgdU5eVn@k<J_%b-gN-gdhbgc0a@Nf&oywHvW+KV+QJT98sleL1 z04cBZeyj0_r6-s&-3v$;pS(1)p9>!`ZAzaee( zThBuJ&Zlyutv<;y-ED)^mGzwJldN5BPu8xaywb|mOir7#+2rdUtJM9?X4>! z&Gki+GechKVMu!XjzZc8y#;RDtK=W=6yz#-h%a{`YG{&c;~;Nx<*Zfv zC8MkOb?8^u#vMr6zTfByD_7Iu-sEaJ(x)d;?hGXTWX?y*bRWZtefkke-*d^)c%!ee z-9FeuNNMhHtX)0V^?+&0K+?T-7*m=%5lPQ_a;>-|u$WJOzC+F{JrLzm zur<^5N9yU*2&Obwh@|^j?g{lCu9%irT4kxm(i2E}25CU*jrP*qN^Zq{EK}eHCF1V@ z@S~FBO%mR|acf@bFP2jG(-GvA4zP5zrIVS`TzJ(@bK_wBeZNzXvVFRprh8+A?^ixc zO|mvLErXrnx5|B(Cb5}@x6}T3(+f*;1CVa=DZCpA+%TA|tH51=GzVMth`QS+`K0A(Tki?wZZmM# zqMWvT6VgK{*KyU!b!HK4bm^VgDhc=FHmV0{9c#$*h)6#pSF4&q^dKfGNFY+dl77;)NP4%_gfz!*ZAV&wl;+YioU8XK6X`XdjzU`P($vGlAZ|KVT z80ES;*}Gr)BX{0QV&{hLp6tX}Ms9II1( z8ipG8_;eA{<37zoTIthsNT2)k8PZmyz-@mp+HaMVhSVLY<54k$TSu3k$TZN6LfYNe z7=zRYNzXo#GTphby}ez6B=g0cK2u)l?MO1qywZm)z08#6nvnMQEw4k8_aON8i(3P? z-67$zkdBmza%t`$q=CLh7Lpzna#lXVZyjkh3XpPOd8Owg>8E-rlDwtExH#q{-jtOSBSgNzM3Q4csZD!pvbsm8mz@)Rh z5B+AkeAq&t#xr$12B%mJITt^Ua=Nl#@yXvK`6O4wuYD_(sPQurzWYQ<`d0RHH2Ub? zBAUk%_JMmJq^_tj&^06NjihIl1AJPVE6V*yuZBv~7QJIm4}OS6!4 zU4?J=_1%)Rtaqw<7xXl1q`9|{q~$dC5z<7IOLH5LCi$^kj}d5ZZLlfcx(o;{1xaSq z@hr7FOv;Te?ZXtf{gL#}@>nDt-vwr6X7?jaMP1$Z|Ms(d4yGfB?is$vijZTWW54gC z+;rcsyh+v6Y$oqi#X4S1Ps1L+#oKU8C6@kfDeNt9pP@ay&q>S*Q*?cc?wj_;R-NUM zO!_IDg1e=>(z9SPqfw<3nbO=1R^z{aF1y2zS)R)@{r`O~(>}I+^19cLU>?T&h)+)= zJ>^sQ3>LWXNluRsJ?CtD@2Gc?a^2S?PfA)&?r1g1{flmu^S$gpoCR>c*LRk3mXF?Y zzJL*Qb#hL8-6y$Df8>)~oxku&&ivo`ByTT&_DR|czLUDkx@yWR{g9tb;5H-efUSX( z`R?q?$vk@cB%|NkCmH=AKFQnABYcuEALo;_Jjy4z3hDb@c{4i(mgaUkRF9*M=d0+u zyPnvpt0{VecBq;Bjre+eMBg@#w5^kn#-fkFO*NZ?BsI2kHAwnwuzQ^n#`E*(Gp=sQadGZ!*#!V(K+m)-JJW>m;3XX zOOLYk*s4dxcAF&0@wTT=a#uCTr(p--9vCUFbUyl(J(O3v%u=(ZR;Iw;;pu%7{(3(* z26fY1GLq~+%%~q)Xpyuh>?~{zTo>#=xo+uM^g>uyC&$)hKFRT`X&H{PYkZAN)V%>I zuk>gn{oMY1g~c5Yx9aDnS4sKYq_=_YdTgDJRtCD;n9|%Vq+4KktBZ6ulHQky>649` zJlUv8dtzzsVbplYr^QH1eGR#PdBZ2U&-olFue2Upbx*!&X)RNlJ0(NcOQvgu$$d$t z+lnOb(W7ti^hxs@-?FSac?Z?i$yrL@OKp2*-RjH9=d0!9^X>F~$M&M{EAShF!@_+U zeixYLQenD^`!V4;8L2B;3EZh>W0AUhyBw*HPq%Pu;KJ)=;KFD2z(wyZqP4pSHDulE zzCReP=!(@R*JWM9XIPohFuzr1G|DHrLg;g?Tn)x~lRIRoi#t5DEbm4Fw;oB~DMTZP zAMRW!YT%a$NR>Vfwsbs_?t{M?eKgCnQ0@WL?f9FWtE_JLc1Ew9IwM&dnq*yR^6Scv zIeJRC8?ESSmwi?1=O!y`6OK#0|I;M*&rkTR{uh$|x+<$gEU)w-wvy?dMAGkP|{J{e7N zrqPyHVpQ77W+XlK_c$Und2g}Vw{k43(+@K936^e0()M0J+TCyc0_gyscFYd9%6``S z4cX5}`K_|A@_mwhRqT@-Q8SP--Jz&^pHD-O9zw$Tg-Jh4vA_B)gq}B^@LQ!{O>+OD z{mOpPer2C(zp_uI->y#fsn(FEz~{ZmQ{calGTp@(fqa6Q?kc1&VR@z3BkB2N4pW+2 z$fQTqHz?Qf?pof+$s6I(rO%?Are#c{N|&?lsM61w2D&YlIv=UM4RpI(%CK~pr4g1+ zvs7qF{$?c^^FTMl(*2g6vh)g)+&v6*ZzIY5#z5DK^vB-@Y=y~NA$(^uB-|SZSsG!f z(9#r3)s_}94Ro&{$-AY2ZndSam@-|$Q96Q5w=4fAa#T3r}bCAby1VGt4Xesd-xj5aJ*@fH-QIwleKn?PqLa$^GV)_oR73i z@Em%ZAj!RjbU9L*`wU5c9n*#+zmDl>$wU8a8D>L~wC>4fe>a1>EX!b9&4Q5flQQhCHT>3HLn2$u#F`xJs zc9w0OXj`L}%gyFmxrfc_&E7Qo!t4iH$1|7wO79x1*wLlGW2+?GTjb~wAGjV!rLerx zeUS87V-V6!zT9xLJWFR=nusL7O#iDhI{s_{w(2h_=OD=uAGjJMy*fXSG}7DqmcB;1 z)%UvvX|_+lBhB@x^RalF>{ANT13vAKq^*pwbT-n%e(MEDwLVQidcvpcEma}uSROHqg>>w)Ip?MV9_yX$q3gZ3a`vm3Rm2YqUJN^d3tKEIn>%sioH~eQ2r0 z(zlj=w3IkP`v}}$eZIS3tIjf78{vBC>Uv}ACO`9Nzw8U!4C^=#xfhW2H@dVRwrY}h zZkpsxo1{P1_{G=AMh#78G6n83Bv~Z`+_gwDz5#9~Q{e8iw20*bx6INTNWY^leg}%x zsq6kDSYn%4D^h2lb~rvv=}5XZ4!3jyQWszDbR_)*FR`uHA?@O~-ip-2r$tD*C*QPk zn=SP|0dIwTxqe8}+dwCObDh)}=uWhBx~1`!uCX+oDR6fp$x+eOJ%=PmKv(ywr4N{5 zt`$l8=$N+9y1V$_aF>fwb$8v64nxAf?S(YLrwmJjWUFuGNT%-Yc%(dEHy`OdpUyG6 zh!%5`kYv6wcQevN-|{_3SNZfXl7DaOorHOI>}Mz1F9dtkLo5 zGyFa;=9;m!!g{Opsm1#E%2Glu-b7<-;Cdm+2m-e+lAam*GvQz2Kn-o>IGDWijk!~h z7WkIWN7B(>g7lcTZD;Q&jgO-f zOh+q;o28Z6HY(Y>oxRP2b@54TH=n{8{crYK)Y~K0Tb(88Bh~lOz<&Q<_3`#LeWd+= z=|e`pk8e5LYyX?kODhMWmH$oMXzzZw&3yZ9Gpd;T5?kdtIp%&q(({3=9R0n(e|3(K zV_!#gs2|^dRqk*b{eLbe-!#PBZ#L4zlftuWw77m5jN*Nb;v) zbfo`zM#tMo|8u#Md>;p*wHpO|~d((_2;eYplp ztC93NxYp9oNYWerUA2?L)Z3D*KmEN$229tV{NCa+-|{iEm^&5e3U8+)UFXyJNb)Wt z<}O8=?(Hfh`2|VfZa~sMr4fBk`~Xb%+6zeXraa9xBhB`8*K=#keP?Mil6=11om}nb zqMY7QEc8k4D3~yh=e)U*;hn+`NR8gQBgu6Jf3=@&&0vbTK}fRdI;Ih3XCUc17>m@5 z_M&x#I}h7B)smbyzVPL4F{?t7y%BSdAj!;Q?r9|1~>0+_C@;HpQR2%8smFA0qLwijIRLpcW;GAx(_Zx()Cw@B){$H*he&~Td=j**PVl; zBUoTb>dG%OWA1sRDZboWNZRrmOY1E~`$a}2d#&T|C%3}neW&i{>At;aEYV)ueH6Zr z@>}K8(sBpEX2Epj=r_Ut)fH^p8Zmbm>&9Hv+X*n8$LUDAhsIf&Y+K8b^p0^hlAOmo zj(*!w>GAfTM|H2YJm1ojmR_**x}|rKwBJuHePb!S2i8$l`@I(4;lrey#DmspMJtxZE-PZnZ z&WT!M16uydxAzm$w?1_q9Ul8BNV*Twk>uO!m^%dN$Dbt0-q_-kyczoqDR75lYr@Zx zPDPTs_^*B(!G{sq|s5PHKH{xZ+mn#J&tm^ z$K`Ibn;-pOt^l6%0-`BST4;)zZ)LH-th0YBaQf3W_gmNHc6wAbie%FQuI!t7^c4t zlt0@iZ%qgIUnQRMv(zocR=xL@QJv;3IyXh1>R*-n?`Ns)MdkiH%V>4R+$@Zxz>nYo zq(83g#jx}Kuyq+se$%06)^UDoboG-N$*({a%Fv3YN+f;S zxx>-}NGpABPgr`@wyr_?(r^6^=_j9lMbay~oO9e3*?-X;Su~H%r-xUU@aXBdu4Jp; zBlSe;>|2q#U3@wKrblShUbJ7LC&=hnI21MX?EUAhy07H(`0J;@sFkD8ihgd#BkA+P zXrx^*Ds4rd+WzW{8QuMlwN@q|_3|^CXlXK%wtNGU{EAEegrI!CkGF%cJ$#J^ko4Rc zm0JkY-vP+qnxi#V!}|If;W<`+r!T)zkGapWRc91kLpQ?Y&kX!``z3mc`5ra&dFD4H z`35WIwm&1><7r6RM+TA}gK~9|`;EWehI-3DlDdKWt8W>HV5_dC-2Yha3~be_+6743@1NU?xht?$ zzJrXpX-LQVk;>5{cc55TuoJvJjHKrSna9cA!j*$3W^B#(w(TnU&u3O`MfyE^n|{+= z9ZdJ%GNkkU*66wH9hi>gUr6Kq)@Xb!unB*#&9KY8MMrI?0(|Rh<@~v+!{6!D9b4ry z8eQ7k(hy50TRPp+c%~6}I>(=&a2KD2Z;;VKAeRp>Qy?=SiLS&;rK_UETuxspQ|W3c zNp2J6HAp3DZSNBI(lTFAc5pqsOiPNLjo*BuOtS0ar7WpCfKFE$Pi)98u z9`Z7jWk#@_F80vb%V(KVmf6V_P%2nvCpVrl7xE}t-q}s0)Ic8hQbJkG zGWhiQcN+OPa?r-Q7sa<1EwN6;PJ3%x-QjC*R>@l5mb9AtM_sFDCyzZuySepNI1qWQYP}Pso`arPDzWX~)WRIuvrgmx;cw za{nnynWJ2}_MVi3GUL5eP)0#6@iN!T`p#irL*0E|%96&S%p{Z<>S|eQB4i5WXjexm zfs}h$Mwt$|88XbRpv;BL^df&Y5vwI>0b~y37}rdxgUG5o#{XGGDYFc6H_GI=O)Rqp za=({$$|lGH$gwW*5N&xYq}I#Ml!URkr-BT3Jt#Xvp7A1oW>Na;4Uu+^a~YIOh_rK@ z8%)W8$hmHW8%h}kk)vsZ%k^?Y(pbogsCB%{r(6nY@NzDt1o9^21UH^?8{{1?#gqpi zYaoAfC6uL*wO*!sS>HKaExB%im*qjY5^`OwmI=buf}1A)-||jxp&eNpC%U=)gq8;% zLuA#R==x-eOiTJ6@`acEC|e=2e@=A$D2eCm{yEWQds*(nmQQk}{e3&O4o`A(y_6^I zgj(OBos-;sl>H%_z0`VHZeu>#E!KA2F(@PZ`DE8f83}1et&y%^z_xwmxrr)C;r_{U zlPRN7EBFP!hj8VTiy?8yDXxNYC1hvFC^y$jS<+NUFG#+t@v_{7do|x}8t8j>Gf_s? zM!rk*r=aL~&v(gQ%9G}yOdsD)3gvmoK3@8$_&s(I-* zmrMB$B5QTD8$|h>ZC> zS4+7DB4a+!E%j2CRLOS6x&|#{YksV2@^XWHreoa(FVT^5zPs;m**^`zqiE+u?6LFR zVlPwOE9||{-FJkPDRb{Z@=>PH6=kcmLC%JZb9G0m#Kz%03}n2^@)w9QwWV4x3-LA>QtnD9`#`>e z+~DL_rZVPZS!SB6P_cV}8=d_AwBr@zMt2`2yn@{5YE}HFvk5Ivck&BW)bjExWQOaP zBT|-h9r}{9`ps@UWi~|C#?5XbWMa~$MOmw_i`sWk+0667v7-AkD}4ss6U9#=ydMVaSXI><#R zbFbUtFQApK5M?Gq?sustNy}w!669LQ18(NYDz`&sKpu3(c*8Aao`A@BYFuNU$}-AB zZty6TCde$5neWQ+O^%GY1@bWDVYkLhncE6^4)Ta=WtsRzc*71^=vLsH45_sTxayCR(_~WjIG65oI{>RDtbl zD3c1QbFF8pG(q-;JmVG=sC)rA1oEs~=ViA09x@Q}yvxBexyb>C_Dccw6=l-T^qV$2tne9!tjuKuO8{GyPUG)pMmAthL;Ys3mLlEmw7}v{UA)A@T{n<#s+# zB)T@e?e_Ch=4x4M75-SY)_Rt`uX0shDxF-L?nFDQ+!~e%uU}1W@cCLxuBQ*9%)73t zP$j$yf9N*hdMx9StMKzE)9l70iM)xv-hixi^^}hwYar`fADrK%%vQ)c$Y(AC=RsLf zDHELA0BLcBUZOjP^)3rXh?EJhp6gw%mudcvQI6a%T|Q-hv?HJ3m+o9IQ9B!4366*G zswaDOgDY1_%0ew^XM?Mt9LaVzxVaR$JC!nDx%((#UthUe%5b)`(bZ8#v7L=>8RcA- z`P!|ZOkkO>ogC+K9lRPME2`BsQ|^G&df7m!gS26fed9J!UV?1(((Wai`?szY$BgXx zH`va%t{tC&$U2DZjqltFB$01SaxPW*#bnh)l`fZrwNfrq>1)zT8Df(94=EF^`R`o1 zmoj%2%KVNof9D2MuA*#mxs(dZ_wHQEy_6qZG380hk8V1pk<#X>y-Z7550OuBv#X)} z43X8c*)66dVaBrOe{xGH-5|2(e{v08qP_aFOS)3#wcLeIqd&WJFO@D8wd8uS#SQin z&3&sIs%7k~wbd=wGPa(#x}}tGjcs)el>QvyFRqbt6i4`rTSLi($V`8A>nNu|WTwBm zRxi;k+Fj{YGK;BhEXv5arQNl7iN^e!YxPp*iclu@8-73Fa;}zIW%j=JcUS6VT2dLx z$UgktO{d%hk$w2PtD;o0j0>tM^H|0O3sn5wsk|o%f|d@_1-&N($=68l(JW%YL@(3a zqi9Fgb1cZZ){oHDL3Tr}SWw}m(!C7n4M_@Wyi9YeAhLft2Sue?>vOiVeUO72FF7uL zG3j%?NTu5WM}vII+Xv@TxS4xmW z*$9!(DZW@6j8#b(eA-y71tfLPR4k43$kyJ5tg~W z5V_adEhzR9eWtqwsW(cQXdUhr^z%~Y`k~etzMTU zr5GanFg2*7l(NhoK|SRrmf0g%K?$FLdIzg0;S*5rpqVm@?d%z}P!_PAJ%de@7g?rH zu!YjZGJS%?>3+{AZDg6WAej=oT*sUi^q_=KZGD42l<=voZ;(Ohi(2wTwpWlv$%M#f zu~(2y84i)pVy|EX|Lwk(3J{MP3Rh;k(McgF;I9u5#~SqL*kz?Guc-N#<4N zE=4<&(at`>xieHs*!z9~9!T-uRCg2PdX(8e81JRb&4JtkIUs1KJOHVJ92k_}s_jV2 z)sTaNddgzT!NG{zw9GPy%=D08nU~qF3GyIn^$RvpzNKUaeP(K{9dP}TC(ZsrHRS+^ z^fe$zpCx6cx}zYAAX&lS*;?x)$kUKR@yG2{6FPM3BFFNupwUaT>W&HK-r+~+#-YqdnCUS=>RgdB zR}A?Sa%?cyOQkE7GMM{u!3vhS4e}Mrj0ke?)LM5#euU%(O!SJ518E-|0a;r5i+6-RVICWp9Wa1*ZqAD1#xgSI-F6P>zPkUOgjdp`5@n zX9lg5Q7m(2u!Rzy!wc|NX3M;$C57kkf*_d^UaigwQYhio>a3s-r2sAOiamCAupi|g zklnpxd5J#dbAqAwNMB`c2Fl31&IyX{6{&R9kiMvOZg8KMGJnL$-Z(GFxKGPGj56}M zpBIeqQs!P@J7a^Q4zfSmIX~!eztk#ojVL4cY~zB7l(m!#gE~q(Wqh#T15yk3Em!Dx z{vIswGS&B$g_bW0rq^i8a-}{Ia#@f%Uu1o-2Wp)Oxje|Hq(Lr#ObXU`iB`)M!4@x- z?ogDG6?H|B{IIlB=}v?cq1Kf_56T6Qe?TS&eY|Kp@;%X2L9Une!L=xJHOgEa6 z1)IFgaq@1t7IJ-%yFgm5blc(1dI_XFD52~Ic^PtJfEQW#Pg|Dnwr&dgs3e8oGtLP1 zqlDix&Iqzp+}>y>7qxBo3#H}R?g+>`kXwSmkBT$|e}jAoxizS! zjE2bf_%nlMFO}|0h`b-06|{MomQ)1!47Fwl?Ow`~u7b3BNvf5Wbr$mcUKO->sdUp& z=4X_d6ZCsb%3#ey+P#dSEP({SOPNy2N-MKK#m?$;f+jE1l0HQlIa1~X&0flqet^jF zH7D4>GTTko^Tr*)CYI>|k@LnKLA#dmPZRPz+T0+2k&f^nl#wyt8Qe!14v{h68PrkE zHd&!!BfK-%z%r9iM&@;AQ2e;Gygmq5)SbZsFVTv+J6P;xj+=#Aan!my$Xe{jV=d1M z?)#H8dWqVZ7c_Mc*&FkMbsa?B4c;5Hb&#Iu>wf%9UHa1R&C?(c2FYHuW%+jdp&-@E za{q1DfhaRSDDhHhBYZfR?q!a@g7in3hl2%_XE3kBA&&$tl;sdPvn>oJJ}JG=@o#&M zLYYT{bDt84T7E1%PIvqWwd5)s$wr7=g(KO@GK;KM=c}|#B;j}Bk6W4WJMl<*qLy5D z7F(HvA+mlpO>QpLE>{F(HZiWAjM0WUAun` zQYqoJ``2JUFYAL((AS;lz1>#$x9r^|q&y$BET4``=;vj9=O0l<&PYK*mY1@mZdc=+ zigtp8Y)Ws4e0v)tjL=%a0T5{?At9GC6tW1lwo9m^oCuNcB0D8CP|k+Pu^dlWq0;#h zh#a{|3C%1sg=LZwS}C_eq_55iJ?do^Wo{m13HsVTA>RwW8-=_ANlsYcWqs%Ho8lc4 zQeTi->jQbFmOI!T6Z%lX{nI6(AEl1HcS$JnqIcA9pyizsDp+O(%B+IyoUn$n8X_a? zmXQ9U_Vo#54a)42P~c^bTMvV;anC)88UgzV|1fpQ^4_F-y5Go{3&jdHt5@;{~b*-48`(kZW*WK-6eb{hN%E^Yi`7)DK%55fDl*deRDQ}t-Qob@Np~RMUL`NvC{jl1=G!V>q6C%Kj!rloL(LDVLd4Q*Jk@qdaTUNcq^Lg%V5; zdvB-gZIaTcvpC))gEGM+hf--$Kv`%~OnJ?ug3@AALkVsQ$5T(~ZPG+J!lacV-{I)- zmAF#JJjo=LGRq{3^0-MZrO~92@|8&mB{n1MtBSIhNi8MEq=E8xlV-}TCT*0bOp@Qy zS*$Thr~GD;P3e1cIG%jUaVABSB9n4TrAakqiAf#hBa=o7e$L7L+(Oygq@6OtB;{?L z#ib@0lsP6jlow12C@m(%lpSsfd#|AMH>shVZcn!du$)LPsl0*5_ zq=2$rW!QT$Wp9%T%5f$&l)szQQ*JhCqC95ON_o{J@n1UTPfb!OznNrFdd><*m`gd- zq>z$lQbL(vQboDRq?YoKNdx6&lV-}tCT*0hCdo}Yi(O}jBTT1cnq*V{W|B|&yGaq{ zMw4>NLnhUfS4`?CpPDpMem7~M>`@htu$^**Ny@uAi_=XqC|8=~P;NIVpgd(#OnJwo zg0j)1h7!L$98Wz(z6sZBQxoNAlUB;vCW-Iqn6EKOrOY+SqC9PqOIdAFNcqmBgpxcb z98VQxf0J6uaFYhg`6kVjsU~fdhfR{-*IB%2l1}-~B%89!9pQNLDT7RkC}*0KQ?4_q zraWj;M|sVpk@B@k3uULdVejpf0VXMHbQY(XWKbrXTws#?q0VBqNjl|alWfX&Ci#>-s>8mDD1S34r(9)HO?lX)j`FTa zBW1gL!j@YonI`R&F(xS==`5z2WKb5F<8S(n4u9 zX{RJV5RNBht zENV^CDW8~RQ@YI$Th6CsniNsw*D-wmL%GtVnsS#(9pwd+M#?&q7Rq)Hhb^~L_AyEM z)ZxF`NynRHP~>+mI-VTLZ6*bjCryee@0wIlwwTmVx-JM?uBRMq(nR^2Nh@W%N#bWZ z<{L~>DG!)rQC>31r8JuqQns3uP`W=7j&vTw_vAnP*Z*dD*0q(qhs=i9Z_l-cC8dB&9`XaiU2E zWui$A!O%lJ*F<)kqO1ae}i}I*R zE@g#DA>|8`5=z2j;drVjdzjQxvP~K&XPPuqt~P0-+-Z`$UT3k?B%SiUNjBvtlYGjq zi^35WQL;?RDWgoPDVLknQ7TOuDT_^7C~ugwQ{*>LdTvSiQfCo+Jd_Mdx=9XYm`MTU zT$5tT6q5={wMh--Ig@(I8j~ibQZl$(kTN>vMD1>@+oJT6j3fW zDW{a1R8#IUsiQ15X{0PSX`wWmv{QaCN!h5gNP04yR|ciGNe(5;q=0gQNipSIlM2ce zCN-1_lX}VnCQXz^lUB+vCW&9`nD=`s9CIpVw2AyJfO1_fH_4?uX;Mg8XHr7xQWv&d zMaed)rCemvK$&aOOj&NyMrkuiZq*U?eLCzbopP*6Hl@f!{`Gm8*KCs_%5x^=luu2n zDM?GhzUnB4m^4yGo3v1_HEE~Rn52B8<9X90gYtt(4yETaVebW$p(gUL(aXFpG^wD> zGO3}wU{X)nVA4d{Wog(~D`kjD;C_$)|LBK5R$+R#lnl;U?vj zOHHaNH70eG4@?>QQUZBjwG$fSlc z#iX7x%cO}i-=vlDoQeDkWHRRWOj0R7nPgGA|1+FdE@hxeA!W2l3FQitD#{#_TFMfW z2FiOT@-IZm2!AqZqjY~I98dCQEi=$0oif@an{tIoK4rQ|5oMl9Ii=2|n(~H89c7(~ z{3{}U%qA_A1voNh_uIt6}enKkImoG)bitm}F6|G0COeWl~6a)}(~8#-xh!vq>$b`|@x+ z4U|Jonko4vZInw)lDFtAXZ`N%F5c!Y57ADeswNQ+_eYr}Ta!?7fIG%%q$$&ZL?$!=#R~ z$VC2@B-vxDO2A_M$uMcA3^Qq?j4?_6H)mmzPANCZrrc$cPg!hIL|JZ9PLW?B>Ab2bn@s8` zNpFQ^8Yz8DS}21}L~v-X{3B^ z(n3jG9ge4+vbRY}g3e;NNe1OYlN`!)lLE>kCdHIDOe!c}nbc5{{}qm>o^psu6Xg_> zR>~xk#O-vU`pwU{{lw6a1N|8wsrNX3~QfpF8X*8*$w3;+hlHUt^Z=qzE zv{Q0TQab4@icB&ncbMc*mYWn%Hk%YvBtqEIB zjOmz%n50rBm}F6^O>!x3niNugF)5*Bd=U0kMHy>SOPOoZKzYlgnUeTn*iIYeaFgV? z&SIiTI%U2|Hl^7lpVIB4u;n7k2$OQk6q9O7ok<;Kqe&yBM|0S63*`ipc8dH?K|ke` zB%Q?~lMKo_lN?I7kHcC8lpK>{$`vLRlm#X=ln+hnDamWYmYXO;Oj;=uO%glnm>)1n zrTohzi<0z7*m5rANRvX!13!nY2*kJ7k@EJLR7yDLVvC z?j<&vWKep49@fgC1~CRLPaCbg8uOd2RnCe4&zP1-1Z*N43)@2ImFW|B@B zXOc~sVUkZ-WKu*~ZBkD8$)uXn>&vkBI?51}M#@-|7Roe}cFIDNlrB1pw@fl9KbYiD zx^D=3FQ5!CDW>F`R8S_F)KKP_)Ki`@X`-w#X{Bs6N!&@t-0Q1wJgJl;OtL5iCb^Vr zO$sUVOiCy(nN(5MnbcC^8^hikDE&>EDQB6qQKp+D@2szlD-XFZlw$|N$jR$9%qtDx!oj-BHvBve$J(AG%2K{ zd>57}p$svpqKr4GrOYyEpwyc*Q`VcbQFh)G_LaPg&f;*BbV{K~Hsw~6e9BUjBFbkb z<&@;_!@jC1@~w{UjXKJ?CXJK|lNQQSlXgmrNlJ>&V&@;imNO{w-HY~>Lm6*UK&dh* zrYtk5ptPFQPq};M#9ejF3rtce%T2N|siVmE7`o>hDXUCcD6J;#l%$`+S}D8fEYeLfC__zhDDtg^_EkWcY*I|AGO3_E zZBj$|(4?O7yGauz{pYZ+R?6`vi9K}8mzbndW}9SDmYU>JJ~k<&By0)$Dxt`~)u%J9 zqU4&?QZ6%Tpv*C8raWcRMrkrh?y0lbY?4mtzBTMUn=;5GpEAazh;o%lIpr>sYRdB_ zb(Ch4M#^s{EtKBBgyU(a9A%QyOJ{MeNe1OwlN`!ClLE?1CdCx_S2c9cS5V|%s8Fe) z^fjrc9AnZ%DKu%N++dQpyN-FjNh;+vlPt;xlUz#Y_HaCflmkpkC?ia&C>NR3Qsn)* z&Z~j4$fTLF%A}3*gGq9#&LZWvu$^?uK$C3B7?XU;)h0!hYLjxxizd~SbtZL`gnx&< zH&XU6X`#qlaGhy8MgBfQm6Sbn7V>uls$@{^G|8bXH7TIHZ&FP8$)tj^>+fO9HIyuq zddeu1CW`#+Ryv+mN~K9+ZyobulT^xTlPrq-T{7BEE~Pi#1gjKMa!g7n7nxL1W}4Je zmY6h9OICdHJU@t#ck zs-PTaQbUq>0jM(n{G2@2<4v#B?3=878TeN|P+g3X@#Q?sj=Da%djC|gV#DF@(viMHHAk+(G}?UX4dDf{RwH~<>dWz76VMuDdSACDR-LWQyNW*D8HMO zQ~Kj+Tl=b}$g{pm9c7_OBV~h03uPZX0c)*xN});0{yK{XOfo3zOmZl_@eHc%6i^CG ziYa%QR8ZEK)KI$PDNx&~r^r*ON)u(4Nh{@_CW!~=n13=!r5u1~HEk!0a<)k>WtK@H zWw}WSL}ae=|p>Pr0iqTLK$Y#PC45oB|~R%wMhnLj!6#XNs|J~TPDSn zjV2Y8?eXNHBdnnuY*J4-#iWUHg-I*rE|bKAbj&ZBq*6XJ$)d#Y#Gt+BQVuXFq?}|@ zLb=?eiZaKfmhzlQ17)pAGbM(5f9<`E(%&TcV4cO;Ch3$JCfSrFCi#@lOo}L7aEGpa zl~WEksiurGsiVv?X{5Yp(n8s2(oX4)`)KVenU1H4vX4nCV$w#DyFzUzIZJ1;&?KF*!X%rr-XxzQ_kh|?5hdNEoHD|s zno?v^N118TNU1Yvp{y}!r?i`-9ICVEgF7P~PX;B&B!@EIq<~UkQcPKFQbB1lsiAB! zsi*9QdmQb(iE_9}E9ESc#DO~IQj=85gC<#&nwgU$)N0oE4KEPLm6pOKq)dQrc{_zP~?6=+o_>6n$%NT zO`0fjcc8UeDH$e-hv}GeO;RaECRvmUlUz!zNg<`tq=eFHQbkF|RaD1QOX+9QKpA7w zOer;Kqbx8q?)qKq>i$|q>-{Su2I@o z3uUlLJ7ug%$`Lw?nI;*O7fo^~ttJJOUbwbsU&WLWCKZ$`Oll}{9n)I%l+`9p6#V00 zEYnIk*d#Gq$6R2NN||AjMOkW+OZmd2kkTDzWgSlmz*l25@udd8NED48bZl(8n&lv_>eC@-2cQoc25q4dUC zM`zJaImskth|Xe)Nd{$+Ne*SLNdct`&JEgfF=dEJ1?3Wx8p=GAddgcSO_YC|v{DYh z5v+YB9;IVG!z7h5!z7FHoJlTaqe&s97mg}zxrB0pNfqVl|3lXO2h=(Ce*nMFeeNIE zeKdv;Vxh4Sh7dx?gvQW!hR`Mx8e?I$(6J<2C?y-QMhGEQTNGHoDM*3MISoc-Uqb!FSNyt*8xX4H<%bi9t zS)MnN$FjjlF^h{eRJB#kvZs*-mXnRNv6L9;W?5-ufaL`v<1AkqNjyr8LSW5Ot*5c< zZ6urJR3rH;R~ac`p*3CAQ^oR}ktUW;jI^`-&qyyzGS(VZ&oIm3Mx3KnpU*Lp%yONP zbe2XVxhyXkDP;M=NGVGgOIx*G$C7HKh2>ZyohTy=vrILTn5{;UYb1^3 zQX|!U0*yGjzKt5M7_lFD+4 zkxZ6bjpVVk8YyOZ*GM_be~mP-Y>i&1*4tQUo|JU6oMdExrP#lYv;1TvonjjI!*2|5g1-I9`q7Fe9lf`9?BX zt}&9wa*vT>mY0o`vwUl$fn_WFuWG%G7_Zn$pdBaFM%b<~77Vkve`Y_AxMjZSXz4JfaNHWVjBk3#^ zMsismFjC0UWu%m4&`2GNm#h2J!m_)OPL}CL`dQ`~8D*(3l7RoBQQT!Dm8H!{Cd*q! z@>m9q6tnzoq?{#bhVF9%%YH_v4Ql-aBi$_L85v+HH8Rd}yOBiv7uEBaku;XqjAXNH zFp|&myO9!>ZBNpDu3|aBNE6FUBke5njr6iCGcwHbpb-cEMSboxlFYKvNIJ`3Msitp zJX!a!kR`)NDa-#Esbjg^NDIp{Bh(M-&wWPvSvri2vV3MF0slp%{xFitvfWJG!%UWR zBY7;R7%67C#7H^Iaw82a%|_Z-UNzFq@`aHBmcNaRvm~FQdzgs-AC2O0BWWyijbyXj zXe6Jd#YhRuyGE*5el^m>vg4_`^>&slBfTu=8yRLI(0E9Cjij^uZX}mw z=hJlSg)EsyN?Fb^QpZwegvLy5-D{+irPD}1%b<}_7Lli0Pe9M8o;{7Evg8`cWVysh z9!s^6VwNY3l(Y02X<+%)NE^$9|I_{HW|?ASfaMq?<1FVGNyG?g6jvKbW2rNe&GNXB ze3mzil(2kdq>2T9l$(!(O)NVaX=gdeNH5DNMuu66jX0PE>hn?~$t?F6NoRS^2rU&V z)oY}XERoZ7pX*pA8);$5GSbO%mXUszD~*h@)EG&?Oi_PYjHI%>W+ap43nO_f z-fZ2&VwQc3l(Wn-(!g@9kv5hGjC8ZSZDfGuS0m#rJD;IjPsH5OD5e`pW4XjgHcP#c ze3loCl&}mMsbbmuOx;!!ONNnlmU%{K=~92{j1059Y{bEAQf99*2Ya*gD& zEH+Ze(rl!Zx?w8+-szbHST;LHw?59YkC8+yZyLoZM$%YHjAXN{ zG?LHqvXK&&?~GKjB%Z5VZ(=#zNIT0sBfTuQ8X0C;W5mIFLw(+8B$*{rpxa7kIlxFR zOTLjpmK%(evOH#l)?;exLnAFL_8i?-C(AxY`dQ92GRjh6BmwIv)%>K9RF?HdGFc*X zbz6BX2N@}5nQNq+bXx-~=|;v`&NPyUb)800W+aW}0VCNg z-A3|R{%fR!C8IRx{)@PTqE5qg+>NgN{x)O)ES|@1C64^NE%D0k!+TJBl#?& zMoL%`iu5R|SW=BNv1A%)XUQ|t%W|oaVU}BrIM^3ZpC2`n%+hTno#i_txh#}@2UWwwzL zmMe@@vD6!BVtLj`J4>IDUY6gD472PoU-!qsUXS{Gn2}_bd?U2arOY)(a#`*%Qpob6 zky4foM(S8Xm+018Savni$#R^LewGDBMp;%GNxeZkEUb-PQoh0Y+$#Pkqiel8C)9$+bq(SelGvv%Fy>pJl{I z3Cp&Z>DH@QGL1B`%r(-^QfZ`@GO3rnsMI!aN0E;Z86a+{G+mZyy*;OIi7`i!Kq{Le@x%g&2*f9Oa? zrH(RE%yPDoa+Yh1G_c%lq>W{*k#3gHj0~{+X=I#br>k{;5^>z3{v2*3jpb}3*(}!> z$!EFSND0eYBULP)8EInq(?~nZE{k=4dRdM)GR$(B5eLUM>hnq?$t*7!NoV=SNG{8k zOLSX>EQc5=Wtn57j^!pJEi6wM>16rXNIy%cRJS$CGSx@|j*&Eq6O5#?TxukfrOrqm z%X3DGS=JjVX9-`UTW?^QVx)~_hLLWT%Z&`M++k##rQJv(j=v5GmT`jEHRSD z(qN>RWv!8Nmi0y&SpGKB#*%Wq?oT(%QAP$><{BAiS!yH^=MXfCCL?K5>aABHldX`I zZ09Nw>mhqXcDD04^C#pWDTQ&837KR!vTU&wE#O!2yV!IFV_EgqITu;hr}(bHu66>> zAuOxLnuN?E$Zj^B!9bFpE3mA|_!2~_T_vR=DFJc}YEHEqq;!eC^DOHQe7$0_-N>>( z>S;!)$##tvLgY`00Nw@yLkOeq~Wic^0IA#)&m*gY&uAVrWVb`y@Ql&OGR z3fa@{WVxM7?Pd3|JjRk{4@;@BIv`h|)ZTV3j*(Q)yO3)k``QI811!_*GL}DB_Oq*{ zREWwsmbDz?+TUKmvI0^qrHLgd4_Q+o2iPqv+d|6G)&ce!mS&WyM<(5-qqt>Nh&IUU zn8S3tTS~py4W({HsRQj^mJCR(lzt`F49E(|L3TBcCA0)jh1>%<*zS-LwJwI-56Q4g zaSXsZW?cz+9CE1LC#Bvhhdcv0%ud0+o9ej((hkYA6R}n~s zV)rGMwHZnsXD4E>N88THkR7BXvz!H)Bqf!_y+LiKId&$?o<{Omjx$osQe>o@xhZXSw!V%lcVLp_DG`G_*xqZm!+U^7&%R8k3onGHSic%17oA zWM_GykU* zvS!+&apHtR)@gPD_QJF+7h%RWhn#K~Nr{TfA&F8-I8(|qKie+lOa2 z_VZ`i8QAN^uXShH1yX9Phf(wHD3xzFN{Ow9XWPyHD0Q}7hi#bl8*5RD*5`BVUY7SD z`=Xw6?J+4)S@Xe=Id(d>koY~xTsu=rY%7>+7fPwIzCt}mpwxMGBg=#vl@!|j|ETAD zd*C1SoNp(fJoWHkE;Y~2VaaE?z%G{(8)=bUB_-CMBD;-C-N2LPpWAEhp` zQ;?1y>BaUGDY2R_wr5JIu^z$y(##jz)&D3p-(I0IW)C^vwkl=Kat}c3&wN|39FBbg ztv~baM3yfLE$dj+e2JaR@+0I#DN|VVx_YTSjYY4km)e;udR<*$XS3*ab%8yTtbjg!k|rbWt9+2^Yv zSJ~|*U)b3-^tuM6)l$e^!?P1R7HLBbm zWqFxvF1M{JwTGm2j9S0J7A&;)qt=y>sc%jcY_v}a3E$Aa~k!<+0(N{8Ep`rP^bzD1JCB97Wtxp!?R=J9xX-uRMJ%*+Q>NN3W7(TC)pivNZQYcq zvFli9pGRY_u^U-vd#B88b~DRyoVm?zW1;;9WoqqqmRX#swbw}*4D?>2&hFvNTx43X z->9?uIivR*x7!1pxsEfp+asLOdy;y4j5GS^S#LYHsO6>iEqB-nQYyqs?#~@|5@+<@ zW`&)?nfp1j!cOCi-nXo@(>bG$uq*8>&ggyAopugqp5c1#wDUNl_gZ(^`J8!$Gk4iV zEZvZwu|_r63Ad^(ZV2)}DTyq!*K+KTb+?_&;$VB-T*?#{+WTz}xyPQyvK=zJNy%i{ z7eZ~VvNy1t0HL;4*+VSnL-s+bMtg+iDoBQuaVZ;vn;>*XwAxOprao^BXn#gWkkxiN z%bP}KvixMEm}NU`d30=AZMU!-YGjxtA3}TV)plo%YO4Z5d-c_J>upM&htQsXwLKyw zwoh1XC)QG?#`+eSV=#()?R=KMASXlavpb~3&S0DDb)4BA>)LE&n(RK7!yxBE?zcCv zoDG=|dB7fFxfW6adC(qXSp~Ts(rjCGsz2{QmO~!0(^>w8G(aA}r;+vD~^KkK2tbCqv$XwA#%q%OM{_p0GPu zUV?l9dD6CSr#{zPUqZfvJY`o(sj+As8HKdjdG%^se?tC*JZ*QZP_hM<(3wGw+O0I_F zLteB?S!y8XLptm_mir-BLSC}husjF30rIjv#PSxT9`cH9-J@!z_3=SSr=7~O8P>;V zAg|gBSoVax2I;a}Sh68Kkk{-^mVC%Y$U1w7r35kpdEHK5CEGIBDR0>2QYu6hGPI|C z!>(ev9YTBTH|!NsV(a{yb~_85d4`;j)ou4liQTDq%N|wrm}BKz_PCVTvGOfjG*W*m z%(=~5_CywacJY>-%yJL!P5r62zJ}}pdC$&Z8Hdmn^!s+Mlo~67JppBU?UZ{roz)NQ6;jmp{y66F z1G`y?pd&Ti$@$Q3l@b+nq^8V=cDs~%Ycguy2Q`0W_eiO+GFbZT>ic9{)~S#Kk@?sj zVmSwLDC85ns7YmtAxA;_?QWJUAvuuG>`|7RAaoCQy*=fARjL6(dz;Vg43>5X?V~o> zIZDj>v(avl5;gO@(QcAbFZxmHB($~BZjmw=(&w}r?bd(P^QGM`Ma>xfU8FDV5hZfn zpsT;H>@gNP%RLX7uWanNvAhNYeckkxohn7uGhb$A$H`*I*LJ@WL2DHKZNwpafJI-C z4cWs|Vl@xh6CYIb^9{xoMLk32x{2=c(YEuAog_1{{rop}N}SNu=r?v+oY2+iw|0h< z!9dsZot-bG#`*^dpx8g>J|JN%tJBt^|LWxlsZq*MrjV*-7{_j`M&nfhEI zc7;$6zqhA6q$I;go|M>D`Mn;O`#G*kR^p!L_jZ9QC71FoXzM3?L7c3B{A{;M84T%C zzuJipQ(IB%477DWGJlwwFNU;2#*LIfo`w8v;5t@y`knZ6Q;Vc&2!=1v5Sac63g)~J;58Y!{ml@jiZ%UptS zrG$Iqgw~c_!W-h`4~%rTaB+)T6W+vZ&{g*C;S!dwAhfpZ9&VOWZ_!ys*v0*?aLc1q zs>VvdF?=FqYB=vP6701glOSo~q{r1r^&IXKULmE%s>jvMzR2tswpu9@wGKzA10e^5 zXS2|m$l;I!!&OoSLwe>j!b6-n8<`wr4h<(iq1w{R;)rmil&DpV%q(QG!abbPOD8)# z^GQ`oFP-DUsZS}F zb!xco8C4Hma}udyu&XnOWhYXDL%-(Yewx$n0<#%Tp|8hC5igAa%&(hYQ!J zQXfH9L(UCXvJA5ngj-lziY@C=Wafm|#L2Ue^TNYYqE-m!TrWfBh0C5(HE#oX9dbdq zie+C&52PsEEG23k520~g819y$*42-ZxiD<~hw7;idC1TjH9st*L`6P?&MxMM6FF1J znM=aSoVkQEmxQNqW)Xze)l0+EII|2w*Ls(RGda@$p(E^qa5iTig3u9mL3pN=E=%7r zyDZ$oqVJepZuSz-pwzM0b}kS1%gkU%%fj%)=jFJ}QLiMNB&F7(z28QRy(FB)@)qj( z39=~MCMCAzE)I|BQpo&?%;NBrcB;A78iwrTVf_dz!!0=X1Y5pH2Q z2XZCk#&8?U#gL_t%5Xc&B1k>trtmtJ8z7HDmWBIRY9Y@;s=`Aot66RikFh)sc>$SQ z!d3^3qTX5yc@1)FIDw@bcP~GI)P!?c-a}?1;Z0HN*c ziEue*p6ASy;VRC&$C)R?D^y0V&-B-{p9+sk!Or^C};r5?u4@SibT$~UOz z8f0QZTfvQxHQ@qTN*!UVA1QZ%a_;S!c(A>`iYvG1BNYosD z2w4|yk`g=XUKehW5JXartyd56qdh|%&4$pp* zT3>1{L4Sskc_&=jO)?nL^Yc!)RZ5L@8#4bzrYGFXvKsO~$h+a3x2ROCKktRhlvqz9 zL#@9TZuwVc;@eb8_1PAb$x&jxh*Bw#zHs}yl!;nzLUxyu`M#2VmXE`tSGF!o?TySQ z;VhQFkvRzRX}FLjX(i4;ApPNLmNdw5koDnimJ=a#J-8w4d_XnVTltVvkl7ecWGQ0# zGMp-jddTIF;c(`sO1_0Gf&3U=#PTcTddPpn zjV#Wc*qR`t;WaEg-3p#3jLw|Psl0ZOdEvu*deEk zGjvt^25PpQO3u8F%m-5HII|H#nXuEqnK90Uon|R&AHES8#~GIr+lRYO)_T?FE$&kN zah(<=)*cWlLEGbU16t!k{Vx+q`X>mgP`CXmNIJpcpr#cNv#7n4$&Zzfr`dHpE zlKu^~ULn4Q&^h)VP9DqOoSEV@vTSj;%1m)Oq~JY92<;)KI;aHyx+YA9&^`P;jbuRR zTsqOom9@yT|Rn#v6qv^8M@0yZ*=VCWN=15k4tm1IHRA(r8&8r z(a+ZRcJeqw&*N5M6ni@boT2A&^o(O4r-(E3JdQH^I3=9X&)@fTN;yN%(H=r(U#F5Y z`icBBrxlH=6i^f zD<$^K_Yh|`XY^KZsFTm4w}L~RLKeLh9O@KHsSqz=eWva7P-hWm^mcKWQ^uKYWLmMj z4s)tFqn|})I(3}U&muFOM$YKB8V+}wIYZ9}+fmQqP8((xnWB^tPO(_atV^)zayXp&r`a zW8&VU_6b=|rmUx2K5wG;F0!1`@2RcZtx3qxUh61lL`v*^o1>jEDK*vs$b62Pk9Mpd zsFd3G(y{v(r!G$D&Re!K7AN1M)O4rlM=GW6<&Hs)cS@yHh?%IF_I@**YANL+dyZul zNm(JK%hGQI&2U8Z`DEQcsj$W`h_wz)+&e-!CeNYf#pTWW{{JdCYH}3iI9_>7M5`c z9noewYgo2lg<~Kxr#Kx-%sK6;&X5!}&vYeqsxuZRG|zcX!LMqhdVXd(YgqKyXE}Ml z$#Iz(o8>HE(K9y78IcmT_D6qczdFmw8lzHCYdVC^A!j+WS@ay9?j-!KX5(yRQZb6t zon)59ko_RDom44N>kdd3mfb!*E@YIdghnvQf?RK=YJSQxskUZ z;mvSI(a6V;36M%>gRDob&)Y+8a+3Z>bGTG|iwvz}%bgS!YqgdsQewY_S?**gF~59S z?&NSrk7BtqTgqTa_i(vW#iDz7v(w6=dw8oe%%azgYA1c1dKfzv)H*qFvKvNG=M+eZ z?bYj@A}LXE+8oQGXE*guiIk-x3FF!urS5P_S*Ah`kWwinHqsT&3Kl)C6;6W^Gxilu z(w}lY4C!&LaB^AnxbAesUn-+#<1QyxN{w|WT0a!6-|ZBz%zzvXS>-H{61C2S%z!jH zyLGsL2Q%W$tV!g3b&B=jqidz~?srKp*{X?dSB z@o(yLt+?OF6qZgSSu7ij%x1Cf)%7f3*~v&H%fUt(Sxz^yhNZ+v56dk^hFG33Vp;T{ zvsS!oB#Gq*Bhy$C@q}NEG>7G2BLys{8(GA1g^_9&dUrt8)6CLrq=V%RBYiC27#U%4 zn{+)Qq(-rakrbBcMlx9D8kxy*oslAzl}5^>#I~mUoE33$KGvuvrzKA2LzIjg<;njm+~-FJ}&fP(3d=Bb+$_@+jm*C(Wgr2SWvrXCNKUqBwaT z@{(hDvUPD0GA~13c2ZcbWa)I$Sjt(toS7`uEbE*imit)Va4K1zWa)MqrNqv$-*%d% zsC!o3X#H(xgfse?-8)W-Pd$ts2YZ|XDY1LYJx)1i)}gKUQBRMvg5@JfKjb}Uos`&F z(+5tE67eN6gUEc~BnWD&Lj20|p_8M;JS+LgS;rZDg#Fm*QDWU%Vp;!1Js&%%0o4<= z5+1;lKFE4!os=3Y1u`K4Z@f535!sfNK^g4pzH;)V;LMX{$my3-W1R!p4yC?x5;jvc zFNUN*esGGV)LP|`sgR$Xb)2~qascFKr*ncT^%R8e$^7C>*_@=tdKp5`Tz+v1S?HeL z(J1w+K2lH6S0!Uc^zkbuAEF*QQvYr;HAR-SJ3=4(3#4Tep|@LHY$J3Y`h0N)wIVQ7M{xqd7Vfo8QIm_Qh?q;!EOd$Ss zO=x2Y8F`zZ2rj%Y5G0{soCVDByL@(vtOg(xj$0P-# zpe2}UGKaEEHgY=4?nW+W*~3UR%M>FmEK`lV$+D*rdb^5R-%E=t9#YF9Z4;qc-PcGm zGS!d+HW6Ba2N^j487g(iCPGW_@J)nD9cAPMlsXevK-osF!Sf4x6YN+c)sP!Dk^3Mu zkm)9~2J$zqbB}Y2iEKbF;Tmax-UUxdT!LLwa1NyLH=9 z2EUX<<}Z|*?G8&B4C#@c;U*_hW-z2jdZt?-1;3O;smKIece^Dl@3G{&l`LPgob5KS zj6=3WsdLkPfZ%d#|#@9pwx?+nR-XN?dD4 z>a%KVF60U~O^L`se=dbAa?_znQI|epq|BU zK4)%+ERlky@vqBmfm{z+;ugtF?3%jFEsql)$FDNCRf&09HR`UB(iyyfdgy2sb=z6! z?K~wNEI%9RWQnxs%sMF*Lhm!9ZV!vzXGYzADdl2!l&VFauXhKeM8$rPl~P8ebOxDR z^HO(I)nlCup*dXY4(&uUUt{G#8d0j;&DohGYF);e8{ApQ|XSf zJjHU8JI2xlc><-D@!qCW&hr|`a(7&2Zm>Q_<|Rm#n>k7K=Lg6ekel6nmcJq2V42?H z_OMKR6lZ$K-0JqR>ryHjp&b^N5jMAoNc1 zqejwL9y3C34LycC9*=J#hoGJ(jJ%2O$xzKt8OcOFbT#_4kr@!Wi~fv}S&-={wZ_OC zmgkII1et-%e~c_7To$o6l-9cAQq)^ad8lWt>+D8r%V40F&RREtMK7JT zZW4=LIxn~>N#Ao^H}uE_qh2idgkAC3t9BczvmXS=$U`tEn(3! z-|LpL=;i)_TgIZ7`-g71l-TuhpIa>@c6{%1+f}JR&-}-3CyQQQAG>`ldU<{94oK+= z>3ROdby8_5cZKx)eCp=L3C+)EZV`)~pU>SADN*ZotWlKN=ytQLhS2)_rJFHX)vTA` zfIGmVm*7|K2n($(RO)LtVRuzZuRlX>H4CjjXQHid-G(@!?S0tI+(VVpYt)Z!JBwaA zBkr)2!H`}$Ke@?MsMKIcFP;Coxl+19dX4(oEnv}W)Tmp+qSu69+)5U`w*2a@lTvQA zV>aY_O|CVS+Ug9w#q%@fCQ2C$={4bZcea$SkX{r1&|9u~Ixw!q{S5V-kFk%tJ+hQq zH;N&Dy3_WQ;|la0kH6d^B_UnvFSk>QdLyU=rT%uiS-wNfOCXll%Q6nR9uo34NKtLw z1hKvJy{Ijt(UMb zm8!9*hnGN-yc8)>>r-SFLw4{Mab^%w0ZI1yS;kp*_L8Tm)}2H%|3hSr$U*-cG95 z&T>0vCVQPMPjP0l*TeEI8ClFUfV&cX8m;X za8G0?(a)`p_GU_{x3)q(nW*PjFXs@-s4aH}B*&Y{vOO|+kmJ1qmdTJgkQ2Njmi-`9 z>O^k=%i)j(kX&yO%S;HJz0dH zDZM@yc+;fBo+=i28B$_T6$`va8Jg=BV zuVd$XjVyXAnCG?X3}*F3jI_vGaU|8V)S@Sk-H?mCf-EIe&w9vwZygIgasDslQV$=s zlch)!w!{;8FH1_)`Vloxf-Lm2V>uskGNjC#lCA2w5>fz( zdbun&LN0_{@8wBR*Pisucd55XO1-!fnMKHyd*xDUtp_02O6ix1A=F*DGZ(q~CnF$xEM3r7Elrl%n;y%FB@wwYnhm-e8rtPD-bxXY6LL zFHUGSZt;es)LQSL6g^d}_QY{ivyx@VRC^1gM6G^g8X&iM**UV$=G~q;Z-El)OJr#5 zbzZp?b*1p2lqxB)tBE?VPD*TBuJam{m@|+%uTe_uEcbS=MT**%A4Sc#dm~DOeyj5? zZ%j(md}ZY>&pBSsn0y*YZ*?|!2~yP4Kzd@>;3aY9CyZh(YQEb`Ve#?H!dIlEvFKkJ z-{YloJ@jjLy613@m&KW#P>PO&tGpb}90H-^;3_YVGylh#MlYW;mvW}jE0PlXrQ2$+ znnho&-|O|U=(k7i^$Jg*k*cR}Z=lcjdW}-ljM2XCKCeAa=*_DpuP;vCMX3k8vJ!nGFT2%7{ zWY&5!<0KjKvR8Eym8!A+Kqd{c&MQBeBx*TrxN?EK={3$&LZirnbbFbnDA^L36Cv+- z&Z(-NoglO=_joBR*^pC_dDly4`9H`U$a`KM%RI=nkoUb3ma8DOkY2Bxr4sTm7I(G8OVI$b;rpmkT1P{DTAQ{P&4fh2fSe^vHiw?mz1ZL&SA*V+m8cYvXojY z2SUqhz?;c3i{&e?OiH~KzDj-LchKul^_aci*Iu8>h(eUQ6?g2v_BOCwX=I3{#z^J= zQGe>K2O!j+uf1w1u|9w8HFKr|nR7AHue~9b*C5pAAuo59s)vMrm;H^`$?_Sb2&KOB z&=CH0P52ga8RQ2e)-y_eG@_rIj~LO<&42Q8PnYY4T&CBcp8tCJO04Zs&yA3uy-t>0 zAhaw-yjj6$hPWG^Y30=oHRiG=MAY+GV=iBPfwgdt;a@T z`NdLVqpeJ^*RUbKHBRUn*7g(5 zRQ;hVM7o9z`}uJ~*RYP?A|-0+YfsltJc~-j?yvfOniO?(_z(Ky`vp>BBMtmgDY20T ze)&J@34B_^XdNrXxae(!m{daOI2e;A2ptEFxE&C>cN)?4xF51?rey-HnO*2%f)QFX zNhTQi8l~QYY;NRtmMyfnG#l%YnYf933)#}h=Fj4;31n*{J3=CfxVvqHW`0Y^Hhx{) z9MUf-w)2P790qzelKe3iJ&Gh>J?BGvf(WBVW&tTEBn(XJW=vhtn zXG@8SH1v5F^l&G?Qf1^CMI+tGACD87jYos^P3)#na z=Bk?agIodG*Uw=&8BzzC<`=Qdhdcz?&#z-y4xyU&_gh#Vf~;^f? z@0T(d(ml-f6AEeUgCX6+WBoKKU7k_cJ-8dw85*5+~Hd9KVG{kL!4UltuUO z1m8JdwWUXKf}bcQYQ0ROKs_h;87!|uGSK=7evXvbe)U9uHfN|mMPIR1 zPJ6DOJCE8@vvCqKGyFm+oz`E-oCP`A?_}BhKUgy%GtI2tUWoM_S8b;lp|5VxIrgbW zrn2N2IRZk*%2`HEVL9C&jq48`n`ZkHFOWSH^N^u^_!<5bDX|`&;jdx20-5>f;hBDF z5tZt+qL3w!v;0L;@Qw$h9Fp&6UPzg!wE{xt3Fr8QQtGW{NEI^Y_)RR<-ng>-S5+ zol-~xq`)7M61%#Y44M?EhNEB z)g}JK1*#NXTfGM<^&426Hg#=vt>1K+%3Od_bR~72Uv;^XMG$&2Qs(!tR6ywcsHmT` zP-W^_uJ_lm=({~j{c)COk)gAT8~of7Dpg~3Li*943V)`QsFjWWd7oozh&?%S$=_R1zF~=UAxIrBSbZueVR z#9GzX?S4DUb`W}JyWa2QdiH_PJKObs4`+^rP@nJc`#3X;>$$@p;LHV_S>X?JW(j9j z_+y;8l`|`S^>Om3xQ{a{{RH{B@(Sx&$nlu@JN=lfH(vBpuUGSv2e|~J z=)LIuq7DHb4JEW+!m(Bq?{a#hdEagsrgOqw}5lWS#RHvW5R4w-?q#n|x=hf+ z6!NLRu7XO%MzP-SiIV|j*83ZzbeYya_ov-RrMg18^)LJpDK*w}sQC|+8uTYtQYLD3 zK)fWZ<$7Eb-i6TK?^`3ELulq>@;!vE#J@9{U;ZU-hdQr|Wp;$nwYd>jf2C(w*W>Po z41MV+CdWYNSJ>bCDXP!%&Iv8kAN(m&)ZT9^jN%8sl!f+e$&eAhTuSWT*H5~w2{TbM z-La3!xez)RjmhQz%0x}3TGkvpp8n*olcK(uv>RIg$?sJes}7~6N||^Q&HPg9ZpdMf zU;LRY4?*Z_Kfn14q(rS&mN9=+O6)%5?|#BEswXzC-~C)A@{38-`tN?86!l~zTS}o6 zb+()b`NOaKM?K?yM_fJUAT#dwO2J<`Lw|}OfBE=$G+I}3H6$#CSbCA6d-#D6RVwop zwF&EE7Zl%Vd_#MYWVz&0B~DDT9H& zGr5K6W6^geCyK;dsOG^y-yX4+lhRZb2zh| zD3UT5=xs1bEMU>wV3H{1Qki%goYwj6MLEkU5ZX?+7u8bg#U-ejzAd(cXkw{?+>GVE zgJ_Y`71G=Cj$%y8U`TJvJBo?bG-LQHLMTOBce2Q1>15eS3`iLa>Fs@I(N?2MZ9slyQ8fZ)H6kNNQqkX6)ak&Q$(MXS3?Jzj8m&d znhkkUmRcaC-Z~BPJY=fqkfQqhDr7G)6em59G?7vl8`l?*eMFWN^)X{~Hvn*lRPZY7-1o;`6{ly}dJ6R48l`N04q>B|S^hHA|b)aZwd6nfLv4-VsmV?DQ zmOcpeCqwkH46_^}hNNr^{R;UDnM1`0XY_OG!^9X%>MJ-_Aaj@yx6^D4hW3Uy+v8r7 zNKz7{L+Bp<5n>9<5fIu6ju2^FiuT4-&k-V>Mc=nMQe;bs-L*av-!PRkFZ6dxkHj}j z7wVzR zbm6R^ng;`Y^gK=^NU0DHBSU+V<3ti?^zk%Dq;O^}*OMdCIHQlS$BT5%baOq&i!3Rz ztE3Y|(MoE)LVS)4ZQUn`1yZ8oI|%JzFyBg){os zF>}Nk&Sc|`Vi9IzuIS**X%Kn~Wv=L!G8pLJJf0^+gIezTH;;uPn??V2@_bRoqJKL% zPqee>-#lI*##r=k9*ac!-Ku8&+sO;X0v7$-$%{mb6g6Wxn2n1?XPnH0%olxeayDdv zD7Z(}tVdcRidghWOGF8a{$=77qEt#$?1fpS-wItJDmkNn1$m{Y=1dl6t`rTN(Z4Od zN;Gjszpr(bXyuH4`*D%zWYKRwE)qQ~`ZqpTi#`_p8=tGi0E_-z+z^L)(n zO(La{YL3dcOcz6L5o@HBODTs`i|W;sDYx=4WAq!18j*CbDs>*qZDL4Dg+;$er9Rh+ zg!@#VNvO|tVp^Qg8y)o`PfAyaGBk>nqBAZ-ZQU&f;$%7cbB}PEsAg4)M%pOyrF4ca z<^J3!2Bbu-GL|MW^L{GT8KUpz)}o&KMKQ}tmIp*B%Y!TriWMwRu{4V|mX}!`5}hnP zEDwtfQli#+$en2G5s~nKnhpA$Fa2(`MNE@27$TwHgg!3H!LbNX#dBSpUpS--V!NNV&B|*OQdmz z?p@He-rFLbGy0o*Z;LF>l;WC^&d}ZwIh@ho+mp41IHtGVh8~DT9Ij=H7dvoJ;9%?!70fIitV1_r6%c8U4+@_eB$D^f&i< zMGI&2H}`tQ8qVnN>3twNIHSL(_krlWX|aC>3t$nIitU)_lcOsLf_M) zdHz&nNQnyio*vEfry`r{dFMv8O#4MHm(t&_>ld?G==*iwq10!hfa{^}*ZnG`nCmG) zhW;wwdQrle-LcNwJCKxdJ@gIi%^{zQO3vg!wwF@J_2@6!Z4eEd(OHat!emEYW>J;1jjqm`t>+Oi8z=aBhcG(4eX2gp3g4}$(U6qTZ{ zeJ_NJh(#<1L6$)NE7m-vGW6@A>mfgjqBfEm>n8lYiDi)A#5l`QD0K(q50U@0%H%@m zZ?gX>nptK+?nUM=k@Aen%!fP#`CH6nSq7n}Fjg>|<#q`D+B6jOu{_UW2kFnMdN#6z zgGQGBaXn7Z%#!q)s@V-%Sq_9y&0f&Qayo?e#(prway5kh-j)bPSnh(*U-AlqF_uRl ztr$flz@N&J^RpK69AvW~f#n?tea$Z+$YA*#vJROEK^Du;5PDx?^B_k`z2&UKn+C{i z5md8m0r>&4RnW<@D}>%L+a?(1da@zjj(FoGXn&6SQ)}fxCP0z`@gI_U>q1B}Bss{H zQe#~Q*#ok3fKTwqQg=hB=3Rp#mNv-2$fO3TYgOho$dQmKK_$z_5W0)CXRw0hTb8|o zVJUd4lO-(}XW8s^To0k1y#w(A)m&rk0hs~WCrD*Ek!9Z?OG@mVe_D{IL@Yoi52dCB z`7GB!Xe*c&6tU1A=}cty3l^}@9;r}DsS^2wh3=j1A2h_trN|r*w8zO+kOPChIJpjT zaFF?;n&)L`y%KV0kQ*nrK{A7h9crZXcd}MNjtugps5dqrfMf*&FR4r|>Y*=z9~C6O ztmI+H@;W3t7-JcLdFj3Sd046$TE#vvyJm7S`d(;)PI%ZWh`O93RDj5i5_iLX+o)}p^To&cE<3`xOL zGtB(fkduPmE>+J$l-dDuaD}E$0SPSZ?Eb3W8}Yja*MbkjXNg|K8S|Ae-eh2wgkO31+esaAs~W zn`J&{<_3i*E0@Yj?)Wr$?diwmJHBK(UtX>fG#K{$qi-Np2 zX@1o63;L$YCBX_Qv1hfH2J2Y#d-w~2Ar}4gc0n*ArNW|Lgk6K07X&lAWzFW?ctJ2* zO6=TtK~Ts-=f-p{aamBzLg&UbuFHZ&T#r6CzC0-7j6OHMJg8!!b7QJ!VNl0H=f+ge z!l03b&W$Nk5;U{uzYkjyv`LAb8($H$b4H&VUlFY1j6OHMGU(xqJ~zHH=;w?+H@+$u z;EX;uzA6~ujQ$?=qF{_O>#>yS{`{iAc}uOmKSAhe*VRD+i{Gs>R|iQfJ3(k%i-Q!F zeIPXU#X&L4kr4Wea!Y~|DN%6-WGR-{lAw$;3nA22X;8^h38DU!26ZfVbLN_$iA8_y z`I?}GMSt!2nqZBT!9aiQ`P!g^Gx}@K*9P4zkD}%(^yj*umt`&FHYppp9{s)UvS5fa zZy`hX=gWdo&gd(SXfVzheZ>(C#M^4k)K?tW2NR`K2z|wIeUQuV=lyF9W z!TZLbjD`OGAgu|NK_v_Qm2FxRDuX(f&oN_^xhZI1`4K{yn}TMRzd5rkXk|%yOJ$Y? z?NSB<{oU{7K__SSLx%RN%Yz=y=&yxW1$~@3mNQks0B7`7(#^pzXY^Il&A}K8T{}>p zZwah-)OJCCjhgy=OOPNXD(0YO%G??xvdrf`-x{Q_==+e>LAsQs;%a18VjZgvGFfhb ztdf$$QUiGaQWMN%X@s;$$!B>S@)YE@U=d3P=ujKDqmX3C-9bA`7bFdGPk_$gUzhtignpH|%E%8ajhhJl>TtCY z`Wh{jx^EMqOjFP!+frxUwDvv_Y={%O*Zp8H9w+pR-R2ujY?w$^+ zS@bvVo(Z~H^tb7r4a7&P9{p{)H9I&NK^cqw#@&B{b}4Gcreo~S2d#Z_Hq0-P zUkuhri3)vZ`^BJB%3z@Hkaq;#oY8m4JAyvW=sWE%1sgb{@3g-Z3`?mH_B%MIMe8pI z__MZ{jS8_nBwtE`l)*rMUGSA4QA$+oycl0iMCO$sg){n2duNc&qVKeK23ainPW!7t z4vW6i{%VlNqVKeK1^F!cPJ35SB&91vU;LwW?6sgOPUx=nx}Y^q=&tM=!BCvgJ=nK{ zl22%)vAgK+2Fah2#O|WMALRBcnTFY*JKMd%fRxVA43-aq(haJfiy#+aHa-kezEBc{ zR6srnDy0mD^mhXLgB4OLEczxPjpDOlO`Onnu|DXN(iPI*E!+@fe@Qhfp;3GpR7#0j zcXEFQgAIeK^&@dlhTfk2I!OLnNjowt(4V28QOZ(_zII5he-m`Z3C-cR!H^WZ6@v`j z>mCkrhg9qO8;K)9tCXm$=YG^Y8WevU%TN!04?3ij%M8uoc(6fA)cOSdq0FDb2+J=J zy3cP#624P?-n>UiC^Cg*cNRO6&633ujuf!yFUdQRW|p&%c^W-*BLgh@KEEHCI81HT zSW#rwAtNHQr9`b-$Y)ZDIP*N@zmOnO%JL;7VQ1XiiIj1v&ECatJ#c>`AyUVg10bo$ zOo%jc<}64CWQ)ie&Mbu-2bmb@U}=HOhHM$>R;9$N5c)dq))Dc&oFDNqOJXEjirSON zcVHrUaWWV6Y#S+zlM5l+MMhLT)(GmMzjU^JWWx_sbL`!o9U~(w`rV%7i1nk&=y!W| zilnmWcYAh@(X=TyhSeO*)loB;(GLs@1 zBh>m*q0eL{MY5&fUL|T?h;gMvN?GVHxGjO~8fj&r_gj`iQX}0`VqY4W9N8dcFr>c+ zGC4BzlN_n}M*WmX$InXix7_!P{q>K%Bi5J_{aum$BZ(~fdm#Hq z3RouNN|XL7#DS5NKU66a`l}EJM~44Lg1<|SkL8t8RwdSn5MUdN7$w6N%PEGIG~r9ym! zw&>jW_{fNqs2GLNp6&R^xRkEYmhWR9j!~Qt$@z=g!rgDk^N<;l)W1oh)-I5BkeQKm z7FvI3yErA1EhTnG@w7;@lp5U7X6968u`}Z z7RVGtx><_R*7lIOkx`bbA(S~UB0{PZ{pOl7=SPyH#J>43FOtHd|8B*+$P^a+w=FJ+ zOk>g8@&%D}Dfs>&+M-{_To5TzV$qvLbpCunWCcqtGIYEsigdEv%W`4l|G9hr_`HVn z|9^7dce#7t=ged&f~X)%R8RyhL6(*xNGphHD~Jk$ASl8n8dU_TW~=BDwk0U4T4I$A zqQcfH8#YUgR1uo;xn|~dopawOy|>->zW4k6e80&b^O)y(&2`PMGiT16Ip^*J zd4h<(#$6CA2%sJoCfxwcf8IksXjflSZH2wXxix)=rm4 zDh-(}))Lu?M#$P&JBWCfMmB1l8)DIzPBlyD-$I&3zNW|0LG*7S=~Q*G43Idb(!Y*$ zV=NmahlqX#G$WP=auktTvU5``A7ly_5`Jx70YsTxvZU5B6r0~63Dwm?u!i=GCkT(In*l*55*#`PBlH+ zMMSU3JQPa@;XM@($1*^8PsPKrY(v_j{1w^eSiT{3-WSrEV`B`djq;sKbF2s&{=V@e zu@Y$b`^JyN$_#0X^7n)vja7i~_kF$TfbFtEG zv}T6T#+($Y=7<~%^R@>Lu`NklE~Y!c1xnaP%8bE-M`0Hf^7Xas(C}Y z3~3YGg73$2w%2*~YyN{+wISA*-YM4=n`?;mHDnjMSBWjNsk|>Ie-vv4;XeOStj&j?G6-(d2=6MG71pS5Tk7JVz zS)ImTPVR~I8e&V4M|M7q4H#leaTJlyV@*41JN{YO7i%`eKP&rUOQ7LWd=py+4VU7Z zSi2$qS-CdW0m5I6UK{H)#2;JM#=1dxY*`!YMJletZ)1^6ox|FwJ_k>sJbxQ=K*m%3 zlp2y@$c$+868jdwx>y!8TM^Nt*1A|OG^LbEk8Iz?@<6U2qBY;e3Jj@>^0(8!kBx&J z{&xEJv0_8oyz~8sSThK>MSpA!$kWt1dT#SmYygB?aD6OmC)0vyw~+S=D#bu-Cdd;+ zt|jtwEN{4NWj^0Uyvz{)d{2va?riEuaJz@%{UGm<9X&$}$H!%94L#u{qR*yCya*)P zX~~dy3CR8+Tf|F2jsw{;UT#ROIGc#J9F12QVtZ6Q<#}klGlASi#EFmEMSJ(>v@%`* z!ZTPI9|yv-Tp2F{;W@2}7aKxf?Rc10QpvK4PXf`S`T|4BL9V16>X}S>ywZ>v(YuJ~ zH`&wU(+pXi#_y7+$7kA9;t|s5*>ZZk3mSfxJUt%W)s!NQ-zEQ5d>jbRDz}PPfbgtx zSbQPK@+SM`fo$Zz`L&M)=+%6v7&HCc|(~Nj&0@3%UJH*$3=p5?r zeeD#_%(kg`O=g$)7(;5so8)UD)z7Z+Vi5gBvZsmc7M}~Ezc=_NBH8hkhRhPHDAh&u zJone}K9IFU^fxVcj}L%E|8B`1@#wF$clu3aBJE@+C!P*+I1z1U&v-V-WN1dja}Ajz zs)@W!srHH&fZRqz`^t^a1ersmlQetB^LN+2=7>j#=o#Za@kt=h68VTU`^Kw5^cRo6 zBeGw-735XY+)DX5AifghU68zZH^|2z2gVEbu)g|04vH6n3=q*dJUBiRWXJcd2HBs8PE`_L12U0_9>-3P_kmnYqUgJ-av~o&z$6 zh_2O1@sS{lh-{!#ljHf8c=rxd;x&fM5ew-23hzc=+lVg$(dV)va(28efoxBtEZ&_! zb|Eq~p1Y5&`DNrQm&p0?0uViJ97^P(_@o3fmdGXXnglX|$Yt@y1acOUtK$9pTFctb zqpUKyaM5f0}K=g=oH<26T^@hwAoutv^rg-~)w#Ia-W+FGo`wf{RG=RjKuO|B#Ie7wPsIYO@u&LZ++ zybEMc(rD5aA9k=!HHye{MBa!O8#2f2LTM+mB3=#6c+$L04@iz)~RL-eP1w&NN0TEA=b{Lq|v?i{rD1)Wgs8K zmw~J#axA6limw3q3Yrh&9U$=!t(||wyFhY?=yT_zcn`?oMD%&l9q$D>oroS=R>jwW zR6+A`d;sJwB4?BLPvUup+B`o=NA)Jsd>(HG(K};} zi)S2e$q{7vEh0a}%Rwf9{1~5>K(w#*@ph0iNz+B-=Xm-N*3RWbS}BKVPO%|##0^9~ zB~92_0-`nF5!u3t6zEiQ#2rXA)TuCJwpa>H-04UlLw2V<*-pl9tS|k2e!ZLbS5Cem zv&3>rwGC;ua>_v7CXz*Dn6nV%A4EnF+1BX?SxrRmu-)E?9;q$Q68%IDAk7X=21x89 zx>gX`(a8qMBytv!ot!+7eTY;M8Sdm8V!!cw6Oo;rF^0^D7LevPLrOqSC324;Wri&G zzQB{^v{;R|j=hW1YKZ?H*)GmHOU#OEGuhe288F1&@93G;u1@4AQ_s=e=_(u-H2-l5 zLG-hd9SkW3(O+-SzrMPwGYO=Ea;SHV@8(p2)O(~J@amLzGw0k5Qq}L<)AkN3^s~9BDBNZSgd87`c(j)UgZuZC$kog{20rHARdO%ir zWB}wRj|@B7mO_2PIm`vw!y{urrg&r$$dw+c0=dN_^&pRWWIo7C9$5y`?U4?UK9BT* zs2z$Bp;;JBgG(}dZYrxUCsHa13B0u^FYq=$P$oSJhB4h4<6|O zdCemOAl)7rcC0N$?4RsC7bMpsV?a*z$RvC#OjJ znk}9q%^O5^cSepk-i4m8zE5ONXQ0@UHqxvovbQtx6r1XOB3tc2@6zX9>tRlxh-@L!3#cYCE&Ukgw^UlE~ps8OSh@Bb*A5-H7PL`3@<>okMZ5V@Yn(asW(*+lLnGR|3M z2z?2h$RCIt>!hD%YvbjI?B0yyoh*>0q|v)I3Y}a-W{H(V9-&kxI2F*WA)>$JbCNR` z6JIfNtfkduzdJ@RtM6P$7v;0)!h}1ZFhRhNdlAU5AH#lX6%of)ZIfKY_X9+ZS zg3NIGpwYj=P)?egosnm2%d^D;q`8d9tqw)$Z@J6Qsc-Yh#n;%~@*WvdKt%sOqfbT< zG0(d>)sX9n=)EEh9=RQwSss}J&7B_6qn7@b>1-zMw^!SzIgLz)v{R~;`9f{d>l_PWd^y?DlNZd7kUkB@q4VNOwCemiWJx;7;3LRv0o% z{0Q=ZlX0%?8|iClM+0ddbf#Gn-GN9Wk@-#|NIsE!3~5HH(}?JuY4e@>^K5=DAfm^^ z1x`n~wR{JW$K$jX=MoKR~ zBCGj3k)NaVG{R{D=>>V*5tmp^=v%9K!pQ|0PUJbV{G^j!X*Gut(WQ9G$xR?HlV-8g z1fs8%D~K$0Ixp3!W{XoPl`dC{Gwd>J=R6{xkmflj-w-;Ri1ZTqqf-jaokV^j^1Raw zjV43)q_%gKT(0fR68D4r$;qp-*xwg649g9Uz}c$ zH$mQV20;2jRyxkr*79!O*;H>knII<;IgGr&<4gm&fXDhLtv!q7naE{pKd&&!G-giW; zCHoTjfXIhVA;>XAdWdv8bqVBKA|E@;K+Yh|mLo{#5@aQjn~7|28bDTq&^D8st>q0w?j%jvZ82n)aDJe3 zhe*V22icxT6Okcq$t~8`K|~%VvW2?{ohFQ6g)JY~^McVn^)NG`HE>9cfAQ1=4&)WE*!B$m>MD zHKf20f9&1HZ3f|AcHYJv0O9vfw{^$W>m2&NwslJ_@qA^t=I?F3<8 z8E)=v+VXP2zP5L#8RGle-fghN^R=Tp7vwwm+R<%-9ez)0M>l%AwamVDbSHtZuS~bu z5Z_m(+iFQP+;8h}xZ4im5Ycrw++7Jf>}$9?>W*Yz!`*ri_O-LyX^8J@XSdIiXcm0! z;`W2=4qv;tqQP2bU%R;FAY86p++`r_YgacqOQ-UE?doP(;*~4g%>g+OzOvnshWPE3 z?al>ZU)gRi2>bfAJL=A4U%z&XEQyXqx%O~NK#JgN54Y41-`5^)I|%#Q!_Ay+%g(-X z+;T(wa^<)+mPF5luMzG{kaGAM;Woey*Wn1aAB264a0?rgeeLDWHN^L|m%GT4Xcc_z z?Y4ki17CZ)t+2zs_I7iAXDzd@z1?aMF4sP8yCJ@>ecWzKqBG!Yq`L;>VfY&9_Q4MO z8tInIN%l3;T?oRy_H+9U@qO**rr)Jg(OdlRb%2`z(gI%xxLJny?R9`V6NJlkfZGMa zzVh6hxyG{HLr&*To?Bo^^iS}0kUI{f8@>*5iwyC79ptuvu&;w$=Wbh4_I0pZVu)X^ zgWXC?ymF0pr-Ags*J!r}cDN2lyK6w$*JwBYo@8H#xHAp$eI4R9SrYvT<;r)PL3a4j zjvM*zBG_SH`EJI&)-wCbcPl`+T!*POS9rksE zJ8oXGuOr-fAndEaU1NyvtH2e%*QxxzaikjsIRd_pbkhy-+v`Yo8VHx`NOuJY`#Q?a zxX)Pj`o>Z2NK3r-8taY%IRU=Lx&?;#zQ(%EAna?bI{?DIj&{f0pX}>sx6~5P*EqKv zM2{qToFC^_8shsJ=XQdyuW@c}ldV+o`Lo{-kl3_9?EsR+XOq@ zUdOx92driGb-X(Xgnbpd&4&2C3f)#qylcvdZac{3@O7fQ5_Z_viSDQelYO1&)`PIG zliW^2d|xNIeU^B>#=HF>`s%6caJ(!2U@fz+@oqT?mutMc41|46aHI2eD&N-xH_H-p z@2UHIv6};;ziy#@6}ux1@!PA|oeRRgirroi_H~LoYC*EEQ`{m;qPL*EPIF5@?nfP- z=9U`b`#Q~S2Vq~QxtR~yva_!ex7-lFTqSOeCDA9~Yoa?7WC?stbQ@rY>u{pm55m4C zx`huX`#Qs&Yl!db40n+w(O2MWvfBdk4tz~^TVaQNO?Gpdt!4H#*{ufQa!qmD4e@E_EqYZJd*6I)LjU|zRq&{4e@=Q<)%NXQ_YC3Lb=XyGeAN= z*?xGAn`MaKUgx+oLAYG!xLqLZtIW-L%vkous4};}lIS*MS@*-K?l_R0;cKc}WQgx; zs@np>zNWg)LR(Vyb)H*dh%J}?2J(4sxh0;ja<>wxxW>xe77(^v?)HJO<@4R_$CFEO zzMF4JbWfDxLU#j@XTRUl*F z>teSWcDP&@yFDQ6>tc7*lgYj=aqA56eO=4UFt4`9rkspJ8Y4) z%)Tyl%R#sfFLRd|;`_SHU13S|0{E(OJ3#bmp&pT{+%DK*Usdjyr;>eDxpP6-*A;G$ zA-=CG+yP6Xm&4apZe+2xGb4Hp%5|0N7~;3rRc;jsm+LCG9fW;NbIAz(Egw=#c7CM$ zN}t?DWM5i!yvEaL65fl}X+6^Rq}BX3iMXVxO(NS6sq@HOSiU)l=v22R5uNIGk32xB zPNSy-vy;fqq-jhddk~r9k!RuSo+P46G0!7y(A=LywB`YK*web6?amp!E;`@MGo)6$ zMXB_2hXrn#)rilD>_@2&f*m>H`0jUOA;*J8D zLqyluQg@so{(hvTZjm9gz5Pf_-MJvVA8D!E1j74~mb%R#3(2zHkMxY&3i7T;Izcvg zqz`280ZtWd(WRIjJ;NheAUAnr6vz^f6oIVrNIA&R4Q#mvWFL>r1v%X#i$HGlNIS@4 zk9324;*ow3=V$hn{+unvXpiK8lzXHA3;SvUnd*@ikb6C{ z5~STDYe48rF=#LGN1Nw^J(2-Z=8=&g_jqI+$lD$%1^LAz(?Ir5V_yv*Q$5lQ(&&*^ zkXJm?3DWP8K9IdaY&rV8EyYBSWP#k_kx?M69w`D@?~!tl-NS6T2Bgprr+VG3G{pb?o!8xkhO~({Jx!NQWk$#+ z$AZ2?Ii5;9NvR=OF>ZP`Z< z(KYtAJIRpcUY_4^D?kn2W}t8ZAA2Uce~ukYdX)%#XUrHe!AUEkcA$}1$n_E`5;Xw=_8`Ao~zsnkoXoh)hBLs0?~QyaqB^bk!AzU$NuRygX9odNICr6 zZAlwNVSrH*SU@ z_I+yYYpt7|Kt7{XYu%9vq>sqAZiyw)S!Bn&o#&QWe{g4lWD^m&no#r6U)zwwi0B;lyN%EsMPy6T^t($yPVq?Y>$;?M(J~^g z(G-DHd88ENW+Gc#JC@LINruGshO|L5kBC0s``r~FJD){wtC;)@7*ZR3($iGDp}kL! zzUq-CLu#X49$92H=C=uUHoi(%=v1}zi(J z$PAEIi0E165IF|q9gkF6Vn)ByjhzNdyfz&wJCRDSbe&I{p>hq#CuB#r@=zIl$L8lt zkK|ZFU*)Fjmq}FyP5MxqpO|a}$?-@V$dN>@qEs=t0_1d$bQn@gUt+c1V{+7AZ5>|c zk$E6@61mRWF=Tp_&w{vI2f}?mE;Bo{o$1jgN~K#cE(;B*jV>l~qanqXh*yc+YRE#6 zRYY{_I5P7+o1ZOXA<;<0mE%CRC2}tjDN7Q_d?HGgfn<|r5s_cXvcFk7qli3X$b3tp z#}ip*NINv=5_#E>nogVQS|YC#*;;1)-IPoGj>x-2wvjVI{s6L_jJ|JEJxxSkt+tni zhWI;7c92DeEcbrraR*tNKt3YNJIXfLd6n$waWzvGd|)lVM?~KN?IhblJ|nW4QVo~$ zyKD}BA);$_XIcHBA<-S;wm)addXT+{=w6p48x5(Ajv}Iac$RDdIng6sAX7a%Js?#? zz9wH;vKQnwBHtM@?jPFwa`Sa=U5YH(nn3iIPj-=cA6dC2HBR#Aw)*YI*@&c{D#OOQhaJt9Zp1-;!v3fGLDG8 zQs>J`kdr;q1u~h)(PSrIrmwbk^gajOqYjtZhRhJ>lSY#xVjkV?|%7W|E@ z0htbRq?`wGHxb=}N6D2SPk5yMpVs?J9vQaAlDCNH9d1X-GLVm<86)R`2x;qQj4b#} zr)nFz4H4~YtQ==ZT{N4B?sa2j2{Z?Lnk68GM2e`SV`YmaX_Gz88f!;fNaO>0uXe2L zPavmKs$*pIbM3v(XwD>ZoE(-wrV=?pjxnSy?Mhf4FAJ?E%JZM`vI&G|JL6?D2+wxL z%T7zg&16Tn@_5;kK=iuJcscG1n;)&IB<~aCIzy%lU9KyM6w8`kt(h+F202YSUuxpd zeom@FHSid%WIjPwO9I|)Re zUz24<0@zVXL#B&9BD!_TL(tJVW zTG;_|G7-H-eVyz{AU}}idbtin&*IYdp?%9T@`LrIuQD!?8{|rmbI6Wvujz6P$W@}oTWK${jBFpn-_Rrdv zC3+NHAg38JTZ|^n{-k+GE(AHA$e~0Yk==&03BEUfTvq&I^6ZVhkIO1U5;e_+)QPjm zj&AqIWx9yy+DJ&1F^J?Cl4xh-AWi-tP17KfnHKbwZHT|}|F|4!NL{poa;WbRAD5*e zohs3LyCI2rUO7lJDjf7xU`V2!ibzmXWl8k6b9XTg(lKFG&J^jYwvTmc>l$c|fC%_7-miPt($$(0GjJgJdI zLv$(Xq6br|GRom%S!ak}(#3M!7FshqT1c7;jAq!DmYnX9Jdn#gQfx?_xQU4FnTusB z$lXLL$2EFVNFEfK%kn(qF{ z`^$1>K=X=hfMx_VugLiUO`BW@&C#TJitMz>)_~?!*$&Nlq|v`P{Ho0SmC2!)4zgU9 zfZR{yS+et*EVsn#3GK2Hn&(OL0%_W18^{Wf*QK*na@pUI!z>XWlI9gk^@c18X#Olq zp!tq8Z;|HDa$bVwLn13=t08Ss{%YA@WSb@4x7Yq6*MRW%zTT2zYg^KNhK0=cqu!Qf zmY8yVL6+Z^y@vQ-mHew*XNdn*$-hc5OxsyKl)oz3A)}Uf--qmwnTE^~`Q+;-vfLq; zfSf=ieP7yjFW12iztQtIIbcX_^i_Ilza_mF@HZLVM*H%=McFBH4e@(sr_8fNl#u1^ z$#SP015yF59Hi{rb{+K^9R`Jl8X#kmG&Z$UsI|N zWprDU=d|HZ>QZWw4iehNjz}NM4oil{iRf|cL)it*2}JbO;X~O2a+ycgAk}R|_BOuy z46(JLM!tMQ9#ozOQ7k5rdw78 zG^=DaG~B~i$(97oD6;&qY&T?e+6nXyR35eRC$cX=qs#uO?1!B`@^vJo`jkE_l3cET z%3+p>A=}!Ou7Ap$fM$&x3C(t-IgaeCkz)dy&txGqd8ENN` zzLmuR%{n;=8ePxyy&JJkRt?frLsLU`wxTv&CmREr@8mpa=8)!k^7WluW{6)u-^;9> zlk4YunFGT0^S$h_#H*k0WfwGDKi|t95U!u^Zm%EYOiR4>`aw2;aDIM}9Z1Fb`9XFW;^*fFIbcYwc${iXw^zT+ z+*SLs8cVV*F)gTjc)uKDh+nHe%4QHQ#gB3k2$$kVIVL-~6hF#BOT1G2D2qY36hF#& zNX4c2QO-BSFU60t&5&C0JWBDC?13Gw=bz-bU)!4J9R4JWKsbj#$==<8f-u9um6Cg*Uy z%(leK;d+@1!Z}u^A3gK!Q9?YzE={Y>?@D+f_0?O6{36#FVlC_X5vtFSRcSq>xr;PHjoUZ5q(T*ea7l$)X35YaGiaTJ zuGB)cLBlmK)Cv$TsZcACic2a~ry+hxh3ZGD`^fSGl%F(JG%`6qX{rQ-^OL4}_Oo>u z-=6LiOsZZ>yqZr_>p(a^X{sNoI6rAh?62+k`AJh*hOA2C{Djmr5I%Q8ss@Df6H;w? z$@vMX6_$AU38@Yc&QC~nA{FN+q`D39^Al3yKtG4N1;Z+8iMW!={uGrxtcncr^Bhsl zAe`rjS_Hy*j;Mx%lJgucg2@%wXBRf2HYqiPxmmp!U_ zEb;ntRP{o00!EUkS_i^qkE(v8;<87T80EK)o^?l6mLYzAhN@{GoS&hp288o7RP|co zw;C9MCvw5j1*cr7b(Edyr-gG+&XOzmhLU^&8^fCAq5T5K{{8F3D9TAY4DL zYCF`Hl<$&UwZamwuez!OgzLvu8TrZe+*hf)=Ra1Ny^Mk;PWsU{iX=TNF@q}q;h_$J*&D%A_Z`B7>e z2okKsrewBAEnkH73W8(enb5HD3wv5Ez@scp~lkHC_}8~ zeQM=&Rb)x@0n+H__*{!tCvy*Bu#ILcPRI4T8aLS=B*G{U-kmb?bd+2?9L&QQdkSjeh&5*We)6HD28bcEEGZU$N?~Ne7_ZAS}dl!iBU5v4L z_Pu8tlIT6xkVNl!gGjz5qJ-*a4Sn5WCsi2G3|Ga_TmsE-RTj|ftSX?HL7E+?m3LNk z0Zo>whvrGrf{@~*Fp`>6VWW?$6^%|p=as}_O03Nliy2&CFibwJYt&3>vcpxIybLlfJ{ zdf#8=9Fttn2dI&jh~cEs`-4kh6#^G^7Tk?05EEyo1$Dkc)}vcjyjQjh1*L+bA_}5Sed@xR@+YA@8G9 zOF%PPwL()(nhQuXT6Gw*Dy@--zE+M_qsHm_S>??xMymo4zE+M_dB+*e(6=a+zE+M_ z`IdOs%F${J2wy8lt8qxh*UHhV#1K15=xf|)H7$YY{&R?`u|!0NheQ?S`4H6*&>X7f zvgT#`+aHIjMT0ag(CkEZ_M*K9hpN7SCSUbKvnOe8BwzW;Io_UM2NKaOc$mty#A}Pg zR30?HAM%7fpgCL>K~qecdr5P+Dhp_iP!-Ue4Le7u#(<_k&4cDz(&%R#1*#{Y z`Hkv@rjaxclCR&W;=<%Uf25jZ34H~ec1MurNHr~>IZD+)^8{&HNOP2G3uwlu70@h) zW{gTdA=%ehm0^kKB+Z}6&RCTf&>XGuq4|n5x^<3LRUiXIIw;jKsv#lOr$ol7W=o>{ zYu(4GMTXQyiyxym#%NqUPA!8bwsXk*YW(r44Vs;a=wFRLUahnmuT2Y8rzIkfGIC80X_4v% z;WMsCJ@y>M>1jF#X}Y1g-_r~jvMTLKB3o16I9ZjRnp~?Vt8x&o z)st23L|dx^lxioFD$f$HR!>&>AY7{_s~V)@T0L3KG{mpflU1`Jwc?mvY~LtW?Fky) zH;UCtOX#H44EFS zCZgM-M6I+MZ`NI+x(!)1luJ>f`k>+ZDN!>g>HMrtn+fkFs?iYtE#ne3-w?m-C92hu z=#0G60dd6R$U3Co94Y|t96F7iROuILGF3H# zaO+G}ndjJg=4;ASm2HXFI#X3H2)E8uRfSaCI#X4(A%5#jRr3s~6^Bp`b-B({%Mvt} zv{@ofSwiaoRL|$B&VZ&|bwe|b>@<_6T&)Xe&Q}A_Od`#bq&Z&=D>LOvt0VFZkqgu` z5U!02R1FB%#s#Y2Tw5FOP^wmwYMdorZCs#=K)5z8P>o2%wQ+%(XNX@L7pP^1_|Kaz zR9y)gT^kpw9!tdV?2xFSdyos&z#vUzs_h&5dzws3yuNXv%7*4B(k!P^T&VIAG;a~9 zP}P=1^&N*kyDw4oNL5U!J|xX0stuY_BD&94s&xr=be=2K08&+uM$btqRo;0fhhBav zRlX&n#?w?;66IfCtyI+@jh?T%1Uvc{bt_e40@2{9RsGsdhu`nwTGy(EfTmi_h2~gTu2wAp&2_33 znu|%Jd%|@pazS!EU#A>PM7^ge8l)*1L`nyd@5SeC)c$9qU`ngWk57IP3(@L5+ z)!21vNkH>kwG5i~NTYub@waMaKvSbSp;-e>jp_|(YSlVu^vb_JyK7bC!sPm?RgNX% z7qYW8<)>C<2Q)XRTxc?Pw|(^nm2Zgu^klkf1mUr6x|#>VW8HKWtC)%54$sM zwki*38dW7U+d|W*ngW{Nsb*+KK=V7*7tqX6{m>jq8ojeNi7R#9#CzA z$O@#&rBu3}A5bH!l3VaWHOdlkBx&@D{e!A7p!tI;hURS2bWjfepvnW9`Kl6{>qw(( zW4@{%q-liaE>F{Jh`;7AUx_PB3wrk;^HmgtTY0|fx5V2oN~x~2sZPL?Yf1&etvp{9 zTH@VT&R4~T_^mu&RT)CRr+^$TP>rzjJdqEm>OsVv< zKe7W2=Wv0V2f{gApqh}1`{4q$$Phn=3)IR4qU-q~)oF=1V$YDcjavC3hN(TuGTrU`B_Wk36*Y0n~;B`JEU#s+fS+>L8D8$NR6{Zyh(O` zq*RO4q=4orRR+zcq|tM~r&LuyvshI_Ge8=B?Ov?v4e|ToV$}n}t-M(Ef^aJ@Rs+|V zcK7<>Vimd8_5^O_#mWKUR$i=%Eb;o`VpU>@-^z>CG(&2|RwL}$^t76rpwYGQv}&?M zWRXT3KyN^(c96q}NFqyAmm#atCKAacvQ*_&+Z=Mama2RZF4t1kew{7XLzHSP)z4D3 z(h_eJU8*`kxLiwB7gBNWU8;Hv@yoSTMXuMD{eJk2ax4*jJDgLRBNEgB~2b_UR3=7O{)^WHMJ@ZC(Yrc zX;t}_c<10tY78_dk!CDuUQ)#%=MXuW$jhoSkm?mR4VtS+qwm&VQ8N=X`kQiXs>zaQ z5v_sf`^x2N5y%~sNbh8=DS9!(vs+_q|wht|DvivdXS&DR2|4K(7dG@ED<~GZO^!sY97d*9+?kv zkVh7Q9OaQ^mPAh^qDTIfstu%+h<+lrQmr&(xhOh^VpNB3tGrrWidu0QX*799b%R__ z}xrxa8s>6_4@jb`~D*Z-V zis(MH7Dt*cm2JrCwB3m4{_~;Av1BN}cl@Cm3BvCkf2c+oG9$XLm#V;!RcVJ4(O-)A zP!(G;w2p}GhyPGzhWL5@hpIQk_x`bJu*CEJv6>6Q-al4UT_TKe>~~ZOCd<>_Bwu<3 zuLqjtX&prFCC#TE=_c|Jku|ExSoX)m&s4J|;&a&fOsxoLK35&k{7f2MW1p+s8M;<| zJD;mOOT@POhQ#CK>vL5S(0rjvp*e{(dbiIPs%ns?8k$L@(bvi^RJ|eo{plB~2ZXOF zU#MOXzNUPkN^a6Q^zTo}yCvQ=2Nk6?OE z*M#)Vu)8P_-}OXY&an8C;Ss@pmP5z-TBD=qaVEcEk!!*T(hc#_y@zxhetSeF7E56_ z6~CrX`=M2mB!BdMz(9T`ApbTWv}*$CZ2rSYA3=WUZto0jcW`)CpkA#Xx=RAL|KWzy z`?5Gb0`UV8KO7?XevWgYC$1KCg}q+?08zTq+)<=}8n#QA(=-9g}d-UY+Mah${8aUM6u z1ay49a>(@&JWo^cO_le@lWjSVgI^uj*O{f}IzwO6H*pR_!uGSUxDD4k*71DD@~}AH z*o%m(OgJQkJ>Tw)@X!62!%g+)O_i?^c7yfZ1g^(_-Of)_T+b(72LCHUPX7_&b{wHQ z8xyCm9-1&Lwmm_g4-upvB7PaK>v0RQkKtQl-p$uz`UOG5>C1Q~4143bjni)+8$S5D z{0y!G$KyEH6NfxcIhoU&>m#k%a2(@4&nLLwaXfk4rSI#R`F%+6^|aNL%fIfI8oP9d zf&TIZu4~M>pO!;ciF`~$$nD4Zz9zu=_^sfq0*JIk}Vd^C;xL&v(gV#&T=^h*Q z7ML(3&K;}Ui+4L|fM}Dl%?>C!D$H!kv zO3&@+&nJ?P`}X;MsRQ-13U+!o8V?J5{b2i>9|p_I`R4F{dOW#YoS#jXJ5_yugYw(s zVcf?=yz_W->nB*RT)&6H{%=g^w}XxQ{gmrv6z(_sq5s*05#gR_uhZKiWW7DF>5Hv~ z)ALw!Uh8u}|M~N89yeLfp`EAFx7%?4&mrqMWWOWfhu0fyoW9hFe2qoO^I9I4CLkT> zo4GwNxgF_AfI0u|^@-lN7VP&N%3p~76XUvkCGf}jC!CMtS0S|i=?PhYZ#*6n z7olG4dV#KITdy2)c~XU357qF;;~tMsHtw$r+-P*Pm*2EI?Vv#XHWP+DZjaMSAm)ej zV4p*tH*(1N7Lm3ERHAGh1gaL0gd7sUBKn(d_0?FK#1=Z76*$AyEyk1!#9+t-9)Q376u z@JbxN3GqgR3lKhsknLGJ^u7rA8-bAdYH$vja~LYJ^~52!AJ-?3stT$*SDcq59jqDj*o@CB7|%w zcwX|j%;UnQuEXaVd-OXru$vqYiz%3gZpw~6uCHsi!_LF7`=klO;^_bn#{U>N&gr<` z+1{%ubQ|Y%zmh^9^dF4>*YksQLvF7XoKM^z*?3sI8{l7yPV|%bSJtT+jCWqWe$wYeIVV3%?_cp6hk6?m%;#+dCo-3+RLOYU5#H z(+$?MU(O$wW8>`|7R--Exl*OKoXe3WSZ~u0&VR68PJw;yUmQONbKzcK6%q z60Rq-n~nSJUyb8@pUU$YYd?hfK0R+DU3$7-k6dr5LLR^7z&?lfn>fwOOc)k={;uuv z{Qc^a^tcrgc3hy{!R9(lzpY`yuy`KVWxhXM1pDmoIm9{q3)ZnYwCi*s(FPr_+wt{{ zL*{lJn4UgD&-KG0>o}h)pyPQv-&foA44eM0%gg7%#*XXt6|Pr4uG>Se+x(5}Z!A6c z2c1qI|0pRRe4f#-Dj7~+1T^)WCeBF;>3JjK`va>1}SbZ*oXq*+9OS+xtIXzc=hA_uDja5d0o#!muc$P}k4N z(A$2PCeB2h^ONfSlG~#^Nl(ADg!>WBABR;&NAFyjFf3-7Fe2ueaELh9jDK5*g(gn- zIjBz#UjXOOa(d?_Nl(vx5dRxO?vLI7GS27M;PrI&|DCCK-#*7z8y*&)!T#`Z_Po8x z%wKq&fZp>k^G|x-2p#9s+MzEqg8zt+@B8h24PE(9w*CHWtW(*3$oDy6b6lS)qfy9&*UK!||M;oYQ06tuk?Xn;x9=`CuUabRcf+gmGPs zh&CMmD?+~Beu%ie@1b>BKAz&artPP+i_`Q=dLse)}7aNAeYtZTE?Rwt)UP4@xIdnY?(Q|yhpYJip{dI3{Z$8eUT`%YBLD*X-5Ax5A zy@;^)7xa4vIKS-mHzYdE^*Ah6q*<=TQXD7l(NB1=cm11@9Wi#q~-gPhX`xgEdg4Fq9x#2kZvZ+2ejXK5pmRVG%se`I4vVaVRWyFd>gqeEx@pJs$Cn|36)STn>9a z4z3?Q9xTV^mM>T@Y%f*4?QQCbo?8dn!}_Cd;b6Yb_mLbPiu$!-NF2Y>cvzhMFL^{v z!|_Ih_a}w4yB6^k2vg;U^YuP-!I0+xn+o}UmG8gp^W?DT;(X!0j=8=5(R+8`+@EaR ze{RqDWB=d7o(*ZNPBFg>pQigW{fY~oXK!obgRh6|cO976U=JK&Hxhs=W^m!IcxiSuzkwEKDJjU4#naXmTY=c}A9Bo2f=ReIZhLgJ`@ zX_xK!;s5RBO0E|_-$5QM|4D)NOxFLq_3PJvvK{&^8}{{VygeyhQhyj6-`su^^qVTq z`#OWigY{#_efs(m>WSm!h;ujs`M=O`fBn!tcZi59p${Iv(eQ{k&Wtxh@SI``(Ez>W z^gEK~_~4M&Cr=3+e-!!PbkD%va{>OJ#$SP54mlruz68V9p||^;=zh_}{p$i>XL+6- zjN5UH_D`DfrXH8|bw$tr$}mpzK3T4ZRQG|OB7dAuZtu;#KTLH$X0LN}eK+#}zr6hX z>r43MkU57u-t)N5<2Z8;hnCp;M-I9E?7W5NarCuf%rk9BZ$~0s@P08l9`cIA9= z$bOUCk^S>La&UZa?31+VdH+9sPbY~7FG4Dz;Qm`IiL1DfmHbk zd-oH;^6+(+*YCOh`8lt>f1&T0qF&kGZx9dKOOE^N9lWoV(+Bf=oUs=YXTh%BPw3wV zS-Z3@XYL!goMCZ^(et{=5K&9<^z<#n-LMmE-%Z7Ne!%yqT+e(TxAAyb@V@XT;g_Eu z^1i?1d=Ks~!Q;vCu(0bxgUcUWHwvb+^QeedA9g=M2=&PQG6MTUy!EHe)A{whaXp=% z*uSu`$Kx`` zgXwwQx;QMZVPR;hVHRb2^=CHUI>4JQT;SujRr{{1} z<=E75zFu+|)U%&dVQ~Cqo#u(-FLU<8VJd&z|2X7vn&WM#uVBdYi&Z$z?*nY=y#05N zuZ4Zy=gHUe;JyUD-t&8Fe4US+VbAAn10j#cdw?g`Z<=7v`&0IX?g)fjeh#_bZAjw= z`eljH`|+tr&-E45+kFMTp3AeTdSm~*?v%>@rO3~$5_`UKyc+qk&r#_8PpnJ*h;jt^ z4Mraksg9@OsnRvTzkR+N5<0H0vk!pV>yPDn9hmu3I3CR3zw4j%FBp6Dn>;C=+wk?| zW%vz_NBms+E!g4p#7>IqeHWh~oj$Jrxm~TEevJmtv%iOKg@e$1kWdj;JuzLJiWEU3J3$S5m$Il8W>5etTW>uj><0ADb%2|MWPY zKiuy*Zr3At{|bFC3G0f_J2boRb8*=~CJ z5bwDl>+F1rpZ^bD&)?kq+vj70%N2aSXZ^At`m!0GyKsIs{+x~V^vl(FFJ^N?_Gim6 z__-SMRL|r1I6ogu^*nE?>BobggQj}E%KasI9^zjoufTa>`)x?r?-GQ~^;Pd1Dll9?IG_MyAAcg>sPZ8x9ve+ki&Zi{9K?3$5YjVeSd@M!=$4xteNwk z`&UT(5$U<#u+H9Rht2aIJs*f*zvtUV@3#+g?$5UU!s0{N{Tv}bN8|CFpS$sTAcuC` z_Mcnw`zy>jveyoFJp^SvHvZok51O}m5|6VAwBmq`xzsc?*{D+UZ>UX8S3>Rt{1L1=E3uU z`ycls?iU>2-Q*(;?`6`uH}VzK?Hkb9`Epn==ld6ae}?B}96ucCInMo^=fH8U5B6UHekDRq$2|BQ0PfG+AKBjjG_?Jc=d-+zHW&|! zI^>W0wY~pj9uc?WIM4I={_}sDum8WNNRnHCZ9jt5BT}UBdC84 z*)E64*IOQ!gYydiximi?;p?RS?vy^yc^!e{Y(E$V{RQK}^8UN=;Jng)&*(p&S8_gV zeh1f2aygRcfx3QdJ*3`GwyAj`r+>n)`tyUhn7oZGPS|3U-3m0q&=p3WMo_aju_K&)d0u zgYBIhr{4}R&+#|CKbxQP1owUG=X-jj+tmK_V0n*2z1nhxz55tmALaJrkl#l-74^!` zFM@Vc#d&?i_7D1Q4%VegkdGke`sI2*4}OE|FTpsE^H)M|!;rWZ@f!HM3Gv{%7+-&b zoY&)0aV|Ia|6s`HOOXHTxWAuHr`P*FW+UI5+rPN|cs-Ec=iqv>aetqL{eFNS=l3r- zpZtCX^Wc2}pT~SW81iwxKBoF!0qgj?N%p$M?>k4ZZ#d`p1@7-;8?cx&1dc*X!Zyk#3hW?RY%Gg#JEx>n~04`wu*C;{N1f9h2j!LQWTK z7hBE<_B;9YYp+9Gzy5c#xc!6aZ2N41{bgHXpK{b&f9Ce!df<@v8S#A4+8KO(Nv=o# zd`!0Ew;S(^v-``3c>`T0IG z80z~Hf4=M2=f&7B&G(7CuZ`P<*9C(0vU%~4I0WZ8+bamfPfUt)zUUWcvChwalJ9%{ z`O6td&-0lJ1M!Q^co7z<_?1ceG-37hEqd@9Oc)ZsH(}WO-fM9EgzG!lFZq5i*iM}O zLD;wRtT5iMi+JzX@%hX3z;V8=C&zgo6@R~vpZ{2{$L;?OJ9^#KK9?IJ9!5QFJRklz zYxP^C)Z2cyY5mfT`U{rVkK6kL+82lGMlhf3cT?rrl+N2fYx2kS%=_5*y6^XI+kRoM zpL4$Kyq@>r`sH9=ZRYJ<|Ne11ZgM()uVV0Vo?mcz?fVy-tMlhOoL~04al5{r&rjC# z`yOn^9uJ%MTXp}^?~Cf^G`hTcA0@vp`mgjt`1Q?tJC5-Dg3HMv{kD*)Pxi~>3jNv$ z`q@O&Z~W)nww!)C+m3#Fa5=er!SlWic7vSH%ODTNc|74T)p3twpDM4v^S)iaKH7DE zdXE(Qr8my$Jvz=e?4?S_>kw>bhp^zsHiy{dFuZ2hZP=bzu?YeBZGt9ot=t za&o@y{D8ibhwBmV2j=e|>A1c>>Zf#iek)3o*BO3=xD9Dv3fA9we#;@xLwMiW4oJt` zu9xw1ny|PL`C-oa=kG5w=XT08_99+7yI&+^_66wm$`SCF$n|`|`l0K%IUW{QnDydd@+|>8&R*2l?ArdVL)#gujV_Fjal=d-EJ}I@`}fVhZe(Bjo2M z+>RR$=~o<~=YBHB#OYV;0(KrmobB*FKO6GOe?DBUQc^<^~!>QH{ z_;@hne!=~dzrSd8^!yI_O-{FI&hr-k;i zY)Ib$55#*ZuJ;8pUuTZ{`z1I(sd)1Fo+k8pq5t{$PxALf`TN0xL-xaAs_%pHal7x` z-yhHOX|7KWd4D_~=lwivhvOSi?$B9wT-g%gHVC<1*{%+CIr(=8_&EFF^c?a$Tepi& z$LBZa!-gTT3;EOI1RuBGQyDydus{C(3BTump4pEU68j?G96xR|2;Z zgRe&%UxxB0$HQX9Cb+dv;{kNMuR55I9@HQ2_YUr}4aV0&{{!su{$bv?$|0BUC+PTm zV;()*o}WQJJizVxqJMwOdcKa?{Q~}a5bOE-Gpy%w+i}*{^ZCm2N!HtS?U2X` z5)vZ=ob^1Ou^s!q3-8zR{j&bBfF1i@f&ZME?HprtAu&FnXFF%4;OC~`JYKQ=OJING zfywQ{^=zNZ`T1wg>%VO0j#2tN42gmL?D@$2D%j`sD&}?^p!W&_`J4^?eF5(I#rvjg z$M(aJcqE{2ft?pO%Kh>L=bLPo{qcN>xgF=|eV2gUx8ScMz=P)v+u{D#13iD&=ySyR zdt#Q;x>pJv_xqrZ`O#x^dxXV!6Grg+1VjF7_MXB0kq!OlhRN};u=$E0Uuno!$jjH} zmOqtlOH*E2pElzxHCI=pM#L&!I0l?43@)k`Ysi|pL{;- za(pQAeIe{~ScvOB$FIck&&~Y|e|Iu0t_Bb8OWu@@zl--W?=%8>*^Z7YvZLcWo6wdg7}x&TKEI#L=Ml&G{NwvGp5O3s+h5Z}u-*1X zIZ|=XKZl1v&*u%_|8U6d#^D&~IsLyEhQvvM^n4zjnZ!fl7QBzUx%N5z^+?C}_fuh) zVS^>&5Ph~n2>(K8DT5zzmND9$hQss{WQVz&ekXWJ_g=*uXTggK}D)dcLx`y3O^QO1BpE%GbT0k-yNndVKTyV~|fU?GO>^Mwfd0=6c%; zy1XPkeFqltV@((mB_<4ub4?f#m*aR{Qb_9rh~I~B5&XY6l%U9R5?(Nq2Zh3LQab&xae)%W-JUVrKFyS|^L z|JLKo^|A4QUN1k5JAdz8pL#w&o%Y4rt>Xv9p1bPjxcWL%r|Wiq%5pN+m4AExKLGuH z(#}}D`Z`UQ*UuMqKK=i+=)axoEKBM6o)>#BL$6o;w{EAdSHF*>|DQ&kuBUpw`tSeO zRA0C0@!x;S?_BTVd#$ncrk}U!|36_*Y(407J=N{jQ~lnMPXFoD`CW0m56b64d$+6J z{_1jP9HHw=E0e!54)_oMkD)H7r~3Dko!=X_l%6KW>YebD`t^KtKl|zS`|00$K0lpK zXT2_!#VMZm{D=AI`RVPb*SmgiPX9jBPwUrzJHLOlcRT6dd(idhsosA7X?p5UwzF=R zp6YVCK0Vd@aWJ-gx?Osz``b^a`gdk@f6(drZ#~ubhxG4wIP=B*tJv>;>#3gKf12vw zOVIT?zrXqa?Qi-%w*I|iz1;e}NL^mHOYhhEIHuEezdt9|4=#=M1O0v8D`M-*S#JD3 zN$hWZJka-(bwAY4vGn=)r|&14_Zef?r@EZpen0KEIiOuYJ}&F$AwO-u{#$Pk=lu{X_MXVz|L;ZDr`xap)>FOR z^mAdo-D+d&Ums`n@m<%Wr@9^g_wz8zyw|Mfqto?NKPS`sryft<7Mri`k9t0OJ?W{A zuZrc@{p&wXb-(=S)H(m`{r?$sJDmGVmeT7-m;3*F-{WDnPv76b?|{e76M>(c*ZwQ3)w>T>eXc`M?l9G^4#VGv;qSxv_sy>P%3~dlPmY@AIuX-T z@HrKq)9@+6=X89|z^53W1uD;4sPgg2vqGxaI#(^gr`Wnst#w@lUj$zSU(C;2_9d7u z#(c_=zXG4jQ1&uRFINrt~b{dc1NJ5`SBE_Dn(3#_~F_g(n=epTW6BR*^Kc>u8o)SUPS;U;{VRV_X;UFv!W zeh6+=ci64!^!QeFR{Wo^4%^U<=dn&U;q#)}?&?rcyF>jUzC%5LPZ~b2;_n`OK5)H+ zI$l!G<5P`K1wN(tEOq@EQKM(@X} z-u^`0ZGVc-XDSDudG>boz4vp>?+Z00affQOcObq4@vqeZiC?QZ_@u>ujrzY;t@bXI z+l_wmJ^IV{nEs#|T|cUj^&{5pk64eE^-7}4I>zO)7FaH;7@r)M+w!^NtR`2ym1DDZ#F`OnMywgJX2c#s>>L9zv`Iu@=Ny5NkoK1+jJ3$F6mVt+Q^7TZh;>#2&W3c0G*P!`93R4|jUy1zyGx2XGDq<@R}80sEFYz%deAvT7Xa{bAxTp!>D;cJ+##^0Ni&vl18+4aJN z4A;l545VeaZj8%7S_abQy57R)1J_*Fh1Oie<|1~C>nwaea2?~CgXwB~HmO|Kr*gSOH?^xx8`bxvJvM$Mh1k;S!`@ zf;L=&^h=PQs{UuLOKm;EQUR4P58vm3(v`1Fse7PYwRyHuA2Idy>y`Q%>QR%BZx<9) zY0w{cDX8;tOZ^XGMe1P0+&Q8gD3L0F#zU#!Cv+pP6q!-GW#S zVkzowNam5E9)gZVY@yl+oygRep;Mqr^%p1&SI9le@?jo`g;gJ7?JPSCorhSCnMbzD z>9W*gOwGAkse7Rw^%0ixQsi@V1r$-gTxY4JjD3Szezixb6|6;;W0_H@5n3?c#ac?$ z@V%D$ahlZUL+n?qWlAiyN6mtMjaZ&()xqjqto!fKmO@o=5B`50EYWIp5b`x47E#AQ zb!^p@u~tn>_|?mns-G(PuKK;DZs$BsN50=t3!p|yTF?q@RTDShf98b-)CGubgsK_a zOkIZ9jYCS+s5cICsed9?Z&K@(PF=;+L8PWw)tK*>P_C*%*?&XDW{H-ml3OgU-PP(& z-PxRn>?tGcb)RB6Mos@4 zGc|{;AF;2#k6s+M#!%K#^5KqpR4OF>#cjzps>9rTmTZ~3Ait&iq~FrZ>oZ$$nRUc7 z*z#!A3hQU6<$jdFIU7sf1Vyal5PJ+-X<8alCn5GUVrwkv&mB;+CB061afJD#zDiYB zu2kCh3g?w#lsI`zbU{oFrZ=UtrP7D8Ea~IYpR+98V+T|jO1y@7%*Y z4>VkwE_AH`KnCWt=4A5zDBIa+5+u@s?9p6=k}0&rp7GgDyBL|>Mu~@2b9=m zeFG&Og)L!HtIa&BS<57(Hkw)*%@!QC_Mq$xq*hz{$Te(PN1&e~R%+_YRq=?;My$fb zI*hhi(~v5?bDMP_bm&}Z)savEN_1NVkZhw~QzBQ%9xCUVkV#Etk1axpRCOkf%CgN< zT+*AQkEg1$P>b~YLTh~|jy^aF6f#xTMzKq8kDzr5j_tAq^IWn-lFw~*KJD~VHbdF|4oZDqAaZ@bcLdL2PTSWaD3RBXH z!>$R7m8xgz-6(N86j5ulU1~AfTqJP_ zZrCb@Iw9j^1g4BMLD0InQD`z@qy~y&Wst2&${s*ZwE`7YOcIl&hz_lD@ zWvdps>Y>D=Ep@BC>mG|oYG>>F5er*?K&tFLeVnfxOB&2NZE!t|RGDvsEAXsy1n%Qj zl`Y+8W#Jqzsy96%RXu?cnW%Y{s|%8?-(XVbyEY@{Ecv^TEP0Acj!m-l8_j$hU7sK| z8?}t=vo_`*i2wN#u`IK$#`a157fKW%wbks&?PhJPa{Yi*X=$TNjv!UYH)?tD_L7VM zI$XNPt}^rOV1Je+cbmESjB=ICZ8>V`Wr@3>Ml+95wn~<~k+q0=O+WDOQ_Cc@;69Y? zGh1-Xw0X#-&jF*R?1)S6jU#*8XX|sQa}@1FeQrxHuiMgFA!_NqyV^A!^L-8Zwz}TI z*{Wy?#wxBX#4dn_UGi4f8%W*elJk*lty~_T-$5+hwBE=0N>B9f(*oI+EeA=zlB2SW zW7AEl&u)6pQaeyfz;z7fAvFhFXG7(vrAJ)^$%wH>T@J~}ZPZ!{@r+~Zo|t9kn`P_c zP?oLtfNYamXy%(^Qj2Z9f8^P^?}kjiJUfE1-X65i8K25>u+QdYJ{GL&c9S87^Sv5$1krDh%}b^~gegt94iBP44h)s(PY>kvyt z>T1`YpaAt8bS%%Ie}-h_7O>?UdJs~p%(Bh5kBK_la=zV-e79S2O#BBlANeX>+o7YO zpsmMTL3^-5sSU`7-i}l`A61+BYK(GmCUvRfQMT2r=QgvRoj$n$sqJQM)SI&1rbLG+ zvDu6_Hk;TgQ=);AJt}OEVZP_1=4LDYD3>aO!gd-|No7LULJ?b9|658%uMb1HY7SCo z$e7iZbI)xDi;hN2`ec#qL#!M5ikSKawAv*j9@%FwE{Wyyx%HlxZT8E=R<0{rO^Jvt*9!+@Zq9jY%QV^gJ;-+|V(aa-9Kt3OrHeCSTpvYRzGLl#>gB|0dXTZda8H!59n z6_bC6)FLDOM^K{Aw0X#AK9AW_){$&;N2xe&I0~3i_|emG+%?-{%xsSU>yxr$X3GTK z`DpV7%s1!`#Z+YG8+0#0>^Y{s|0~7uc+mYhBxCQOd$);sSLoOS(4SFak^2Ue&Bxpp zxfkH-WDKo$BrR|>7o(d+?gEsMWpm`@lbDlFB>95wTGS^s2i>Lz*K!M3%XxNs$7W# z-S0r3A=R13m&~WnAM@SYk@_`LrOiLvjWMCpXPjJH&Z|61aqY_7R_vn^jKXDkSGY$} z%T;Kz-;6<0Rl+eY^(|^nRpX&WxL)wb=_A*GN<%F9SR6~@^fg_2++|oVa*rY_PWrf9 zwWX@B&?=d4s*-VRDoPZ&4nWyV$Qe&c3s$;6#i(>PVyoRJAax$K0J=RUv1_j~=x)rj=Z_qfmB@`w7JE!ts8M`=yw&P3wbJH)1CsU!jTRP$wgn6DQ*r z89fv;Cao`ylf6gIAIsERD0?|ZJRwuoIfgAj*@*q+KuraGwkLqHbTVOI;6ba=(sRZp8N3_c(Mj zdZ%;G!#)n@g}D21&6!BaJw`v&W@2q|I@TIjdJB3wQ?=T;4h}%9!>o+}CH-Y~++k=z zHd1HD@aCnMIQ?nbN_+RSw-eR7$S z7RWhyvzf2ulB3>^f6JBOZ&CJKlvo(|Aao&=t1kN1QZfn;TTjMPOUvTOfB0{6^_U6{c@wnOL?_o>8Guu^A~8C+M;0 zssufv-R6ov4tFoH4i@pq>KwCWG%VL@4g1t0_sn0#{yV}p%h7fuzUMpVe!GwPB<4$a z-^5;mWUt6EsW}P%j-}=#=+rhd53eQX?saI(R!gow$u-F{r|Ac)_t6}b_y@|aj?07IgVyjYIRLHTcKZ@)=X`k= zR>|Gu?-1)YZE>!T@E}@kF#F)9I9a>z@D9DKUAc$X%Mx-N8ZjC&wTx0S%FQz)Ri8(1 zBezFyBfm!<1y;D_-kfYBXQUwa$FrC!eLTxkj3r7$tHRd#ko40+lUi(Q$uTPQ=&hQ! zPfYLUg&y6Xb8+-=&OL=5JyLb<2KX^I=MK1BLxinL!7QOM_v;LpbxX{$@dG%~ubGD|ef z?Rz}(&9`4b*>j-Xaox}b)cepSl&oi2le^guDiCX6EwW!Wcy7fnH(!e5c!Oshv};(@ z4J|>ek@=)-tGU)~Gt1j(mZROYDp$$X&NY}v$3C&$Xv^)0b(>hPk#mRY5v2OPx|ig5 zbuY>G>ia|4UfF_jj_>p6BXzb{-(?yy^^FhwOe*YAy^X*?CUlsO>9(NrSL_W(U*I=*XYG;VONv_mKOmA|oYp%G$Eb;!2>v0QsotVE@|2Re^S*&9oBC;G70%K37Pt&*z*U!rUy zIrl7#{}lP!(68p>r+zR_L<^R=-N(Ds#i(V4yDWA*?QzR!Q%2Hj?1@Odg7e5itFDHE z@p8vij<$Z*EaTYu_RC1k@ZhKyp8&~{FN+VLti+bZ9|kQ)*>tu~N~9;|A||a$PYlg) zMh^?)&p=GZ5%cXz=rQ!pO8hvE)6z<}UTgF1y;_cZvP28x?}%w-{2!sNL5Zz{M$ndp z@tYv2d3F3>pcQD#>i9mRQ7FP#{0T0#l9~qnj*@jMd*cF*eU3xxgB-8P(}ygx#uxCM zJR7Oa+|u`>?0Sxso1p>qGt?(f53>{Xb!c{?z7E|KKNqR%kuQhq^hs0p1t`yy$TKA} z+~2Oj6&2QQp(#;p@)etWA*ObsmQs^iYEmmqS?4Lwo5;6X9gnv3LJ@U3^d7X{mUjc> z`mNq9Z$qL!Bleh{-N;lK8Pz1d`3lBE&?-}+m6CBpYofk8-I}P6&uxkNc(B=&7@}m| zk0$D){79lc%6D^=A{7I%&iTa*?k^MBTdgbn_KS>`goGbbPtkbBq7%g?K z7DaNkm}T11Yt{ko)}sF{Jyh=2%2m14m%{PE7pSF2U5BzVy4hsjl-Q)?{_ZGJy}Z-A z2P!l*S0w3YPt_(?V^nX{O38R_XZ&rLhvx**T~G>SB|Hqte!erl9h!#NaQv~i;#`YZ z14YXo@#A2^>v&2LP>gj6*9{aw*DEbWz4jt z*J#Md=hMB(Z!}_3vrH_TG3i%*NxogqmBT1w((6W(4m7b0h(~YU@sjgLsi~#fsK&^- zOMDX6S_NZLUxn|;Sba`!x(s(59G!uDUTYA`b`BJ4_%H}FoF9j zRFt5foE6!VF}`xn_Op!2uzi;!-wI3a3tTTHY`I5K3kBGpe+zBmeHG~?ixT8%+Fgi+ z6XH(9y$5Jff{fB6)@GKim6Eesu00v~WItbKjy}$tHIJf1t52VabM4GnKIb05lSoZZ z)Txp9T{s>*i1SP&J|~th9IwxG&QpNrP{O&-Aa|3)@p=T1Wk!No9=Xdd%aLWqgjwcl zVuf`&YL;grAztIi@h_C1k4+(GouVg(67(LB#j6B4s&xCzQN^c^AniWAN9Ea4LY~Rx z@i~)}*lbGV*?N?_*_6$*_4qVbU4d40Vc9y&+U+wMqGXBM%#)uHQ}YP7po}t#Y}u1} zM6#|%eSb%NL#BO0K7F(uqolVx&&%FHzJd5_QR0dn3RfpGLXp&{B~SbMkhZ&md!%GVJK?p*lO2(vDB!gwc7QViS1Ps)Q6Bd5u>GK zv;P`p8KoOlB^S+6>Jwb|R3(QYxdN(6J{S6ix(Ql<(Rx+#jXtG*q@Kk!@p0pEgiKzJ zRO@p|twU@)V;4bvIG0r=e+u1#*y@BUp^xQ?KH)lOJCx(o$G@uNy^_>olnC1+SPr>s zx{588Ex0OKeq~HrwJLcn-hKN4qxx0JKc9i?JdB1L%o0UxdHc?Tbr7-tjasHaRmtz8 z=Ic>&Rq~->yp4etw6erTj2&B(FUQ_3$B3$A_fz<`Ir6oc`l^!eN7+=wIvA7d#tzeh zj%0nc*v(XFX?ODJXjKVXRgs+fEWWLXd2}a-5o_Z-x>4WxsINQu=l3}8MQ%1_H=Cv0 zZ04~!*)tQr`Gop_Ifj~LjgK&&T#ZE%^zu4O{&S?RNk~PB zVOzHF0kd)&uZ&pXhT8WYQ+;rKm zOu><`gE3i-j&bt;H&%Wts(|E)Ne7pxkg*1=-G{IoZKjs=ar$bk+r%2FmFPL8CbrqA zcbvW=t+wnW*KEuvNkHrJaUZkjGS{%1zMVI>Z>-X^+qA1ER$~_z9lvusu-`Y zr?X7^vd7E4zN=7kfagm&3l)yncM^(CeWl}<#Flc^c-@DxIKG$BL!-&pX4L1_b89p6 ztr@4U78^{yRh0CwaGab|WNi%d{Tz83xk(*|ZIp{6oO7?L8ZGTGWj7o384VeY7>ybE zCg^SHH%d3!=92dSqy<|!2EG|{>to`v-KObJqf2^9o&kxtg+968lawpOJlJfJ~O;u zVyY4`8MSOpxE^XkY-_?TP#YyzSveTXN0dDI-iX+I*J`A0qFQ5G82{1B&Rva4UW>ns z)L^`985tc`B=;VMCpROaen|G(>SR5Rs4!=Z0ggswO;%0Fnu}|0l(;)y&Lkh=XjV1h zP{fWxtZKs5I2OzCzG{L#N8KGi?m1j_A-4B?)WotDj*c}G%$aE)<)A*E8z<MvTUc+@=;^O6G27Pw}P9hoq%`lbUYSXq0U< z#QJ3a7@9Dm_ahSVx6{{F>7n`eaI*4r8RlnJf)^0)jd(4 zYg?1|poENQi#!Kl9x_t(P4vY^JRA7-o!m3>O-#yk#%t*l4?{jV7W*cCh4peL_FDJE zqY#sk{zA`jv0Cs>8{Q#s=S!>*vGGtLYnFAtCgEDN>T0%LpQoKSQr(keo6D9NaQy}~ zpM;v1@q16ZP)qhCeN4=oq}O=PBz>C)=amv@c}ZyvoF? zC+WUcGf76Yd)>v*DroDz(O}9}y4E1|UZgfm(*3m3^%P+88m)Ro|hOO_;|v*D-&z)IXpUmvxde_Hq05 zabu-<>K36sK)$g_di3h{>yfY9ueXQWpNv}Mn-*^WROkzoh2weyrbUwb@1sYPK6So z`F26n8IgtK)9~y`o=k<~7b8{fkXo)Q@Z3HH`AR29`%@!vVvml!|0>OPzNSqn?jVY)6HTJ$sw8 z&7+>3CiNCPjdaTX7>iZ9%$0*DrNG%M8qFIi?S6gi+!;RysTZ>?!PhKxCDg%|%J%5+ z%hC2a#JWw1Am102_dC1&`gm1u*2XHM%`7X=PI^t*2EQJ?I->iPR3rLE=9h3P+}{OW``Xt$F<|}v{c@=7;sf0wF#-aJ@T&o!_;+% z$#q_iXyg4rkK= zm!5A0%gWqJO)XwN6PE3kW!}WEnymX;iuVtwZGv8H4jYe(p`fg9dAW4 zGl_qSzSsP^=e@7+KT6|>UTmKCq)*YuCZBm{te8)JWGs+1MX!UbDSDqtpCbE~q-LAc zY?JCU_uF$!YK}=QPSPWDpZO+DpLsVbk8PH1S)8QzD7^cDp2P1I*;}wSKEc{3H2H?i zIv6#|Rr1}reR``BTP*d=4BTa&tk0ZPCf_`7H`dx_^z3TJ zr04XR^Le#dyCa;NjJZZ8FOQA1+$Cu~Qmygjxv;Uk=98gXQ3;M^J};rq=V`My=f0f?J6H=33@_cA_5 zKBq(@)H+4pdLECfgs}ZhY(0nV-O!0xL-njhw#+J{UP_j&cS_Q=c*mcT=fpC`4BJ+r zOPztT5nFzdpp24lx61c%a_u;z%2BG>r{9HjPt~IlA60cU*4k8kob(y_r|PG)*(R1g zRX;UKpQ^``=_X&!KKb-$$v;&;$;{eEIyGym-e-n*-&T&w{;4`Oo%PAxQ2#!2lb&-m zmeOxhvy4Kf1yx4XMm0vIMioX8OU42#P;_#)+Hlv}bQdYj%UdXcY&Gy!*`pDHf zRp0k1oTiT@^=6&coAwQj(_5yPt&-Nan|Xw$>C}d)x5ZlJo7Ns{m2cV;rlqoM(mOlM zd`qY4Em%5D_mXPPL$+m=nOlWPt(vB<%B#$LSD7_bW7bgpG`*Dd)AUkCZ29hW1lz4) z@7hh$Z}em*>9>4ZP3o=oNB3CjhdA6>GcjjRzU~0MPv1>F^*-`Z&tX6Bgt|@ZyQls9 z0(|pjJH|QFuQl@s+f%SN%6{HwV$OBS9f*bP z8Ax^Ki{v|Y>ll;oQ9Vh?QCZ6NDEZw0IkV@b>VEa8=Wl4~qnSGOMdT}HD$nPs5wx!h zcTS2^?}g;8$>P)v&>LuTsi~#PsK#i>w6s1|pTQc8oU7v~=2oAo&p53{jg(w%x0(9d zjhy43ED=UaSaSI;#h96!&*V!t$~GFAHW2H7qto=;32vrJPxPniUgA!bcLQXPa;M6> z0dmC5Vm`U*%`y41Oujxdk0GN`qcJ0Qn%?)_Y5JbR2vem6BdJHl=9Zo&M_W0@rl;wx z<+J7e&LJ#$mWgE>g^coyD$?}PqBPAMElkaQlcfbRj~tee7UZNA##)eTAC9#vZ}U0t zAMB7d&gbXfL1FtOq*?`d0%)BL$$4)cPlst6oAZ1*wb7n~GthN~2{)skZzYgxu3gWlOVAw#Kx;Yst4D<{-Ah z`WtGNHyfOJ9EX_Y`X^$t9L}5M=OVV%CD(M9LQZUniB&_Z;|5Wp4J8IRcC1A#*WQJg zoHR9Y}TFerkf8_f;N+iV~Xfo~pcu;=J?p6k12FAkMct= zQB62ry#h6GtIBb_m0MMg<85jBxYm}YdvR-;K8`!1Tscw~+496r-g{{0dg0MKE&D>H zlJMT)Jnvcmw$$iL*xWXANjc{L;RR46ncI%l>&P=QJksg=S zr)f2q)CLo4WK7n5qlr2933p+=Y;$E`ntlV zx5>B7H6N+cpSw+oM&^@i=glVHqB#BinO?3__6U=&&*bZ!uD|CWjw?XTGD03QTj3b* zDc|94$Q+!v%w5Dj-f81;&EzYdpx;p+HL(gKuPyhp16ZOl6RR@vrRzRaV`BA2tBm~V zx<^!cWXyFS>I=smj9A~X7GjBmOgyWXR zG{9fPI2`q*r|Vzc$WE6dwVW5y)AgCInr)FKub#d<)~^cFKR(fUrdgOi3d#56{+n;d zm74lW(|!5>eSOuYzUuVs{ZQYk^nBDJ&u~|zpS>UIYfZ0=)z_MS?S81Q!?dp>{kHv3 zUvGL-tbM)d8}>teBk8Zj>KjRadq33Yp7B|%KKG1o_CtN?Gvb2I_D!FW4gI(MImfgw zXGXz(sIPd&f>`^CXI!@*>Z>sAtC+EJKh#$o4Is9v@bOC=2(59nQQk$eN{7`j@4H+^UwRCzWSN}h}Bm= zbNha%uhDFe#{IIq?K6LfwXc06PYpEu7u9<@k+-aPZn{ZL<@sqcRqwe0=={^-n$ zWAh!IxpY6w*Eh@TN3-hpLw#AZ?u)fAYt|$Cp}xFX9kKfIX8mnH)EAodNvyumtgrV& zeWkNx)GVX=h?3_$GV(2*C08dhW{s%pF}9WWZX*1Zo4i6i4dSFbZGS^D~; zYL>qKi0~N;YmU33%lR%vB<|onTJkF`GPe!Nhiij_Qc<7M&rYh8+~JdN093Pma_@Qr z$K$8qyNnyWzeTIs(B?I)*|{qJ17e~L-Y$=G#k|4$R!nPn6(e)2o+aaP`4!7GagW7H zl=9oW@=T=0EXOAA%Sb&NOX;@seUKjSSBK&bINDNUmU63CkLpX6zPDL#)@l7LeW!9O zzkey;9Pjb!U+Su#rSE&!&(ia7Tlzk*+x$AE+tT;US8hgLPPrE=$~!L-HsRZ&^v z1Kz))_41rE5~sh{5sA}p6D(72xSj8H?B+UMEL+uF?KpZ4>)DZf&w7}tQu9tKiqv86 zneq4^Ua0%yoce~nix(?(73%XQzK<5j8XxxR77TljGx>s$e9v&$dln?$P#yMO2=RK$ zdl@9Z(KGD57J8eiHK}(%H>XKz8zik7_P%8FhRN4wV*i5V+2^n~32k=fA#!T=LqA|m z4to!Vq+dC5YCg`SmO@V1CD0&S6^X^xo9+90;_3vv_mC}Hy0pns2~bL+?vp8rx=$8b z`VE5~ZvMa}J|hxK@mL5XCxMcTX@ zSM)vJQxW?OVjDb*pl6|G_L7xspWJzpwlsUMK&otoX0Ohdn^29|-N-lFeCu*Hzrpwi z#I|}PNL>ppO`L+`MlRO)nz%a=llq)DDn63=dIzvy{+lhhAE|N_u=q}S8umBKy#X

f@-- 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 D>RC3& diff --git a/TableAndHeaderConfig.json b/TableAndHeaderConfig.json index e27fe282..68b8c036 100644 --- a/TableAndHeaderConfig.json +++ b/TableAndHeaderConfig.json @@ -8,7 +8,7 @@ }, { "type": "EnumerationValues", - "titles": [ "Enumerated Values" ], + "titles": [ "Enumerated Values", "{x} values" ], "parseAs": "EnumerationDefinition" }, { diff --git a/apidoc.sh b/apidoc.sh new file mode 100644 index 00000000..162fd7dd --- /dev/null +++ b/apidoc.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mono ApiDoctor.Console/bin/Debug/apidoc.exe "$@" diff --git a/apidocs.sh b/apidocs.sh deleted file mode 100755 index 205b063b..00000000 --- a/apidocs.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -mono ApiDocs.Console/bin/Debug/apidocs.exe "$@" diff --git a/docs/markdown-customizations.md b/docs/markdown-customizations.md index 44f1916c..59aea78d 100644 --- a/docs/markdown-customizations.md +++ b/docs/markdown-customizations.md @@ -1,6 +1,6 @@ # Markdown customizations -Markdown-scanner implements the following additional features on top of the Markdown language. +API Doctor implements the following additional features on top of the Markdown language. - [Tagging of content](#tagging-of-content) - [Including other Markdown files](#including-other-markdown-files) @@ -36,7 +36,7 @@ There are some limits to what you can do with this. To specify tags at build time, the `--parameters` parameter must include a `TAGS` key, with the value set to a comma-delimited list of tags to include. For example: ```Shell -apidocs.exe publish --format html --path .\src --output .\out --parameters "TAGS=OUTLOOK,v2" +apidoc.exe publish --format html --path .\src --output .\out --parameters "TAGS=OUTLOOK,v2" ``` ### Example diff --git a/docs/markdown-requirements.md b/docs/markdown-requirements.md index 21e4c286..481620c9 100644 --- a/docs/markdown-requirements.md +++ b/docs/markdown-requirements.md @@ -1,6 +1,6 @@ # Markdown requirements -Markdown-scanner tries to be a very flexible tool to ensure that the format +API Doctor tries to be a very flexible tool to ensure that the format of the documentation does not have strict requirements. This allows the tool to be used with the widest range of documentation sources. @@ -61,12 +61,12 @@ The following properties are defined for code block annotations: | **isEmpty** | Boolean | If true, indicates that the response block should not contain a body. This is useful for API calls that expect to return a 204 No Content. | | **truncated** | Boolean | If true, indicates that the example of the resource provided does not include all required fields, and that state shouldn't generate errors or warnings. | | **expectError** | Boolean | If true, indicates that the response should be an error response and not the standard resource response expected. | -| **nullableProperties** | Array of strings | For a resource, define which properties can return null values. By default Markdown-scanner expects no null properties to be returned. | -| **scopes** | String | A space-seperated value of scopes which are required for this method to be useful. All scopes listed are required to be provided by the account for this method to be run. A warning is generated if the account doesn't have the required scopes. | +| **nullableProperties** | Array of strings | For a resource, define which properties can return null values. By default API Doctor expects no null properties to be returned. | +| **scopes** | String | A space-separated value of scopes which are required for this method to be useful. All scopes listed are required to be provided by the account for this method to be run. A warning is generated if the account doesn't have the required scopes. | ## Pairing requests and responses -Markdown-scanner assumes that request and response blocks always come in pairs +API Doctor assumes that request and response blocks always come in pairs and that the first response block encountered after a request block should be paired with that request. diff --git a/docs/publishing.md b/docs/publishing.md index 78dcd531..a5387fcd 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -4,7 +4,7 @@ To publish the markdown documentation into other formats you can use the publish command: ``` -apidocs.exe publish --output C:\html-output --template .\html-template --format mustache +apidoc.exe publish --output C:\html-output --template .\html-template --format mustache ``` ### Parameters @@ -20,12 +20,12 @@ apidocs.exe publish --output C:\html-output --template .\html-template --format ## Publishing Basic HTML -APIDocs has two different HTML output formats. The first format `HTML` provides +API Doctor has two different HTML output formats. The first format `HTML` provides basic HTML document output that can be customized through CSS or editing the HTML directly. This format has no options, but can be used as follows: ``` -apidocs.exe publish --format html --output C:\html-output +apidoc.exe publish --format html --output C:\html-output ``` This method fixes up relative links within the documentation to point to the @@ -33,12 +33,12 @@ properly named HTML output file. ## Publishing HTML with Templates -APIDocs can also use a Mustache formatted HTML template to generate HTML output +API Doctor can also use a Mustache formatted HTML template to generate HTML output for your website. Using this format you provide a template directory which contains a Mustache template and any other content that should be published to the output folder (stylesheets, images, scripts, etc). -APIDocs will read the template and use it to generate an output HTML document +API Doctor will read the template and use it to generate an output HTML document per input markdown file. Output files are written into the same place in the folder hierarchy that the input file was located. @@ -84,7 +84,7 @@ inserted into the output document: ### Additional Mustache Template Tags In addition to the standard Mustache template language tags, the following -tags have been added to APIDocs to enable additional scenarios when publishing +tags have been added to API Doctor to enable additional scenarios when publishing static HTML from Markdown: | Tag Name | Description | Example | diff --git a/license.txt b/license.txt index ac124d75..81d39ae5 100644 --- a/license.txt +++ b/license.txt @@ -1,4 +1,4 @@ -Markdown Scanner +API Doctor Copyright (c) Microsoft Corporation All rights reserved. diff --git a/readme.md b/readme.md index 94d44eb7..d2951774 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,8 @@ -# API Documentation Test Tool +# API Doctor tool -[![Build status](https://ci.appveyor.com/api/projects/status/ahf4yakswc3np2qu/branch/master?svg=true)](https://ci.appveyor.com/project/OneDrive/markdown-scanner/branch/master) +[![Build status](https://ci.appveyor.com/api/projects/status/ahf4yakswc3np2qu/branch/master?svg=true)](https://ci.appveyor.com/project/OneDrive/apidoctor/branch/master) -The API documentation test tool makes it easy to validate that the Markdown-based API +The API Doctor documentation test tool makes it easy to validate that the Markdown-based API documentation matches a REST service implementation. The toolset includes a command line and GUI application that can be used to @@ -26,7 +26,7 @@ platform. This tool is compatible with Mono or .NET. ![Screen shot of the command line tool in action](example-console.png) -`apidocs.exe [command] [options]` +`apidoc.exe [command] [options]` Available commands are: @@ -64,7 +64,7 @@ Check for broken links in the documentation. No specific options are required. Using `--verbose` will include warnings about links that were not verified. -Example: `apidocs.exe check-links --path ~/github/api-docs --method search` +Example: `apidoc.exe check-links --path ~/github/api-docs --method search` ### Check-docs Command The `check-docs` command ensures that the documentation is internally consistent. @@ -78,7 +78,7 @@ It verifies that: |:-------------------------|:---------------------------------------------------------------------------------------------------| | `--method ` | Optional. Specify the name of a request method to evaluate. If missing, all methods are evaluated. | -Example: `apidocs.exe check-docs --path ~/github/api-docs --method search` +Example: `apidoc.exe check-docs --path ~/github/api-docs --method search` ### Check-service Command @@ -104,13 +104,13 @@ automatically be loaded and used by the check-service method. Example: ``` -apidocs check-service --method "search" --access-token "foo" --url https://example.org/v1.0 -apidocs check-service --headers "If-Match: *|Application: apidocs-test-app" --odata-metadata "odata.metadata=none" +apidoc check-service --method "search" --access-token "foo" --url https://example.org/v1.0 +apidoc check-service --headers "If-Match: *|Application: apidoc-test-app" --odata-metadata "odata.metadata=none" ``` #### Account configuration file You can specify account information in a configuration file stored inside the -documentation set. Apidocs will look for any .json file that includes an +documentation set. Apidoc will look for any .json file that includes an `accounts` property include an array of account objects. These accounts will be used by the `check-service` command. @@ -140,7 +140,7 @@ token service to retrieve an access token when necessary. ### Publish Command The `publish` command uses the documentation to generate a new set of outputs. -See the documentation on [publishing using APIDocs](docs/publishing.md) for +See the documentation on [publishing using API Doctor](docs/publishing.md) for more details. ## Documentation format @@ -340,6 +340,6 @@ properties defined: ## Open Source -See [OpenSourceNotes](OpenSourceNotes.md) for more details about open source usage in markdown-scanner. +See [OpenSourceNotes](OpenSourceNotes.md) for more details about open source usage in API Doctor. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.