From 3aa884ecfab4830ae1ecfb47a5f7ac5e3ed59f13 Mon Sep 17 00:00:00 2001 From: AndrewFossAWS <108305217+AndrewFossAWS@users.noreply.github.com> Date: Mon, 27 Feb 2023 13:18:03 -0800 Subject: [PATCH 01/22] Refactor types generation (#118) * Refactor types generation * Follow DirectedCodegen's "golden-path" during shape generation * Remove the use indirection and visitor in generators * Created specific generators handling each shape directive * Configure DirectedCodegen to "feed" shapes in alphabetical ordering based on shape name * Pin rbs version to 2.8.4 in hearth/Gemfile * Upgrade Smithy version --- .github/workflows/codegen_ci.yml | 2 +- codegen/build.gradle.kts | 2 +- .../lib/high_score_service/types.rb | 1 + .../projections/weather/lib/weather/types.rb | 1 + .../model/weather.smithy | 6 +- .../ruby/codegen/DirectedRubyCodegen.java | 71 ++- .../codegen/DirectedRubyCodegenPlugin.java | 3 + .../smithy/ruby/codegen/RubyCodeWriter.java | 10 +- .../codegen/generators/EnumGenerator.java | 98 ++++ .../codegen/generators/IntEnumGenerator.java | 51 ++ .../generators/StructureGenerator.java | 191 +++++++ .../generators/TypesFileBlockGenerator.java | 46 ++ .../generators/TypesFileGenerator.java | 46 ++ .../codegen/generators/TypesGenerator.java | 495 ------------------ .../codegen/generators/UnionGenerator.java | 119 +++++ .../docs/TraitExampleGenerator.java | 9 +- hearth/Gemfile | 2 +- hearth/lib/hearth/middleware_builder.rb | 8 +- 18 files changed, 618 insertions(+), 543 deletions(-) create mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java create mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java create mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java create mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java create mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileGenerator.java delete mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesGenerator.java create mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java diff --git a/.github/workflows/codegen_ci.yml b/.github/workflows/codegen_ci.yml index 126a52408..3fc79bd36 100644 --- a/.github/workflows/codegen_ci.yml +++ b/.github/workflows/codegen_ci.yml @@ -42,7 +42,7 @@ jobs: ruby-rbs-type-check: needs: [generate-test-sdk] - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: diff --git a/codegen/build.gradle.kts b/codegen/build.gradle.kts index d2c178b5b..e3dfda656 100644 --- a/codegen/build.gradle.kts +++ b/codegen/build.gradle.kts @@ -28,7 +28,7 @@ allprojects { version = "0.1.0" } -extra["smithyVersion"] = "1.26.0" +extra["smithyVersion"] = "1.28.0" // The root project doesn't produce a JAR. tasks["jar"].enabled = false diff --git a/codegen/projections/high_score_service/lib/high_score_service/types.rb b/codegen/projections/high_score_service/lib/high_score_service/types.rb index 4a41bed57..fbe74bd5b 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/types.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/types.rb @@ -9,6 +9,7 @@ module HighScoreService module Types + # Input structure for CreateHighScore # # @!attribute high_score diff --git a/codegen/projections/weather/lib/weather/types.rb b/codegen/projections/weather/lib/weather/types.rb index 56fbf3d36..c83b10327 100644 --- a/codegen/projections/weather/lib/weather/types.rb +++ b/codegen/projections/weather/lib/weather/types.rb @@ -584,6 +584,7 @@ module Resolution ULTRA = 3 end + # Includes enum constants for TypedYesNo # module TypedYesNo diff --git a/codegen/smithy-ruby-codegen-test/model/weather.smithy b/codegen/smithy-ruby-codegen-test/model/weather.smithy index feb35ffc2..2cdd125b1 100644 --- a/codegen/smithy-ruby-codegen-test/model/weather.smithy +++ b/codegen/smithy-ruby-codegen-test/model/weather.smithy @@ -400,8 +400,10 @@ structure OtherStructure {} @enum([{value: "YES"}, {value: "NO"}]) string SimpleYesNo -@enum([{value: "YES", name: "YES"}, {value: "NO", name: "NO"}]) -string TypedYesNo +enum TypedYesNo { + YES = "YES" + NO = "NO" +} map StringMap { key: String, diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java index 397ef1826..eeb4489e1 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java @@ -15,7 +15,6 @@ package software.amazon.smithy.ruby.codegen; -import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; @@ -38,17 +37,20 @@ import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.ruby.codegen.config.ClientConfig; import software.amazon.smithy.ruby.codegen.generators.ClientGenerator; import software.amazon.smithy.ruby.codegen.generators.ConfigGenerator; +import software.amazon.smithy.ruby.codegen.generators.EnumGenerator; import software.amazon.smithy.ruby.codegen.generators.GemspecGenerator; import software.amazon.smithy.ruby.codegen.generators.HttpProtocolTestGenerator; +import software.amazon.smithy.ruby.codegen.generators.IntEnumGenerator; import software.amazon.smithy.ruby.codegen.generators.ModuleGenerator; import software.amazon.smithy.ruby.codegen.generators.PaginatorsGenerator; import software.amazon.smithy.ruby.codegen.generators.ParamsGenerator; -import software.amazon.smithy.ruby.codegen.generators.TypesGenerator; +import software.amazon.smithy.ruby.codegen.generators.StructureGenerator; +import software.amazon.smithy.ruby.codegen.generators.TypesFileBlockGenerator; +import software.amazon.smithy.ruby.codegen.generators.UnionGenerator; import software.amazon.smithy.ruby.codegen.generators.ValidatorsGenerator; import software.amazon.smithy.ruby.codegen.generators.WaitersGenerator; import software.amazon.smithy.ruby.codegen.generators.YardOptsGenerator; @@ -60,7 +62,11 @@ public class DirectedRubyCodegen private static final Logger LOGGER = Logger.getLogger(DirectedRubyCodegen.class.getName()); - private List typeShapes = new ArrayList<>(); + private StructureGenerator structureGenerator; + private UnionGenerator unionGenerator; + private EnumGenerator enumGenerator; + private IntEnumGenerator intEnumGenerator; + private TypesFileBlockGenerator typesFileBlockGenerator; @Override public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { @@ -151,9 +157,21 @@ public void generateService(GenerateServiceDirective directive) { + this.structureGenerator = new StructureGenerator(directive); + this.unionGenerator = new UnionGenerator(directive); + this.enumGenerator = new EnumGenerator(directive); + this.intEnumGenerator = new IntEnumGenerator(directive); + this.typesFileBlockGenerator = new TypesFileBlockGenerator(directive); + + // Pre-populate module blocks for types.rb and types.rbs files + this.typesFileBlockGenerator.openBlocks(); + } + @Override public void generateStructure(GenerateStructureDirective directive) { - typeShapes.add(directive.shape()); + structureGenerator.accept(directive); } @Override @@ -161,54 +179,29 @@ public void generateError(GenerateErrorDirective directive) { - typeShapes.add(directive.shape()); + unionGenerator.accept(directive); } @Override public void generateEnumShape(GenerateEnumDirective directive) { - typeShapes.add(directive.shape()); + enumGenerator.accept(directive); } @Override public void generateIntEnumShape(GenerateIntEnumDirective directive) { - typeShapes.add(directive.shape()); + intEnumGenerator.accept(directive); } @Override public void customizeBeforeIntegrations(CustomizeDirective directive) { GenerationContext context = directive.context(); - - // Generate types - TypesGenerator typesGenerator = new TypesGenerator(context); - context.writerDelegator().useFileWriter( - typesGenerator.getFile(), typesGenerator.getNameSpace(), writer -> { - writer.includePreamble().includeRequires(); - - TypesGenerator.TypesVisitor visitor = typesGenerator.getTypeVisitor(writer); - - writer.addModule(directive.settings().getModule()); - writer.addModule("Types"); - - typeShapes.stream() - .sorted(Comparator.comparing(a -> a.getId().getName())) - .forEach(shape -> shape.accept(visitor)); - - writer.closeAllModules(); - }); - - typesGenerator.renderRbs(); - - ParamsGenerator paramsGenerator = new ParamsGenerator(context); - paramsGenerator.render(); - - ValidatorsGenerator validatorsGenerator = new ValidatorsGenerator(context); - validatorsGenerator.render(); - + new ParamsGenerator(context).render(); + new ValidatorsGenerator(context).render(); if (directive.context().protocolGenerator().isPresent()) { ProtocolGenerator generator = directive.context().protocolGenerator().get(); @@ -241,6 +234,12 @@ public void customizeBeforeIntegrations(CustomizeDirective directive) { + // Close all module blocks for types.rb and types.rbs files + this.typesFileBlockGenerator.closeAllBlocks(); + } + private Set collectDependencies( Model model, ServiceShape service, diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java index 38ec26b7a..06d31c0d7 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java @@ -17,6 +17,7 @@ import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.codegen.core.ShapeGenerationOrder; import software.amazon.smithy.codegen.core.directed.CodegenDirector; import software.amazon.smithy.utils.SmithyInternalApi; @@ -53,6 +54,8 @@ public void execute(PluginContext context) { runner.createDedicatedInputsAndOutputs(); + runner.shapeGenerationOrder(ShapeGenerationOrder.ALPHABETICAL); + runner.run(); } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java index 2c3495f7a..546099022 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java @@ -15,6 +15,8 @@ package software.amazon.smithy.ruby.codegen; +import java.util.HashSet; +import java.util.Set; import java.util.Stack; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -48,6 +50,7 @@ public class RubyCodeWriter extends SymbolWriter modules = new Stack<>(); + private Set modulesSet = new HashSet<>(); /** * @param namespace namespace to write in @@ -63,6 +66,10 @@ public RubyCodeWriter(String namespace) { } public RubyCodeWriter addModule(String name) { + if (modulesSet.contains(name)) { + return this; + } + modulesSet.add(name); modules.push(name); this.openBlock("module $L", name); return this; @@ -73,7 +80,8 @@ public RubyCodeWriter closeModule() { throw new RuntimeException("No modules were opened"); } - modules.pop(); + String module = modules.pop(); + modulesSet.remove(module); this.closeBlock("end"); return this; } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java new file mode 100644 index 000000000..82fc44095 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.codegen.core.directed.GenerateEnumDirective; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubySettings; + +public final class EnumGenerator extends TypesFileGenerator + implements Consumer> { + + public EnumGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + public void accept(GenerateEnumDirective directive) { + var shape = directive.shape(); + directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + final EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); + + List enumDefinitions = enumTrait.getValues().stream() + .filter(value -> value.getName().isPresent()) + .collect(Collectors.toList()); + + // only write out a module if there is at least one enum constant + if (enumDefinitions.size() > 0) { + String shapeName = symbolProvider.toSymbol(shape).getName(); + + writer + .writeDocstring("Includes enum constants for " + shapeName) + .openBlock("module $L", shapeName); + + enumDefinitions.forEach(enumDefinition -> { + String enumName = enumDefinition.getName().get(); + String enumValue = enumDefinition.getValue(); + String enumDocumentation = enumDefinition.getDocumentation() + .orElse("No documentation available."); + writer.writeDocstring(enumDocumentation); + if (enumDefinition.isDeprecated()) { + writer.writeYardDeprecated("This enum value is deprecated.", ""); + } + if (!enumDefinition.getTags().isEmpty()) { + String enumTags = enumDefinition.getTags().stream() + .map((tag) -> "\"" + tag + "\"") + .collect(Collectors.joining(", ")); + writer.writeDocstring("Tags: [" + enumTags + "]"); + } + writer.write("$L = $S\n", enumName, enumValue); + }); + + writer + .unwrite("\n") + .closeBlock("end\n"); + } + }); + + directive.context().writerDelegator().useFileWriter(rbsFile(), nameSpace(), writer -> { + // Only write out string shapes for enums + EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); + List enumDefinitions = enumTrait.getValues().stream() + .filter(value -> value.getName().isPresent()) + .collect(Collectors.toList()); + + // only write out a module if there is at least one enum constant + if (enumDefinitions.size() > 0) { + String shapeName = symbolProvider.toSymbol(shape).getName(); + writer.openBlock("module $L", shapeName); + enumDefinitions.forEach(enumDefinition -> { + String enumName = enumDefinition.getName().get(); + writer.write("$L: ::String\n", enumName); + }); + writer + .unwrite("\n") + .closeBlock("end\n"); + } + }); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java new file mode 100644 index 000000000..a821aaf74 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.codegen.core.directed.GenerateIntEnumDirective; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubySettings; + +public final class IntEnumGenerator extends TypesFileGenerator + implements Consumer> { + + public IntEnumGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + public void accept(GenerateIntEnumDirective directive) { + var shape = (IntEnumShape) directive.shape(); + + directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + // only write out a module if there is at least one enum constant + if (shape.getEnumValues().size() > 0) { + String shapeName = symbolProvider.toSymbol(shape).getName(); + + writer.writeDocstring("Includes enum constants for " + shapeName) + .addModule(shapeName); + + shape.getEnumValues() + .forEach((enumName, enumValue) -> writer.write("$L = $L\n", enumName, enumValue)); + + writer.unwrite("\n").closeModule(); + } + }); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java new file mode 100644 index 000000000..e81c71f92 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java @@ -0,0 +1,191 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.codegen.core.directed.ShapeDirective; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.NullableIndex; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.Hearth; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubyFormatter; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; + +public final class StructureGenerator extends TypesFileGenerator + implements Consumer> { + + public StructureGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + public void accept(ShapeDirective directive) { + var model = directive.model(); + var shape = directive.shape(); + + directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + String membersBlock = "nil"; + if (!shape.members().isEmpty()) { + membersBlock = shape + .members() + .stream() + .map(memberShape -> RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape))) + .collect(Collectors.joining(",\n")); + } + membersBlock += ","; + + String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); + + writer.writeInline("$L", documentation); + + shape.members().forEach(memberShape -> { + String attribute = symbolProvider.toMemberName(memberShape); + Shape target = model.expectShape(memberShape.getTarget()); + String returnType = (String) symbolProvider.toSymbol(target) + .getProperty("yardType").orElseThrow(IllegalArgumentException::new); + + String memberDocumentation = + new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); + + writer.writeYardAttribute(attribute, () -> { + // delegate to member shape in this visitor + writer.writeInline("$L", memberDocumentation); + writer.writeYardReturn(returnType, ""); + }); + }); + + writer + .openBlock("$T = ::Struct.new(", symbolProvider.toSymbol(shape)) + .write(membersBlock) + .write("keyword_init: true") + .closeBlock(") do") + .indent() + .write("include $T", Hearth.STRUCTURE) + .call(() -> renderStructureInitializeMethod(writer, model, shape)) + .call(() -> renderStructureToSMethod(writer, model, shape)) + .closeBlock("end\n"); + }); + + directive.context().writerDelegator().useFileWriter(rbsFile(), nameSpace(), writer -> { + Symbol symbol = symbolProvider.toSymbol(shape); + String shapeName = symbol.getName(); + writer.write(shapeName + ": untyped\n"); + }); + } + + private void renderStructureInitializeMethod( + RubyCodeWriter writer, + Model model, + StructureShape structureShape + ) { + NullableIndex nullableIndex = new NullableIndex(model); + List defaultMembers = structureShape.members().stream() + .filter((m) -> !nullableIndex.isNullable(m)) + .toList(); + + if (!defaultMembers.isEmpty()) { + writer + .openBlock("\ndef initialize(*)") + .write("super") + .call(() -> { + defaultMembers.forEach((m) -> { + String attribute = symbolProvider.toMemberName(m); + Shape target = model.expectShape(m.getTarget()); + + writer.write("self.$L ||= $L", + attribute, + target.accept(new MemberDefaultVisitor())); + }); + }) + .closeBlock("end"); + } + } + + private void renderStructureToSMethod( + RubyCodeWriter writer, + Model model, + StructureShape structureShape + ) { + String fullQualifiedShapeName = settings.getModule() + "::Types::" + + symbolProvider.toSymbol(structureShape).getName(); + + boolean hasSensitiveMember = structureShape.members().stream() + .anyMatch(memberShape -> memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()); + + if (structureShape.hasTrait(SensitiveTrait.class)) { + // structure is itself sensitive + writer + .openBlock("\ndef to_s") + .write("\"#\"", fullQualifiedShapeName) + .closeBlock("end"); + } else if (hasSensitiveMember) { + // at least one member is sensitive + Iterator iterator = structureShape.members().iterator(); + + writer + .openBlock("\ndef to_s") + .write("\"#\"", key, value); + } + } + writer + .dedent() + .closeBlock("end"); + } + } + + private static class MemberDefaultVisitor extends ShapeVisitor.Default { + @Override + protected String getDefault(Shape shape) { + return "0"; + } + + @Override + public String booleanShape(BooleanShape s) { + return "false"; + } + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java new file mode 100644 index 000000000..d73d97888 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubySettings; + +public class TypesFileBlockGenerator extends TypesFileGenerator { + + public TypesFileBlockGenerator(ContextualDirective directive) { + super(directive); + } + + public void openBlocks() { + context.writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + writer.includePreamble().includeRequires(); + writer.addModule(settings.getModule()); + writer.addModule("Types"); + }); + context.writerDelegator().useFileWriter(rbsFile(), nameSpace(), writer -> { + writer.includePreamble(); + writer.addModule(settings.getModule()); + writer.addModule("Types"); + }); + } + + public void closeAllBlocks() { + context.writerDelegator().useFileWriter(rbFile(), nameSpace(), RubyCodeWriter::closeAllModules); + context.writerDelegator().useFileWriter(rbsFile(), nameSpace(), RubyCodeWriter::closeAllModules); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileGenerator.java new file mode 100644 index 000000000..fd58009a4 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileGenerator.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubySettings; + +abstract class TypesFileGenerator { + final SymbolProvider symbolProvider; + final RubySettings settings; + + final GenerationContext context; + + TypesFileGenerator(ContextualDirective directive) { + this.symbolProvider = directive.symbolProvider(); + this.settings = directive.settings(); + this.context = directive.context(); + } + + public final String nameSpace() { + return settings.getModule() + "::Types"; + } + + public final String rbFile() { + return settings.getGemName() + "/lib/" + settings.getGemName() + "/types.rb"; + } + + public final String rbsFile() { + return settings.getGemName() + "/sig/" + settings.getGemName() + "/types.rbs"; + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesGenerator.java deleted file mode 100644 index 24bfcfb40..000000000 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesGenerator.java +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.ruby.codegen.generators; - -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.Symbol; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.NullableIndex; -import software.amazon.smithy.model.neighbor.Walker; -import software.amazon.smithy.model.shapes.BooleanShape; -import software.amazon.smithy.model.shapes.EnumShape; -import software.amazon.smithy.model.shapes.IntEnumShape; -import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeVisitor; -import software.amazon.smithy.model.shapes.StringShape; -import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.shapes.UnionShape; -import software.amazon.smithy.model.traits.EnumDefinition; -import software.amazon.smithy.model.traits.EnumTrait; -import software.amazon.smithy.model.traits.SensitiveTrait; -import software.amazon.smithy.model.transform.ModelTransformer; -import software.amazon.smithy.ruby.codegen.GenerationContext; -import software.amazon.smithy.ruby.codegen.Hearth; -import software.amazon.smithy.ruby.codegen.RubyCodeWriter; -import software.amazon.smithy.ruby.codegen.RubyFormatter; -import software.amazon.smithy.ruby.codegen.RubySettings; -import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; -import software.amazon.smithy.utils.SmithyInternalApi; - -@SmithyInternalApi -public class TypesGenerator { - private static final Logger LOGGER = - Logger.getLogger(TypesGenerator.class.getName()); - - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; - - public TypesGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(getNameSpace()); - this.rbsWriter = new RubyCodeWriter(getNameSpace()); - this.symbolProvider = context.symbolProvider(); - } - - public TypesVisitor getTypeVisitor(RubyCodeWriter writer) { - return new TypesVisitor(writer); - } - - public String getFile() { - return settings.getGemName() + "/lib/" + settings.getGemName() + "/types.rb"; - } - - public String getNameSpace() { - return context.settings().getModule() + "::Types"; - } - - public void render() { - FileManifest fileManifest = context.fileManifest(); - - writer - .includePreamble() - .includeRequires() - .openBlock("module $L", settings.getModule()) - .openBlock("module Types") - .write("") - .call(() -> renderTypes(getTypeVisitor(writer))) - .closeBlock("end") - .closeBlock("end"); - - String fileName = getFile(); - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote types to " + fileName); - } - - public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter - .includePreamble() - .openBlock("module $L", settings.getModule()) - .openBlock("module Types") - .write("") - .call(() -> renderTypes(new TypesRbsVisitor())) - .closeBlock("end") - .closeBlock("end"); - - String fileName = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/types.rbs"; - fileManifest.writeFile(fileName, rbsWriter.toString()); - LOGGER.fine("Wrote types rbs to " + fileName); - } - - public void renderTypes(ShapeVisitor visitor) { - Model modelWithoutTraitShapes = ModelTransformer.create() - .getModelWithoutTraitShapes(model); - - new Walker(modelWithoutTraitShapes) - .walkShapes(context.service()) - .stream() - .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach((shape) -> shape.accept(visitor)); - } - - public class TypesVisitor extends ShapeVisitor.Default { - - private RubyCodeWriter writer; - - public TypesVisitor(RubyCodeWriter writer) { - this.writer = writer; - } - - @Override - protected Void getDefault(Shape shape) { - return null; - } - - @Override - public Void structureShape(StructureShape shape) { - String membersBlock = "nil"; - if (!shape.members().isEmpty()) { - membersBlock = shape - .members() - .stream() - .map(memberShape -> RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape))) - .collect(Collectors.joining(",\n")); - } - membersBlock += ","; - - String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); - - writer.writeInline("$L", documentation); - - shape.members().forEach(memberShape -> { - String attribute = symbolProvider.toMemberName(memberShape); - Shape target = model.expectShape(memberShape.getTarget()); - String returnType = (String) symbolProvider.toSymbol(target) - .getProperty("yardType").orElseThrow(IllegalArgumentException::new); - - String memberDocumentation = - new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); - - writer.writeYardAttribute(attribute, () -> { - // delegate to member shape in this visitor - writer.writeInline("$L", memberDocumentation); - writer.writeYardReturn(returnType, ""); - }); - }); - - writer - .openBlock("$T = ::Struct.new(", symbolProvider.toSymbol(shape)) - .write(membersBlock) - .write("keyword_init: true") - .closeBlock(") do") - .indent() - .write("include $T", Hearth.STRUCTURE) - .call(() -> renderStructureInitializeMethod(shape)) - .call(() -> renderStructureToSMethod(shape)) - .closeBlock("end\n"); - - return null; - } - - @Override - public Void unionShape(UnionShape shape) { - String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); - - writer.writeInline("$L", documentation); - writer.openBlock("class $T < $T", symbolProvider.toSymbol(shape), Hearth.UNION); - - for (MemberShape memberShape : shape.members()) { - String memberDocumentation = - new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); - - writer - .writeInline("$L", memberDocumentation) - .openBlock("class $L < $T", - symbolProvider.toMemberName(memberShape), symbolProvider.toSymbol(shape)) - .openBlock("def to_h") - .write("{ $L: super(__getobj__) }", - RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape))) - .closeBlock("end") - .call(() -> renderUnionToSMethod(memberShape)) - .closeBlock("end\n"); - } - - writer - .writeDocstring("Handles unknown future members") - .openBlock("class Unknown < $T", symbolProvider.toSymbol(shape)) - .openBlock("def to_h") - .write("{ unknown: super(__getobj__) }") - .closeBlock("end\n") - .openBlock("def to_s") - .write("\"#<$L::Types::Unknown #{__getobj__ || 'nil'}>\"", settings.getModule()) - .closeBlock("end") - .closeBlock("end") - .closeBlock("end\n"); - - return null; - } - - @Override - public Void stringShape(StringShape shape) { - // Only write out string shapes for enums - if (shape.hasTrait(EnumTrait.class)) { - EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); - List enumDefinitions = enumTrait.getValues().stream() - .filter(value -> value.getName().isPresent()) - .collect(Collectors.toList()); - - // only write out a module if there is at least one enum constant - if (enumDefinitions.size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - - writer - .writeDocstring("Includes enum constants for " + shapeName) - .openBlock("module $L", shapeName); - - enumDefinitions.forEach(enumDefinition -> { - String enumName = enumDefinition.getName().get(); - String enumValue = enumDefinition.getValue(); - String enumDocumentation = enumDefinition.getDocumentation() - .orElse("No documentation available."); - writer.writeDocstring(enumDocumentation); - if (enumDefinition.isDeprecated()) { - writer.writeYardDeprecated("This enum value is deprecated.", ""); - } - if (!enumDefinition.getTags().isEmpty()) { - String enumTags = enumDefinition.getTags().stream() - .map((tag) -> "\"" + tag + "\"") - .collect(Collectors.joining(", ")); - writer.writeDocstring("Tags: [" + enumTags + "]"); - } - writer.write("$L = $S\n", enumName, enumValue); - }); - - writer - .unwrite("\n") - .closeBlock("end\n"); - } - } - - return null; - } - - @Override - public Void enumShape(EnumShape shape) { - EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); - List enumDefinitions = enumTrait.getValues().stream() - .filter(value -> value.getName().isPresent()) - .collect(Collectors.toList()); - - // only write out a module if there is at least one enum constant - if (enumDefinitions.size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - - writer - .writeDocstring("Includes enum constants for " + shapeName) - .openBlock("module $L", shapeName); - - enumDefinitions.forEach(enumDefinition -> { - String enumName = enumDefinition.getName().get(); - String enumValue = enumDefinition.getValue(); - String enumDocumentation = enumDefinition.getDocumentation() - .orElse("No documentation available."); - writer.writeDocstring(enumDocumentation); - if (enumDefinition.isDeprecated()) { - writer.writeYardDeprecated("This enum value is deprecated.", ""); - } - if (!enumDefinition.getTags().isEmpty()) { - String enumTags = enumDefinition.getTags().stream() - .map((tag) -> "\"" + tag + "\"") - .collect(Collectors.joining(", ")); - writer.writeDocstring("Tags: [" + enumTags + "]"); - } - writer.write("$L = $S\n", enumName, enumValue); - }); - - writer - .unwrite("\n") - .closeBlock("end\n"); - } - - return null; - } - - @Override - public Void intEnumShape(IntEnumShape shape) { - // only write out a module if there is at least one enum constant - if (shape.getEnumValues().size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - - writer.writeDocstring("Includes enum constants for " + shapeName) - .addModule(shapeName); - - shape.getEnumValues() - .forEach((enumName, enumValue) -> writer.write("$L = $L\n", enumName, enumValue)); - - writer.unwrite("\n").closeModule(); - } - - return null; - } - - private void renderStructureInitializeMethod(StructureShape structureShape) { - NullableIndex nullableIndex = new NullableIndex(model); - List defaultMembers = structureShape.members().stream() - .filter((m) -> !nullableIndex.isNullable(m)) - .collect(Collectors.toList()); - if (!defaultMembers.isEmpty()) { - writer - .openBlock("\ndef initialize(*)") - .write("super") - .call(() -> { - defaultMembers.forEach((m) -> { - String attribute = symbolProvider.toMemberName(m); - Shape target = model.expectShape(m.getTarget()); - writer.write("self.$L ||= $L", - attribute, - target.accept(new MemberDefaultVisitor())); - }); - }) - .closeBlock("end"); - } - } - - private void renderStructureToSMethod(StructureShape structureShape) { - String fullQualifiedShapeName = settings.getModule() + "::Types::" - + symbolProvider.toSymbol(structureShape).getName(); - - Boolean hasSensitiveMember = structureShape.members().stream() - .anyMatch(memberShape -> memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()); - - if (structureShape.hasTrait(SensitiveTrait.class)) { - // structure is itself sensitive - writer - .openBlock("\ndef to_s") - .write("\"#\"", fullQualifiedShapeName) - .closeBlock("end"); - } else if (hasSensitiveMember) { - // at least one member is sensitive - Iterator iterator = structureShape.members().iterator(); - - writer - .openBlock("\ndef to_s") - .write("\"#\"", key, value); - } - } - writer - .dedent() - .closeBlock("end"); - } - } - - private void renderUnionToSMethod(MemberShape memberShape) { - String fullQualifiedShapeName = settings.getModule() + "::Types::" - + symbolProvider.toMemberName(memberShape); - - writer.write("") - .openBlock("def to_s"); - - if (memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()) { - writer.write("\"#<$L [SENSITIVE]>\"", fullQualifiedShapeName); - } else { - writer.write("\"#<$L #{__getobj__ || 'nil'}>\"", fullQualifiedShapeName); - } - - writer.closeBlock("end"); - } - } - - private class MemberDefaultVisitor extends ShapeVisitor.Default { - - @Override - protected String getDefault(Shape shape) { - return "0"; - } - - @Override - public String booleanShape(BooleanShape s) { - return "false"; - } - } - - private class TypesRbsVisitor extends ShapeVisitor.Default { - @Override - protected Void getDefault(Shape shape) { - return null; - } - - @Override - public Void structureShape(StructureShape shape) { - Symbol symbol = symbolProvider.toSymbol(shape); - String shapeName = symbol.getName(); - - rbsWriter.write(shapeName + ": untyped\n"); - return null; - } - - @Override - public Void unionShape(UnionShape shape) { - Symbol symbol = symbolProvider.toSymbol(shape); - rbsWriter.openBlock("class $T < $T", symbol, Hearth.UNION); - - for (MemberShape memberShape : shape.members()) { - rbsWriter - .openBlock("class $L < $T", - symbolProvider.toMemberName(memberShape), symbol) - .write("def to_h: () -> { $L: Hash[Symbol,$T] }", - RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape)), - symbol) - .closeBlock("end\n"); - } - - rbsWriter - .openBlock("class Unknown < $T", symbol) - .write("def to_h: () -> { unknown: Hash[Symbol,$T] }", - symbol) - .closeBlock("end") - .closeBlock("end\n"); - - return null; - } - - @Override - public Void stringShape(StringShape shape) { - // Only write out string shapes for enums - if (shape.hasTrait(EnumTrait.class)) { - EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); - List enumDefinitions = enumTrait.getValues().stream() - .filter(value -> value.getName().isPresent()) - .collect(Collectors.toList()); - - // only write out a module if there is at least one enum constant - if (enumDefinitions.size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - rbsWriter.openBlock("module $L", shapeName); - enumDefinitions.forEach(enumDefinition -> { - String enumName = enumDefinition.getName().get(); - rbsWriter.write("$L: ::String\n", enumName); - }); - rbsWriter - .unwrite("\n") - .closeBlock("end\n"); - } - } - - return null; - } - } -} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java new file mode 100644 index 000000000..16a28f389 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java @@ -0,0 +1,119 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.Hearth; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubyFormatter; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; + +public final class UnionGenerator extends TypesFileGenerator + implements Consumer> { + + public UnionGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + public void accept(GenerateUnionDirective directive) { + var shape = directive.shape(); + var model = directive.model(); + String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); + + directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + writer.writeInline("$L", documentation); + writer.openBlock("class $T < $T", symbolProvider.toSymbol(shape), Hearth.UNION); + + for (MemberShape memberShape : shape.members()) { + String memberDocumentation = + new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); + + writer + .writeInline("$L", memberDocumentation) + .openBlock("class $L < $T", + symbolProvider.toMemberName(memberShape), symbolProvider.toSymbol(shape)) + .openBlock("def to_h") + .write("{ $L: super(__getobj__) }", + RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape))) + .closeBlock("end") + .call(() -> renderUnionToSMethod(writer, model, memberShape)) + .closeBlock("end\n"); + } + + writer + .writeDocstring("Handles unknown future members") + .openBlock("class Unknown < $T", symbolProvider.toSymbol(shape)) + .openBlock("def to_h") + .write("{ unknown: super(__getobj__) }") + .closeBlock("end\n") + .openBlock("def to_s") + .write("\"#<$L::Types::Unknown #{__getobj__ || 'nil'}>\"", directive.settings().getModule()) + .closeBlock("end") + .closeBlock("end") + .closeBlock("end\n"); + }); + + directive.context().writerDelegator().useFileWriter(rbsFile(), nameSpace(), writer -> { + Symbol symbol = symbolProvider.toSymbol(shape); + writer.openBlock("class $T < $T", symbol, Hearth.UNION); + + for (MemberShape memberShape : shape.members()) { + writer + .openBlock("class $L < $T", + symbolProvider.toMemberName(memberShape), symbol) + .write("def to_h: () -> { $L: Hash[Symbol,$T] }", + RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape)), + symbol) + .closeBlock("end\n"); + } + + writer + .openBlock("class Unknown < $T", symbol) + .write("def to_h: () -> { unknown: Hash[Symbol,$T] }", + symbol) + .closeBlock("end") + .closeBlock("end\n"); + }); + } + + private void renderUnionToSMethod( + RubyCodeWriter writer, + Model model, + MemberShape memberShape) { + String fullQualifiedShapeName = settings.getModule() + "::Types::" + + symbolProvider.toMemberName(memberShape); + + writer.write("") + .openBlock("def to_s"); + + if (memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()) { + writer.write("\"#<$L [SENSITIVE]>\"", fullQualifiedShapeName); + } else { + writer.write("\"#<$L #{__getobj__ || 'nil'}>\"", fullQualifiedShapeName); + } + + writer.closeBlock("end"); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java index 961e32e70..48dbea1ba 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java @@ -18,6 +18,7 @@ import java.util.Optional; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.Shape; @@ -36,7 +37,7 @@ public class TraitExampleGenerator { private final RubyCodeWriter writer; private final Optional documentation; private final ObjectNode input; - private final ObjectNode output; + private final Node output; private final Optional error; private final String operationName; private final Shape operationInput; @@ -50,7 +51,11 @@ public TraitExampleGenerator(OperationShape operation, SymbolProvider symbolProv this.writer = new RubyCodeWriter(""); this.documentation = example.getDocumentation(); this.input = example.getInput(); - this.output = example.getOutput(); + if (example.getOutput().isPresent()) { + this.output = example.getOutput().get(); + } else { + this.output = ObjectNode.nullNode(); + } this.error = example.getError(); this.operationName = diff --git a/hearth/Gemfile b/hearth/Gemfile index ec18820e3..09933658b 100644 --- a/hearth/Gemfile +++ b/hearth/Gemfile @@ -13,7 +13,7 @@ group :test do end group :development do - gem 'rbs' + gem 'rbs', '~>2' gem 'rubocop' gem 'steep' end diff --git a/hearth/lib/hearth/middleware_builder.rb b/hearth/lib/hearth/middleware_builder.rb index 10dc94ec8..acefb10c1 100755 --- a/hearth/lib/hearth/middleware_builder.rb +++ b/hearth/lib/hearth/middleware_builder.rb @@ -205,21 +205,21 @@ def remove(klass) %w[before after around].each do |method| method_name = "#{method}_#{simple_step_name}" define_method(method_name) do |*args, &block| - return send(method, klass, *args, &block) + send(method, klass, *args, &block) end define_singleton_method(method_name) do |*args, &block| - return send(method, klass, *args, &block) + send(method, klass, *args, &block) end end remove_method_name = "remove_#{simple_step_name}" define_method(remove_method_name) do - return remove(klass) + remove(klass) end define_singleton_method(remove_method_name) do - return remove(klass) + remove(klass) end end From ea274c967cdcea6d73470f37a3e43d4143d144c7 Mon Sep 17 00:00:00 2001 From: AndrewFossAWS <108305217+AndrewFossAWS@users.noreply.github.com> Date: Thu, 9 Mar 2023 15:05:27 -0800 Subject: [PATCH 02/22] Refactor generators to extend RubyGeneratorBase (#119) --- .../ruby/codegen/DirectedRubyCodegen.java | 48 +- .../codegen/generators/ClientGenerator.java | 141 ++-- .../codegen/generators/ConfigGenerator.java | 83 +-- .../codegen/generators/EnumGenerator.java | 24 +- .../codegen/generators/IntEnumGenerator.java | 20 +- .../codegen/generators/ModuleGenerator.java | 58 +- .../generators/PaginatorsGenerator.java | 90 +-- .../codegen/generators/ParamsGenerator.java | 631 +++++++++--------- ...eGenerator.java => RubyGeneratorBase.java} | 37 +- .../generators/StructureGenerator.java | 23 +- .../generators/TypesFileBlockGenerator.java | 9 +- .../codegen/generators/UnionGenerator.java | 27 +- .../generators/ValidatorsGenerator.java | 406 +++++------ .../codegen/generators/WaitersGenerator.java | 106 ++- 14 files changed, 834 insertions(+), 869 deletions(-) rename codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/{TypesFileGenerator.java => RubyGeneratorBase.java} (54%) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java index eeb4489e1..5b2d2d155 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java @@ -62,10 +62,6 @@ public class DirectedRubyCodegen private static final Logger LOGGER = Logger.getLogger(DirectedRubyCodegen.class.getName()); - private StructureGenerator structureGenerator; - private UnionGenerator unionGenerator; - private EnumGenerator enumGenerator; - private IntEnumGenerator intEnumGenerator; private TypesFileBlockGenerator typesFileBlockGenerator; @Override @@ -116,7 +112,6 @@ public GenerationContext createContext(CreateContextDirective directive) { GenerationContext context = directive.context(); - // Register all middleware MiddlewareBuilder middlewareBuilder = new MiddlewareBuilder(); middlewareBuilder.addDefaultMiddleware(context); @@ -146,23 +141,23 @@ public void generateService(GenerateServiceDirective m.toString()).collect(Collectors.joining(","))); - ConfigGenerator configGenerator = new ConfigGenerator(context); - configGenerator.render(clientConfigList); + ConfigGenerator configGenerator = new ConfigGenerator(directive, clientConfigList); + configGenerator.render(); configGenerator.renderRbs(); LOGGER.info("generated config"); - ClientGenerator clientGenerator = new ClientGenerator(context); - clientGenerator.render(middlewareBuilder); + ClientGenerator clientGenerator = new ClientGenerator(directive, middlewareBuilder); + clientGenerator.render(); clientGenerator.renderRbs(); LOGGER.info("generated client"); + + WaitersGenerator waitersGenerator = new WaitersGenerator(directive); + waitersGenerator.render(); + waitersGenerator.renderRbs(); } @Override public void customizeBeforeShapeGeneration(CustomizeDirective directive) { - this.structureGenerator = new StructureGenerator(directive); - this.unionGenerator = new UnionGenerator(directive); - this.enumGenerator = new EnumGenerator(directive); - this.intEnumGenerator = new IntEnumGenerator(directive); this.typesFileBlockGenerator = new TypesFileBlockGenerator(directive); // Pre-populate module blocks for types.rb and types.rbs files @@ -171,7 +166,7 @@ public void customizeBeforeShapeGeneration(CustomizeDirective directive) { - structureGenerator.accept(directive); + new StructureGenerator(directive).render(); } @Override @@ -179,29 +174,29 @@ public void generateError(GenerateErrorDirective directive) { - unionGenerator.accept(directive); + new UnionGenerator(directive).render(); } @Override public void generateEnumShape(GenerateEnumDirective directive) { - enumGenerator.accept(directive); + new EnumGenerator(directive).render(); } @Override public void generateIntEnumShape(GenerateIntEnumDirective directive) { - intEnumGenerator.accept(directive); + new IntEnumGenerator(directive).render(); } @Override public void customizeBeforeIntegrations(CustomizeDirective directive) { GenerationContext context = directive.context(); - new ParamsGenerator(context).render(); - new ValidatorsGenerator(context).render(); + new ParamsGenerator(directive).render(); + new ValidatorsGenerator(directive).render(); if (directive.context().protocolGenerator().isPresent()) { ProtocolGenerator generator = directive.context().protocolGenerator().get(); @@ -210,20 +205,11 @@ public void customizeBeforeIntegrations(CustomizeDirective additionalFiles = context.integrations().stream() - .map((integration) -> integration.writeAdditionalFiles(context)) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - new ModuleGenerator(context).render(additionalFiles); + new ModuleGenerator(directive).render(); new GemspecGenerator(context).render(); new YardOptsGenerator(context).render(); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java index bc0c63f86..df2fff3ed 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java @@ -18,13 +18,9 @@ import java.util.Comparator; import java.util.List; import java.util.Set; -import java.util.TreeSet; import java.util.logging.Logger; -import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.Symbol; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; @@ -44,61 +40,59 @@ * Generate the service Client. */ @SmithyInternalApi -public class ClientGenerator { +public class ClientGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ClientGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final SymbolProvider symbolProvider; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; + private final Set operations; + + private final MiddlewareBuilder middlewareBuilder; + private boolean hasStreamingOperation; - /** - * @param context generation context - */ - public ClientGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule()); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule()); - this.symbolProvider = context.symbolProvider(); + public ClientGenerator( + GenerateServiceDirective directive, + MiddlewareBuilder middlewareBuilder + ) { + super(directive); this.hasStreamingOperation = false; + this.operations = directive.operations(); + this.middlewareBuilder = middlewareBuilder; + } + + @Override + String getModule() { + return "Client"; } /** * Render/Generate the service client. - * - * @param middlewareBuilder set of middleware to be added to the client */ - public void render(MiddlewareBuilder middlewareBuilder) { - FileManifest fileManifest = context.fileManifest(); + public void render() { + List additionalFiles = middlewareBuilder.writeAdditionalFiles(context); - writer.includePreamble().includeRequires(); + write(writer -> { - List additionalFiles = - middlewareBuilder.writeAdditionalFiles(context); - for (String require : additionalFiles) { - writer.write("require_relative '$L'", removeRbExtension(require)); - LOGGER.finer("Adding client require: " + require); - } + writer.includePreamble().includeRequires(); - if (additionalFiles.size() > 0) { - writer.write(""); - } + for (String require : additionalFiles) { + writer.write("require_relative '$L'", removeRbExtension(require)); + LOGGER.finer("Adding client require: " + require); + } - writer + if (additionalFiles.size() > 0) { + writer.write(""); + } + + writer .openBlock("module $L", settings.getModule()) .write("# An API client for $L", settings.getService().getName()) .write("# See {#initialize} for a full list of supported configuration options"); - String documentation = new ShapeDocumentationGenerator(model, symbolProvider, context.service()).render(); + String documentation = new ShapeDocumentationGenerator(model, symbolProvider, context.service()).render(); - writer + writer .writeInline("$L", documentation) .openBlock("class Client") .write("include $T", Hearth.CLIENT_STUBS) @@ -106,48 +100,41 @@ public void render(MiddlewareBuilder middlewareBuilder) { .openBlock("\ndef self.middleware") .write("@middleware") .closeBlock("end\n") - .call(() -> renderInitializeMethod()) - .call(() -> renderOperations(middlewareBuilder)) + .call(() -> renderInitializeMethod(writer)) + .call(() -> renderOperations(writer)) .write("\nprivate") - .call(() -> renderApplyMiddlewareMethod()) + .call(() -> renderApplyMiddlewareMethod(writer)) .call(() -> { if (hasStreamingOperation) { - renderOutputStreamMethod(); + renderOutputStreamMethod(writer); } }) .closeBlock("end") .closeBlock("end"); + }); - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() - + "/client.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote client to " + fileName); + LOGGER.fine("Wrote client to " + rbFile()); } /** * Render/generate the RBS types for the client. */ public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .includePreamble() .openBlock("module $L", settings.getModule()) .openBlock("class Client") .write("include $T\n", Hearth.CLIENT_STUBS) .write("def self.middleware: () -> untyped\n") .write("def initialize: (?untyped config, ?::Hash[untyped, untyped] options) -> void") - .call(() -> renderRbsOperations()) + .call(() -> renderRbsOperations(writer)) .write("") .closeBlock("end") .closeBlock("end"); + }); - String fileName = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/client.rbs"; - fileManifest.writeFile(fileName, rbsWriter.toString()); - LOGGER.fine("Wrote client rbs to " + fileName); + LOGGER.fine("Wrote client rbs to " + rbsFile()); } private Object removeRbExtension(String s) { @@ -157,7 +144,7 @@ private Object removeRbExtension(String s) { return s; } - private void renderInitializeMethod() { + private void renderInitializeMethod(RubyCodeWriter writer) { writer .writeYardParam("Config", "config", "An instance of {Config}") .openBlock("def initialize(config = $L::Config.new, options = {})", settings.getModule()) @@ -169,30 +156,20 @@ private void renderInitializeMethod() { .closeBlock("end"); } - private void renderOperations(MiddlewareBuilder middlewareBuilder) { - // Generate each operation for the service. We do this here instead of via the operation visitor method to - // limit it to the operations bound to the service. - TopDownIndex topDownIndex = TopDownIndex.of(model); - Set containedOperations = new TreeSet<>( - topDownIndex.getContainedOperations(context.service())); - containedOperations.stream() - .filter((o) -> !Streaming.isEventStreaming(model, o)) - .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach(o -> renderOperation(o, middlewareBuilder)); + private void renderOperations(RubyCodeWriter writer) { + operations.stream() + .filter((o) -> !Streaming.isEventStreaming(model, o)) + .sorted(Comparator.comparing((o) -> o.getId().getName())) + .forEach(o -> renderOperation(writer, o)); } - private void renderRbsOperations() { - // Generate each operation for the service. We do this here instead of via the operation visitor method to - // limit it to the operations bound to the service. - TopDownIndex topDownIndex = TopDownIndex.of(model); - Set containedOperations = new TreeSet<>( - topDownIndex.getContainedOperations(context.service())); - containedOperations.stream() - .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach(o -> renderRbsOperation(o)); + private void renderRbsOperations(RubyCodeWriter writer) { + operations.stream() + .sorted(Comparator.comparing((o) -> o.getId().getName())) + .forEach(o -> renderRbsOperation(writer, o)); } - private void renderOperation(OperationShape operation, MiddlewareBuilder middlewareBuilder) { + private void renderOperation(RubyCodeWriter writer, OperationShape operation) { Symbol symbol = symbolProvider.toSymbol(operation); ShapeId inputShapeId = operation.getInputShape(); Shape inputShape = model.expectShape(inputShapeId); @@ -240,16 +217,16 @@ private void renderOperation(OperationShape operation, MiddlewareBuilder middlew LOGGER.finer("Generated client operation method " + operationName); } - private void renderRbsOperation(OperationShape operation) { + private void renderRbsOperation(RubyCodeWriter writer, OperationShape operation) { Symbol symbol = symbolProvider.toSymbol(operation); String operationName = RubyFormatter.toSnakeCase(symbol.getName()); - rbsWriter.write("def $L: (?::Hash[untyped, untyped] params, ?::Hash[untyped, untyped] options)" + writer.write("def $L: (?::Hash[untyped, untyped] params, ?::Hash[untyped, untyped] options)" + "{ () -> untyped } -> untyped", operationName); } - private void renderApplyMiddlewareMethod() { + private void renderApplyMiddlewareMethod(RubyCodeWriter writer) { writer .openBlock( "\ndef apply_middleware(middleware_stack, middleware)") @@ -259,7 +236,7 @@ private void renderApplyMiddlewareMethod() { .closeBlock("end"); } - private void renderOutputStreamMethod() { + private void renderOutputStreamMethod(RubyCodeWriter writer) { writer .openBlock("\ndef output_stream(options = {}, &block)") .write("return options[:output_stream] if options[:output_stream]") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java index 5eb64ad2c..c02ea9508 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java @@ -18,9 +18,7 @@ import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.Hearth; import software.amazon.smithy.ruby.codegen.RubyCodeWriter; @@ -34,51 +32,40 @@ * Generate Config class for a Client. */ @SmithyInternalApi -public class ConfigGenerator { +public class ConfigGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ConfigGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; + private final List clientConfigList; - /** - * @param context generation context - */ - public ConfigGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Config"); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule() + "::Config"); - this.symbolProvider = context.symbolProvider(); + public ConfigGenerator( + ContextualDirective directive, List clientConfigList) { + super(directive); + this.clientConfigList = clientConfigList; } - /** - * Render/Generate the Config for the client. - * @param clientConfigList list of config to apply to the client. - */ - public void render(List clientConfigList) { - FileManifest fileManifest = context.fileManifest(); + @Override + String getModule() { + return "Config"; + } - String membersBlock = "nil"; - if (!clientConfigList.isEmpty()) { - membersBlock = clientConfigList + public void render() { + write(writer -> { + String membersBlock = "nil"; + if (!clientConfigList.isEmpty()) { + membersBlock = clientConfigList .stream() .map(clientConfig -> RubyFormatter.asSymbol( RubySymbolProvider.toMemberName(clientConfig.getName()))) .collect(Collectors.joining(",\n")); - } - membersBlock += ","; + } + membersBlock += ","; - writer + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) - .call(() -> renderConfigDocumentation(clientConfigList)) + .call(() -> renderConfigDocumentation(writer)) .openBlock("Config = ::Struct.new(") .write(membersBlock) .write("keyword_init: true") @@ -86,38 +73,30 @@ public void render(List clientConfigList) { .indent() .write("include $T", Hearth.CONFIGURATION) .write("\nprivate\n") - .call(() -> renderValidateMethod(clientConfigList)) + .call(() -> renderValidateMethod(writer)) .write("") - .call(() -> renderDefaultsMethod(clientConfigList)) + .call(() -> renderDefaultsMethod(writer)) .closeBlock("end") .closeBlock("end\n"); + }); - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() - + "/config.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote config to " + fileName); + LOGGER.fine("Wrote config to " + rbFile()); } /** * Render/generate the RBS types for Config. */ public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .openBlock("module $L", settings.getModule()) .write("Config: untyped") .closeBlock("end"); - - String fileName = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/config.rbs"; - fileManifest.writeFile(fileName, rbsWriter.toString()); - LOGGER.fine("Wrote config rbs to " + fileName); + }); + LOGGER.fine("Wrote config rbs to " + rbsFile()); } - private void renderConfigDocumentation(List clientConfigList) { + private void renderConfigDocumentation(RubyCodeWriter writer) { writer.writeYardMethod("initialize(*options)", () -> { clientConfigList.forEach((clientConfig) -> { String member = RubyFormatter.asSymbol(RubySymbolProvider.toMemberName(clientConfig.getName())); @@ -135,7 +114,7 @@ private void renderConfigDocumentation(List clientConfigList) { }); } - private void renderValidateMethod(List clientConfigList) { + private void renderValidateMethod(RubyCodeWriter writer) { writer.openBlock("def validate!"); clientConfigList.stream().forEach(clientConfig -> { String member = RubySymbolProvider.toMemberName(clientConfig.getName()); @@ -150,7 +129,7 @@ private void renderValidateMethod(List clientConfigList) { writer.closeBlock("end"); } - private void renderDefaultsMethod(List clientConfigList) { + private void renderDefaultsMethod(RubyCodeWriter writer) { writer .openBlock("def self.defaults") .openBlock("@defaults ||= {"); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java index 82fc44095..a4f346abd 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java @@ -16,26 +16,32 @@ package software.amazon.smithy.ruby.codegen.generators; import java.util.List; -import java.util.function.Consumer; import java.util.stream.Collectors; -import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.codegen.core.directed.GenerateEnumDirective; +import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.utils.SmithyInternalApi; -public final class EnumGenerator extends TypesFileGenerator - implements Consumer> { +@SmithyInternalApi +public final class EnumGenerator extends RubyGeneratorBase { - public EnumGenerator(ContextualDirective directive) { + private final Shape shape; + + public EnumGenerator(GenerateEnumDirective directive) { super(directive); + this.shape = directive.shape(); } @Override - public void accept(GenerateEnumDirective directive) { - var shape = directive.shape(); - directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + String getModule() { + return "Types"; + } + + public void render() { + write(writer -> { final EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); List enumDefinitions = enumTrait.getValues().stream() @@ -74,7 +80,7 @@ public void accept(GenerateEnumDirective direct } }); - directive.context().writerDelegator().useFileWriter(rbsFile(), nameSpace(), writer -> { + writeRbs(writer -> { // Only write out string shapes for enums EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); List enumDefinitions = enumTrait.getValues().stream() diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java index a821aaf74..b989605ea 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java @@ -15,25 +15,29 @@ package software.amazon.smithy.ruby.codegen.generators; -import java.util.function.Consumer; -import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.codegen.core.directed.GenerateIntEnumDirective; import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.utils.SmithyInternalApi; -public final class IntEnumGenerator extends TypesFileGenerator - implements Consumer> { +@SmithyInternalApi +public final class IntEnumGenerator extends RubyGeneratorBase { - public IntEnumGenerator(ContextualDirective directive) { + private final IntEnumShape shape; + + public IntEnumGenerator(GenerateIntEnumDirective directive) { super(directive); + this.shape = (IntEnumShape) directive.shape(); } @Override - public void accept(GenerateIntEnumDirective directive) { - var shape = (IntEnumShape) directive.shape(); + String getModule() { + return "Types"; + } - directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + public void render() { + write(writer -> { // only write out a module if there is at least one enum constant if (shape.getEnumValues().size() > 0) { String shapeName = symbolProvider.toSymbol(shape).getName(); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java index a3d57c045..68148751b 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java @@ -15,11 +15,11 @@ package software.amazon.smithy.ruby.codegen.generators; +import java.util.Collection; import java.util.List; import java.util.logging.Logger; -import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.ruby.codegen.GenerationContext; -import software.amazon.smithy.ruby.codegen.RubyCodeWriter; import software.amazon.smithy.ruby.codegen.RubySettings; import software.amazon.smithy.utils.SmithyInternalApi; @@ -34,42 +34,44 @@ public class ModuleGenerator { }; private final GenerationContext context; + private final RubySettings settings; - public ModuleGenerator(GenerationContext context) { - this.context = context; + public ModuleGenerator(ContextualDirective directive) { + this.context = directive.context(); + this.settings = directive.settings(); } - public void render(List additionalFiles) { - FileManifest fileManifest = context.fileManifest(); - RubySettings settings = context.settings(); - RubyCodeWriter writer = new RubyCodeWriter(context.settings().getModule()); + public void render() { + List additionalFiles = context.integrations().stream() + .map((integration) -> integration.writeAdditionalFiles(context)) + .flatMap(Collection::stream) + .toList(); - writer.includePreamble().includeRequires(); - context.getRubyDependencies().forEach((rubyDependency -> { - writer.write("require '$L'", rubyDependency.getImportPath()); - })); - writer.write("\n"); + String fileName = + settings.getGemName() + "/lib/" + settings.getGemName() + ".rb"; + + context.writerDelegator().useFileWriter(fileName, settings.getModule(), writer -> { + writer.includePreamble().includeRequires(); + context.getRubyDependencies().forEach((rubyDependency -> { + writer.write("require '$L'", rubyDependency.getImportPath()); + })); + writer.write("\n"); - for (String require : DEFAULT_REQUIRES) { - writer.write("require_relative '$L/$L'", settings.getGemName(), - require); - } + for (String require : DEFAULT_REQUIRES) { + writer.write("require_relative '$L/$L'", settings.getGemName(), require); + } - for (String require : additionalFiles) { - writer.write("require_relative '$L'", require); - LOGGER.finer("Adding additional module require: " + require); - } + for (String require : additionalFiles) { + writer.write("require_relative '$L'", require); + LOGGER.finer("Adding additional module require: " + require); + } - writer.write(""); + writer.write(""); - writer.openBlock("module $L", settings.getModule()) + writer.openBlock("module $L", settings.getModule()) .write("GEM_VERSION = '$L'", settings.getGemVersion()) .closeBlock("end"); - - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() + ".rb"; - - fileManifest.writeFile(fileName, writer.toString()); + }); LOGGER.fine("Wrote module file to " + fileName); } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java index d56c1877b..c2755fca6 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java @@ -18,9 +18,7 @@ import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.model.knowledge.PaginatedIndex; import software.amazon.smithy.model.knowledge.PaginationInfo; import software.amazon.smithy.model.knowledge.TopDownIndex; @@ -31,65 +29,51 @@ import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi -public class PaginatorsGenerator { +public class PaginatorsGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(PaginatorsGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; - - public PaginatorsGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Paginators"); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule() + "::Paginators"); - this.symbolProvider = context.symbolProvider(); + public PaginatorsGenerator(ContextualDirective directive) { + super(directive); } - public void render() { - FileManifest fileManifest = context.fileManifest(); + @Override + String getModule() { + return "Paginators"; + } - writer + public void render() { + write(writer -> { + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) .openBlock("module Paginators") - .call(() -> renderPaginators()) + .call(() -> renderPaginators(writer)) .write("") .closeBlock("end") .closeBlock("end"); - String fileName = settings.getGemName() + "/lib/" + settings.getGemName() + "/paginators.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote paginators to " + fileName); + }); + LOGGER.fine("Wrote paginators to " + rbFile()); } public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .includePreamble() .openBlock("module $L", settings.getModule()) .openBlock("module Paginators") - .call(() -> renderRbsPaginators()) + .call(() -> renderRbsPaginators(writer)) .write("") .closeBlock("end") .closeBlock("end"); - - String typesFile = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/paginators.rbs"; - fileManifest.writeFile(typesFile, rbsWriter.toString()); - LOGGER.fine("Wrote paginators types to " + typesFile); + }); + LOGGER.fine("Wrote paginators types to " + rbsFile()); } - private void renderPaginators() { + private void renderPaginators(RubyCodeWriter writer) { TopDownIndex topDownIndex = TopDownIndex.of(model); PaginatedIndex paginatedIndex = PaginatedIndex.of(model); @@ -99,12 +83,12 @@ private void renderPaginators() { if (paginationInfoOptional.isPresent()) { PaginationInfo paginationInfo = paginationInfoOptional.get(); String operationName = symbolProvider.toSymbol(operation).getName(); - renderPaginator(operationName, paginationInfo); + renderPaginator(writer, operationName, paginationInfo); } }); } - private void renderRbsPaginators() { + private void renderRbsPaginators(RubyCodeWriter writer) { TopDownIndex topDownIndex = TopDownIndex.of(model); PaginatedIndex paginatedIndex = PaginatedIndex.of(model); @@ -114,33 +98,33 @@ private void renderRbsPaginators() { if (paginationInfoOptional.isPresent()) { PaginationInfo paginationInfo = paginationInfoOptional.get(); String operationName = symbolProvider.toSymbol(operation).getName(); - renderRbsPaginator(operationName, paginationInfo); + renderRbsPaginator(writer, operationName, paginationInfo); } }); } - private void renderPaginator(String operationName, PaginationInfo paginationInfo) { + private void renderPaginator(RubyCodeWriter writer, String operationName, PaginationInfo paginationInfo) { writer .write("") .openBlock("class $L", operationName) - .call(() -> renderPaginatorInitializeDocumentation(operationName)) + .call(() -> renderPaginatorInitializeDocumentation(writer, operationName)) .openBlock("def initialize(client, params = {}, options = {})") .write("@params = params") .write("@options = options") .write("@client = client") .closeBlock("end") - .call(() -> renderPaginatorPages(operationName, paginationInfo)) + .call(() -> renderPaginatorPages(writer, operationName, paginationInfo)) .call(() -> { if (!paginationInfo.getItemsMemberPath().isEmpty()) { - renderPaginatorItems(paginationInfo, operationName); + renderPaginatorItems(writer, paginationInfo, operationName); } }) .closeBlock("end"); LOGGER.finer("Generated paginator for " + operationName); } - private void renderRbsPaginator(String operationName, PaginationInfo paginationInfo) { - rbsWriter + private void renderRbsPaginator(RubyCodeWriter writer, String operationName, PaginationInfo paginationInfo) { + writer .write("") .openBlock("class $L", operationName) .write("def initialize: (untyped client, ?::Hash[untyped, untyped] params, " @@ -148,13 +132,13 @@ private void renderRbsPaginator(String operationName, PaginationInfo paginationI .write("def pages: () -> untyped") .call(() -> { if (!paginationInfo.getItemsMemberPath().isEmpty()) { - rbsWriter.write("def items: () -> untyped"); + writer.write("def items: () -> untyped"); } }) .closeBlock("end"); } - private void renderPaginatorInitializeDocumentation(String operationName) { + private void renderPaginatorInitializeDocumentation(RubyCodeWriter writer, String operationName) { String snakeOperationName = RubyFormatter.toSnakeCase(operationName); writer.writeDocs((w) -> w @@ -163,7 +147,7 @@ private void renderPaginatorInitializeDocumentation(String operationName) { .write("@param [Hash] options (see Client#$L)", snakeOperationName)); } - private void renderPaginatorPages(String operationName, PaginationInfo paginationInfo) { + private void renderPaginatorPages(RubyCodeWriter writer, String operationName, PaginationInfo paginationInfo) { String inputToken = symbolProvider.toMemberName(paginationInfo.getInputTokenMember()); String outputToken = paginationInfo.getOutputTokenMemberPath().stream() .map((member) -> symbolProvider.toMemberName(member)) @@ -171,7 +155,7 @@ private void renderPaginatorPages(String operationName, PaginationInfo paginatio String snakeOperationName = RubyFormatter.toSnakeCase(operationName); writer - .call(() -> renderPaginatorPagesDocumentation(snakeOperationName)) + .call(() -> renderPaginatorPagesDocumentation(writer, snakeOperationName)) .openBlock("def pages") .write("params = @params") .openBlock("Enumerator.new do |e|") @@ -190,20 +174,20 @@ private void renderPaginatorPages(String operationName, PaginationInfo paginatio .closeBlock("end"); } - private void renderPaginatorPagesDocumentation(String snakeOperationName) { + private void renderPaginatorPagesDocumentation(RubyCodeWriter writer, String snakeOperationName) { writer.writeDocs((w) -> w .write("Iterate all response pages of the $L operation.", snakeOperationName) .write("@return [Enumerator]")); } - private void renderPaginatorItems(PaginationInfo paginationInfo, String operationName) { + private void renderPaginatorItems(RubyCodeWriter writer, PaginationInfo paginationInfo, String operationName) { String items = paginationInfo.getItemsMemberPath().stream() .map((member) -> symbolProvider.toMemberName(member)) .collect(Collectors.joining("&.")); writer .write("") - .call(() -> renderPaginatorItemsDocumentation(operationName)) + .call(() -> renderPaginatorItemsDocumentation(writer, operationName)) .openBlock("def items") .openBlock("Enumerator.new do |e|") .openBlock("pages.each do |page|") @@ -215,7 +199,7 @@ private void renderPaginatorItems(PaginationInfo paginationInfo, String operatio .closeBlock("end"); } - private void renderPaginatorItemsDocumentation(String operationName) { + private void renderPaginatorItemsDocumentation(RubyCodeWriter writer, String operationName) { String snakeOperationName = RubyFormatter.toSnakeCase(operationName); writer.writeDocs((w) -> w .write("Iterate all items from pages in the $L operation.", snakeOperationName) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java index c5f85d145..aeb793892 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java @@ -20,9 +20,9 @@ import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.neighbor.Walker; import software.amazon.smithy.model.node.Node; @@ -63,44 +63,34 @@ import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi -public class ParamsGenerator extends ShapeVisitor.Default { +public class ParamsGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ParamsGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - - public ParamsGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Params"); - this.symbolProvider = context.symbolProvider(); + public ParamsGenerator(ContextualDirective directive) { + super(directive); } - public void render() { - FileManifest fileManifest = context.fileManifest(); + @Override + String getModule() { + return "Params"; + } - writer + public void render() { + write(writer -> { + writer .includePreamble() .includeRequires() .addModule(settings.getModule()) .addModule("Params") - .call(() -> renderParams()) + .call(() -> renderParams(writer)) .write("") .closeAllModules(); - - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() - + "/params.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote params to " + fileName); + }); + LOGGER.fine("Wrote params to " + rbFile()); } - private void renderParams() { + private void renderParams(RubyCodeWriter writer) { Model modelWithoutTraitShapes = ModelTransformer.create() .getModelWithoutTraitShapes(model); @@ -108,17 +98,25 @@ private void renderParams() { .walkShapes(context.service()) .stream() .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach((shape) -> shape.accept(this)); + .forEach((shape) -> shape.accept(new Visitor(writer))); } - @Override - protected Void getDefault(Shape shape) { - return null; - } + private final class Visitor extends ShapeVisitor.Default { - @Override - public Void structureShape(StructureShape structureShape) { - writer + private final RubyCodeWriter writer; + + private Visitor(RubyCodeWriter writer) { + this.writer = writer; + } + + @Override + protected Void getDefault(Shape shape) { + return null; + } + + @Override + public Void structureShape(StructureShape structureShape) { + writer .write("") .openBlock("module $L", symbolProvider.toSymbol(structureShape).getName()) .openBlock("def self.build(params, context: '')") @@ -126,35 +124,35 @@ public Void structureShape(StructureShape structureShape) { context.symbolProvider().toSymbol(structureShape), structureShape.members())) .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - private void renderBuilderForStructureMembers(Symbol symbol, Collection members) { - writer + private void renderBuilderForStructureMembers(Symbol symbol, Collection members) { + writer .write("$T.validate_types!(params, ::Hash, $T, context: context)", Hearth.VALIDATOR, symbol) .write("type = $T.new", symbol); - members.forEach(member -> { - Shape target = model.expectShape(member.getTarget()); - String memberName = symbolProvider.toMemberName(member); - String memberSetter = "type." + memberName + " = "; - String symbolName = RubyFormatter.asSymbol(memberName); - String input = "params[" + symbolName + "]"; - String contextKey = "\"#{context}[" + symbolName + "]\""; - target.accept(new MemberBuilder(model, writer, context.symbolProvider(), + members.forEach(member -> { + Shape target = model.expectShape(member.getTarget()); + String memberName = symbolProvider.toMemberName(member); + String memberSetter = "type." + memberName + " = "; + String symbolName = RubyFormatter.asSymbol(memberName); + String input = "params[" + symbolName + "]"; + String contextKey = "\"#{context}[" + symbolName + "]\""; + target.accept(new MemberBuilder(model, writer, context.symbolProvider(), memberSetter, input, contextKey, member, true)); - }); + }); - writer.write("type"); - } + writer.write("type"); + } - @Override - public Void listShape(ListShape listShape) { - Shape memberTarget = + @Override + public Void listShape(ListShape listShape) { + Shape memberTarget = model.expectShape(listShape.getMember().getTarget()); - writer + writer .write("") .openBlock("module $L", symbolProvider.toSymbol(listShape).getName()) .openBlock("def self.build(params, context: '')") @@ -176,14 +174,14 @@ public Void listShape(ListShape listShape) { .write("data") .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void mapShape(MapShape mapShape) { - Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); + @Override + public Void mapShape(MapShape mapShape) { + Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); - writer + writer .write("") .openBlock("module $L", symbolProvider.toSymbol(mapShape).getName()) .openBlock("def self.build(params, context: '')") @@ -198,15 +196,15 @@ public Void mapShape(MapShape mapShape) { .write("data") .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void unionShape(UnionShape shape) { - String name = symbolProvider.toSymbol(shape).getName(); - Symbol typeSymbol = context.symbolProvider().toSymbol(shape); + @Override + public Void unionShape(UnionShape shape) { + String name = symbolProvider.toSymbol(shape).getName(); + Symbol typeSymbol = context.symbolProvider().toSymbol(shape); - writer + writer .write("") .openBlock("module $L", name) .openBlock("def self.build(params, context: '')") @@ -222,326 +220,327 @@ public Void unionShape(UnionShape shape) { .write("key, value = params.flatten") .write("case key"); //start a case statement. This does NOT indent - for (MemberShape member : shape.members()) { - Shape target = model.expectShape(member.getTarget()); - String memberClassName = symbolProvider.toMemberName(member); - String memberName = RubyFormatter.asSymbol(memberClassName); - writer.write("when $L", memberName) + for (MemberShape member : shape.members()) { + Shape target = model.expectShape(member.getTarget()); + String memberClassName = symbolProvider.toMemberName(member); + String memberName = RubyFormatter.asSymbol(memberClassName); + writer.write("when $L", memberName) .indent() .openBlock("$T.new(", context.symbolProvider().toSymbol(member)); - String input = "params[" + memberName + "]"; - String contextString = "\"#{context}[" + memberName + "]\""; - target.accept(new MemberBuilder(model, writer, symbolProvider, "", input, contextString, - member, false)); - writer.closeBlock(")") + String input = "params[" + memberName + "]"; + String contextString = "\"#{context}[" + memberName + "]\""; + target.accept(new MemberBuilder(model, writer, symbolProvider, "", input, contextString, + member, false)); + writer.closeBlock(")") .dedent(); - } - String expectedMembers = + } + String expectedMembers = shape.members().stream().map((member) -> RubyFormatter.asSymbol(member.getMemberName())) .collect(Collectors.joining(", ")); - writer.write("else") + writer.write("else") .indent() .write("raise ArgumentError,") .indent(3) .write("\"Expected #{context} to have one of $L set\"", expectedMembers) .dedent(4); - writer.write("end") //end of case statement, NOT indented + writer.write("end") //end of case statement, NOT indented .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - private boolean isComplexShape(Shape shape) { - return shape.isStructureShape() || shape.isListShape() || shape.isMapShape() + private boolean isComplexShape(Shape shape) { + return shape.isStructureShape() || shape.isListShape() || shape.isMapShape() || shape.isUnionShape() || shape.isOperationShape(); - } + } - private static class MemberBuilder extends ShapeVisitor.Default { - private final Model model; - private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - private final String memberSetter; - private final String input; - private final String context; - private final MemberShape memberShape; - private final Optional defaultValue; - private final boolean checkRequired; - private final String rubySymbol; - - MemberBuilder( - Model model, - RubyCodeWriter writer, - SymbolProvider symbolProvider, - String memberSetter, - String input, - String context, - MemberShape memberShape, - boolean checkRequired - ) { - this.model = model; - this.writer = writer; - this.symbolProvider = symbolProvider; - this.memberSetter = memberSetter; - this.input = input; - this.context = context; - this.memberShape = memberShape; - this.checkRequired = checkRequired; - this.rubySymbol = RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape)); - - // Note: No need to check for box trait for V1 Smithy models. - // Smithy convert V1 to V2 model and populate Default trait automatically - boolean containsRequiredAndDefaultTraits = - memberShape.hasTrait(DefaultTrait.class) && memberShape.hasTrait(RequiredTrait.class); - - if (containsRequiredAndDefaultTraits) { - Shape targetShape = model.expectShape(memberShape.getTarget()); - this.defaultValue = Optional.of(targetShape.accept(new DefaultValueRetriever(model, memberShape))); - } else { - this.defaultValue = Optional.empty(); + private static class MemberBuilder extends ShapeVisitor.Default { + private final Model model; + private final RubyCodeWriter writer; + private final SymbolProvider symbolProvider; + private final String memberSetter; + private final String input; + private final String context; + private final MemberShape memberShape; + private final Optional defaultValue; + private final boolean checkRequired; + private final String rubySymbol; + + MemberBuilder( + Model model, + RubyCodeWriter writer, + SymbolProvider symbolProvider, + String memberSetter, + String input, + String context, + MemberShape memberShape, + boolean checkRequired + ) { + this.model = model; + this.writer = writer; + this.symbolProvider = symbolProvider; + this.memberSetter = memberSetter; + this.input = input; + this.context = context; + this.memberShape = memberShape; + this.checkRequired = checkRequired; + this.rubySymbol = RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape)); + + // Note: No need to check for box trait for V1 Smithy models. + // Smithy convert V1 to V2 model and populate Default trait automatically + boolean containsRequiredAndDefaultTraits = + memberShape.hasTrait(DefaultTrait.class) && memberShape.hasTrait(RequiredTrait.class); + + if (containsRequiredAndDefaultTraits) { + Shape targetShape = model.expectShape(memberShape.getTarget()); + this.defaultValue = Optional.of(targetShape.accept(new DefaultValueRetriever(model, memberShape))); + } else { + this.defaultValue = Optional.empty(); + } } - } - @Override - protected Void getDefault(Shape shape) { - if (defaultValue.isPresent()) { - writer.write("$1Lparams.fetch($2L, $3L)", memberSetter, rubySymbol, defaultValue.get()); - } else { - writer.write("$L$L", memberSetter, input); + @Override + protected Void getDefault(Shape shape) { + if (defaultValue.isPresent()) { + writer.write("$1Lparams.fetch($2L, $3L)", memberSetter, rubySymbol, defaultValue.get()); + } else { + writer.write("$L$L", memberSetter, input); + } + return null; } - return null; - } - @Override - public Void blobShape(BlobShape shape) { - if (shape.hasTrait(StreamingTrait.class)) { - writer + @Override + public Void blobShape(BlobShape shape) { + if (shape.hasTrait(StreamingTrait.class)) { + writer .write("io = $L || StringIO.new", input) .openBlock("unless io.respond_to?(:read) " + "|| io.respond_to?(:readpartial)") .write("io = StringIO.new(io)") .closeBlock("end") .write("$Lio", memberSetter); - } else { - getDefault(shape); + } else { + getDefault(shape); + } + return null; } - return null; - } - @Override - public Void stringShape(StringShape shape) { - if (memberShape.hasTrait(IdempotencyTokenTrait.class) - || shape.hasTrait(IdempotencyTokenTrait.class)) { - writer.write("$L$L || $T.uuid", memberSetter, input, RubyImportContainer.SECURE_RANDOM); - } else { - getDefault(shape); - } - return null; - } - - @Override - public Void listShape(ListShape shape) { - defaultComplex(shape); - return null; - } - - @Override - public Void mapShape(MapShape shape) { - defaultComplex(shape); - return null; - } - - @Override - public Void structureShape(StructureShape shape) { - defaultComplex(shape); - return null; - } - - @Override - public Void unionShape(UnionShape shape) { - defaultComplex(shape); - return null; - } - - private void defaultComplex(Shape shape) { - if (defaultValue.isPresent()) { - if (checkRequired) { - writer.write("$1L$2L.build(params.fetch($3L, $5L), context: $4L)", - memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, - defaultValue.get()); + @Override + public Void stringShape(StringShape shape) { + if (memberShape.hasTrait(IdempotencyTokenTrait.class) + || shape.hasTrait(IdempotencyTokenTrait.class)) { + writer.write("$L$L || $T.uuid", memberSetter, input, RubyImportContainer.SECURE_RANDOM); } else { - writer.write("$1L($2L.build(params.fetch($3L, $5L), context: $4L))", - memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, - defaultValue.get()); + getDefault(shape); } - return; + return null; } - if (checkRequired) { - writer.write("$1L$2L.build($3L, context: $4L) unless $3L.nil?", memberSetter, - symbolProvider.toSymbol(shape).getName(), input, context); - } else { - writer.write("$1L($2L.build($3L, context: $4L) unless $3L.nil?)", memberSetter, - symbolProvider.toSymbol(shape).getName(), input, context); + @Override + public Void listShape(ListShape shape) { + defaultComplex(shape); + return null; } - } - } - /** - * Default value constrains: - * enum: can be set to any valid string value of the enum. - * intEnum: can be set to any valid integer value of the enum. - * document: can be set to null, `true, false, string, numbers, an empty list, or an empty map. - * list: can only be set to an empty list. - * map: can only be set to an empty map. - * structure: no default value. - * union: no default value. - * - * See https://awslabs.github.io/smithy/2.0/spec/type-refinement-traits.html?highlight=required#default-value-constraints - */ - private static final class DefaultValueRetriever extends ShapeVisitor.Default { - - private final MemberShape memberShape; - private final Node defaultNode; - private final Model model; - - private DefaultValueRetriever(Model model, MemberShape memberShape) { - this.model = model; - this.memberShape = memberShape; - this.defaultNode = memberShape.expectTrait(DefaultTrait.class).toNode(); - } + @Override + public Void mapShape(MapShape shape) { + defaultComplex(shape); + return null; + } - @Override - protected String getDefault(Shape shape) { - return "nil"; - } + @Override + public Void structureShape(StructureShape shape) { + defaultComplex(shape); + return null; + } - @Override - public String blobShape(BlobShape shape) { - return getDefaultString(); - } + @Override + public Void unionShape(UnionShape shape) { + defaultComplex(shape); + return null; + } - @Override - public String booleanShape(BooleanShape shape) { - return getDefaultBoolean(); - } + private void defaultComplex(Shape shape) { + if (defaultValue.isPresent()) { + if (checkRequired) { + writer.write("$1L$2L.build(params.fetch($3L, $5L), context: $4L)", + memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, + defaultValue.get()); + } else { + writer.write("$1L($2L.build(params.fetch($3L, $5L), context: $4L))", + memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, + defaultValue.get()); + } + return; + } - @Override - public String stringShape(StringShape shape) { - return getDefaultString(); + if (checkRequired) { + writer.write("$1L$2L.build($3L, context: $4L) unless $3L.nil?", memberSetter, + symbolProvider.toSymbol(shape).getName(), input, context); + } else { + writer.write("$1L($2L.build($3L, context: $4L) unless $3L.nil?)", memberSetter, + symbolProvider.toSymbol(shape).getName(), input, context); + } + } } - @Override - public String byteShape(ByteShape shape) { - return String.valueOf(getDefaultNumber().byteValue()); - } + /** + * Default value constrains: + * enum: can be set to any valid string value of the enum. + * intEnum: can be set to any valid integer value of the enum. + * document: can be set to null, `true, false, string, numbers, an empty list, or an empty map. + * list: can only be set to an empty list. + * map: can only be set to an empty map. + * structure: no default value. + * union: no default value. + *

+ * See https://awslabs.github.io/smithy/2.0/spec/type-refinement-traits.html?highlight=required#default-value-constraints + */ + private static final class DefaultValueRetriever extends ShapeVisitor.Default { + + private final MemberShape memberShape; + private final Node defaultNode; + private final Model model; + + private DefaultValueRetriever(Model model, MemberShape memberShape) { + this.model = model; + this.memberShape = memberShape; + this.defaultNode = memberShape.expectTrait(DefaultTrait.class).toNode(); + } - @Override - public String shortShape(ShortShape shape) { - return String.valueOf(getDefaultNumber().shortValue()); - } + @Override + protected String getDefault(Shape shape) { + return "nil"; + } - @Override - public String integerShape(IntegerShape shape) { - return String.valueOf(getDefaultNumber().intValue()); - } + @Override + public String blobShape(BlobShape shape) { + return getDefaultString(); + } - @Override - public String longShape(LongShape shape) { - return String.valueOf(getDefaultNumber().longValue()); - } + @Override + public String booleanShape(BooleanShape shape) { + return getDefaultBoolean(); + } - @Override - public String floatShape(FloatShape shape) { - return String.valueOf(getDefaultNumber().shortValue()); - } + @Override + public String stringShape(StringShape shape) { + return getDefaultString(); + } - @Override - public String doubleShape(DoubleShape shape) { - return String.valueOf(getDefaultNumber().doubleValue()); - } + @Override + public String byteShape(ByteShape shape) { + return String.valueOf(getDefaultNumber().byteValue()); + } - @Override - public String bigIntegerShape(BigIntegerShape shape) { - return String.valueOf(getDefaultNumber().intValue()); - } + @Override + public String shortShape(ShortShape shape) { + return String.valueOf(getDefaultNumber().shortValue()); + } - @Override - public String bigDecimalShape(BigDecimalShape shape) { - return String.valueOf(getDefaultNumber().floatValue()); - } + @Override + public String integerShape(IntegerShape shape) { + return String.valueOf(getDefaultNumber().intValue()); + } - @Override - public String enumShape(EnumShape shape) { - return shape.getEnumValues().get(getDefaultString()); - } + @Override + public String longShape(LongShape shape) { + return String.valueOf(getDefaultNumber().longValue()); + } - @Override - public String intEnumShape(IntEnumShape shape) { - return String.valueOf(getDefaultNumber().intValue()); - } + @Override + public String floatShape(FloatShape shape) { + return String.valueOf(getDefaultNumber().shortValue()); + } - @Override - public String listShape(ListShape shape) { - if (defaultNode.asArrayNode().isPresent()) { - return "[]"; + @Override + public String doubleShape(DoubleShape shape) { + return String.valueOf(getDefaultNumber().doubleValue()); } - return "nil"; - } - @Override - public String mapShape(MapShape shape) { - if (defaultNode.asObjectNode().isPresent()) { - return "{}"; + @Override + public String bigIntegerShape(BigIntegerShape shape) { + return String.valueOf(getDefaultNumber().intValue()); } - return "nil"; - } - @Override - public String documentShape(DocumentShape shape) { - if (defaultNode.asNumberNode().isPresent()) { - return getDefaultNumber().toString(); + @Override + public String bigDecimalShape(BigDecimalShape shape) { + return String.valueOf(getDefaultNumber().floatValue()); } - if (defaultNode.asBooleanNode().isPresent()) { - return getDefaultBoolean(); + @Override + public String enumShape(EnumShape shape) { + return shape.getEnumValues().get(getDefaultString()); } - if (defaultNode.asStringNode().isPresent()) { - return getDefaultString(); + @Override + public String intEnumShape(IntEnumShape shape) { + return String.valueOf(getDefaultNumber().intValue()); } - if (defaultNode.asArrayNode().isPresent()) { - return "[]"; + @Override + public String listShape(ListShape shape) { + if (defaultNode.asArrayNode().isPresent()) { + return "[]"; + } + return "nil"; } - if (defaultNode.asObjectNode().isPresent()) { - return "{}"; + @Override + public String mapShape(MapShape shape) { + if (defaultNode.asObjectNode().isPresent()) { + return "{}"; + } + return "nil"; } - return "nil"; - } + @Override + public String documentShape(DocumentShape shape) { + if (defaultNode.asNumberNode().isPresent()) { + return getDefaultNumber().toString(); + } + + if (defaultNode.asBooleanNode().isPresent()) { + return getDefaultBoolean(); + } + + if (defaultNode.asStringNode().isPresent()) { + return getDefaultString(); + } + + if (defaultNode.asArrayNode().isPresent()) { + return "[]"; + } + + if (defaultNode.asObjectNode().isPresent()) { + return "{}"; + } - @Override - public String timestampShape(TimestampShape shape) { - if (defaultNode.isStringNode()) { - return getDefaultString(); - } else if (defaultNode.isNumberNode()) { - return String.valueOf(getDefaultNumber()); - } else { return "nil"; } - } - private String getDefaultString() { - return String.format("\"%s\"", defaultNode.expectStringNode().getValue()); - } + @Override + public String timestampShape(TimestampShape shape) { + if (defaultNode.isStringNode()) { + return getDefaultString(); + } else if (defaultNode.isNumberNode()) { + return String.valueOf(getDefaultNumber()); + } else { + return "nil"; + } + } - private String getDefaultBoolean() { - return String.valueOf(defaultNode.expectBooleanNode().getValue()); - } + private String getDefaultString() { + return String.format("\"%s\"", defaultNode.expectStringNode().getValue()); + } - private Number getDefaultNumber() { - return defaultNode.expectNumberNode().getValue(); + private String getDefaultBoolean() { + return String.valueOf(defaultNode.expectBooleanNode().getValue()); + } + + private Number getDefaultNumber() { + return defaultNode.expectNumberNode().getValue(); + } } } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RubyGeneratorBase.java similarity index 54% rename from codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileGenerator.java rename to codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RubyGeneratorBase.java index fd58009a4..3e10acf3c 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RubyGeneratorBase.java @@ -15,32 +15,53 @@ package software.amazon.smithy.ruby.codegen.generators; +import java.util.function.Consumer; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.model.Model; import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; import software.amazon.smithy.ruby.codegen.RubySettings; -abstract class TypesFileGenerator { +abstract class RubyGeneratorBase { + final Model model; + final SymbolProvider symbolProvider; + final RubySettings settings; final GenerationContext context; - TypesFileGenerator(ContextualDirective directive) { + RubyGeneratorBase(ContextualDirective directive) { this.symbolProvider = directive.symbolProvider(); this.settings = directive.settings(); this.context = directive.context(); + this.model = directive.model(); + } + + abstract String getModule(); + + public final void write(Consumer writerConsumer) { + write(rbFile(), nameSpace(), writerConsumer); + } + + public final void writeRbs(Consumer writerConsumer) { + write(rbsFile(), nameSpace(), writerConsumer); + } + + public final void write(String file, String namespace, Consumer writerConsumer) { + context.writerDelegator().useFileWriter(file, namespace, writerConsumer); } - public final String nameSpace() { - return settings.getModule() + "::Types"; + public String nameSpace() { + return settings.getModule() + "::" + getModule(); } - public final String rbFile() { - return settings.getGemName() + "/lib/" + settings.getGemName() + "/types.rb"; + public String rbFile() { + return settings.getGemName() + "/lib/" + settings.getGemName() + "/" + getModule().toLowerCase() + ".rb"; } - public final String rbsFile() { - return settings.getGemName() + "/sig/" + settings.getGemName() + "/types.rbs"; + public String rbsFile() { + return settings.getGemName() + "/sig/" + settings.getGemName() + "/" + getModule().toLowerCase() + ".rbs"; } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java index e81c71f92..98b47c99d 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java @@ -17,10 +17,8 @@ import java.util.Iterator; import java.util.List; -import java.util.function.Consumer; import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.Symbol; -import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.codegen.core.directed.ShapeDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.NullableIndex; @@ -36,20 +34,25 @@ import software.amazon.smithy.ruby.codegen.RubyFormatter; import software.amazon.smithy.ruby.codegen.RubySettings; import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; +import software.amazon.smithy.utils.SmithyInternalApi; -public final class StructureGenerator extends TypesFileGenerator - implements Consumer> { +@SmithyInternalApi +public final class StructureGenerator extends RubyGeneratorBase { - public StructureGenerator(ContextualDirective directive) { + private final StructureShape shape; + + public StructureGenerator(ShapeDirective directive) { super(directive); + this.shape = directive.shape(); } @Override - public void accept(ShapeDirective directive) { - var model = directive.model(); - var shape = directive.shape(); + String getModule() { + return "Types"; + } - directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + public void render() { + write(writer -> { String membersBlock = "nil"; if (!shape.members().isEmpty()) { membersBlock = shape @@ -92,7 +95,7 @@ public void accept(ShapeDirective { + writeRbs(writer -> { Symbol symbol = symbolProvider.toSymbol(shape); String shapeName = symbol.getName(); writer.write(shapeName + ": untyped\n"); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java index d73d97888..11d208ab4 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java @@ -19,13 +19,20 @@ import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.RubyCodeWriter; import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.utils.SmithyInternalApi; -public class TypesFileBlockGenerator extends TypesFileGenerator { +@SmithyInternalApi +public class TypesFileBlockGenerator extends RubyGeneratorBase { public TypesFileBlockGenerator(ContextualDirective directive) { super(directive); } + @Override + String getModule() { + return "Types"; + } + public void openBlocks() { context.writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { writer.includePreamble().includeRequires(); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java index 16a28f389..f73095e9e 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java @@ -15,12 +15,11 @@ package software.amazon.smithy.ruby.codegen.generators; -import java.util.function.Consumer; import software.amazon.smithy.codegen.core.Symbol; -import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.SensitiveTrait; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.Hearth; @@ -28,21 +27,27 @@ import software.amazon.smithy.ruby.codegen.RubyFormatter; import software.amazon.smithy.ruby.codegen.RubySettings; import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; +import software.amazon.smithy.utils.SmithyInternalApi; -public final class UnionGenerator extends TypesFileGenerator - implements Consumer> { +@SmithyInternalApi +public final class UnionGenerator extends RubyGeneratorBase { - public UnionGenerator(ContextualDirective directive) { + private final UnionShape shape; + + public UnionGenerator(GenerateUnionDirective directive) { super(directive); + this.shape = directive.shape(); } @Override - public void accept(GenerateUnionDirective directive) { - var shape = directive.shape(); - var model = directive.model(); + String getModule() { + return "Types"; + } + + public void render() { String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); - directive.context().writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + write(writer -> { writer.writeInline("$L", documentation); writer.openBlock("class $T < $T", symbolProvider.toSymbol(shape), Hearth.UNION); @@ -69,13 +74,13 @@ public void accept(GenerateUnionDirective direc .write("{ unknown: super(__getobj__) }") .closeBlock("end\n") .openBlock("def to_s") - .write("\"#<$L::Types::Unknown #{__getobj__ || 'nil'}>\"", directive.settings().getModule()) + .write("\"#<$L::Types::Unknown #{__getobj__ || 'nil'}>\"", settings.getModule()) .closeBlock("end") .closeBlock("end") .closeBlock("end\n"); }); - directive.context().writerDelegator().useFileWriter(rbsFile(), nameSpace(), writer -> { + writeRbs(writer -> { Symbol symbol = symbolProvider.toSymbol(shape); writer.openBlock("class $T < $T", symbol, Hearth.UNION); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java index 658e641de..35ecc5829 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java @@ -18,9 +18,9 @@ import java.util.Collection; import java.util.Comparator; import java.util.logging.Logger; -import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.neighbor.Walker; import software.amazon.smithy.model.shapes.BigDecimalShape; @@ -54,42 +54,35 @@ import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi -public class ValidatorsGenerator extends ShapeVisitor.Default { +public class ValidatorsGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ValidatorsGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - - public ValidatorsGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Validators"); - this.symbolProvider = context.symbolProvider(); + public ValidatorsGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + String getModule() { + return "Validators"; } public void render() { - FileManifest fileManifest = context.fileManifest(); - writer + write(writer -> { + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) .openBlock("module Validators") - .call(() -> renderValidators()) + .call(() -> renderValidators(writer)) .write("") .closeBlock("end") .closeBlock("end"); - - String fileName = settings.getGemName() + "/lib/" + settings.getGemName() + "/validators.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote validators to " + fileName); + }); + LOGGER.fine("Wrote validators to " + rbFile()); } - private void renderValidators() { + private void renderValidators(RubyCodeWriter writer) { Model modelWithoutTraitShapes = ModelTransformer.create() .getModelWithoutTraitShapes(model); @@ -97,13 +90,21 @@ private void renderValidators() { .walkShapes(context.service()) .stream() .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach((shape) -> shape.accept(this)); + .forEach((shape) -> shape.accept(new Visitor(writer))); } - @Override - public Void structureShape(StructureShape structureShape) { - Collection members = structureShape.members(); - writer + private final class Visitor extends ShapeVisitor.Default { + + private final RubyCodeWriter writer; + + private Visitor(RubyCodeWriter writer) { + this.writer = writer; + } + + @Override + public Void structureShape(StructureShape structureShape) { + Collection members = structureShape.members(); + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(structureShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -114,27 +115,27 @@ public Void structureShape(StructureShape structureShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - private void renderValidatorsForStructureMembers(Collection members) { - members.forEach(member -> { - Shape target = model.expectShape(member.getTarget()); - String symbolName = ":" + symbolProvider.toMemberName(member); - String input = "input[" + symbolName + "]"; - String context = "\"#{context}[" + symbolName + "]\""; - if (member.hasTrait(RequiredTrait.class)) { - writer.write("$T.validate_required!($L, context: $L)", Hearth.VALIDATOR, input, context); - } - target.accept(new MemberValidator(writer, symbolProvider, input, context, false)); - }); - } + private void renderValidatorsForStructureMembers(Collection members) { + members.forEach(member -> { + Shape target = model.expectShape(member.getTarget()); + String symbolName = ":" + symbolProvider.toMemberName(member); + String input = "input[" + symbolName + "]"; + String context = "\"#{context}[" + symbolName + "]\""; + if (member.hasTrait(RequiredTrait.class)) { + writer.write("$T.validate_required!($L, context: $L)", Hearth.VALIDATOR, input, context); + } + target.accept(new MemberValidator(writer, symbolProvider, input, context, false)); + }); + } - @Override - public Void mapShape(MapShape mapShape) { - Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); + @Override + public Void mapShape(MapShape mapShape) { + Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); - writer + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(mapShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -147,15 +148,15 @@ public Void mapShape(MapShape mapShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void listShape(ListShape listShape) { - Shape memberTarget = - model.expectShape(listShape.getMember().getTarget()); + @Override + public Void listShape(ListShape listShape) { + Shape memberTarget = + model.expectShape(listShape.getMember().getTarget()); - writer + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(listShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -167,16 +168,16 @@ public Void listShape(ListShape listShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void unionShape(UnionShape unionShape) { - Symbol unionType = context.symbolProvider().toSymbol(unionShape); - String shapeName = unionType.getName(); - Collection unionMemberShapes = unionShape.members(); + @Override + public Void unionShape(UnionShape unionShape) { + Symbol unionType = context.symbolProvider().toSymbol(unionShape); + String shapeName = unionType.getName(); + Collection unionMemberShapes = unionShape.members(); - writer + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(unionShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -197,15 +198,15 @@ public Void unionShape(UnionShape unionShape) { .write("end") // end switch case .closeBlock("end") // end validate method .withQualifiedNamespace("Validators", - () -> renderValidatorsForUnionMembers(unionMemberShapes)) + () -> renderValidatorsForUnionMembers(unionMemberShapes)) .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void documentShape(DocumentShape documentShape) { - writer + @Override + public Void documentShape(DocumentShape documentShape) { + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(documentShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -229,15 +230,15 @@ public Void documentShape(DocumentShape documentShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - private void renderValidatorsForUnionMembers(Collection members) { - members.forEach(member -> { - String name = symbolProvider.toMemberName(member); - Shape target = model.expectShape(member.getTarget()); + private void renderValidatorsForUnionMembers(Collection members) { + members.forEach(member -> { + String name = symbolProvider.toMemberName(member); + Shape target = model.expectShape(member.getTarget()); - writer + writer .write("") .openBlock("class $L", name) .openBlock("def self.validate!(input, context:)") @@ -245,31 +246,7 @@ private void renderValidatorsForUnionMembers(Collection members) { new MemberValidator(writer, symbolProvider, "input", "context", true))) .closeBlock("end") .closeBlock("end"); - }); - } - - @Override - protected Void getDefault(Shape shape) { - return null; - } - - private static class MemberValidator extends ShapeVisitor.Default { - private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - private final String input; - private final String context; - private Boolean renderUnionMemberValidator; - - MemberValidator(RubyCodeWriter writer, - SymbolProvider symbolProvider, - String input, - String context, - Boolean renderUnionMemberValidator) { - this.writer = writer; - this.symbolProvider = symbolProvider; - this.input = input; - this.context = context; - this.renderUnionMemberValidator = renderUnionMemberValidator; + }); } @Override @@ -277,136 +254,161 @@ protected Void getDefault(Shape shape) { return null; } - @Override - public Void blobShape(BlobShape shape) { - if (shape.hasTrait(StreamingTrait.class)) { - writer - .openBlock("unless $1L.respond_to?(:read) || $1L.respond_to?(:readpartial)", - input) - .write("raise ArgumentError, \"Expected #{context} to be an IO like object," - + " got #{$L.class}\"", input) - .closeBlock("end"); - if (shape.hasTrait(RequiresLengthTrait.class)) { + private static class MemberValidator extends ShapeVisitor.Default { + private final RubyCodeWriter writer; + private final SymbolProvider symbolProvider; + private final String input; + private final String context; + private Boolean renderUnionMemberValidator; + + MemberValidator(RubyCodeWriter writer, + SymbolProvider symbolProvider, + String input, + String context, + Boolean renderUnionMemberValidator) { + this.writer = writer; + this.symbolProvider = symbolProvider; + this.input = input; + this.context = context; + this.renderUnionMemberValidator = renderUnionMemberValidator; + } + + @Override + protected Void getDefault(Shape shape) { + return null; + } + + @Override + public Void blobShape(BlobShape shape) { + if (shape.hasTrait(StreamingTrait.class)) { writer - .openBlock("\nunless $1L.respond_to?(:size)", input) - .write("raise ArgumentError, \"Expected #{context} to respond_to(:size)\"") + .openBlock("unless $1L.respond_to?(:read) || $1L.respond_to?(:readpartial)", + input) + .write("raise ArgumentError, \"Expected #{context} to be an IO like object," + + " got #{$L.class}\"", input) .closeBlock("end"); + if (shape.hasTrait(RequiresLengthTrait.class)) { + writer + .openBlock("\nunless $1L.respond_to?(:size)", input) + .write("raise ArgumentError, \"Expected #{context} to respond_to(:size)\"") + .closeBlock("end"); + } + } else { + writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); } - } else { - writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); + return null; } - return null; - } - @Override - public Void booleanShape(BooleanShape shape) { - writer.write("$T.validate_types!($L, ::TrueClass, ::FalseClass, context: $L)", - Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void booleanShape(BooleanShape shape) { + writer.write("$T.validate_types!($L, ::TrueClass, ::FalseClass, context: $L)", + Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void listShape(ListShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + @Override + public Void listShape(ListShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } - @Override - public Void byteShape(ByteShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void byteShape(ByteShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void shortShape(ShortShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void shortShape(ShortShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void integerShape(IntegerShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void integerShape(IntegerShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void longShape(LongShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void longShape(LongShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void floatShape(FloatShape shape) { - writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void floatShape(FloatShape shape) { + writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void documentShape(DocumentShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + @Override + public Void documentShape(DocumentShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } - @Override - public Void doubleShape(DoubleShape shape) { - writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void doubleShape(DoubleShape shape) { + writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void bigDecimalShape(BigDecimalShape shape) { - writer.write("$T.validate_types!($L, $T, context: $L)", + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + writer.write("$T.validate_types!($L, $T, context: $L)", Hearth.VALIDATOR, input, RubyImportContainer.BIG_DECIMAL, context); - return null; - } - - @Override - public Void mapShape(MapShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } + @Override + public Void mapShape(MapShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } - @Override - public Void stringShape(StringShape shape) { - writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; + } - @Override - public Void structureShape(StructureShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + @Override + public Void stringShape(StringShape shape) { + writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } - @Override - public Void unionShape(UnionShape shape) { - writer.write("$1L.validate!($2L, context: $3L) unless $2L.nil?", + @Override + public Void structureShape(StructureShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + writer.write("$1L.validate!($2L, context: $3L) unless $2L.nil?", symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } + return null; + } - @Override - public Void timestampShape(TimestampShape shape) { - writer.write("$T.validate_types!($L, $T, context: $L)", + @Override + public Void timestampShape(TimestampShape shape) { + writer.write("$T.validate_types!($L, $T, context: $L)", Hearth.VALIDATOR, input, RubyImportContainer.TIME, context); - return null; + return null; + } } } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java index 955db9770..2d8216995 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java @@ -19,10 +19,10 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; import software.amazon.smithy.jmespath.ExpressionSerializer; import software.amazon.smithy.jmespath.ExpressionVisitor; import software.amazon.smithy.jmespath.JmespathExpression; @@ -44,8 +44,6 @@ import software.amazon.smithy.jmespath.ast.ProjectionExpression; import software.amazon.smithy.jmespath.ast.SliceExpression; import software.amazon.smithy.jmespath.ast.Subexpression; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.Hearth; @@ -61,68 +59,54 @@ import software.amazon.smithy.waiters.Waiter; @SmithyInternalApi -public class WaitersGenerator { +public class WaitersGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(WaitersGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; - - public WaitersGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Waiters"); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule() + "::Waiters"); - this.symbolProvider = context.symbolProvider(); + private final Set operations; + + public WaitersGenerator(GenerateServiceDirective directive) { + super(directive); + this.operations = directive.operations(); } - public void render() { - FileManifest fileManifest = context.fileManifest(); + @Override + String getModule() { + return "Waiters"; + } - writer + public void render() { + write(writer -> { + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) .openBlock("module Waiters") - .call(() -> renderWaiters(false)) + .call(() -> renderWaiters(writer, false)) .write("") .closeBlock("end") .closeBlock("end"); - - String fileName = settings.getGemName() + "/lib/" + settings.getGemName() + "/waiters.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote waiters to " + fileName); + }); + LOGGER.fine("Wrote waiters to " + rbFile()); } public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .includePreamble() .openBlock("module $L", settings.getModule()) .openBlock("module Waiters") - .call(() -> renderWaiters(true)) + .call(() -> renderWaiters(writer, true)) .write("") .closeBlock("end") .closeBlock("end"); - - String typesFile = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/waiters.rbs"; - fileManifest.writeFile(typesFile, rbsWriter.toString()); - LOGGER.fine("Wrote waiters rbs to " + typesFile); + }); + LOGGER.fine("Wrote waiters rbs to " + rbsFile()); } - private void renderWaiters(Boolean rbs) { - TopDownIndex topDownIndex = TopDownIndex.of(model); - - topDownIndex.getContainedOperations(context.service()).stream().forEach((operation) -> { + private void renderWaiters(RubyCodeWriter writer, Boolean rbs) { + operations.forEach((operation) -> { if (operation.hasTrait(WaitableTrait.class)) { Map waiters = operation.getTrait(WaitableTrait.class).get().getWaiters(); Iterator> iterator = waiters.entrySet().iterator(); @@ -132,9 +116,9 @@ private void renderWaiters(Boolean rbs) { String waiterName = entry.getKey(); Waiter waiter = entry.getValue(); if (rbs) { - renderRbsWaiter(waiterName); + renderRbsWaiter(writer, waiterName); } else { - renderWaiter(waiterName, waiter, operation); + renderWaiter(writer, waiterName, waiter, operation); } if (iterator.hasNext()) { writer.write(""); @@ -144,14 +128,14 @@ private void renderWaiters(Boolean rbs) { }); } - private void renderWaiter(String waiterName, Waiter waiter, OperationShape operation) { + private void renderWaiter(RubyCodeWriter writer, String waiterName, Waiter waiter, OperationShape operation) { String operationName = RubyFormatter.toSnakeCase(symbolProvider.toSymbol(operation).getName()); writer .write("") - .call(() -> renderWaiterDocumentation(waiter)) + .call(() -> renderWaiterDocumentation(writer, waiter)) .openBlock("class $L", waiterName) - .call(() -> renderWaiterInitializeDocumentation(waiter)) + .call(() -> renderWaiterInitializeDocumentation(writer, waiter)) .openBlock("def initialize(client, options = {})") .write("@client = client") .openBlock("@waiter = $T.new({", Hearth.WAITER) @@ -160,15 +144,15 @@ private void renderWaiter(String waiterName, Waiter waiter, OperationShape opera .write("max_delay: $L || options[:max_delay],", waiter.getMaxDelay()) .openBlock("poller: $T.new(", Hearth.POLLER) .write("operation_name: :$L,", operationName) - .call(() -> renderAcceptors(waiter)) + .call(() -> renderAcceptors(writer, waiter)) .closeBlock(")") .closeBlock("}.merge(options))") - .call(() -> renderWaiterTags(waiter)) + .call(() -> renderWaiterTags(writer, waiter)) .closeBlock("end") .write("") .write("attr_reader :tags") .write("") - .call(() -> renderWaiterWaitDocumentation(operation, operationName)) + .call(() -> renderWaiterWaitDocumentation(writer, operation, operationName)) .openBlock("def wait(params = {}, options = {})") .write("@waiter.wait(@client, params, options)") .closeBlock("end") @@ -177,8 +161,8 @@ private void renderWaiter(String waiterName, Waiter waiter, OperationShape opera LOGGER.finer("Generated waiter " + waiterName + " for operation: " + operationName); } - private void renderRbsWaiter(String waiterName) { - rbsWriter + private void renderRbsWaiter(RubyCodeWriter writer, String waiterName) { + writer .write("") .openBlock("class $L", waiterName) .write("def initialize: (untyped client, ?::Hash[untyped, untyped] options) -> void\n") @@ -187,7 +171,7 @@ private void renderRbsWaiter(String waiterName) { .closeBlock("end"); } - private void renderWaiterDocumentation(Waiter waiter) { + private void renderWaiterDocumentation(RubyCodeWriter writer, Waiter waiter) { if (waiter.getDocumentation().isPresent()) { writer.writeDocstring(waiter.getDocumentation().get()); } @@ -196,14 +180,14 @@ private void renderWaiterDocumentation(Waiter waiter) { } } - private void renderWaiterTags(Waiter waiter) { + private void renderWaiterTags(RubyCodeWriter writer, Waiter waiter) { String tags = waiter.getTags().stream() .map((tag) -> "\"" + tag + "\"") .collect(Collectors.joining(", ")); writer.write("@tags = [$L]", tags); } - private void renderAcceptors(Waiter waiter) { + private void renderAcceptors(RubyCodeWriter writer, Waiter waiter) { List acceptorsList = waiter.getAcceptors(); if (acceptorsList.isEmpty()) { @@ -221,7 +205,7 @@ private void renderAcceptors(Waiter waiter) { .write("state: '$L',", state) .openBlock("matcher: {") .call(() -> { - matcher.accept(new AcceptorVisitor()); + matcher.accept(new AcceptorVisitor(writer)); }) .closeBlock("}"); @@ -241,7 +225,7 @@ private String translatePath(String path) { return transformedPath; } - private void renderWaiterWaitDocumentation(OperationShape operation, String operationName) { + private void renderWaiterWaitDocumentation(RubyCodeWriter writer, OperationShape operation, String operationName) { String operationReturnType = "Types::" + symbolProvider.toSymbol(operation).getName(); String operationReference = "(see Client#" + operationName + ")"; @@ -251,7 +235,7 @@ private void renderWaiterWaitDocumentation(OperationShape operation, String oper .writeYardReturn(operationReturnType, operationReference); } - private void renderWaiterInitializeDocumentation(Waiter waiter) { + private void renderWaiterInitializeDocumentation(RubyCodeWriter writer, Waiter waiter) { writer .writeYardParam("Client", "client", "") .writeYardParam("Hash", "options", "") @@ -275,7 +259,13 @@ private void renderWaiterInitializeDocumentation(Waiter waiter) { "The maximum time in seconds to delay polling attempts."); } - private class AcceptorVisitor implements Matcher.Visitor { + private final class AcceptorVisitor implements Matcher.Visitor { + + private final RubyCodeWriter writer; + + private AcceptorVisitor(RubyCodeWriter writer) { + this.writer = writer; + } private void renderPathMatcher(String memberName, String path, String comparator, String expected) { writer From 07bdd680789af1aa66d3eba748bb3d6d8dbe7b70 Mon Sep 17 00:00:00 2001 From: fossand Date: Tue, 14 Mar 2023 15:03:38 -0700 Subject: [PATCH 03/22] Mark Builders, Params, Parsers, Stubs and Validators with api private --- .../high_score_service/lib/high_score_service/builders.rb | 1 + .../high_score_service/lib/high_score_service/params.rb | 1 + .../high_score_service/lib/high_score_service/parsers.rb | 1 + .../high_score_service/lib/high_score_service/stubs.rb | 1 + .../high_score_service/lib/high_score_service/validators.rb | 1 + codegen/projections/rails_json/lib/rails_json/builders.rb | 1 + codegen/projections/rails_json/lib/rails_json/params.rb | 1 + codegen/projections/rails_json/lib/rails_json/parsers.rb | 1 + codegen/projections/rails_json/lib/rails_json/stubs.rb | 1 + codegen/projections/rails_json/lib/rails_json/validators.rb | 1 + codegen/projections/weather/lib/weather/builders.rb | 1 + codegen/projections/weather/lib/weather/params.rb | 1 + codegen/projections/weather/lib/weather/parsers.rb | 1 + codegen/projections/weather/lib/weather/stubs.rb | 1 + codegen/projections/weather/lib/weather/validators.rb | 1 + codegen/projections/white_label/lib/white_label/builders.rb | 1 + codegen/projections/white_label/lib/white_label/params.rb | 1 + codegen/projections/white_label/lib/white_label/parsers.rb | 1 + codegen/projections/white_label/lib/white_label/stubs.rb | 1 + .../projections/white_label/lib/white_label/validators.rb | 1 + .../software/amazon/smithy/ruby/codegen/RubyCodeWriter.java | 5 +++++ .../smithy/ruby/codegen/generators/BuilderGeneratorBase.java | 1 + .../smithy/ruby/codegen/generators/ParamsGenerator.java | 1 + .../smithy/ruby/codegen/generators/ParserGeneratorBase.java | 1 + .../smithy/ruby/codegen/generators/StubsGeneratorBase.java | 1 + .../smithy/ruby/codegen/generators/ValidatorsGenerator.java | 1 + 26 files changed, 30 insertions(+) diff --git a/codegen/projections/high_score_service/lib/high_score_service/builders.rb b/codegen/projections/high_score_service/lib/high_score_service/builders.rb index bfbaa5444..4e8c26701 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/builders.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/builders.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Builders # Operation Builder for CreateHighScore diff --git a/codegen/projections/high_score_service/lib/high_score_service/params.rb b/codegen/projections/high_score_service/lib/high_score_service/params.rb index cfd5e5d8e..731defdfd 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/params.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/params.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Params module AttributeErrors diff --git a/codegen/projections/high_score_service/lib/high_score_service/parsers.rb b/codegen/projections/high_score_service/lib/high_score_service/parsers.rb index 03248fdad..7c8972146 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/parsers.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/parsers.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Parsers class AttributeErrors diff --git a/codegen/projections/high_score_service/lib/high_score_service/stubs.rb b/codegen/projections/high_score_service/lib/high_score_service/stubs.rb index a8d686613..d07abb058 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/stubs.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/stubs.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Stubs # Operation Stubber for CreateHighScore diff --git a/codegen/projections/high_score_service/lib/high_score_service/validators.rb b/codegen/projections/high_score_service/lib/high_score_service/validators.rb index 630c96453..2c63c915e 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/validators.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/validators.rb @@ -10,6 +10,7 @@ require 'time' module HighScoreService + # @api private module Validators class AttributeErrors diff --git a/codegen/projections/rails_json/lib/rails_json/builders.rb b/codegen/projections/rails_json/lib/rails_json/builders.rb index 6ca6bde5e..780a99822 100644 --- a/codegen/projections/rails_json/lib/rails_json/builders.rb +++ b/codegen/projections/rails_json/lib/rails_json/builders.rb @@ -10,6 +10,7 @@ require 'base64' module RailsJson + # @api private module Builders # Operation Builder for AllQueryStringTypes diff --git a/codegen/projections/rails_json/lib/rails_json/params.rb b/codegen/projections/rails_json/lib/rails_json/params.rb index d54e5da54..e85b599d9 100644 --- a/codegen/projections/rails_json/lib/rails_json/params.rb +++ b/codegen/projections/rails_json/lib/rails_json/params.rb @@ -10,6 +10,7 @@ require 'securerandom' module RailsJson + # @api private module Params module AllQueryStringTypesInput diff --git a/codegen/projections/rails_json/lib/rails_json/parsers.rb b/codegen/projections/rails_json/lib/rails_json/parsers.rb index d2f3a2ec3..3a703b3ab 100644 --- a/codegen/projections/rails_json/lib/rails_json/parsers.rb +++ b/codegen/projections/rails_json/lib/rails_json/parsers.rb @@ -10,6 +10,7 @@ require 'base64' module RailsJson + # @api private module Parsers # Operation Parser for AllQueryStringTypes diff --git a/codegen/projections/rails_json/lib/rails_json/stubs.rb b/codegen/projections/rails_json/lib/rails_json/stubs.rb index 5714ec5e8..14e76d866 100644 --- a/codegen/projections/rails_json/lib/rails_json/stubs.rb +++ b/codegen/projections/rails_json/lib/rails_json/stubs.rb @@ -10,6 +10,7 @@ require 'base64' module RailsJson + # @api private module Stubs # Operation Stubber for AllQueryStringTypes diff --git a/codegen/projections/rails_json/lib/rails_json/validators.rb b/codegen/projections/rails_json/lib/rails_json/validators.rb index 93f5610cd..10ce50555 100644 --- a/codegen/projections/rails_json/lib/rails_json/validators.rb +++ b/codegen/projections/rails_json/lib/rails_json/validators.rb @@ -10,6 +10,7 @@ require 'time' module RailsJson + # @api private module Validators class AllQueryStringTypesInput diff --git a/codegen/projections/weather/lib/weather/builders.rb b/codegen/projections/weather/lib/weather/builders.rb index af8232e29..a0838e331 100644 --- a/codegen/projections/weather/lib/weather/builders.rb +++ b/codegen/projections/weather/lib/weather/builders.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Builders # Operation Builder for GetCity diff --git a/codegen/projections/weather/lib/weather/params.rb b/codegen/projections/weather/lib/weather/params.rb index c25decb5c..b63b56ad6 100644 --- a/codegen/projections/weather/lib/weather/params.rb +++ b/codegen/projections/weather/lib/weather/params.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Params module Announcements diff --git a/codegen/projections/weather/lib/weather/parsers.rb b/codegen/projections/weather/lib/weather/parsers.rb index d8fbd2523..a5024262a 100644 --- a/codegen/projections/weather/lib/weather/parsers.rb +++ b/codegen/projections/weather/lib/weather/parsers.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Parsers class Baz diff --git a/codegen/projections/weather/lib/weather/stubs.rb b/codegen/projections/weather/lib/weather/stubs.rb index b1d1acc34..7180fa288 100644 --- a/codegen/projections/weather/lib/weather/stubs.rb +++ b/codegen/projections/weather/lib/weather/stubs.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Stubs # Union Stubber for Announcements diff --git a/codegen/projections/weather/lib/weather/validators.rb b/codegen/projections/weather/lib/weather/validators.rb index efae20eae..8f97ab760 100644 --- a/codegen/projections/weather/lib/weather/validators.rb +++ b/codegen/projections/weather/lib/weather/validators.rb @@ -10,6 +10,7 @@ require 'time' module Weather + # @api private module Validators class Announcements diff --git a/codegen/projections/white_label/lib/white_label/builders.rb b/codegen/projections/white_label/lib/white_label/builders.rb index 28b7406af..7ae949716 100644 --- a/codegen/projections/white_label/lib/white_label/builders.rb +++ b/codegen/projections/white_label/lib/white_label/builders.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Builders # Operation Builder for DefaultsTest diff --git a/codegen/projections/white_label/lib/white_label/params.rb b/codegen/projections/white_label/lib/white_label/params.rb index 0e92ea6f8..69a77ddd9 100644 --- a/codegen/projections/white_label/lib/white_label/params.rb +++ b/codegen/projections/white_label/lib/white_label/params.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Params module ClientError diff --git a/codegen/projections/white_label/lib/white_label/parsers.rb b/codegen/projections/white_label/lib/white_label/parsers.rb index 3881f9976..491975939 100644 --- a/codegen/projections/white_label/lib/white_label/parsers.rb +++ b/codegen/projections/white_label/lib/white_label/parsers.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Parsers # Error Parser for ClientError diff --git a/codegen/projections/white_label/lib/white_label/stubs.rb b/codegen/projections/white_label/lib/white_label/stubs.rb index d9ac06816..85861260d 100644 --- a/codegen/projections/white_label/lib/white_label/stubs.rb +++ b/codegen/projections/white_label/lib/white_label/stubs.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Stubs # Operation Stubber for DefaultsTest diff --git a/codegen/projections/white_label/lib/white_label/validators.rb b/codegen/projections/white_label/lib/white_label/validators.rb index b5f6c4783..55c1f3624 100644 --- a/codegen/projections/white_label/lib/white_label/validators.rb +++ b/codegen/projections/white_label/lib/white_label/validators.rb @@ -10,6 +10,7 @@ require 'time' module WhiteLabel + # @api private module Validators class ClientError diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java index 546099022..81ba9c66d 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java @@ -92,6 +92,11 @@ public void closeAllModules() { } } + public RubyCodeWriter apiPrivate() { + this.write("# @api private"); + return this; + } + /** * Preamble comments will be included in the generated code. * This should be called for writers that are used to generate full files. diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java index 5ee93994a..92aa425de 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java @@ -222,6 +222,7 @@ public void render(FileManifest fileManifest) { .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Builders") .call(() -> renderBuilders()) .closeBlock("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java index aeb793892..922977104 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java @@ -82,6 +82,7 @@ public void render() { .includePreamble() .includeRequires() .addModule(settings.getModule()) + .apiPrivate() .addModule("Params") .call(() -> renderParams(writer)) .write("") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java index 4738d5ca5..aa550c11a 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java @@ -220,6 +220,7 @@ public void render(FileManifest fileManifest) { .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Parsers") .call(() -> renderParsers()) .closeBlock("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java index 64a7c6d17..19c5d95df 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java @@ -202,6 +202,7 @@ public void render(FileManifest fileManifest) { .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Stubs") .call(() -> renderStubs()) .closeBlock("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java index 35ecc5829..fe122c1c0 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java @@ -73,6 +73,7 @@ public void render() { .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Validators") .call(() -> renderValidators(writer)) .write("") From 54fb42229bdd26a4f0e2007c460c55cb888fd6b3 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 10 Apr 2023 12:41:03 -0700 Subject: [PATCH 04/22] Fix missed filtering of integrations (#124) * Fix missed filtering of integrations * Add dep on rules enginge + fix default values when missing --- codegen/smithy-ruby-codegen/build.gradle.kts | 1 + .../amazon/smithy/ruby/codegen/DirectedRubyCodegen.java | 5 ++++- .../smithy/ruby/codegen/generators/ParamsGenerator.java | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/codegen/smithy-ruby-codegen/build.gradle.kts b/codegen/smithy-ruby-codegen/build.gradle.kts index 0d1d7ef3a..ac5e6c225 100644 --- a/codegen/smithy-ruby-codegen/build.gradle.kts +++ b/codegen/smithy-ruby-codegen/build.gradle.kts @@ -33,6 +33,7 @@ buildscript { dependencies { api("software.amazon.smithy:smithy-codegen-core:${rootProject.extra["smithyVersion"]}") + api("software.amazon.smithy:smithy-rules-engine:${rootProject.extra["smithyVersion"]}") implementation("software.amazon.smithy:smithy-waiters:${rootProject.extra["smithyVersion"]}") implementation("software.amazon.smithy:smithy-protocol-test-traits:${rootProject.extra["smithyVersion"]}") } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java index 5b2d2d155..b837b052e 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java @@ -73,7 +73,10 @@ public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { ServiceShape service = directive.service(); Model model = directive.model(); - List integrations = directive.integrations(); + List integrations = directive.integrations().stream() + .filter((integration) -> integration + .includeFor(service, model)) + .collect(Collectors.toList()); Map supportedProtocols = ProtocolGenerator .collectSupportedProtocolGenerators(integrations); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java index 922977104..d5756fc2c 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java @@ -290,7 +290,9 @@ private static class MemberBuilder extends ShapeVisitor.Default { // Note: No need to check for box trait for V1 Smithy models. // Smithy convert V1 to V2 model and populate Default trait automatically boolean containsRequiredAndDefaultTraits = - memberShape.hasTrait(DefaultTrait.class) && memberShape.hasTrait(RequiredTrait.class); + memberShape.hasTrait(DefaultTrait.class) && + !memberShape.expectTrait(DefaultTrait.class).toNode().isNullNode() && + memberShape.hasTrait(RequiredTrait.class); if (containsRequiredAndDefaultTraits) { Shape targetShape = model.expectShape(memberShape.getTarget()); From 6a69fcb39e07f5fcc5c90cbb25482007acbfd36e Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 10 Apr 2023 12:54:41 -0700 Subject: [PATCH 05/22] Fix checkstyle issue --- .../white_label/lib/white_label/params.rb | 4 +- .../codegen/generators/ParamsGenerator.java | 182 +++++++++--------- 2 files changed, 93 insertions(+), 93 deletions(-) diff --git a/codegen/projections/white_label/lib/white_label/params.rb b/codegen/projections/white_label/lib/white_label/params.rb index 69a77ddd9..93e60856a 100644 --- a/codegen/projections/white_label/lib/white_label/params.rb +++ b/codegen/projections/white_label/lib/white_label/params.rb @@ -34,7 +34,7 @@ def self.build(params, context: '') type.simple_enum = params.fetch(:simple_enum, "YES") type.typed_enum = params.fetch(:typed_enum, "NO") type.int_enum = params.fetch(:int_enum, 1) - type.null_document = params.fetch(:null_document, nil) + type.null_document = params[:null_document] type.string_document = params.fetch(:string_document, "some string document") type.boolean_document = params.fetch(:boolean_document, true) type.numbers_document = params.fetch(:numbers_document, 1.23) @@ -62,7 +62,7 @@ def self.build(params, context: '') type.simple_enum = params.fetch(:simple_enum, "YES") type.typed_enum = params.fetch(:typed_enum, "NO") type.int_enum = params.fetch(:int_enum, 1) - type.null_document = params.fetch(:null_document, nil) + type.null_document = params[:null_document] type.string_document = params.fetch(:string_document, "some string document") type.boolean_document = params.fetch(:boolean_document, true) type.numbers_document = params.fetch(:numbers_document, 1.23) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java index d5756fc2c..57ccb7c88 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java @@ -79,14 +79,14 @@ String getModule() { public void render() { write(writer -> { writer - .includePreamble() - .includeRequires() - .addModule(settings.getModule()) - .apiPrivate() - .addModule("Params") - .call(() -> renderParams(writer)) - .write("") - .closeAllModules(); + .includePreamble() + .includeRequires() + .addModule(settings.getModule()) + .apiPrivate() + .addModule("Params") + .call(() -> renderParams(writer)) + .write("") + .closeAllModules(); }); LOGGER.fine("Wrote params to " + rbFile()); } @@ -118,21 +118,21 @@ protected Void getDefault(Shape shape) { @Override public Void structureShape(StructureShape structureShape) { writer - .write("") - .openBlock("module $L", symbolProvider.toSymbol(structureShape).getName()) - .openBlock("def self.build(params, context: '')") - .call(() -> renderBuilderForStructureMembers( - context.symbolProvider().toSymbol(structureShape), structureShape.members())) - .closeBlock("end") - .closeBlock("end"); + .write("") + .openBlock("module $L", symbolProvider.toSymbol(structureShape).getName()) + .openBlock("def self.build(params, context: '')") + .call(() -> renderBuilderForStructureMembers( + context.symbolProvider().toSymbol(structureShape), structureShape.members())) + .closeBlock("end") + .closeBlock("end"); return null; } private void renderBuilderForStructureMembers(Symbol symbol, Collection members) { writer - .write("$T.validate_types!(params, ::Hash, $T, context: context)", - Hearth.VALIDATOR, symbol) - .write("type = $T.new", symbol); + .write("$T.validate_types!(params, ::Hash, $T, context: context)", + Hearth.VALIDATOR, symbol) + .write("type = $T.new", symbol); members.forEach(member -> { Shape target = model.expectShape(member.getTarget()); @@ -142,7 +142,7 @@ private void renderBuilderForStructureMembers(Symbol symbol, Collection { - if (isComplexShape(memberTarget)) { - writer.openBlock("params.each_with_index do |element, index|"); - } else { - writer.openBlock("params.each do |element|"); - } - }) - .call(() -> memberTarget - .accept(new MemberBuilder(model, writer, symbolProvider, "data << ", - "element", "\"#{context}[#{index}]\"", - listShape.getMember(), - !listShape.hasTrait(SparseTrait.class)))) - .closeBlock("end") - .write("data") - .closeBlock("end") - .closeBlock("end"); + .write("") + .openBlock("module $L", symbolProvider.toSymbol(listShape).getName()) + .openBlock("def self.build(params, context: '')") + .write("$T.validate_types!(params, ::Array, context: context)", Hearth.VALIDATOR) + .write("data = []") + .call(() -> { + if (isComplexShape(memberTarget)) { + writer.openBlock("params.each_with_index do |element, index|"); + } else { + writer.openBlock("params.each do |element|"); + } + }) + .call(() -> memberTarget + .accept(new MemberBuilder(model, writer, symbolProvider, "data << ", + "element", "\"#{context}[#{index}]\"", + listShape.getMember(), + !listShape.hasTrait(SparseTrait.class)))) + .closeBlock("end") + .write("data") + .closeBlock("end") + .closeBlock("end"); return null; } @@ -183,20 +183,20 @@ public Void mapShape(MapShape mapShape) { Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); writer - .write("") - .openBlock("module $L", symbolProvider.toSymbol(mapShape).getName()) - .openBlock("def self.build(params, context: '')") - .write("$T.validate_types!(params, ::Hash, context: context)", Hearth.VALIDATOR) - .write("data = {}") - .openBlock("params.each do |key, value|") - .call(() -> valueTarget - .accept(new MemberBuilder(model, writer, context.symbolProvider(), "data[key] = ", - "value", "\"#{context}[:#{key}]\"", mapShape.getValue(), - !mapShape.hasTrait(SparseTrait.class)))) - .closeBlock("end") - .write("data") - .closeBlock("end") - .closeBlock("end"); + .write("") + .openBlock("module $L", symbolProvider.toSymbol(mapShape).getName()) + .openBlock("def self.build(params, context: '')") + .write("$T.validate_types!(params, ::Hash, context: context)", Hearth.VALIDATOR) + .write("data = {}") + .openBlock("params.each do |key, value|") + .call(() -> valueTarget + .accept(new MemberBuilder(model, writer, context.symbolProvider(), "data[key] = ", + "value", "\"#{context}[:#{key}]\"", mapShape.getValue(), + !mapShape.hasTrait(SparseTrait.class)))) + .closeBlock("end") + .write("data") + .closeBlock("end") + .closeBlock("end"); return null; } @@ -206,53 +206,53 @@ public Void unionShape(UnionShape shape) { Symbol typeSymbol = context.symbolProvider().toSymbol(shape); writer - .write("") - .openBlock("module $L", name) - .openBlock("def self.build(params, context: '')") - .write("return params if params.is_a?($T)", typeSymbol) - .write("$T.validate_types!(params, ::Hash, $T, context: context)", - Hearth.VALIDATOR, typeSymbol) - .openBlock("unless params.size == 1") - .write("raise ArgumentError,") - .indent(3) - .write("\"Expected #{context} to have exactly one member, got: #{params}\"") - .dedent(3) - .closeBlock("end") - .write("key, value = params.flatten") - .write("case key"); //start a case statement. This does NOT indent + .write("") + .openBlock("module $L", name) + .openBlock("def self.build(params, context: '')") + .write("return params if params.is_a?($T)", typeSymbol) + .write("$T.validate_types!(params, ::Hash, $T, context: context)", + Hearth.VALIDATOR, typeSymbol) + .openBlock("unless params.size == 1") + .write("raise ArgumentError,") + .indent(3) + .write("\"Expected #{context} to have exactly one member, got: #{params}\"") + .dedent(3) + .closeBlock("end") + .write("key, value = params.flatten") + .write("case key"); //start a case statement. This does NOT indent for (MemberShape member : shape.members()) { Shape target = model.expectShape(member.getTarget()); String memberClassName = symbolProvider.toMemberName(member); String memberName = RubyFormatter.asSymbol(memberClassName); writer.write("when $L", memberName) - .indent() - .openBlock("$T.new(", context.symbolProvider().toSymbol(member)); + .indent() + .openBlock("$T.new(", context.symbolProvider().toSymbol(member)); String input = "params[" + memberName + "]"; String contextString = "\"#{context}[" + memberName + "]\""; target.accept(new MemberBuilder(model, writer, symbolProvider, "", input, contextString, member, false)); writer.closeBlock(")") - .dedent(); + .dedent(); } String expectedMembers = - shape.members().stream().map((member) -> RubyFormatter.asSymbol(member.getMemberName())) - .collect(Collectors.joining(", ")); + shape.members().stream().map((member) -> RubyFormatter.asSymbol(member.getMemberName())) + .collect(Collectors.joining(", ")); writer.write("else") - .indent() - .write("raise ArgumentError,") - .indent(3) - .write("\"Expected #{context} to have one of $L set\"", expectedMembers) - .dedent(4); + .indent() + .write("raise ArgumentError,") + .indent(3) + .write("\"Expected #{context} to have one of $L set\"", expectedMembers) + .dedent(4); writer.write("end") //end of case statement, NOT indented - .closeBlock("end") - .closeBlock("end"); + .closeBlock("end") + .closeBlock("end"); return null; } private boolean isComplexShape(Shape shape) { return shape.isStructureShape() || shape.isListShape() || shape.isMapShape() - || shape.isUnionShape() || shape.isOperationShape(); + || shape.isUnionShape() || shape.isOperationShape(); } private static class MemberBuilder extends ShapeVisitor.Default { @@ -290,9 +290,9 @@ private static class MemberBuilder extends ShapeVisitor.Default { // Note: No need to check for box trait for V1 Smithy models. // Smithy convert V1 to V2 model and populate Default trait automatically boolean containsRequiredAndDefaultTraits = - memberShape.hasTrait(DefaultTrait.class) && - !memberShape.expectTrait(DefaultTrait.class).toNode().isNullNode() && - memberShape.hasTrait(RequiredTrait.class); + memberShape.hasTrait(DefaultTrait.class) + && !memberShape.expectTrait(DefaultTrait.class).toNode().isNullNode() + && memberShape.hasTrait(RequiredTrait.class); if (containsRequiredAndDefaultTraits) { Shape targetShape = model.expectShape(memberShape.getTarget()); @@ -316,12 +316,12 @@ protected Void getDefault(Shape shape) { public Void blobShape(BlobShape shape) { if (shape.hasTrait(StreamingTrait.class)) { writer - .write("io = $L || StringIO.new", input) - .openBlock("unless io.respond_to?(:read) " - + "|| io.respond_to?(:readpartial)") - .write("io = StringIO.new(io)") - .closeBlock("end") - .write("$Lio", memberSetter); + .write("io = $L || StringIO.new", input) + .openBlock("unless io.respond_to?(:read) " + + "|| io.respond_to?(:readpartial)") + .write("io = StringIO.new(io)") + .closeBlock("end") + .write("$Lio", memberSetter); } else { getDefault(shape); } From b2ce09f0016e2d00c997084c612cdd588935fcb2 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 11 Apr 2023 12:32:16 -0700 Subject: [PATCH 06/22] Fix issue with float rounding of fractional seconds (#126) * Fix issue with float rounding of fractional seconds * Replace with longs --- .../amazon/smithy/ruby/codegen/util/ParamsToHash.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java index d0dcbc5a5..a4eb975a4 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java @@ -192,7 +192,15 @@ public String timestampShape(TimestampShape shape) { return ""; } if (node.isNumberNode()) { - return "Time.at(" + node.expectNumberNode().getValue().toString() + ")"; + if (node.expectNumberNode().isFloatingPointNumber()) { + // rounding of floats cause an issue in the precision of fractional seconds + Double n = node.expectNumberNode().getValue().doubleValue(); + long seconds = (long) Math.floor(n); + long ms = (long) ((n - Math.floor(n)) * 1000); + return "Time.at(" + seconds + ", " + ms + ", :millisecond)"; + } else { + return "Time.at(" + node.expectNumberNode().getValue().toString() + ")"; + } } return "Time.parse('" + node + "')"; } From e10525708b816800006e4b72964e1d5302974f33 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 11 Apr 2023 12:38:02 -0700 Subject: [PATCH 07/22] Ensure stubs write to the body instead of replacing it (#123) * Ensure stubs write to the body instead of replacing it * Fix rubocop --- .../lib/high_score_service/stubs.rb | 8 +- .../rails_json/lib/rails_json/stubs.rb | 28 +-- .../rails_json/spec/protocol_spec.rb | 216 ++++++++++++------ .../projections/weather/spec/protocol_spec.rb | 15 +- .../generators/HttpProtocolTestGenerator.java | 3 +- .../generators/RestStubsGeneratorBase.java | 4 +- .../generators/StubsGeneratorBase.java | 2 +- .../railsjson/generators/StubsGenerator.java | 10 +- hearth/lib/hearth/middleware/send.rb | 3 + hearth/spec/hearth/middleware/send_spec.rb | 10 +- 10 files changed, 194 insertions(+), 105 deletions(-) diff --git a/codegen/projections/high_score_service/lib/high_score_service/stubs.rb b/codegen/projections/high_score_service/lib/high_score_service/stubs.rb index d07abb058..457ef1771 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/stubs.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/stubs.rb @@ -26,7 +26,7 @@ def self.stub(http_resp, stub:) http_resp.headers['Location'] = stub[:location] unless stub[:location].nil? || stub[:location].empty? http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScoreAttributes.stub(stub[:high_score]) unless stub[:high_score].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -56,7 +56,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScoreAttributes.stub(stub[:high_score]) unless stub[:high_score].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -119,7 +119,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScores.stub(stub[:high_scores]) unless stub[:high_scores].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -136,7 +136,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScoreAttributes.stub(stub[:high_score]) unless stub[:high_score].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end end diff --git a/codegen/projections/rails_json/lib/rails_json/stubs.rb b/codegen/projections/rails_json/lib/rails_json/stubs.rb index 14e76d866..cbc32d9cb 100644 --- a/codegen/projections/rails_json/lib/rails_json/stubs.rb +++ b/codegen/projections/rails_json/lib/rails_json/stubs.rb @@ -200,7 +200,7 @@ def self.stub(http_resp, stub:) http_resp.headers['Content-Type'] = 'application/json' data[:string_value] = stub[:string_value] unless stub[:string_value].nil? data[:document_value] = stub[:document_value] unless stub[:document_value].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -216,7 +216,7 @@ def self.stub(http_resp, stub:) data = {} http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' - http_resp.body = StringIO.new(Hearth::JSON.dump(stub[:document_value])) + http_resp.body.write(Hearth::JSON.dump(stub[:document_value])) end end @@ -366,7 +366,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:greeting] = stub[:greeting] unless stub[:greeting].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -384,7 +384,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['X-Foo'] = stub[:foo] unless stub[:foo].nil? || stub[:foo].empty? http_resp.headers['Content-Type'] = 'application/octet-stream' - http_resp.body = StringIO.new(stub[:blob] || '') + http_resp.body.write(stub[:blob] || '') end end @@ -402,7 +402,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['X-Foo'] = stub[:foo] unless stub[:foo].nil? || stub[:foo].empty? http_resp.headers['Content-Type'] = 'text/plain' - http_resp.body = StringIO.new(stub[:blob] || '') + http_resp.body.write(stub[:blob] || '') end end @@ -419,7 +419,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::NestedPayload.stub(stub[:nested]) unless stub[:nested].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -658,7 +658,7 @@ def self.stub(http_resp, stub:) data[:foo_enum_list] = Stubs::FooEnumList.stub(stub[:foo_enum_list]) unless stub[:foo_enum_list].nil? data[:foo_enum_set] = Stubs::FooEnumSet.stub(stub[:foo_enum_set]) unless stub[:foo_enum_set].nil? data[:foo_enum_map] = Stubs::FooEnumMap.stub(stub[:foo_enum_map]) unless stub[:foo_enum_map].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -693,7 +693,7 @@ def self.stub(http_resp, stub:) data[:sparse_string_map] = Stubs::SparseStringMap.stub(stub[:sparse_string_map]) unless stub[:sparse_string_map].nil? data[:dense_set_map] = Stubs::DenseSetMap.stub(stub[:dense_set_map]) unless stub[:dense_set_map].nil? data[:sparse_set_map] = Stubs::SparseSetMap.stub(stub[:sparse_set_map]) unless stub[:sparse_set_map].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -710,7 +710,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:contents] = Stubs::MyUnion.stub(stub[:contents]) unless stub[:contents].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -845,7 +845,7 @@ def self.stub(http_resp, stub:) data[:struct_with_location_name] = Stubs::StructWithLocationName.stub(stub[:struct_with_location_name]) unless stub[:struct_with_location_name].nil? data[:timestamp] = Hearth::TimeHelper.to_date_time(stub[:timestamp]) unless stub[:timestamp].nil? data[:unix_timestamp] = Hearth::TimeHelper.to_epoch_seconds(stub[:unix_timestamp]).to_i unless stub[:unix_timestamp].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1117,7 +1117,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:value] = stub[:value] unless stub[:value].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1182,7 +1182,7 @@ def self.stub(http_resp, stub:) data[:string] = stub[:string] unless stub[:string].nil? data[:sparse_string_list] = Stubs::SparseStringList.stub(stub[:sparse_string_list]) unless stub[:sparse_string_list].nil? data[:sparse_string_map] = Stubs::SparseStringMap.stub(stub[:sparse_string_map]) unless stub[:sparse_string_map].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1212,7 +1212,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:value] = stub[:value] unless stub[:value].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1551,7 +1551,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:member] = Stubs::Struct____456efg.stub(stub[:member]) unless stub[:member].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end end diff --git a/codegen/projections/rails_json/spec/protocol_spec.rb b/codegen/projections/rails_json/spec/protocol_spec.rb index 818714a0c..7120faa1e 100644 --- a/codegen/projections/rails_json/spec/protocol_spec.rb +++ b/codegen/projections/rails_json/spec/protocol_spec.rb @@ -51,7 +51,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"member":{"__123foo":"foo value"}}') + response.body.write('{"member":{"__123foo":"foo value"}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -388,12 +389,13 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "string_value": "string", "document_value": { "foo": "bar" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -410,10 +412,11 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "string_value": "string", "document_value": "hello" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -430,10 +433,11 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "string_value": "string", "document_value": 10 }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -450,10 +454,11 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "string_value": "string", "document_value": false }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -470,13 +475,14 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "string_value": "string", "document_value": [ true, false ] }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -632,9 +638,10 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "foo": "bar" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -650,7 +657,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('"hello"') + response.body.write('"hello"') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -711,7 +719,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -730,9 +739,10 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "foo": true }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -752,7 +762,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -888,9 +899,10 @@ module RailsJson response = context.response response.status = 400 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'InvalidGreeting' }) - response.body = StringIO.new('{ + response.body.write('{ "message": "Hi" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -912,12 +924,13 @@ module RailsJson response = context.response response.status = 400 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'ComplexError' }) - response.body = StringIO.new('{ + response.body.write('{ "top_level": "Top level", "nested": { "Fooooo": "bar" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -939,8 +952,9 @@ module RailsJson response = context.response response.status = 400 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'ComplexError' }) - response.body = StringIO.new('{ + response.body.write('{ }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1005,7 +1019,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo' }) - response.body = StringIO.new('blobby blob blob') + response.body.write('blobby blob blob') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1022,7 +1037,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1104,7 +1120,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'text/plain', 'X-Foo' => 'Foo' }) - response.body = StringIO.new('blobby blob blob') + response.body.write('blobby blob blob') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1176,10 +1193,11 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "greeting": "hello", "name": "Phreddy" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1545,7 +1563,8 @@ module RailsJson response = context.response response.status = 201 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1562,7 +1581,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 201 - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1629,7 +1649,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1646,7 +1667,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1811,7 +1833,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-String' => 'Hello', 'X-StringList' => 'a, b, c', 'X-StringSet' => 'a, b, c' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1837,7 +1860,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-StringList' => '"b,c", "\"def\"", a' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1857,7 +1881,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-Byte' => '1', 'X-Double' => '1.1', 'X-Float' => '1.1', 'X-Integer' => '123', 'X-IntegerList' => '1, 2, 3', 'X-Long' => '123', 'X-Short' => '123' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1883,7 +1908,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-Boolean1' => 'true', 'X-Boolean2' => 'false', 'X-BooleanList' => 'true, false, true' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1905,7 +1931,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-Enum' => 'Foo', 'X-EnumList' => 'Foo, Bar, Baz' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2136,7 +2163,7 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "foo_enum1": "Foo", "foo_enum2": "0", "foo_enum3": "1", @@ -2153,6 +2180,7 @@ module RailsJson "zero": "0" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2462,7 +2490,7 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "dense_struct_map": { "foo": { "hi": "there" @@ -2480,6 +2508,7 @@ module RailsJson } } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2510,7 +2539,7 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "sparse_boolean_map": { "x": null }, @@ -2524,6 +2553,7 @@ module RailsJson "x": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2550,7 +2580,7 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "dense_number_map": { "x": 0 }, @@ -2564,6 +2594,7 @@ module RailsJson "x": false } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2590,12 +2621,13 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "sparse_set_map": { "x": [], "y": ["a", "b"] } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2619,12 +2651,13 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "dense_set_map": { "x": [], "y": ["a", "b"] } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2648,13 +2681,14 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "sparse_set_map": { "x": [], "y": ["a", "b"], "z": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2680,13 +2714,14 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "dense_set_map": { "x": [], "y": ["a", "b"], "z": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3196,11 +3231,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "string_value": "foo" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3218,11 +3254,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "boolean_value": true } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3240,11 +3277,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "number_value": 1 } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3262,11 +3300,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "blob_value": "Zm9v" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3284,11 +3323,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "timestamp_value": "2014-04-29T18:30:38Z" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3306,11 +3346,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "enum_value": "Foo" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3328,11 +3369,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "list_value": ["foo", "bar"] } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3353,7 +3395,7 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "map_value": { "foo": "bar", @@ -3361,6 +3403,7 @@ module RailsJson } } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3381,13 +3424,14 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "contents": { "structure_value": { "hi": "hello" } } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4217,7 +4261,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4233,7 +4278,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"string":"string-value"}') + response.body.write('{"string":"string-value"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4249,7 +4295,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"integer":1234}') + response.body.write('{"integer":1234}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4265,7 +4312,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"long":1234567890123456789}') + response.body.write('{"long":1234567890123456789}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4281,7 +4329,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"float":1234.5}') + response.body.write('{"float":1234.5}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4297,7 +4346,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"double":123456789.12345679}') + response.body.write('{"double":123456789.12345679}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4313,7 +4363,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"boolean":true}') + response.body.write('{"boolean":true}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4329,7 +4380,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"boolean":false}') + response.body.write('{"boolean":false}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4345,7 +4397,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"blob":"YmluYXJ5LXZhbHVl"}') + response.body.write('{"blob":"YmluYXJ5LXZhbHVl"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4361,7 +4414,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"timestamp":"2000-01-02T20:34:56Z"}') + response.body.write('{"timestamp":"2000-01-02T20:34:56Z"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4377,7 +4431,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"iso8601_timestamp":"2000-01-02T20:34:56Z"}') + response.body.write('{"iso8601_timestamp":"2000-01-02T20:34:56Z"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4393,7 +4448,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.000 GMT"}') + response.body.write('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.000 GMT"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4409,7 +4465,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_strings":["abc","mno","xyz"]}') + response.body.write('{"list_of_strings":["abc","mno","xyz"]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4429,7 +4486,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_maps_of_strings":[{"size":"large"},{"color":"red"}]}') + response.body.write('{"list_of_maps_of_strings":[{"size":"large"},{"color":"red"}]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4452,7 +4510,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_lists":[["abc","mno","xyz"],["hjk","qrs","tuv"]]}') + response.body.write('{"list_of_lists":[["abc","mno","xyz"],["hjk","qrs","tuv"]]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4479,7 +4538,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_structs":[{"value":"value-1"},{"value":"value-2"}]}') + response.body.write('{"list_of_structs":[{"value":"value-1"},{"value":"value-2"}]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4502,7 +4562,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"recursive_list":[{"recursive_list":[{"recursive_list":[{"string":"value"}]}]}]}') + response.body.write('{"recursive_list":[{"recursive_list":[{"recursive_list":[{"string":"value"}]}]}]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4530,7 +4591,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_strings":{"size":"large","color":"red"}}') + response.body.write('{"map_of_strings":{"size":"large","color":"red"}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4549,7 +4611,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_lists_of_strings":{"sizes":["large","small"],"colors":["red","green"]}}') + response.body.write('{"map_of_lists_of_strings":{"sizes":["large","small"],"colors":["red","green"]}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4574,7 +4637,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_maps":{"sizes":{"large":"L","medium":"M"},"colors":{"red":"R","blue":"B"}}}') + response.body.write('{"map_of_maps":{"sizes":{"large":"L","medium":"M"},"colors":{"red":"R","blue":"B"}}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4599,7 +4663,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_structs":{"size":{"value":"small"},"color":{"value":"red"}}}') + response.body.write('{"map_of_structs":{"size":{"value":"small"},"color":{"value":"red"}}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4622,7 +4687,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"recursive_map":{"key-1":{"recursive_map":{"key-2":{"recursive_map":{"key-3":{"string":"value"}}}}}}}') + response.body.write('{"recursive_map":{"key-1":{"recursive_map":{"key-2":{"recursive_map":{"key-3":{"string":"value"}}}}}}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4650,7 +4716,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'X-Amzn-Requestid' => 'amazon-uniq-request-id' }) - response.body = StringIO.new('{}') + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5226,7 +5293,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-Json' => 'dHJ1ZQ==' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5388,9 +5456,10 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "string": null }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5406,11 +5475,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "sparse_string_map": { "foo": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5428,11 +5498,12 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.body.write('{ "sparse_string_list": [ null ] }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5716,7 +5787,8 @@ module RailsJson response = context.response response.status = 200 response.headers = Hearth::HTTP::Headers.new({ 'X-defaultFormat' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-memberDateTime' => '2019-12-16T23:48:18Z', 'X-memberEpochSeconds' => '1576540098', 'X-memberHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-targetDateTime' => '2019-12-16T23:48:18Z', 'X-targetEpochSeconds' => '1576540098', 'X-targetHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT' }) - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry diff --git a/codegen/projections/weather/spec/protocol_spec.rb b/codegen/projections/weather/spec/protocol_spec.rb index 5ad498059..5d2c08340 100644 --- a/codegen/projections/weather/spec/protocol_spec.rb +++ b/codegen/projections/weather/spec/protocol_spec.rb @@ -26,10 +26,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -74,7 +75,7 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.body = StringIO.new('{ + response.body.write('{ "name": "Seattle", "coordinates": { "latitude": 12.34, @@ -87,6 +88,7 @@ module Weather "case": "Upper" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -153,10 +155,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -182,10 +185,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -211,10 +215,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java index bf34dfb1c..5420b914c 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java @@ -332,7 +332,8 @@ private void renderResponseMiddleware(HttpResponseTestCase testCase) { private void renderResponseMiddlewareBody(Optional body) { if (body.isPresent()) { - writer.write("response.body = StringIO.new('$L')", body.get()); + writer.write("response.body.write('$L')", body.get()); + writer.write("response.body.rewind"); } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java index 6230b7fd8..6f8f625e1 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java @@ -86,7 +86,7 @@ public RestStubsGeneratorBase(GenerationContext context) { * http_resp.status = 200 * #### START code generated by this method * http_resp.headers['Content-Type'] = 'application/octet-stream' - * http_resp.body = StringIO.new(stub[:blob] || '') + * http_resp.body.write(stub[:blob] || '') * #### END code generated by this method * end * end @@ -125,7 +125,7 @@ protected abstract void renderPayloadBodyStub(OperationShape operation, Shape ou * ### START code generated by this method * data[:value] = stub[:value] unless stub[:value].nil? * #### END code generated by this method - * http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + * http_resp.body.write(Hearth::JSON.dump(data)) * end * end * } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java index 19c5d95df..6d42a76db 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java @@ -185,7 +185,7 @@ public StubsGeneratorBase(GenerationContext context) { * http_resp.status = 200 * http_resp.headers['Content-Type'] = 'application/json' * data['contents'] = Stubs::Contents.stub(stub[:contents]) unless stub[:contents].nil? - * http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + * http_resp.body.write(Hearth::JSON.dump(data)) * end * #### END code generated by this method * } diff --git a/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java b/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java index 16606d896..c97ef14f3 100644 --- a/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java +++ b/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java @@ -115,7 +115,7 @@ protected void renderBodyStub(OperationShape operation, Shape outputShape) { writer .write("http_resp.headers['Content-Type'] = 'application/json'") .call(() -> renderMemberStubbers(outputShape)) - .write("http_resp.body = StringIO.new(Hearth::JSON.dump(data))"); + .write("http_resp.body.write(Hearth::JSON.dump(data))"); } @Override @@ -300,7 +300,7 @@ protected Void getDefault(Shape shape) { public Void stringShape(StringShape shape) { writer .write("http_resp.headers['Content-Type'] = 'text/plain'") - .write("http_resp.body = StringIO.new($L || '')", inputGetter); + .write("http_resp.body.write($L || '')", inputGetter); return null; } @@ -314,7 +314,7 @@ public Void blobShape(BlobShape shape) { writer .write("http_resp.headers['Content-Type'] = '$L'", mediaType) - .write("http_resp.body = StringIO.new($L || '')", inputGetter); + .write("http_resp.body.write($L || '')", inputGetter); return null; } @@ -323,7 +323,7 @@ public Void blobShape(BlobShape shape) { public Void documentShape(DocumentShape shape) { writer .write("http_resp.headers['Content-Type'] = 'application/json'") - .write("http_resp.body = StringIO.new(Hearth::JSON.dump($1L))", inputGetter); + .write("http_resp.body.write(Hearth::JSON.dump($1L))", inputGetter); return null; } @@ -356,7 +356,7 @@ private void defaultComplexSerializer(Shape shape) { .write("http_resp.headers['Content-Type'] = 'application/json'") .write("data = Stubs::$1L.stub($2L) unless $2L.nil?", symbolProvider.toSymbol(shape).getName(), inputGetter) - .write("http_resp.body = StringIO.new(Hearth::JSON.dump(data))"); + .write("http_resp.body.write(Hearth::JSON.dump(data))"); } } diff --git a/hearth/lib/hearth/middleware/send.rb b/hearth/lib/hearth/middleware/send.rb index 347de906a..4ff651173 100755 --- a/hearth/lib/hearth/middleware/send.rb +++ b/hearth/lib/hearth/middleware/send.rb @@ -32,6 +32,9 @@ def call(input, context) stub = @stubs.next(context.operation_name) output = Output.new apply_stub(stub, input, context, output) + if context.response.body.respond_to?(:rewind) + context.response.body.rewind + end output else @client.transmit( diff --git a/hearth/spec/hearth/middleware/send_spec.rb b/hearth/spec/hearth/middleware/send_spec.rb index b29fd2389..937215962 100644 --- a/hearth/spec/hearth/middleware/send_spec.rb +++ b/hearth/spec/hearth/middleware/send_spec.rb @@ -30,7 +30,8 @@ module Middleware let(:input) { double('Type::OperationInput') } let(:request) { double('request') } - let(:response) { double('response') } + let(:body) { StringIO.new } + let(:response) { double('response', body: body) } let(:context) do Hearth::Context.new( request: request, @@ -60,6 +61,13 @@ module Middleware expect(output.error).to be_a(Exception) end + it 'rewinds the body' do + expect(stubs).to receive(:next) + .with(:operation).and_return(Exception) + expect(body).to receive(:rewind) + subject.call(input, context) + end + context 'stub is a proc' do before { stubs.add_stubs(operation, [stub_proc]) } From 66a41566deec7dc36b04d8970de6c6f76db91557 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 12 Apr 2023 10:34:19 -0700 Subject: [PATCH 08/22] Add support for fractional timestamps (#127) --- .../rails_json/spec/protocol_spec.rb | 84 +++++++++++++++++++ .../ruby/codegen/util/ParamsToHash.java | 2 +- .../model/protocol-test/kitchen-sink.smithy | 40 +++++++++ hearth/lib/hearth/time_helper.rb | 6 +- hearth/spec/hearth/time_helper_spec.rb | 22 +++++ 5 files changed, 151 insertions(+), 3 deletions(-) diff --git a/codegen/projections/rails_json/spec/protocol_spec.rb b/codegen/projections/rails_json/spec/protocol_spec.rb index 7120faa1e..647b8757f 100644 --- a/codegen/projections/rails_json/spec/protocol_spec.rb +++ b/codegen/projections/rails_json/spec/protocol_spec.rb @@ -3830,6 +3830,24 @@ module RailsJson timestamp: Time.at(946845296) }, **opts) end + # Serializes fractional timestamp shapes + # + it 'rails_json_serializes_fractional_timestamp_shapes' do + middleware = Hearth::MiddlewareBuilder.before_send do |input, context| + request = context.request + request_uri = URI.parse(request.url) + expect(request.http_method).to eq('POST') + expect(request_uri.path).to eq('/') + { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } + ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } + expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"timestamp":"2000-01-02T20:34:56.123Z"}')) + Hearth::Output.new + end + opts = {middleware: middleware} + client.kitchen_sink_operation({ + timestamp: Time.at(946845296, 123, :millisecond) + }, **opts) + end # Serializes timestamp shapes with iso8601 timestampFormat # it 'rails_json_serializes_timestamp_shapes_with_iso8601_timestampformat' do @@ -4424,6 +4442,23 @@ module RailsJson timestamp: Time.at(946845296) }) end + # Parses fractional timestamp shapes + # + it 'rails_json_parses_fractional_timestamp_shapes' do + middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| + response = context.response + response.status = 200 + response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.body.write('{"timestamp":"2000-01-02T20:34:56.123Z"}') + response.body.rewind + Hearth::Output.new + end + middleware.remove_send.remove_build.remove_retry + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses iso8601 timestamps # it 'rails_json_parses_iso8601_timestamps' do @@ -4458,6 +4493,23 @@ module RailsJson httpdate_timestamp: Time.at(946845296) }) end + # Parses fractional httpdate timestamps + # + it 'rails_json_parses_fractional_httpdate_timestamps' do + middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| + response = context.response + response.status = 200 + response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.body.write('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.123 GMT"}') + response.body.rewind + Hearth::Output.new + end + middleware.remove_send.remove_build.remove_retry + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + httpdate_timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses list shapes # it 'rails_json_parses_list_shapes' do @@ -4889,6 +4941,22 @@ module RailsJson timestamp: Time.at(946845296) }) end + # Parses fractional timestamp shapes + # + it 'stubs rails_json_parses_fractional_timestamp_shapes' do + middleware = Hearth::MiddlewareBuilder.after_send do |input, context| + response = context.response + expect(response.status).to eq(200) + end + middleware.remove_build.remove_retry + client.stub_responses(:kitchen_sink_operation, { + timestamp: Time.at(946845296, 123, :millisecond) + }) + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses iso8601 timestamps # it 'stubs rails_json_parses_iso8601_timestamps' do @@ -4921,6 +4989,22 @@ module RailsJson httpdate_timestamp: Time.at(946845296) }) end + # Parses fractional httpdate timestamps + # + it 'stubs rails_json_parses_fractional_httpdate_timestamps' do + middleware = Hearth::MiddlewareBuilder.after_send do |input, context| + response = context.response + expect(response.status).to eq(200) + end + middleware.remove_build.remove_retry + client.stub_responses(:kitchen_sink_operation, { + httpdate_timestamp: Time.at(946845296, 123, :millisecond) + }) + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + httpdate_timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses list shapes # it 'stubs rails_json_parses_list_shapes' do diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java index a4eb975a4..a0a8a9036 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java @@ -196,7 +196,7 @@ public String timestampShape(TimestampShape shape) { // rounding of floats cause an issue in the precision of fractional seconds Double n = node.expectNumberNode().getValue().doubleValue(); long seconds = (long) Math.floor(n); - long ms = (long) ((n - Math.floor(n)) * 1000); + long ms = Math.round((n - Math.floor(n)) * 1000); return "Time.at(" + seconds + ", " + ms + ", :millisecond)"; } else { return "Time.at(" + node.expectNumberNode().getValue().toString() + ")"; diff --git a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy index 683c9a535..9c4d9023b 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy @@ -168,6 +168,22 @@ use smithy.test#httpResponseTests method: "POST", uri: "/", }, + { + id: "rails_json_serializes_fractional_timestamp_shapes", + protocol: railsJson, + documentation: "Serializes fractional timestamp shapes", + body: "{\"timestamp\":\"2000-01-02T20:34:56.123Z\"}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + requireHeaders: [ + "Content-Length" + ], + params: { + Timestamp: 946845296.123, + }, + method: "POST", + uri: "/", + }, { id: "rails_json_serializes_timestamp_shapes_with_iso8601_timestampformat", protocol: railsJson, @@ -664,6 +680,18 @@ use smithy.test#httpResponseTests }, code: 200, }, + { + id: "rails_json_parses_fractional_timestamp_shapes", + protocol: railsJson, + documentation: "Parses fractional timestamp shapes", + body: "{\"timestamp\":\"2000-01-02T20:34:56.123Z\"}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + Timestamp: 946845296.123, + }, + code: 200, + }, { id: "rails_json_parses_iso8601_timestamps", protocol: railsJson, @@ -688,6 +716,18 @@ use smithy.test#httpResponseTests }, code: 200, }, + { + id: "rails_json_parses_fractional_httpdate_timestamps", + protocol: railsJson, + documentation: "Parses fractional httpdate timestamps", + body: "{\"httpdate_timestamp\":\"Sun, 02 Jan 2000 20:34:56.123 GMT\"}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + HttpdateTimestamp: 946845296.123, + }, + code: 200, + }, { id: "rails_json_parses_list_shapes", protocol: railsJson, diff --git a/hearth/lib/hearth/time_helper.rb b/hearth/lib/hearth/time_helper.rb index 2797cb98c..168f11208 100755 --- a/hearth/lib/hearth/time_helper.rb +++ b/hearth/lib/hearth/time_helper.rb @@ -11,7 +11,8 @@ class << self # @param [Time] time # @return [String] The time as an ISO8601 string. def to_date_time(time) - time.utc.strftime('%Y-%m-%dT%H:%M:%SZ') + optional_ms_digits = time.subsec.zero? ? nil : 3 + time.utc.iso8601(optional_ms_digits) end # @param [Time] time @@ -28,7 +29,8 @@ def to_epoch_seconds(time) # @return [String] Returns the time formatted # as an HTTP header date. def to_http_date(time) - time.utc.httpdate + fractional = '.%L' unless time.subsec.zero? + time.utc.strftime("%a, %d %b %Y %H:%M:%S#{fractional} GMT") end end end diff --git a/hearth/spec/hearth/time_helper_spec.rb b/hearth/spec/hearth/time_helper_spec.rb index 306ea03a1..6ffcb3ec4 100644 --- a/hearth/spec/hearth/time_helper_spec.rb +++ b/hearth/spec/hearth/time_helper_spec.rb @@ -8,18 +8,40 @@ module Hearth it 'converts a time object to date time format' do expect(subject.to_date_time(time)).to eq '1970-01-01T00:00:00Z' end + + context 'fractional seconds' do + let(:time) { Time.at(946_845_296, 123, :millisecond) } + it 'converts to date time format with milliseconds' do + expect(subject.to_date_time(time)).to eq '2000-01-02T20:34:56.123Z' + end + end end describe '.to_epoch_seconds' do it 'converts a time object to epoch seconds format' do expect(subject.to_epoch_seconds(time)).to eq 0.0 end + + context 'fractional seconds' do + let(:time) { Time.at(946_845_296, 123, :millisecond) } + it 'converts to date time format with milliseconds' do + expect(subject.to_epoch_seconds(time)).to eq 946_845_296.123 + end + end end describe '.to_http_date' do it 'converts a time object to http date format' do expect(subject.to_http_date(time)).to eq 'Thu, 01 Jan 1970 00:00:00 GMT' end + + context 'fractional seconds' do + let(:time) { Time.at(946_845_296, 123, :millisecond) } + it 'converts to http date format with milliseconds' do + expect(subject.to_http_date(time)) + .to eq 'Sun, 02 Jan 2000 20:34:56.123 GMT' + end + end end end end From 5eaad6e9aa30d198fbd4246186475370895ff06d Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 12 Apr 2023 13:29:37 -0700 Subject: [PATCH 09/22] Fix bug in rendering of middleware --- .../lib/high_score_service/client.rb | 30 +-- .../rails_json/lib/rails_json/builders.rb | 2 +- .../rails_json/lib/rails_json/client.rb | 216 +++++++++--------- .../projections/weather/lib/weather/client.rb | 36 +-- .../white_label/lib/white_label/client.rb | 66 +++--- .../generators/RestBuilderGeneratorBase.java | 4 +- .../ruby/codegen/middleware/Middleware.java | 2 +- 7 files changed, 178 insertions(+), 178 deletions(-) diff --git a/codegen/projections/high_score_service/lib/high_score_service/client.rb b/codegen/projections/high_score_service/lib/high_score_service/client.rb index 0c37a9b2d..4db8e00fd 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/client.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/client.rb @@ -80,11 +80,11 @@ def create_high_score(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 201, errors: [Errors::UnprocessableEntityError]), @@ -148,11 +148,11 @@ def delete_high_score(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -222,11 +222,11 @@ def get_high_score(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -292,11 +292,11 @@ def list_high_scores(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -373,11 +373,11 @@ def update_high_score(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::UnprocessableEntityError]), diff --git a/codegen/projections/rails_json/lib/rails_json/builders.rb b/codegen/projections/rails_json/lib/rails_json/builders.rb index 780a99822..8c128c921 100644 --- a/codegen/projections/rails_json/lib/rails_json/builders.rb +++ b/codegen/projections/rails_json/lib/rails_json/builders.rb @@ -360,7 +360,7 @@ def self.build(http_req, input:) params = Hearth::Query::ParamList.new http_req.append_query_params(params) http_req.headers['X-Foo'] = input[:foo] unless input[:foo].nil? || input[:foo].empty? - input[:foo_map].each do |key, value| + input[:foo_map]&.each do |key, value| http_req.headers["X-Foo-#{key}"] = value unless value.nil? || value.empty? end end diff --git a/codegen/projections/rails_json/lib/rails_json/client.rb b/codegen/projections/rails_json/lib/rails_json/client.rb index 8e846344f..fe6f83215 100644 --- a/codegen/projections/rails_json/lib/rails_json/client.rb +++ b/codegen/projections/rails_json/lib/rails_json/client.rb @@ -102,11 +102,11 @@ def all_query_string_types(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -170,11 +170,11 @@ def constant_and_variable_query_string(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -238,11 +238,11 @@ def constant_query_string(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -313,11 +313,11 @@ def document_type(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -386,11 +386,11 @@ def document_type_as_payload(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -447,11 +447,11 @@ def empty_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -512,11 +512,11 @@ def endpoint_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -579,11 +579,11 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -650,11 +650,11 @@ def greeting_with_errors(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::InvalidGreeting, Errors::ComplexError]), @@ -721,11 +721,11 @@ def http_payload_traits(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -790,11 +790,11 @@ def http_payload_traits_with_media_type(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -864,11 +864,11 @@ def http_payload_with_structure(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -937,11 +937,11 @@ def http_prefix_headers(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1002,11 +1002,11 @@ def http_prefix_headers_in_response(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1066,11 +1066,11 @@ def http_request_with_float_labels(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1130,11 +1130,11 @@ def http_request_with_greedy_label_in_path(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1209,11 +1209,11 @@ def http_request_with_labels(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1281,11 +1281,11 @@ def http_request_with_labels_and_timestamp_format(params = {}, options = {}, &bl stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1343,11 +1343,11 @@ def http_response_code(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1409,11 +1409,11 @@ def ignore_query_params_in_response(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1524,11 +1524,11 @@ def input_and_output_with_headers(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1609,11 +1609,11 @@ def json_enums(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1721,11 +1721,11 @@ def json_maps(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1819,11 +1819,11 @@ def json_unions(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1988,11 +1988,11 @@ def kitchen_sink_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::ErrorWithMembers, Errors::ErrorWithoutMembers]), @@ -2054,11 +2054,11 @@ def media_type_header(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2120,11 +2120,11 @@ def nested_attributes_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2195,11 +2195,11 @@ def null_and_empty_headers_client(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2269,11 +2269,11 @@ def null_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2335,11 +2335,11 @@ def omits_null_serializes_empty_string(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2399,11 +2399,11 @@ def operation_with_optional_input_output(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2466,11 +2466,11 @@ def query_idempotency_token_auto_fill(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2534,11 +2534,11 @@ def query_params_as_string_list_map(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2597,11 +2597,11 @@ def streaming_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2675,11 +2675,11 @@ def timestamp_format_headers(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2743,11 +2743,11 @@ def operation____789_bad_name(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), diff --git a/codegen/projections/weather/lib/weather/client.rb b/codegen/projections/weather/lib/weather/client.rb index 4f778970e..eb3cf0a47 100644 --- a/codegen/projections/weather/lib/weather/client.rb +++ b/codegen/projections/weather/lib/weather/client.rb @@ -72,11 +72,11 @@ def get_city(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), @@ -144,11 +144,11 @@ def get_city_image(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), @@ -205,11 +205,11 @@ def get_current_time(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -283,11 +283,11 @@ def get_forecast(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -366,11 +366,11 @@ def list_cities(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -434,11 +434,11 @@ def operation____789_bad_name(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), diff --git a/codegen/projections/white_label/lib/white_label/client.rb b/codegen/projections/white_label/lib/white_label/client.rb index 4d2ccac24..49e993990 100644 --- a/codegen/projections/white_label/lib/white_label/client.rb +++ b/codegen/projections/white_label/lib/white_label/client.rb @@ -134,11 +134,11 @@ def defaults_test(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -198,11 +198,11 @@ def endpoint_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -264,11 +264,11 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -491,11 +491,11 @@ def kitchen_sink(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::ClientError, Errors::ServerError]), @@ -555,11 +555,11 @@ def mixin_test(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -620,11 +620,11 @@ def paginators_test(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -685,11 +685,11 @@ def paginators_test_with_items(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -747,11 +747,11 @@ def streaming_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -809,11 +809,11 @@ def streaming_with_length(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -872,11 +872,11 @@ def waiters_test(params = {}, options = {}, &block) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -938,11 +938,11 @@ def operation____paginators_test_with_bad_names(params = {}, options = {}, &bloc stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, retry_mode: @config.retry_mode, + client_rate_limiter: @client_rate_limiter, + adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, error_inspector_class: Hearth::Retry::ErrorInspector, retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + max_attempts: @config.max_attempts ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java index 59b8e9226..ebfd14d69 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java @@ -214,7 +214,7 @@ protected void renderHeadersBuilder(Shape inputShape) { * @param inputShape inputShape to render for */ protected void renderPrefixHeadersBuilder(Shape inputShape) { - // get a list of all of HttpLabel members + // get a list of all of HttpPrefixHeaders members List headerMembers = inputShape.members() .stream() .filter((m) -> m.hasTrait(HttpPrefixHeadersTrait.class)) @@ -230,7 +230,7 @@ protected void renderPrefixHeadersBuilder(Shape inputShape) { String symbolName = ":" + symbolProvider.toMemberName(m); String headerSetter = "http_req.headers[\"" + prefix + "#{key}\"] = "; writer - .openBlock("input[$L].each do |key, value|", symbolName) + .openBlock("input[$L]&.each do |key, value|", symbolName) .call(() -> valueShape.accept(new HeaderSerializer(m, headerSetter, "value"))) .closeBlock("end"); LOGGER.finest("Generated prefix header builder for " + m.getMemberName()); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java index 1de3c7b06..3baafe51a 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java @@ -210,7 +210,7 @@ public static class Builder implements SmithyBuilder { Set config = middleware.getClientConfig(); Map params = - middleware.getAdditionalParams(); + new HashMap<>(middleware.getAdditionalParams()); params.putAll(middleware.operationParams .params(context, operation)); From dd65d9d386d712de858c343c7e4b070b052cb0fc Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Thu, 13 Apr 2023 09:46:05 -0400 Subject: [PATCH 10/22] Request/Responses with Field/Fields (#122) --- .../lib/high_score_service/builders.rb | 10 +- .../lib/high_score_service/client.rb | 10 +- .../rails_json/lib/rails_json/builders.rb | 121 ++--- .../rails_json/lib/rails_json/client.rb | 72 +-- .../rails_json/spec/protocol_spec.rb | 458 ++++++++---------- .../rails_json/spec/request_id_spec.rb | 7 +- .../projections/weather/lib/weather/client.rb | 12 +- .../projections/weather/spec/protocol_spec.rb | 10 +- .../white_label/lib/white_label/client.rb | 22 +- .../white_label/spec/client_spec.rb | 4 +- .../white_label/spec/endpoints_spec.rb | 4 +- .../white_label/spec/errors_spec.rb | 2 +- .../white_label/spec/streaming_spec.rb | 4 +- .../integration-specs/client_spec.rb | 4 +- .../integration-specs/endpoints_spec.rb | 4 +- .../integration-specs/errors_spec.rb | 2 +- .../integration-specs/streaming_spec.rb | 4 +- .../ruby/codegen/ApplicationTransport.java | 3 +- .../generators/HttpProtocolTestGenerator.java | 17 +- .../generators/RestBuilderGeneratorBase.java | 45 +- .../integration-specs/request_id_spec.rb | 7 +- hearth/lib/hearth.rb | 5 + hearth/lib/hearth/http.rb | 12 +- hearth/lib/hearth/http/api_error.rb | 6 +- hearth/lib/hearth/http/client.rb | 21 +- hearth/lib/hearth/http/field.rb | 64 +++ hearth/lib/hearth/http/fields.rb | 103 ++++ hearth/lib/hearth/http/headers.rb | 84 ---- .../hearth/http/middleware/content_length.rb | 2 +- hearth/lib/hearth/http/request.rb | 94 ++-- hearth/lib/hearth/http/response.rb | 35 +- hearth/lib/hearth/query/param.rb | 2 +- hearth/lib/hearth/request.rb | 23 + hearth/lib/hearth/response.rb | 17 + hearth/sig/lib/hearth/http/headers.rbs | 47 -- hearth/sig/lib/hearth/http/response.rbs | 31 +- hearth/sig/lib/hearth/response.rbs | 11 + hearth/spec/hearth/http/api_error_spec.rb | 10 +- hearth/spec/hearth/http/client_spec.rb | 77 +-- hearth/spec/hearth/http/error_parser_spec.rb | 13 +- hearth/spec/hearth/http/field_spec.rb | 91 ++++ hearth/spec/hearth/http/fields_spec.rb | 149 ++++++ hearth/spec/hearth/http/headers_spec.rb | 95 ---- .../http/middleware/content_length_spec.rb | 6 +- .../http/middleware/content_md5_spec.rb | 5 +- hearth/spec/hearth/http/request_spec.rb | 89 ++-- hearth/spec/hearth/http/response_spec.rb | 10 +- .../hearth/middleware/host_prefix_spec.rb | 10 +- hearth/spec/hearth/middleware/retry_spec.rb | 1 + hearth/spec/hearth/query/param_spec.rb | 4 +- hearth/spec/hearth/request_spec.rb | 18 + hearth/spec/hearth/response_spec.rb | 12 + .../spec/hearth/retry/error_inspector_spec.rb | 6 +- 53 files changed, 1083 insertions(+), 892 deletions(-) create mode 100644 hearth/lib/hearth/http/field.rb create mode 100644 hearth/lib/hearth/http/fields.rb delete mode 100755 hearth/lib/hearth/http/headers.rb create mode 100644 hearth/lib/hearth/request.rb create mode 100644 hearth/lib/hearth/response.rb delete mode 100644 hearth/sig/lib/hearth/http/headers.rbs create mode 100644 hearth/sig/lib/hearth/response.rbs create mode 100644 hearth/spec/hearth/http/field_spec.rb create mode 100644 hearth/spec/hearth/http/fields_spec.rb delete mode 100644 hearth/spec/hearth/http/headers_spec.rb create mode 100644 hearth/spec/hearth/request_spec.rb create mode 100644 hearth/spec/hearth/response_spec.rb diff --git a/codegen/projections/high_score_service/lib/high_score_service/builders.rb b/codegen/projections/high_score_service/lib/high_score_service/builders.rb index 4e8c26701..5edf793b5 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/builders.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/builders.rb @@ -17,7 +17,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/high_scores') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -39,7 +39,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -56,7 +56,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -76,7 +76,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/high_scores') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -93,7 +93,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} diff --git a/codegen/projections/high_score_service/lib/high_score_service/client.rb b/codegen/projections/high_score_service/lib/high_score_service/client.rb index 4db8e00fd..7a55e8ff9 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/client.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/client.rb @@ -103,7 +103,7 @@ def create_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -171,7 +171,7 @@ def delete_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -245,7 +245,7 @@ def get_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -315,7 +315,7 @@ def list_high_scores(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -396,7 +396,7 @@ def update_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/rails_json/lib/rails_json/builders.rb b/codegen/projections/rails_json/lib/rails_json/builders.rb index 8c128c921..e89e70e72 100644 --- a/codegen/projections/rails_json/lib/rails_json/builders.rb +++ b/codegen/projections/rails_json/lib/rails_json/builders.rb @@ -74,7 +74,7 @@ def self.build(http_req, input:) value.to_s unless value.nil? end end - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -100,7 +100,7 @@ def self.build(http_req, input:) params = Hearth::Query::ParamList.new params['baz'] = input[:baz].to_s unless input[:baz].nil? params['maybeSet'] = input[:maybe_set].to_s unless input[:maybe_set].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -120,7 +120,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -185,7 +185,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/DocumentType') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -201,7 +201,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/DocumentTypeAsPayload') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' http_req.body = StringIO.new(Hearth::JSON.dump(input[:document_value])) end @@ -224,7 +224,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/emptyoperation') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -242,7 +242,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/endpoint') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -252,7 +252,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/endpointwithhostlabel') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -309,7 +309,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/greetingwitherrors') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -319,7 +319,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/HttpPayloadTraits') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/octet-stream' http_req.body = StringIO.new(input[:blob] || '') http_req.headers['X-Foo'] = input[:foo] unless input[:foo].nil? || input[:foo].empty? @@ -332,7 +332,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/HttpPayloadTraitsWithMediaType') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'text/plain' http_req.body = StringIO.new(input[:blob] || '') http_req.headers['X-Foo'] = input[:foo] unless input[:foo].nil? || input[:foo].empty? @@ -345,7 +345,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/HttpPayloadWithStructure') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = Builders::NestedPayload.build(input[:nested]) unless input[:nested].nil? http_req.body = StringIO.new(Hearth::JSON.dump(data)) @@ -358,7 +358,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/HttpPrefixHeaders') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-Foo'] = input[:foo] unless input[:foo].nil? || input[:foo].empty? input[:foo_map]&.each do |key, value| http_req.headers["X-Foo-#{key}"] = value unless value.nil? || value.empty? @@ -372,7 +372,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/HttpPrefixHeadersResponse') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -393,7 +393,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -414,7 +414,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -459,7 +459,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -500,7 +500,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -510,7 +510,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/HttpResponseCode') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -520,7 +520,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/IgnoreQueryParamsInResponse') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -530,7 +530,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/InputAndOutputWithHeaders') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-String'] = input[:header_string] unless input[:header_string].nil? || input[:header_string].empty? http_req.headers['X-Byte'] = input[:header_byte].to_s unless input[:header_byte].nil? http_req.headers['X-Short'] = input[:header_short].to_s unless input[:header_short].nil? @@ -540,43 +540,13 @@ def self.build(http_req, input:) http_req.headers['X-Double'] = Hearth::NumberHelper.serialize(input[:header_double]) unless input[:header_double].nil? http_req.headers['X-Boolean1'] = input[:header_true_bool].to_s unless input[:header_true_bool].nil? http_req.headers['X-Boolean2'] = input[:header_false_bool].to_s unless input[:header_false_bool].nil? - unless input[:header_string_list].nil? || input[:header_string_list].empty? - http_req.headers['X-StringList'] = input[:header_string_list] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end - unless input[:header_string_set].nil? || input[:header_string_set].empty? - http_req.headers['X-StringSet'] = input[:header_string_set] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end - unless input[:header_integer_list].nil? || input[:header_integer_list].empty? - http_req.headers['X-IntegerList'] = input[:header_integer_list] - .compact - .map { |s| s.to_s } - .join(', ') - end - unless input[:header_boolean_list].nil? || input[:header_boolean_list].empty? - http_req.headers['X-BooleanList'] = input[:header_boolean_list] - .compact - .map { |s| s.to_s } - .join(', ') - end - unless input[:header_timestamp_list].nil? || input[:header_timestamp_list].empty? - http_req.headers['X-TimestampList'] = input[:header_timestamp_list] - .compact - .map { |s| Hearth::TimeHelper.to_http_date(s) } - .join(', ') - end + http_req.headers['X-StringList'] = input[:header_string_list] unless input[:header_string_list].nil? || input[:header_string_list].empty? + http_req.headers['X-StringSet'] = input[:header_string_set] unless input[:header_string_set].nil? || input[:header_string_set].empty? + http_req.headers['X-IntegerList'] = input[:header_integer_list] unless input[:header_integer_list].nil? || input[:header_integer_list].empty? + http_req.headers['X-BooleanList'] = input[:header_boolean_list] unless input[:header_boolean_list].nil? || input[:header_boolean_list].empty? + http_req.headers['X-TimestampList'] = input[:header_timestamp_list] unless input[:header_timestamp_list].nil? || input[:header_timestamp_list].empty? http_req.headers['X-Enum'] = input[:header_enum] unless input[:header_enum].nil? || input[:header_enum].empty? - unless input[:header_enum_list].nil? || input[:header_enum_list].empty? - http_req.headers['X-EnumList'] = input[:header_enum_list] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end + http_req.headers['X-EnumList'] = input[:header_enum_list] unless input[:header_enum_list].nil? || input[:header_enum_list].empty? end end @@ -608,7 +578,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/jsonenums') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -628,7 +598,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/JsonMaps') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -652,7 +622,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/jsonunions') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -701,7 +671,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -851,7 +821,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/MediaTypeHeader') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-Json'] = ::Base64::encode64(input[:json]).strip unless input[:json].nil? || input[:json].empty? end end @@ -894,7 +864,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/nestedattributes') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -919,15 +889,10 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/NullAndEmptyHeadersClient') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-A'] = input[:a] unless input[:a].nil? || input[:a].empty? http_req.headers['X-B'] = input[:b] unless input[:b].nil? || input[:b].empty? - unless input[:c].nil? || input[:c].empty? - http_req.headers['X-C'] = input[:c] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end + http_req.headers['X-C'] = input[:c] unless input[:c].nil? || input[:c].empty? end end @@ -937,7 +902,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/nulloperation') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -956,7 +921,7 @@ def self.build(http_req, input:) params = Hearth::Query::ParamList.new params['Null'] = input[:null_value].to_s unless input[:null_value].nil? params['Empty'] = input[:empty_string].to_s unless input[:empty_string].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -966,7 +931,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/operationwithoptionalinputoutput') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -982,7 +947,7 @@ def self.build(http_req, input:) http_req.append_path('/QueryIdempotencyTokenAutoFill') params = Hearth::Query::ParamList.new params['token'] = input[:token].to_s unless input[:token].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -1002,7 +967,7 @@ def self.build(http_req, input:) end end params['corge'] = input[:qux].to_s unless input[:qux].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -1087,7 +1052,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/streamingoperation') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.body = input[:output] http_req.headers['Transfer-Encoding'] = 'chunked' http_req.headers['Content-Type'] = 'application/octet-stream' @@ -1153,7 +1118,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/TimestampFormatHeaders') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-memberEpochSeconds'] = Hearth::TimeHelper.to_epoch_seconds(input[:member_epoch_seconds]).to_i unless input[:member_epoch_seconds].nil? http_req.headers['X-memberHttpDate'] = Hearth::TimeHelper.to_http_date(input[:member_http_date]) unless input[:member_http_date].nil? http_req.headers['X-memberDateTime'] = Hearth::TimeHelper.to_date_time(input[:member_date_time]) unless input[:member_date_time].nil? @@ -1197,7 +1162,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} diff --git a/codegen/projections/rails_json/lib/rails_json/client.rb b/codegen/projections/rails_json/lib/rails_json/client.rb index fe6f83215..cb3ad19f8 100644 --- a/codegen/projections/rails_json/lib/rails_json/client.rb +++ b/codegen/projections/rails_json/lib/rails_json/client.rb @@ -125,7 +125,7 @@ def all_query_string_types(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -193,7 +193,7 @@ def constant_and_variable_query_string(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -261,7 +261,7 @@ def constant_query_string(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -336,7 +336,7 @@ def document_type(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -409,7 +409,7 @@ def document_type_as_payload(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -470,7 +470,7 @@ def empty_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -535,7 +535,7 @@ def endpoint_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -602,7 +602,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -673,7 +673,7 @@ def greeting_with_errors(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -744,7 +744,7 @@ def http_payload_traits(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -813,7 +813,7 @@ def http_payload_traits_with_media_type(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -887,7 +887,7 @@ def http_payload_with_structure(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -960,7 +960,7 @@ def http_prefix_headers(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1025,7 +1025,7 @@ def http_prefix_headers_in_response(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1089,7 +1089,7 @@ def http_request_with_float_labels(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1153,7 +1153,7 @@ def http_request_with_greedy_label_in_path(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1232,7 +1232,7 @@ def http_request_with_labels(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1304,7 +1304,7 @@ def http_request_with_labels_and_timestamp_format(params = {}, options = {}, &bl resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1366,7 +1366,7 @@ def http_response_code(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1432,7 +1432,7 @@ def ignore_query_params_in_response(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1547,7 +1547,7 @@ def input_and_output_with_headers(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1632,7 +1632,7 @@ def json_enums(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1744,7 +1744,7 @@ def json_maps(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1842,7 +1842,7 @@ def json_unions(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2011,7 +2011,7 @@ def kitchen_sink_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2077,7 +2077,7 @@ def media_type_header(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2143,7 +2143,7 @@ def nested_attributes_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2218,7 +2218,7 @@ def null_and_empty_headers_client(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2292,7 +2292,7 @@ def null_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2358,7 +2358,7 @@ def omits_null_serializes_empty_string(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2422,7 +2422,7 @@ def operation_with_optional_input_output(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2489,7 +2489,7 @@ def query_idempotency_token_auto_fill(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2557,7 +2557,7 @@ def query_params_as_string_list_map(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2620,7 +2620,7 @@ def streaming_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2698,7 +2698,7 @@ def timestamp_format_headers(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2766,7 +2766,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/rails_json/spec/protocol_spec.rb b/codegen/projections/rails_json/spec/protocol_spec.rb index 647b8757f..edf3c7631 100644 --- a/codegen/projections/rails_json/spec/protocol_spec.rb +++ b/codegen/projections/rails_json/spec/protocol_spec.rb @@ -25,9 +25,8 @@ module RailsJson it 'rails_json_serializes_bad_names' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/BadName/abc_value') + expect(request.uri.path).to eq('/BadName/abc_value') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"member":{"__123foo":"foo value"}}')) @@ -50,7 +49,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"member":{"__123foo":"foo value"}}') response.body.rewind Hearth::Output.new @@ -98,11 +97,10 @@ module RailsJson it 'RailsJsonAllQueryStringTypes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/AllQueryStringTypesInput') + expect(request.uri.path).to eq('/AllQueryStringTypesInput') expected_query = ::CGI.parse(['String=Hello%20there', 'StringList=a', 'StringList=b', 'StringList=c', 'StringSet=a', 'StringSet=b', 'StringSet=c', 'Byte=1', 'Short=2', 'Integer=3', 'IntegerList=1', 'IntegerList=2', 'IntegerList=3', 'IntegerSet=1', 'IntegerSet=2', 'IntegerSet=3', 'Long=4', 'Float=1.1', 'Double=1.1', 'DoubleList=1.1', 'DoubleList=2.1', 'DoubleList=3.1', 'Boolean=true', 'BooleanList=true', 'BooleanList=false', 'BooleanList=true', 'Timestamp=1970-01-01T00%3A00%3A01Z', 'TimestampList=1970-01-01T00%3A00%3A01Z', 'TimestampList=1970-01-01T00%3A00%3A02Z', 'TimestampList=1970-01-01T00%3A00%3A03Z', 'Enum=Foo', 'EnumList=Foo', 'EnumList=Baz', 'EnumList=Bar', 'QueryParamsStringKeyA=Foo', 'QueryParamsStringKeyB=Bar'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -179,16 +177,15 @@ module RailsJson it 'RailsJsonConstantAndVariableQueryStringMissingOneValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/ConstantAndVariableQueryString') + expect(request.uri.path).to eq('/ConstantAndVariableQueryString') expected_query = ::CGI.parse(['foo=bar', 'baz=bam'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end forbid_query = ['maybeSet'] - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) forbid_query.each do |query| expect(actual_query.key?(query)).to be false end @@ -205,11 +202,10 @@ module RailsJson it 'RailsJsonConstantAndVariableQueryStringAllValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/ConstantAndVariableQueryString') + expect(request.uri.path).to eq('/ConstantAndVariableQueryString') expected_query = ::CGI.parse(['foo=bar', 'baz=bam', 'maybeSet=yes'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -234,11 +230,10 @@ module RailsJson it 'RailsJsonConstantQueryString' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/ConstantQueryString/hi') + expect(request.uri.path).to eq('/ConstantQueryString/hi') expected_query = ::CGI.parse(['foo=bar', 'hello'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -262,9 +257,8 @@ module RailsJson it 'RailsJsonDocumentTypeInputWithObject' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -285,9 +279,8 @@ module RailsJson it 'RailsJsonDocumentInputWithString' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -306,9 +299,8 @@ module RailsJson it 'RailsJsonDocumentInputWithNumber' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -327,9 +319,8 @@ module RailsJson it 'RailsJsonDocumentInputWithBoolean' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -348,9 +339,8 @@ module RailsJson it 'RailsJsonDocumentInputWithList' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -388,7 +378,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "string_value": "string", "document_value": { @@ -411,7 +401,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "string_value": "string", "document_value": "hello" @@ -432,7 +422,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "string_value": "string", "document_value": 10 @@ -453,7 +443,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "string_value": "string", "document_value": false @@ -474,7 +464,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "string_value": "string", "document_value": [ @@ -597,9 +587,8 @@ module RailsJson it 'RailsJsonDocumentTypeAsPayloadInput' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentTypeAsPayload') + expect(request.uri.path).to eq('/DocumentTypeAsPayload') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "foo": "bar" @@ -616,9 +605,8 @@ module RailsJson it 'RailsJsonDocumentTypeAsPayloadInputString' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentTypeAsPayload') + expect(request.uri.path).to eq('/DocumentTypeAsPayload') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('"hello"')) Hearth::Output.new @@ -637,7 +625,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "foo": "bar" }') @@ -656,7 +644,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('"hello"') response.body.rewind Hearth::Output.new @@ -718,7 +706,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{}') response.body.rewind Hearth::Output.new @@ -738,7 +726,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "foo": true }') @@ -761,7 +749,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('') response.body.rewind Hearth::Output.new @@ -846,10 +834,9 @@ module RailsJson it 'RailsJsonEndpointTrait' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.host).to eq('foo.example.com') - expect(request_uri.path).to eq('/endpoint') + expect(request.uri.host).to eq('foo.example.com') + expect(request.uri.path).to eq('/endpoint') Hearth::Output.new end opts = {middleware: middleware} @@ -872,10 +859,9 @@ module RailsJson it 'RailsJsonEndpointTraitWithHostLabel' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.host).to eq('foo.bar.example.com') - expect(request_uri.path).to eq('/endpointwithhostlabel') + expect(request.uri.host).to eq('foo.bar.example.com') + expect(request.uri.path).to eq('/endpointwithhostlabel') expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"label_member": "bar"}')) Hearth::Output.new end @@ -898,7 +884,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'InvalidGreeting' }) + response.headers['Content-Type'] = 'application/json' + response.headers['x-smithy-rails-error'] = 'InvalidGreeting' response.body.write('{ "message": "Hi" }') @@ -923,7 +910,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'ComplexError' }) + response.headers['Content-Type'] = 'application/json' + response.headers['x-smithy-rails-error'] = 'ComplexError' response.body.write('{ "top_level": "Top level", "nested": { @@ -951,7 +939,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'ComplexError' }) + response.headers['Content-Type'] = 'application/json' + response.headers['x-smithy-rails-error'] = 'ComplexError' response.body.write('{ }') response.body.rewind @@ -978,9 +967,8 @@ module RailsJson it 'RailsJsonHttpPayloadTraitsWithBlob' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/HttpPayloadTraits') + expect(request.uri.path).to eq('/HttpPayloadTraits') { 'Content-Type' => 'application/octet-stream', 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(request.body.read).to eq('blobby blob blob') @@ -997,9 +985,8 @@ module RailsJson it 'RailsJsonHttpPayloadTraitsWithNoBlobBody' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/HttpPayloadTraits') + expect(request.uri.path).to eq('/HttpPayloadTraits') { 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1018,7 +1005,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo' }) + response.headers['X-Foo'] = 'Foo' response.body.write('blobby blob blob') response.body.rewind Hearth::Output.new @@ -1036,7 +1023,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo' }) + response.headers['X-Foo'] = 'Foo' response.body.write('') response.body.rewind Hearth::Output.new @@ -1096,9 +1083,8 @@ module RailsJson it 'RailsJsonHttpPayloadTraitsWithMediaTypeWithBlob' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/HttpPayloadTraitsWithMediaType') + expect(request.uri.path).to eq('/HttpPayloadTraitsWithMediaType') { 'Content-Type' => 'text/plain', 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(request.body.read).to eq('blobby blob blob') @@ -1119,7 +1105,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'text/plain', 'X-Foo' => 'Foo' }) + response.headers['Content-Type'] = 'text/plain' + response.headers['X-Foo'] = 'Foo' response.body.write('blobby blob blob') response.body.rewind Hearth::Output.new @@ -1164,9 +1151,8 @@ module RailsJson it 'RailsJsonHttpPayloadWithStructure' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/HttpPayloadWithStructure') + expect(request.uri.path).to eq('/HttpPayloadWithStructure') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ @@ -1192,7 +1178,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "greeting": "hello", "name": "Phreddy" @@ -1246,9 +1232,8 @@ module RailsJson it 'RailsJsonHttpPrefixHeadersArePresent' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpPrefixHeaders') + expect(request.uri.path).to eq('/HttpPrefixHeaders') { 'X-Foo' => 'Foo', 'X-Foo-Abc' => 'Abc value', 'X-Foo-Def' => 'Def value' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1267,9 +1252,8 @@ module RailsJson it 'RailsJsonHttpPrefixHeadersAreNotPresent' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpPrefixHeaders') + expect(request.uri.path).to eq('/HttpPrefixHeaders') { 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1291,7 +1275,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo', 'X-Foo-Abc' => 'Abc value', 'X-Foo-Def' => 'Def value' }) + response.headers['X-Foo'] = 'Foo' + response.headers['X-Foo-Abc'] = 'Abc value' + response.headers['X-Foo-Def'] = 'Def value' Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1344,7 +1330,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Hello' => 'Hello', 'X-Foo' => 'Foo' }) + response.headers['Hello'] = 'Hello' + response.headers['X-Foo'] = 'Foo' Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1393,9 +1380,8 @@ module RailsJson it 'RailsJsonSupportsNaNFloatLabels' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/FloatHttpLabels/NaN/NaN') + expect(request.uri.path).to eq('/FloatHttpLabels/NaN/NaN') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1410,9 +1396,8 @@ module RailsJson it 'RailsJsonSupportsInfinityFloatLabels' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/FloatHttpLabels/Infinity/Infinity') + expect(request.uri.path).to eq('/FloatHttpLabels/Infinity/Infinity') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1427,9 +1412,8 @@ module RailsJson it 'RailsJsonSupportsNegativeInfinityFloatLabels' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/FloatHttpLabels/-Infinity/-Infinity') + expect(request.uri.path).to eq('/FloatHttpLabels/-Infinity/-Infinity') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1451,9 +1435,8 @@ module RailsJson it 'RailsJsonHttpRequestWithGreedyLabelInPath' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithGreedyLabelInPath/foo/hello%2Fescape/baz/there/guy') + expect(request.uri.path).to eq('/HttpRequestWithGreedyLabelInPath/foo/hello%2Fescape/baz/there/guy') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1475,9 +1458,8 @@ module RailsJson it 'RailsJsonInputWithHeadersAndAllParams' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithLabels/string/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') + expect(request.uri.path).to eq('/HttpRequestWithLabels/string/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1498,9 +1480,8 @@ module RailsJson it 'RailsJsonHttpRequestLabelEscaping' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithLabels/%25%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%F0%9F%98%B9/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') + expect(request.uri.path).to eq('/HttpRequestWithLabels/%25%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%F0%9F%98%B9/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1528,9 +1509,8 @@ module RailsJson it 'RailsJsonHttpRequestWithLabelsAndTimestampFormat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithLabelsAndTimestampFormat/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z/2019-12-16T23%3A48%3A18Z/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z') + expect(request.uri.path).to eq('/HttpRequestWithLabelsAndTimestampFormat/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z/2019-12-16T23%3A48%3A18Z/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1562,7 +1542,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 201 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{}') response.body.rewind Hearth::Output.new @@ -1648,7 +1628,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{}') response.body.rewind Hearth::Output.new @@ -1729,9 +1709,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithStringHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-String' => 'Hello', 'X-StringList' => 'a, b, c', 'X-StringSet' => 'a, b, c' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1756,9 +1735,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithNumericHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-Byte' => '1', 'X-Double' => '1.1', 'X-Float' => '1.1', 'X-Integer' => '123', 'X-IntegerList' => '1, 2, 3', 'X-Long' => '123', 'X-Short' => '123' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1783,9 +1761,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithBooleanHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-Boolean1' => 'true', 'X-Boolean2' => 'false', 'X-BooleanList' => 'true, false, true' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1806,9 +1783,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithEnumHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-Enum' => 'Foo', 'X-EnumList' => 'Foo, Bar, Baz' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1832,7 +1808,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-String' => 'Hello', 'X-StringList' => 'a, b, c', 'X-StringSet' => 'a, b, c' }) + response.headers['X-String'] = 'Hello' + response.headers['X-StringList'] = 'a, b, c' + response.headers['X-StringSet'] = 'a, b, c' response.body.write('') response.body.rewind Hearth::Output.new @@ -1859,7 +1837,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-StringList' => '"b,c", "\"def\"", a' }) + response.headers['X-StringList'] = '"b,c", "\"def\"", a' response.body.write('') response.body.rewind Hearth::Output.new @@ -1880,7 +1858,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Byte' => '1', 'X-Double' => '1.1', 'X-Float' => '1.1', 'X-Integer' => '123', 'X-IntegerList' => '1, 2, 3', 'X-Long' => '123', 'X-Short' => '123' }) + response.headers['X-Byte'] = '1' + response.headers['X-Double'] = '1.1' + response.headers['X-Float'] = '1.1' + response.headers['X-Integer'] = '123' + response.headers['X-IntegerList'] = '1, 2, 3' + response.headers['X-Long'] = '123' + response.headers['X-Short'] = '123' response.body.write('') response.body.rewind Hearth::Output.new @@ -1907,7 +1891,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Boolean1' => 'true', 'X-Boolean2' => 'false', 'X-BooleanList' => 'true, false, true' }) + response.headers['X-Boolean1'] = 'true' + response.headers['X-Boolean2'] = 'false' + response.headers['X-BooleanList'] = 'true, false, true' response.body.write('') response.body.rewind Hearth::Output.new @@ -1930,7 +1916,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Enum' => 'Foo', 'X-EnumList' => 'Foo, Bar, Baz' }) + response.headers['X-Enum'] = 'Foo' + response.headers['X-EnumList'] = 'Foo, Bar, Baz' response.body.write('') response.body.rewind Hearth::Output.new @@ -2111,9 +2098,8 @@ module RailsJson it 'RailsJsonEnums' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonenums') + expect(request.uri.path).to eq('/jsonenums') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "foo_enum1": "Foo", @@ -2162,7 +2148,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "foo_enum1": "Foo", "foo_enum2": "0", @@ -2262,9 +2248,8 @@ module RailsJson it 'RailsJsonJsonMaps' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "dense_struct_map": { @@ -2311,9 +2296,8 @@ module RailsJson it 'RailsJsonSerializesNullMapValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_boolean_map": { @@ -2352,9 +2336,8 @@ module RailsJson it 'RailsJsonSerializesZeroValuesInMaps' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "dense_number_map": { @@ -2393,9 +2376,8 @@ module RailsJson it 'RailsJsonSerializesSparseSetMap' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_set_map": { @@ -2423,9 +2405,8 @@ module RailsJson it 'RailsJsonSerializesDenseSetMap' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "dense_set_map": { @@ -2453,9 +2434,8 @@ module RailsJson it 'RailsJsonSerializesSparseSetMapAndRetainsNull' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_set_map": { @@ -2489,7 +2469,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "dense_struct_map": { "foo": { @@ -2538,7 +2518,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "sparse_boolean_map": { "x": null @@ -2579,7 +2559,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "dense_number_map": { "x": 0 @@ -2620,7 +2600,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "sparse_set_map": { "x": [], @@ -2650,7 +2630,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "dense_set_map": { "x": [], @@ -2680,7 +2660,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "sparse_set_map": { "x": [], @@ -2713,7 +2693,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "dense_set_map": { "x": [], @@ -3006,9 +2986,8 @@ module RailsJson it 'RailsJsonSerializeStringUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3029,9 +3008,8 @@ module RailsJson it 'RailsJsonSerializeBooleanUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3052,9 +3030,8 @@ module RailsJson it 'RailsJsonSerializeNumberUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3075,9 +3052,8 @@ module RailsJson it 'RailsJsonSerializeBlobUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3098,9 +3074,8 @@ module RailsJson it 'RailsJsonSerializeTimestampUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3121,9 +3096,8 @@ module RailsJson it 'RailsJsonSerializeEnumUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3144,9 +3118,8 @@ module RailsJson it 'RailsJsonSerializeListUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3170,9 +3143,8 @@ module RailsJson it 'RailsJsonSerializeMapUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3199,9 +3171,8 @@ module RailsJson it 'RailsJsonSerializeStructureUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3230,7 +3201,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "string_value": "foo" @@ -3253,7 +3224,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "boolean_value": true @@ -3276,7 +3247,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "number_value": 1 @@ -3299,7 +3270,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "blob_value": "Zm9v" @@ -3322,7 +3293,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "timestamp_value": "2014-04-29T18:30:38Z" @@ -3345,7 +3316,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "enum_value": "Foo" @@ -3368,7 +3339,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "list_value": ["foo", "bar"] @@ -3394,7 +3365,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "map_value": { @@ -3423,7 +3394,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "contents": { "structure_value": { @@ -3655,9 +3626,8 @@ module RailsJson it 'rails_json_rails_json_serializes_string_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"string":"abc xyz"}')) @@ -3673,9 +3643,8 @@ module RailsJson it 'rails_json_serializes_string_shapes_with_jsonvalue_trait' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"json_value":"{\"string\":\"value\",\"number\":1234.5,\"boolTrue\":true,\"boolFalse\":false,\"array\":[1,2,3,4],\"object\":{\"key\":\"value\"},\"null\":null}"}')) @@ -3691,9 +3660,8 @@ module RailsJson it 'rails_json_serializes_integer_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"integer":1234}')) @@ -3709,9 +3677,8 @@ module RailsJson it 'rails_json_serializes_long_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"long":999999999999}')) @@ -3727,9 +3694,8 @@ module RailsJson it 'rails_json_serializes_float_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"float":1234.5}')) @@ -3745,9 +3711,8 @@ module RailsJson it 'rails_json_serializes_double_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"double":1234.5}')) @@ -3763,9 +3728,8 @@ module RailsJson it 'rails_json_serializes_blob_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"blob":"YmluYXJ5LXZhbHVl"}')) @@ -3781,9 +3745,8 @@ module RailsJson it 'rails_json_serializes_boolean_shapes_true' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"boolean":true}')) @@ -3799,9 +3762,8 @@ module RailsJson it 'rails_json_serializes_boolean_shapes_false' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"boolean":false}')) @@ -3817,9 +3779,8 @@ module RailsJson it 'rails_json_serializes_timestamp_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"timestamp":"2000-01-02T20:34:56Z"}')) @@ -3853,9 +3814,8 @@ module RailsJson it 'rails_json_serializes_timestamp_shapes_with_iso8601_timestampformat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"iso8601_timestamp":"2000-01-02T20:34:56Z"}')) @@ -3871,9 +3831,8 @@ module RailsJson it 'rails_json_serializes_timestamp_shapes_with_httpdate_timestampformat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56 GMT"}')) @@ -3889,9 +3848,8 @@ module RailsJson it 'rails_json_serializes_timestamp_shapes_with_unixtimestamp_timestampformat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"unix_timestamp":946845296}')) @@ -3907,9 +3865,8 @@ module RailsJson it 'rails_json_serializes_list_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_strings":["abc","mno","xyz"]}')) @@ -3929,9 +3886,8 @@ module RailsJson it 'rails_json_serializes_empty_list_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_strings":[]}')) @@ -3949,9 +3905,8 @@ module RailsJson it 'rails_json_serializes_list_of_map_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_maps_of_strings":[{"foo":"bar"},{"abc":"xyz"},{"red":"blue"}]}')) @@ -3977,9 +3932,8 @@ module RailsJson it 'rails_json_serializes_list_of_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_structs":[{"value":"abc"},{"value":"mno"},{"value":"xyz"}]}')) @@ -4005,9 +3959,8 @@ module RailsJson it 'rails_json_serializes_list_of_recursive_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"recursive_list":[{"recursive_list":[{"recursive_list":[{"integer":123}]}]}]}')) @@ -4035,9 +3988,8 @@ module RailsJson it 'rails_json_serializes_map_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_strings":{"abc":"xyz","mno":"hjk"}}')) @@ -4056,9 +4008,8 @@ module RailsJson it 'rails_json_serializes_empty_map_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_strings":{}}')) @@ -4076,9 +4027,8 @@ module RailsJson it 'rails_json_serializes_map_of_list_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_lists_of_strings":{"abc":["abc","xyz"],"mno":["xyz","abc"]}}')) @@ -4103,9 +4053,8 @@ module RailsJson it 'rails_json_serializes_map_of_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_structs":{"key1":{"value":"value-1"},"key2":{"value":"value-2"}}}')) @@ -4128,9 +4077,8 @@ module RailsJson it 'rails_json_serializes_map_of_recursive_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"recursive_map":{"key1":{"recursive_map":{"key2":{"recursive_map":{"key3":{"boolean":false}}}}}}}')) @@ -4158,9 +4106,8 @@ module RailsJson it 'rails_json_serializes_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"simple_struct":{"value":"abc"}}')) @@ -4178,9 +4125,8 @@ module RailsJson it 'rails_json_serializes_structure_members_with_locationname_traits' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"struct_with_location_name":{"RenamedMember":"some-value"}}')) @@ -4198,9 +4144,8 @@ module RailsJson it 'rails_json_serializes_empty_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"simple_struct":{}}')) @@ -4218,9 +4163,8 @@ module RailsJson it 'rails_json_serializes_structure_which_have_no_members' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"empty_struct":{}}')) @@ -4238,9 +4182,8 @@ module RailsJson it 'rails_json_serializes_recursive_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"string":"top-value","boolean":false,"recursive_struct":{"string":"nested-value","boolean":true,"recursive_list":[{"string":"string-only"},{"recursive_struct":{"map_of_strings":{"color":"red","size":"large"}}}]}}')) @@ -4278,7 +4221,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{}') response.body.rewind Hearth::Output.new @@ -4295,7 +4238,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"string":"string-value"}') response.body.rewind Hearth::Output.new @@ -4312,7 +4255,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"integer":1234}') response.body.rewind Hearth::Output.new @@ -4329,7 +4272,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"long":1234567890123456789}') response.body.rewind Hearth::Output.new @@ -4346,7 +4289,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"float":1234.5}') response.body.rewind Hearth::Output.new @@ -4363,7 +4306,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"double":123456789.12345679}') response.body.rewind Hearth::Output.new @@ -4380,7 +4323,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"boolean":true}') response.body.rewind Hearth::Output.new @@ -4397,7 +4340,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"boolean":false}') response.body.rewind Hearth::Output.new @@ -4414,7 +4357,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"blob":"YmluYXJ5LXZhbHVl"}') response.body.rewind Hearth::Output.new @@ -4431,7 +4374,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"timestamp":"2000-01-02T20:34:56Z"}') response.body.rewind Hearth::Output.new @@ -4465,7 +4408,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"iso8601_timestamp":"2000-01-02T20:34:56Z"}') response.body.rewind Hearth::Output.new @@ -4482,7 +4425,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.000 GMT"}') response.body.rewind Hearth::Output.new @@ -4516,7 +4459,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"list_of_strings":["abc","mno","xyz"]}') response.body.rewind Hearth::Output.new @@ -4537,7 +4480,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"list_of_maps_of_strings":[{"size":"large"},{"color":"red"}]}') response.body.rewind Hearth::Output.new @@ -4561,7 +4504,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"list_of_lists":[["abc","mno","xyz"],["hjk","qrs","tuv"]]}') response.body.rewind Hearth::Output.new @@ -4589,7 +4532,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"list_of_structs":[{"value":"value-1"},{"value":"value-2"}]}') response.body.rewind Hearth::Output.new @@ -4613,7 +4556,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"recursive_list":[{"recursive_list":[{"recursive_list":[{"string":"value"}]}]}]}') response.body.rewind Hearth::Output.new @@ -4642,7 +4585,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"map_of_strings":{"size":"large","color":"red"}}') response.body.rewind Hearth::Output.new @@ -4662,7 +4605,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"map_of_lists_of_strings":{"sizes":["large","small"],"colors":["red","green"]}}') response.body.rewind Hearth::Output.new @@ -4688,7 +4631,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"map_of_maps":{"sizes":{"large":"L","medium":"M"},"colors":{"red":"R","blue":"B"}}}') response.body.rewind Hearth::Output.new @@ -4714,7 +4657,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"map_of_structs":{"size":{"value":"small"},"color":{"value":"red"}}}') response.body.rewind Hearth::Output.new @@ -4738,7 +4681,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"recursive_map":{"key-1":{"recursive_map":{"key-2":{"recursive_map":{"key-3":{"string":"value"}}}}}}}') response.body.rewind Hearth::Output.new @@ -4767,7 +4710,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'X-Amzn-Requestid' => 'amazon-uniq-request-id' }) + response.headers['Content-Type'] = 'application/json' + response.headers['X-Amzn-Requestid'] = 'amazon-uniq-request-id' response.body.write('{}') response.body.rewind Hearth::Output.new @@ -5355,9 +5299,8 @@ module RailsJson it 'RailsJsonMediaTypeHeaderInputBase64' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/MediaTypeHeader') + expect(request.uri.path).to eq('/MediaTypeHeader') { 'X-Json' => 'dHJ1ZQ==' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -5376,7 +5319,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Json' => 'dHJ1ZQ==' }) + response.headers['X-Json'] = 'dHJ1ZQ==' response.body.write('') response.body.rewind Hearth::Output.new @@ -5418,9 +5361,8 @@ module RailsJson it 'rails_json_nested_attributes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nestedattributes') + expect(request.uri.path).to eq('/nestedattributes') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"simple_struct_attributes":{"value":"simple struct value"}}')) Hearth::Output.new @@ -5444,9 +5386,8 @@ module RailsJson it 'RailsJsonNullAndEmptyHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/NullAndEmptyHeadersClient') + expect(request.uri.path).to eq('/NullAndEmptyHeadersClient') ['X-A', 'X-B', 'X-C'].each { |k| expect(request.headers.key?(k)).to be(false) } expect(request.body.read).to eq('') Hearth::Output.new @@ -5472,9 +5413,8 @@ module RailsJson it 'RailsJsonStructuresDontSerializeNullValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nulloperation') + expect(request.uri.path).to eq('/nulloperation') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{}')) Hearth::Output.new @@ -5489,9 +5429,8 @@ module RailsJson it 'RailsJsonMapsSerializeNullValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nulloperation') + expect(request.uri.path).to eq('/nulloperation') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_string_map": { @@ -5512,9 +5451,8 @@ module RailsJson it 'RailsJsonListsSerializeNull' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nulloperation') + expect(request.uri.path).to eq('/nulloperation') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_string_list": [ @@ -5539,7 +5477,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "string": null }') @@ -5558,7 +5496,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "sparse_string_map": { "foo": null @@ -5581,7 +5519,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{ "sparse_string_list": [ null @@ -5669,9 +5607,8 @@ module RailsJson it 'RailsJsonOmitsNullQuery' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/OmitsNullSerializesEmptyString') + expect(request.uri.path).to eq('/OmitsNullSerializesEmptyString') expect(request.body.read).to eq('') Hearth::Output.new end @@ -5685,11 +5622,10 @@ module RailsJson it 'RailsJsonSerializesEmptyQueryValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/OmitsNullSerializesEmptyString') + expect(request.uri.path).to eq('/OmitsNullSerializesEmptyString') expected_query = ::CGI.parse(['Empty='].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5713,9 +5649,8 @@ module RailsJson it 'rails_json_can_call_operation_with_no_input_or_output' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/operationwithoptionalinputoutput') + expect(request.uri.path).to eq('/operationwithoptionalinputoutput') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{}')) Hearth::Output.new @@ -5730,9 +5665,8 @@ module RailsJson it 'rails_json_can_call_operation_with_optional_input' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/operationwithoptionalinputoutput') + expect(request.uri.path).to eq('/operationwithoptionalinputoutput') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"value":"Hi"}')) Hearth::Output.new @@ -5755,11 +5689,10 @@ module RailsJson allow(SecureRandom).to receive(:uuid).and_return('00000000-0000-4000-8000-000000000000') middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/QueryIdempotencyTokenAutoFill') + expect(request.uri.path).to eq('/QueryIdempotencyTokenAutoFill') expected_query = ::CGI.parse(['token=00000000-0000-4000-8000-000000000000'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5777,11 +5710,10 @@ module RailsJson allow(SecureRandom).to receive(:uuid).and_return('00000000-0000-4000-8000-000000000000') middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/QueryIdempotencyTokenAutoFill') + expect(request.uri.path).to eq('/QueryIdempotencyTokenAutoFill') expected_query = ::CGI.parse(['token=00000000-0000-4000-8000-000000000000'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5805,11 +5737,10 @@ module RailsJson it 'RailsJsonQueryParamsStringListMap' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/StringListMap') + expect(request.uri.path).to eq('/StringListMap') expected_query = ::CGI.parse(['corge=named', 'baz=bar', 'baz=qux'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5843,9 +5774,8 @@ module RailsJson it 'RailsJsonTimestampFormatHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/TimestampFormatHeaders') + expect(request.uri.path).to eq('/TimestampFormatHeaders') { 'X-defaultFormat' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-memberDateTime' => '2019-12-16T23:48:18Z', 'X-memberEpochSeconds' => '1576540098', 'X-memberHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-targetDateTime' => '2019-12-16T23:48:18Z', 'X-targetEpochSeconds' => '1576540098', 'X-targetHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -5870,7 +5800,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-defaultFormat' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-memberDateTime' => '2019-12-16T23:48:18Z', 'X-memberEpochSeconds' => '1576540098', 'X-memberHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-targetDateTime' => '2019-12-16T23:48:18Z', 'X-targetEpochSeconds' => '1576540098', 'X-targetHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT' }) + response.headers['X-defaultFormat'] = 'Mon, 16 Dec 2019 23:48:18 GMT' + response.headers['X-memberDateTime'] = '2019-12-16T23:48:18Z' + response.headers['X-memberEpochSeconds'] = '1576540098' + response.headers['X-memberHttpDate'] = 'Mon, 16 Dec 2019 23:48:18 GMT' + response.headers['X-targetDateTime'] = '2019-12-16T23:48:18Z' + response.headers['X-targetEpochSeconds'] = '1576540098' + response.headers['X-targetHttpDate'] = 'Mon, 16 Dec 2019 23:48:18 GMT' response.body.write('') response.body.rewind Hearth::Output.new diff --git a/codegen/projections/rails_json/spec/request_id_spec.rb b/codegen/projections/rails_json/spec/request_id_spec.rb index 257d89c85..350607ef5 100644 --- a/codegen/projections/rails_json/spec/request_id_spec.rb +++ b/codegen/projections/rails_json/spec/request_id_spec.rb @@ -13,7 +13,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'x-request-id' => '123' }) + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end @@ -28,9 +28,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new( - { 'x-smithy-rails-error' => 'InvalidGreeting', 'x-request-id' => '123' } - ) + response.headers['x-smithy-rails-error'] = 'InvalidGreeting' + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end diff --git a/codegen/projections/weather/lib/weather/client.rb b/codegen/projections/weather/lib/weather/client.rb index eb3cf0a47..a8e342480 100644 --- a/codegen/projections/weather/lib/weather/client.rb +++ b/codegen/projections/weather/lib/weather/client.rb @@ -94,7 +94,7 @@ def get_city(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -166,7 +166,7 @@ def get_city_image(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -227,7 +227,7 @@ def get_current_time(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -305,7 +305,7 @@ def get_forecast(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -388,7 +388,7 @@ def list_cities(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -456,7 +456,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/weather/spec/protocol_spec.rb b/codegen/projections/weather/spec/protocol_spec.rb index 5d2c08340..42641b521 100644 --- a/codegen/projections/weather/spec/protocol_spec.rb +++ b/codegen/projections/weather/spec/protocol_spec.rb @@ -55,9 +55,8 @@ module Weather it 'WriteGetCityAssertions' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/cities/123') + expect(request.uri.path).to eq('/cities/123') expect(request.body.read).to eq('') Hearth::Output.new end @@ -252,16 +251,15 @@ module Weather it 'WriteListCitiesAssertions' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/cities') + expect(request.uri.path).to eq('/cities') expected_query = ::CGI.parse(['pageSize=50'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end forbid_query = ['nextToken'] - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) forbid_query.each do |query| expect(actual_query.key?(query)).to be false end diff --git a/codegen/projections/white_label/lib/white_label/client.rb b/codegen/projections/white_label/lib/white_label/client.rb index 49e993990..5ed2de95c 100644 --- a/codegen/projections/white_label/lib/white_label/client.rb +++ b/codegen/projections/white_label/lib/white_label/client.rb @@ -156,7 +156,7 @@ def defaults_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -220,7 +220,7 @@ def endpoint_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -286,7 +286,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -513,7 +513,7 @@ def kitchen_sink(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -577,7 +577,7 @@ def mixin_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -642,7 +642,7 @@ def paginators_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -707,7 +707,7 @@ def paginators_test_with_items(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -769,7 +769,7 @@ def streaming_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -831,7 +831,7 @@ def streaming_with_length(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -894,7 +894,7 @@ def waiters_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -960,7 +960,7 @@ def operation____paginators_test_with_bad_names(params = {}, options = {}, &bloc resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/white_label/spec/client_spec.rb b/codegen/projections/white_label/spec/client_spec.rb index 640d3c264..7734cefcd 100644 --- a/codegen/projections/white_label/spec/client_spec.rb +++ b/codegen/projections/white_label/spec/client_spec.rb @@ -67,7 +67,7 @@ module WhiteLabel it 'uses endpoint from config' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: config.endpoint)) + .with(hash_including(uri: URI(config.endpoint))) .and_call_original client.kitchen_sink @@ -76,7 +76,7 @@ module WhiteLabel it 'uses endpoint from options' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: 'endpoint')) + .with(hash_including(uri: URI('endpoint'))) .and_call_original client.kitchen_sink({}, endpoint: 'endpoint') diff --git a/codegen/projections/white_label/spec/endpoints_spec.rb b/codegen/projections/white_label/spec/endpoints_spec.rb index 6ecc1afa3..b5b2609ab 100644 --- a/codegen/projections/white_label/spec/endpoints_spec.rb +++ b/codegen/projections/white_label/spec/endpoints_spec.rb @@ -9,7 +9,7 @@ module WhiteLabel describe '#endpoint_operation' do it 'prepends to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include('foo') + expect(context.request.uri.to_s).to include('foo') end client.endpoint_operation( {}, middleware: middleware) end @@ -28,7 +28,7 @@ module WhiteLabel it 'prepends the label to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include("foo.#{label}") + expect(context.request.uri.to_s).to include("foo.#{label}") end client.endpoint_with_host_label_operation({ label_member: label }, middleware: middleware) end diff --git a/codegen/projections/white_label/spec/errors_spec.rb b/codegen/projections/white_label/spec/errors_spec.rb index 10c92e75d..f93dcd685 100644 --- a/codegen/projections/white_label/spec/errors_spec.rb +++ b/codegen/projections/white_label/spec/errors_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require_relative 'spec_helper' module WhiteLabel module Errors diff --git a/codegen/projections/white_label/spec/streaming_spec.rb b/codegen/projections/white_label/spec/streaming_spec.rb index 06efb42a0..b4e40c63e 100644 --- a/codegen/projections/white_label/spec/streaming_spec.rb +++ b/codegen/projections/white_label/spec/streaming_spec.rb @@ -96,7 +96,7 @@ module WhiteLabel streaming_input = StringIO.new("test") middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Transfer-Encoding']).to eq('chunked') - expect(context.request.headers.key?('Content-Length')).to be_falsy + expect(context.request.fields.key?('Content-Length')).to eq(false) end expect(streaming_input).not_to receive(:size) client.streaming_operation({stream: streaming_input}, middleware: middleware) @@ -110,7 +110,7 @@ module WhiteLabel it 'sets content-length and does not set Transfer-Encoding' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Content-Length']).to eq(data.length.to_s) - expect(context.request.headers.key?('Transfer-Encoding')).to be_falsy + expect(context.request.fields.key?('Transfer-Encoding')).to eq(false) end client.streaming_with_length({stream: data}, middleware: middleware) end diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb index 640d3c264..7734cefcd 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb @@ -67,7 +67,7 @@ module WhiteLabel it 'uses endpoint from config' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: config.endpoint)) + .with(hash_including(uri: URI(config.endpoint))) .and_call_original client.kitchen_sink @@ -76,7 +76,7 @@ module WhiteLabel it 'uses endpoint from options' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: 'endpoint')) + .with(hash_including(uri: URI('endpoint'))) .and_call_original client.kitchen_sink({}, endpoint: 'endpoint') diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb index 6ecc1afa3..b5b2609ab 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb @@ -9,7 +9,7 @@ module WhiteLabel describe '#endpoint_operation' do it 'prepends to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include('foo') + expect(context.request.uri.to_s).to include('foo') end client.endpoint_operation( {}, middleware: middleware) end @@ -28,7 +28,7 @@ module WhiteLabel it 'prepends the label to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include("foo.#{label}") + expect(context.request.uri.to_s).to include("foo.#{label}") end client.endpoint_with_host_label_operation({ label_member: label }, middleware: middleware) end diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb index 10c92e75d..f93dcd685 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require_relative 'spec_helper' module WhiteLabel module Errors diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb index 06efb42a0..b4e40c63e 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb @@ -96,7 +96,7 @@ module WhiteLabel streaming_input = StringIO.new("test") middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Transfer-Encoding']).to eq('chunked') - expect(context.request.headers.key?('Content-Length')).to be_falsy + expect(context.request.fields.key?('Content-Length')).to eq(false) end expect(streaming_input).not_to receive(:size) client.streaming_operation({stream: streaming_input}, middleware: middleware) @@ -110,7 +110,7 @@ module WhiteLabel it 'sets content-length and does not set Transfer-Encoding' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Content-Length']).to eq(data.length.to_s) - expect(context.request.headers.key?('Transfer-Encoding')).to be_falsy + expect(context.request.fields.key?('Transfer-Encoding')).to eq(false) end client.streaming_with_length({stream: data}, middleware: middleware) end diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java index 65b33fcf0..09d636e79 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java @@ -93,7 +93,8 @@ public static ApplicationTransport createDefaultHttpApplicationTransport() { ClientFragment request = (new ClientFragment.Builder()) .addConfig(endpoint) - .render((self, ctx) -> "Hearth::HTTP::Request.new(url: " + endpoint.renderGetConfigValue() + ")") + // TODO: Replace URI with Endpoint middleware - should be a blank request + .render((self, ctx) -> "Hearth::HTTP::Request.new(uri: URI(" + endpoint.renderGetConfigValue() + "))") .build(); ClientFragment response = (new ClientFragment.Builder()) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java index 5420b914c..80d05f4d7 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java @@ -338,8 +338,10 @@ private void renderResponseMiddlewareBody(Optional body) { } private void renderResponseMiddlewareHeaders(Map headers) { - if (!headers.isEmpty()) { - writer.write("response.headers = Hearth::HTTP::Headers.new($L)", getRubyHashFromMap(headers)); + Iterator> iterator = headers.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry header = iterator.next(); + writer.write("response.headers['$L'] = '$L'", header.getKey(), header.getValue()); } } @@ -347,10 +349,9 @@ private void renderRequestMiddleware(HttpRequestTestCase testCase) { writer .openBlock("middleware = Hearth::MiddlewareBuilder.before_send do |input, context|") .write("request = context.request") - .write("request_uri = URI.parse(request.url)") .write("expect(request.http_method).to eq('$L')", testCase.getMethod()) .call(() -> renderRequestMiddlewareHost(testCase.getResolvedHost())) - .write("expect(request_uri.path).to eq('$L')", testCase.getUri()) + .write("expect(request.uri.path).to eq('$L')", testCase.getUri()) .call(() -> renderRequestMiddlewareQueryParams(testCase.getQueryParams())) .call(() -> renderRequestMiddlewareForbidQueryParams(testCase.getForbidQueryParams())) .call(() -> renderRequestMiddlewareRequireQueryParams(testCase.getRequireQueryParams())) @@ -364,7 +365,7 @@ private void renderRequestMiddleware(HttpRequestTestCase testCase) { private void renderRequestMiddlewareHost(Optional resolvedHost) { if (resolvedHost.isPresent()) { - writer.write("expect(request_uri.host).to eq('$L')", resolvedHost.get()); + writer.write("expect(request.uri.host).to eq('$L')", resolvedHost.get()); } } @@ -428,7 +429,7 @@ private void renderRequestMiddlewareQueryParams(List queryParams) { writer .write("expected_query = $T.parse($L.join('&'))", RubyImportContainer.CGI, getRubyArrayFromList(queryParams)) - .write("actual_query = $T.parse(request_uri.query)", + .write("actual_query = $T.parse(request.uri.query)", RubyImportContainer.CGI) .openBlock("expected_query.each do |k, v|") .write("expect(actual_query[k]).to eq(v)") @@ -440,7 +441,7 @@ private void renderRequestMiddlewareForbidQueryParams(List forbidQueryPa if (!forbidQueryParams.isEmpty()) { writer .write("forbid_query = $L", getRubyArrayFromList(forbidQueryParams)) - .write("actual_query = $T.parse(request_uri.query)", + .write("actual_query = $T.parse(request.uri.query)", RubyImportContainer.CGI) .openBlock("forbid_query.each do |query|") .write("expect(actual_query.key?(query)).to be false") @@ -452,7 +453,7 @@ private void renderRequestMiddlewareRequireQueryParams(List requireQuery if (!requireQueryParams.isEmpty()) { writer .write("require_query = $L", getRubyArrayFromList(requireQueryParams)) - .write("actual_query = $T.parse(request_uri.query)", + .write("actual_query = $T.parse(request.uri.query)", RubyImportContainer.CGI) .openBlock("require_query.each do |query|") .write("expect(actual_query.key?(query)).to be true") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java index ebfd14d69..6f834194e 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java @@ -187,7 +187,7 @@ protected void renderQueryInputBuilder(Shape inputShape) { LOGGER.finest("Generated query input builder for " + m.getMemberName()); } - writer.write("http_req.append_query_params(params)"); + writer.write("http_req.append_query_param_list(params)"); } /** @@ -248,7 +248,7 @@ protected void renderUriBuilder(OperationShape operation, Shape inputShape) { String[] uriParts = uri.split("[?]"); if (uriParts.length > 1) { uri = uriParts[0]; - // TODO this should use append_query_params? interface needs to be changed in Hearth if so + // TODO this should use append_query_param_list? interface needs to be changed in Hearth if so writer .openBlock("CGI.parse('$L').each do |k,v|", uriParts[1]) .write("v.each { |q_v| http_req.append_query_param(k, q_v) }") @@ -396,15 +396,7 @@ public Void timestampShape(TimestampShape shape) { @Override public Void listShape(ListShape shape) { - writer.openBlock("unless $1L.nil? || $1L.empty?", inputGetter) - .write("$1L$2L", dataSetter, inputGetter) - .indent() - .write(".compact") - .call(() -> model.expectShape(shape.getMember().getTarget()) - .accept(new HeaderListMemberSerializer(shape.getMember()))) - .write(".join(', ')") - .dedent() - .closeBlock("end"); + writer.write("$1L$2L unless $2L.nil? || $2L.empty?", dataSetter, inputGetter); return null; } @@ -427,37 +419,6 @@ public Void unionShape(UnionShape shape) { } } - protected class HeaderListMemberSerializer extends ShapeVisitor.Default { - - private final MemberShape memberShape; - - HeaderListMemberSerializer(MemberShape memberShape) { - this.memberShape = memberShape; - } - - @Override - protected Void getDefault(Shape shape) { - writer.write(".map { |s| s.to_s }"); - return null; - } - - @Override - public Void stringShape(StringShape shape) { - writer - .write(".map { |s| (s.include?('\"') || s.include?(\",\"))" - + " ? \"\\\"#{s.gsub('\"', '\\\"')}\\\"\" : s }"); - return null; - } - - @Override - public Void timestampShape(TimestampShape shape) { - writer.write(".map { |s| $L }", - TimestampFormat.serializeTimestamp( - shape, memberShape, "s", TimestampFormatTrait.Format.HTTP_DATE, false)); - return null; - } - } - protected class LabelMemberSerializer extends ShapeVisitor.Default { private final MemberShape memberShape; diff --git a/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb b/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb index 257d89c85..350607ef5 100644 --- a/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb +++ b/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb @@ -13,7 +13,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'x-request-id' => '123' }) + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end @@ -28,9 +28,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new( - { 'x-smithy-rails-error' => 'InvalidGreeting', 'x-request-id' => '123' } - ) + response.headers['x-smithy-rails-error'] = 'InvalidGreeting' + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end diff --git a/hearth/lib/hearth.rb b/hearth/lib/hearth.rb index c219fcd56..a79346a9f 100755 --- a/hearth/lib/hearth.rb +++ b/hearth/lib/hearth.rb @@ -7,6 +7,11 @@ require_relative 'hearth/config/env_provider' require_relative 'hearth/config/resolver' require_relative 'hearth/context' + +# must be required before http +require_relative 'hearth/request' +require_relative 'hearth/response' + require_relative 'hearth/http' require_relative 'hearth/json' require_relative 'hearth/middleware' diff --git a/hearth/lib/hearth/http.rb b/hearth/lib/hearth/http.rb index dd374cbc8..22e95ac6b 100755 --- a/hearth/lib/hearth/http.rb +++ b/hearth/lib/hearth/http.rb @@ -4,7 +4,8 @@ require_relative 'http/api_error' require_relative 'http/client' require_relative 'http/error_parser' -require_relative 'http/headers' +require_relative 'http/field' +require_relative 'http/fields' require_relative 'http/middleware/content_length' require_relative 'http/middleware/content_md5' require_relative 'http/networking_error' @@ -19,7 +20,7 @@ module HTTP class << self # URI escapes the given value. # - # Hearth::_escape("a b/c") + # Hearth.uri_escape("a b/c") # #=> "a%20b%2Fc" # # @param [String] value @@ -28,6 +29,13 @@ def uri_escape(value) CGI.escape(value.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~') end + # URI escapes the given path. + # + # Hearth.uri_escape_path("a b/c") + # #=> "a%20b/c" + # + # @param [String] path + # @return [String] URI encoded path except for '+' and '~'. def uri_escape_path(path) path.gsub(%r{[^/]+}) { |part| uri_escape(part) } end diff --git a/hearth/lib/hearth/http/api_error.rb b/hearth/lib/hearth/http/api_error.rb index f88079808..25431ac82 100644 --- a/hearth/lib/hearth/http/api_error.rb +++ b/hearth/lib/hearth/http/api_error.rb @@ -7,7 +7,7 @@ module HTTP class ApiError < Hearth::ApiError def initialize(http_resp:, **kwargs) @http_status = http_resp.status - @http_headers = http_resp.headers + @http_fields = http_resp.fields @http_body = http_resp.body super(**kwargs) end @@ -15,8 +15,8 @@ def initialize(http_resp:, **kwargs) # @return [Integer] attr_reader :http_status - # @return [Hash] - attr_reader :http_headers + # @return [Fields] + attr_reader :http_fields # @return [String] attr_reader :http_body diff --git a/hearth/lib/hearth/http/client.rb b/hearth/lib/hearth/http/client.rb index abb1282ec..a2977d348 100644 --- a/hearth/lib/hearth/http/client.rb +++ b/hearth/lib/hearth/http/client.rb @@ -51,11 +51,10 @@ def initialize(options = {}) # @param [Response] response # @return [Response] def transmit(request:, response:) - uri = URI.parse(request.url) - http = create_http(uri) + http = create_http(request.uri) http.set_debug_output(@logger) if @http_wire_trace - if uri.scheme == 'https' + if request.uri.scheme == 'https' configure_ssl(http) else http.use_ssl = false @@ -77,7 +76,7 @@ def _transmit(http, request, response) http.start do |conn| conn.request(build_net_request(request)) do |net_resp| response.status = net_resp.code.to_i - response.headers = extract_headers(net_resp) + net_resp.each_header { |k, v| response.headers[k] = v } net_resp.read_body do |chunk| response.body.write(chunk) end @@ -115,7 +114,7 @@ def configure_ssl(http) # @return [Net::HTTP::Request] def build_net_request(request) request_class = net_http_request_class(request) - req = request_class.new(request.url, request.headers.to_h) + req = request_class.new(request.uri.to_s, net_headers_for(request)) # Net::HTTP adds a default Content-Type when a body is present. # Set the body stream when it has an unknown size or when it is > 0. @@ -127,10 +126,16 @@ def build_net_request(request) req end - # @param [Net::HTTP::Response] response + # Validate that fields are not trailers and return a hash of headers. + # @param [HTTP::Request] request # @return [Hash] - def extract_headers(response) - response.to_hash.transform_values(&:first) + def net_headers_for(request) + # Trailers are not supported in Net::HTTP + if request.trailers.any? + raise NotImplementedError, 'Trailers are not supported in Net::HTTP' + end + + request.headers.to_h end # @param [Http::Request] request diff --git a/hearth/lib/hearth/http/field.rb b/hearth/lib/hearth/http/field.rb new file mode 100644 index 000000000..ac9eb43e1 --- /dev/null +++ b/hearth/lib/hearth/http/field.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + # Represents an HTTP field. + # @api private + class Field + # @param [String] name The name of the field. + # @param [Array|#to_s] value (nil) The values for the field. It can be any + # object that responds to `#to_s` or an Array of objects that respond to + # `#to_s`. + # @param [Symbol] kind The kind of field, either :header or :trailer. + def initialize(name, value = nil, kind: :header) + if name.nil? || name.empty? + raise ArgumentError, 'Field name must be a non-empty String' + end + + @name = name + @value = value + @kind = kind + end + + # @return [String] + attr_reader :name + + # @return [Symbol] + attr_reader :kind + + # Returns an escaped string representation of the field. + # @return [String] + def value(encoding = nil) + value = + if @value.is_a?(Array) + @value.compact.map { |v| escape_value(v.to_s) }.join(', ') + else + @value.to_s + end + value = value.encode(encoding) if encoding + value + end + + # @return [Boolean] + def header? + @kind == :header + end + + # @return [Boolean] + def trailer? + @kind == :trailer + end + + def to_h + { @name => value } + end + + private + + def escape_value(str) + s = str + s.include?('"') || s.include?(',') ? "\"#{s.gsub('"', '\"')}\"" : s + end + end + end +end diff --git a/hearth/lib/hearth/http/fields.rb b/hearth/lib/hearth/http/fields.rb new file mode 100644 index 000000000..1edeef74b --- /dev/null +++ b/hearth/lib/hearth/http/fields.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + # Provides Hash like access for Headers and Trailers with key normalization + # @api private + class Fields + include Enumerable + + # @param [Array] fields + # @param [String] encoding + def initialize(fields = [], encoding: 'utf-8') + unless fields.is_a?(Array) + raise ArgumentError, 'fields must be an Array' + end + + @entries = {} + fields.each { |field| self[field.name] = field } + @encoding = encoding + end + + # @return [String] + attr_reader :encoding + + # @param [String] key + def [](key) + @entries[key.downcase] + end + + # @param [String] key + # @param [Field] value + def []=(key, value) + raise ArgumentError, 'value must be a Field' unless value.is_a?(Field) + + @entries[key.downcase] = value + end + + # @param [String] key + # @return [Boolean] Returns `true` if there is a Field with the given key. + def key?(key) + @entries.key?(key.downcase) + end + + # @param [String] key + # @return [Field, nil] Returns the Field for the deleted Field key. + def delete(key) + @entries.delete(key.downcase) + end + + # @return [Enumerable] + def each(&block) + @entries.each(&block) + end + alias each_pair each + + # @return [Integer] Returns the number of Field entries. + def size + @entries.size + end + + # @return [Hash] + def clear + @entries = {} + end + + # Proxy class that wraps Fields to create Headers and Trailers + class Proxy + include Enumerable + + def initialize(fields, kind) + @fields = fields + @kind = kind + end + + # @param [String] key + def [](key) + @fields[key].value(@fields.encoding) if key?(key) + end + + # @param [String] key + # @param [#to_s, Array<#to_s>] value + def []=(key, value) + @fields[key] = Field.new(key, value, kind: @kind) + end + + # @param [String] key + # @return [Boolean] Returns `true` if there is a Field with the given + # key and kind. + def key?(key) + @fields.key?(key) && @fields[key].kind == @kind + end + + # @return [Enumerable] + def each(&block) + @fields.filter { |_k, v| v.kind == @kind } + .to_h { |_k, v| [v.name, v.value(@fields.encoding)] } + .each(&block) + end + alias each_pair each + end + end + end +end diff --git a/hearth/lib/hearth/http/headers.rb b/hearth/lib/hearth/http/headers.rb deleted file mode 100755 index 5995270b8..000000000 --- a/hearth/lib/hearth/http/headers.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module HTTP - # Provides Hash like access for Headers with key normalization - # @api private - class Headers - # @param [Hash] headers - def initialize(headers = {}) - @headers = {} - headers.each_pair do |key, value| - self[key] = value - end - end - - # @param [String] key - def [](key) - @headers[normalize(key)] - end - - # @param [String] key - # @param [String] value - def []=(key, value) - @headers[normalize(key)] = value.to_s - end - - # @param [String] key - # @return [Boolean] Returns `true` if there is a header with - # the given key. - def key?(key) - @headers.key?(normalize(key)) - end - - # @return [Array] - def keys - @headers.keys - end - - # @param [String] key - # @return [String, nil] Returns the value for the deleted key. - def delete(key) - @headers.delete(normalize(key)) - end - - # @return [Enumerable] - def each_pair(&block) - @headers.each(&block) - end - alias each each_pair - - # @return [Hash] - def to_hash - @headers.dup - end - alias to_h to_hash - - # @return [Integer] Returns the number of entries in the headers - # hash. - def size - @headers.size - end - - # @param [Hash] headers - # @return [Headers] - def update(headers) - headers.each_pair do |k, v| - self[k] = v - end - self - end - - # @return [Hash] - def clear - @headers = {} - end - - private - - def normalize(key) - key.to_s.gsub(/[^-]+/, &:capitalize) - end - end - end -end diff --git a/hearth/lib/hearth/http/middleware/content_length.rb b/hearth/lib/hearth/http/middleware/content_length.rb index 4ce5532dd..9edea3614 100644 --- a/hearth/lib/hearth/http/middleware/content_length.rb +++ b/hearth/lib/hearth/http/middleware/content_length.rb @@ -15,7 +15,7 @@ def initialize(app, _ = {}) # @return [Output] def call(input, context) request = context.request - if request&.body.respond_to?(:size) && + if request.body.respond_to?(:size) && !request.headers.key?('Content-Length') length = request.body.size request.headers['Content-Length'] = length diff --git a/hearth/lib/hearth/http/request.rb b/hearth/lib/hearth/http/request.rb index 5001ba770..96d5f4694 100755 --- a/hearth/lib/hearth/http/request.rb +++ b/hearth/lib/hearth/http/request.rb @@ -1,83 +1,77 @@ # frozen_string_literal: true -require 'stringio' -require 'uri' - module Hearth module HTTP # Represents an HTTP request. # @api private - class Request + class Request < Hearth::Request # @param [String] http_method - # @param [String] url - # @param [Headers] headers - # @param [IO] body - def initialize(http_method: nil, url: nil, headers: Headers.new, - body: StringIO.new) + # @param [Fields] fields + # @param (see Hearth::Request#initialize) + def initialize(http_method: nil, fields: Fields.new, **kwargs) + super(**kwargs) @http_method = http_method - @url = url - @headers = headers - @body = body + @fields = fields + @headers = Fields::Proxy.new(@fields, :header) + @trailers = Fields::Proxy.new(@fields, :trailer) end # @return [String] attr_accessor :http_method - # @return [String] - attr_accessor :url + # @return [Fields] + attr_reader :fields - # @return [Headers] - attr_accessor :headers + # @return [Fields::Proxy] + attr_reader :headers - # @return [IO] - attr_accessor :body + # @return [Fields::Proxy] + attr_reader :trailers - # Append a path to the HTTP request URL. + # Append a path to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # http_req.append_path('/') - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/" # # Paths will be joined by a single '/': # - # http_req.url = "https://example.com/path-prefix/" + # http_req.uri = "https://example.com/path-prefix/" # http_req.append_path('/path-suffix') - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/path-prefix/path-suffix" # - # Resultant URL preserves the querystring: + # Resultant URI preserves the querystring: # - # http_req.url = "https://example.com/path-prefix?querystring + # http_req.uri = "https://example.com/path-prefix?querystring # http_req.append_path('/path-suffix') - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/path-prefix/path-suffix?querystring" # # The provided path should be URI escaped before being passed. # - # http_req.url = "https://example.com + # http_req.uri = "https://example.com # http_req.append_path( # Hearth::HTTP.uri_escape_path('/part 1/part 2') # ) - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/part%201/part%202" # # @param [String] path A URI escaped path. def append_path(path) - uri = URI.parse(@url) base_path = uri.path.sub(%r{/$}, '') # remove trailing slash path = path.sub(%r{^/}, '') # remove prefix slash uri.path = "#{base_path}/#{path}" # join on single slash - @url = uri.to_s end - # Append querystring parameter to the HTTP request URL. + # Append querystring parameter to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # http_req.append_query_param('query') # http_req.append_query_param('key 1', 'value 1') # - # http_req.url + # http_req.uri.to_s # #=> "https://example.com?query&key%201=value%201 # # @overload append_query_param(name) @@ -96,57 +90,45 @@ def append_path(path) def append_query_param(*args) param = case args.size - when 1 then escape(args[0]) - when 2 then "#{escape(args[0])}=#{escape(args[1])}" + when 1 then Hearth::Query::Param.new(args[0]) + when 2 then Hearth::Query::Param.new(args[0], args[1]) else raise ArgumentError, 'wrong number of arguments ' \ "(given #{args.size}, expected 1 or 2)" end - uri = URI.parse(@url) - uri.query = uri.query ? "#{uri.query}&#{param}" : param - @url = uri.to_s + uri.query = uri.query ? "#{uri.query}&#{param}" : param.to_s end - # Append querystring parameters to the HTTP request URL. + # Append querystring parameter list to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # query_params = Hearth::Query::ParamList.new # query_params['key 1'] = nil # query_params['key 2'] = 'value 2' - # http_req.append_query_params(query_params) + # http_req.append_query_param_list(query_params) # - # http_req.url + # http_req.uri.to_s # #=> "https://example.com?key%201=&key%202=value%202" # # @param [ParamList] param_list # An instance of Hearth::Query::ParamList containing the list of # querystring parameters to add. The names and values are URI escaped. # - def append_query_params(param_list) - uri = URI.parse(@url) + def append_query_param_list(param_list) uri.query = uri.query ? "#{uri.query}&#{param_list}" : param_list.to_s - @url = uri.to_s end - # Append a host prefix to the HTTP request URL. + # Append a host prefix to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # http_req.prefix_host('data.') # - # http_req.url + # http_req.uri.to_s # #=> "https://data.foo.com # # @param [String] prefix A dot (.) terminated prefix for the host. # def prefix_host(prefix) - uri = URI.parse(@url) uri.host = prefix + uri.host - @url = uri.to_s - end - - private - - def escape(value) - Hearth::HTTP.uri_escape(value.to_s) end end end diff --git a/hearth/lib/hearth/http/response.rb b/hearth/lib/hearth/http/response.rb index 3003d4d2a..4bf1ab069 100755 --- a/hearth/lib/hearth/http/response.rb +++ b/hearth/lib/hearth/http/response.rb @@ -1,35 +1,44 @@ # frozen_string_literal: true -require 'stringio' - module Hearth module HTTP # Represents an HTTP Response. # @api private - class Response + class Response < Hearth::Response # @param [Integer] status - # @param [Headers] headers - # @param [IO] body - def initialize(status: 0, headers: Headers.new, body: StringIO.new) + # @param [String, nil] reason + # @param [Fields] fields + # @param (see Hearth::Response#initialize) + def initialize(status: 0, reason: nil, fields: Fields.new, **kwargs) + super(**kwargs) @status = status - @headers = headers - @body = body + @reason = reason + @fields = fields + @headers = Fields::Proxy.new(@fields, :header) + @trailers = Fields::Proxy.new(@fields, :trailer) end # @return [Integer] attr_accessor :status - # @return [Headers] - attr_accessor :headers + # @return [String, nil] + attr_accessor :reason + + # @return [Fields] + attr_reader :fields + + # @return [Fields::Proxy] + attr_reader :headers - # @return [IO] - attr_accessor :body + # @return [Fields::Proxy] + attr_reader :trailers # Resets the HTTP response. # @return [Response] def reset @status = 0 - @headers.clear + @reason = nil + @fields.clear @body.truncate(0) self end diff --git a/hearth/lib/hearth/query/param.rb b/hearth/lib/hearth/query/param.rb index 6d6e0aad3..5a127de4f 100644 --- a/hearth/lib/hearth/query/param.rb +++ b/hearth/lib/hearth/query/param.rb @@ -43,7 +43,7 @@ def <=>(other) private def serialize(name, value) - value.nil? ? "#{escape(name)}=" : "#{escape(name)}=#{escape(value)}" + value.nil? ? escape(name) : "#{escape(name)}=#{escape(value)}" end def escape(value) diff --git a/hearth/lib/hearth/request.rb b/hearth/lib/hearth/request.rb new file mode 100644 index 000000000..f5111d729 --- /dev/null +++ b/hearth/lib/hearth/request.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'stringio' +require 'uri' + +module Hearth + # Represents a base request. + # @api private + class Request + # @param [URI] uri (URI('')) + # @param [IO] (StringIO.new) body + def initialize(uri: URI(''), body: StringIO.new) + @uri = uri + @body = body + end + + # @return [URI] + attr_accessor :uri + + # @return [IO] + attr_accessor :body + end +end diff --git a/hearth/lib/hearth/response.rb b/hearth/lib/hearth/response.rb new file mode 100644 index 000000000..2451f5e36 --- /dev/null +++ b/hearth/lib/hearth/response.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'stringio' + +module Hearth + # Represents a base response. + # @api private + class Response + # @param [IO] body (StringIO.new) + def initialize(body: StringIO.new) + @body = body + end + + # @return [IO] + attr_accessor :body + end +end diff --git a/hearth/sig/lib/hearth/http/headers.rbs b/hearth/sig/lib/hearth/http/headers.rbs deleted file mode 100644 index 80af81377..000000000 --- a/hearth/sig/lib/hearth/http/headers.rbs +++ /dev/null @@ -1,47 +0,0 @@ -module Hearth - module HTTP - # Provides Hash like access for Headers with key normalization - # @api private - class Headers - # @param [Hash] headers - def initialize: (?::Hash[String, String] headers) -> Headers - - # @param [String] key - def []: (String key) -> String - - # @param [String] key - # @param [String] value - def []=: (String key, String value) -> String - - # @param [String] key - # @return [Boolean] Returns `true` if there is a header with - # the given key. - def key?: (String key) -> bool - - # @return [Array] - def keys: () -> Array[String] - - # @param [String] key - # @return [String, nil] Returns the value for the deleted key. - def delete: (String key) -> (String | nil) - - # @return [Enumerable] - def each_pair: () { () -> String } -> Enumerable[Array[String]] - - alias each each_pair - - # @return [Hash] - def to_hash: () -> Hash[String, String] - - alias to_h to_hash - - # @return [Integer] Returns the number of entries in the headers - # hash. - def size: () -> Integer - - private - - def normalize: (String key) -> String - end - end -end diff --git a/hearth/sig/lib/hearth/http/response.rbs b/hearth/sig/lib/hearth/http/response.rbs index beb9d3f4c..660c40d9b 100644 --- a/hearth/sig/lib/hearth/http/response.rbs +++ b/hearth/sig/lib/hearth/http/response.rbs @@ -2,20 +2,31 @@ module Hearth module HTTP # Represents an HTTP Response. # @api private - class Response + class Response < Hearth::Response # @param [Integer] status - # @param [Headers] headers - # @param [IO] body - def initialize: (?status: ::Integer status, ?headers: Headers headers, ?body: IO body) -> Response + # @param [String, nil] reason + # @param [Fields] fields + # @param (see Hearth::Response#initialize) + def initialize: (?status: ::Integer, ?reason: untyped?, ?fields: untyped, **untyped kwargs) -> void # @return [Integer] - attr_accessor status: Integer + attr_accessor status: untyped - # @return [Headers] - attr_accessor headers: Headers + # @return [String, nil] + attr_accessor reason: untyped - # @return [IO] - attr_accessor body: IO + # @return [Fields] + attr_reader fields: untyped + + # @return [Fields::Proxy] + attr_reader headers: untyped + + # @return [Fields::Proxy] + attr_reader trailers: untyped + + # Resets the HTTP response. + # @return [Response] + def reset: () -> self end end -end +end \ No newline at end of file diff --git a/hearth/sig/lib/hearth/response.rbs b/hearth/sig/lib/hearth/response.rbs new file mode 100644 index 000000000..00f236cdb --- /dev/null +++ b/hearth/sig/lib/hearth/response.rbs @@ -0,0 +1,11 @@ +module Hearth + # Represents a base response. + # @api private + class Response + # @param [IO] body (StringIO.new) + def initialize: (?body: untyped) -> void + + # @return [IO] + attr_accessor body: untyped + end +end diff --git a/hearth/spec/hearth/http/api_error_spec.rb b/hearth/spec/hearth/http/api_error_spec.rb index b94a1e8b0..07ddd2efc 100644 --- a/hearth/spec/hearth/http/api_error_spec.rb +++ b/hearth/spec/hearth/http/api_error_spec.rb @@ -4,12 +4,12 @@ module Hearth module HTTP describe ApiError do let(:http_status) { 404 } - let(:http_headers) { Headers.new } + let(:http_fields) { Fields.new } let(:http_body) { 'body' } let(:http_resp) do Response.new( status: http_status, - headers: http_headers, + fields: http_fields, body: http_body ) end @@ -34,9 +34,9 @@ module HTTP end end - describe '#http_headers' do - it 'returns the http headers' do - expect(subject.http_headers).to eq(http_headers) + describe '#http_fields' do + it 'returns the http fields' do + expect(subject.http_fields).to eq(http_fields) end end diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index 49acd30c3..00d0ba13d 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -27,32 +27,31 @@ module HTTP end let(:http_method) { :get } - let(:url) { 'http://example.com' } - let(:headers) { {} } + let(:uri) { URI('http://example.com') } + let(:fields) { Fields.new } let(:request_body) { StringIO.new('') } let(:request) do Request.new( http_method: http_method, - url: url, - headers: headers, + uri: uri, + fields: fields, body: request_body ) end - let(:response) { Response.new } describe '#transmit' do - it 'sends the request to the url' do - stub_request(:any, url) + it 'sends the request to the uri' do + stub_request(:any, uri.to_s) subject.transmit(request: request, response: response) end %i[get post put patch delete].each do |http_method| it "sends a #{http_method} request" do - request = Request.new(http_method: http_method, url: url) + request = Request.new(http_method: http_method, uri: uri) - stub_request(http_method, url) + stub_request(http_method, uri.to_s) subject.transmit(request: request, response: response) end end @@ -65,7 +64,7 @@ module HTTP # webmock sets to nil expect_any_instance_of(Net::HTTP::Get) .to receive(:body_stream=).with(nil).and_call_original - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .with(body: 'TEST_STRING') subject.transmit(request: request, response: response) end @@ -75,7 +74,7 @@ module HTTP it 'does not set the body stream' do expect_any_instance_of(Net::HTTP::Get) .to_not receive(:body_stream=) - stub_request(http_method, url) + stub_request(http_method, uri.to_s) subject.transmit(request: request, response: response) end end @@ -88,8 +87,7 @@ module HTTP wr.close request = Request.new( http_method: http_method, - url: url, - headers: headers, + uri: uri, body: rd ) # webmock sets to nil @@ -97,22 +95,33 @@ module HTTP .to receive(:body_stream=).with(nil).and_call_original expect_any_instance_of(Net::HTTP::Get) .to receive(:body_stream=).with(rd).and_call_original - stub_request(http_method, url) + stub_request(http_method, uri.to_s) subject.transmit(request: request, response: response) end end context 'request headers are set' do - let(:headers) { { 'Header-Name' => 'Header-Value' } } + before { request.headers['Header-Name'] = 'Header-Value' } + it 'transmits the headers' do - stub_request(http_method, url) - .with(headers: headers) + stub_request(http_method, uri.to_s) + .with(headers: request.headers.to_h) subject.transmit(request: request, response: response) end end + context 'request trailers are set' do + before { request.trailers['Trailer-Name'] = 'Trailer-Value' } + + it 'raises NotImplementedError' do + expect do + subject.transmit(request: request, response: response) + end.to raise_error(NotImplementedError) + end + end + it 'sets the response status code' do - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .to_return(status: 242) subject.transmit(request: request, response: response) expect(response.status).to eq(242) @@ -123,15 +132,15 @@ module HTTP 'test-header' => 'value', 'test-header-2' => 'value2' } - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .to_return(headers: response_headers) subject.transmit(request: request, response: response) - expect(response.headers).to eq(response_headers) + expect(response.headers.to_h).to eq(response_headers) end it 'writes the response body' do response_body = 'TEST-BODY' - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .to_return(body: response_body) expect(response.body).to receive(:write).with(response_body) @@ -139,7 +148,7 @@ module HTTP end it 'rewinds the body' do - stub_request(http_method, url) + stub_request(http_method, uri.to_s) expect(response.body).to receive(:rewind) subject.transmit(request: request, response: response) @@ -147,23 +156,23 @@ module HTTP it 'raises ArgumentError on invalid http verbs' do expect do - request = Request.new(http_method: :invalid_verb, url: url) + request = Request.new(http_method: :invalid_verb, uri: uri) subject.transmit(request: request, response: response) end.to raise_error(ArgumentError) end it 'rescues StandardError and converts to a NetworkingError' do - stub_request(:any, url).to_raise(StandardError) + stub_request(:any, uri.to_s).to_raise(StandardError) expect do subject.transmit(request: request, response: response) end.to raise_error(NetworkingError) end context 'https' do - let(:url) { 'https://example.com' } + let(:uri) { URI('https://example.com') } it 'sets use_ssl' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.use_ssl?).to be true http @@ -176,7 +185,7 @@ module HTTP let(:ssl_verify_peer) { false } it 'sets verify_peer to NONE' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.verify_mode).to eq OpenSSL::SSL::VERIFY_NONE http @@ -190,7 +199,7 @@ module HTTP let(:ssl_verify_peer) { true } it 'sets verify_peer to VERIFY_PEER' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.verify_mode).to eq OpenSSL::SSL::VERIFY_PEER http @@ -203,7 +212,7 @@ module HTTP let(:ssl_ca_bundle) { 'ca_bundle' } it 'sets ca_file' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.ca_file).to eq 'ca_bundle' http @@ -217,7 +226,7 @@ module HTTP let(:ssl_ca_directory) { 'ca_directory' } it 'sets ca_path' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.ca_path).to eq 'ca_directory' http @@ -231,7 +240,7 @@ module HTTP let(:ssl_ca_store) { 'ca_store' } it 'sets cert_store' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.cert_store).to eq 'ca_store' http @@ -246,7 +255,7 @@ module HTTP context 'http_proxy set' do let(:http_proxy) { 'http://my-proxy-host.com:88' } it 'sets the http proxy' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.proxyaddr).to eq('my-proxy-host.com') expect(http.proxyport).to eq(88) @@ -263,7 +272,7 @@ module HTTP end it 'unescapes and sets user and password' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.proxyaddr).to eq('my-proxy-host.com') expect(http.proxy_user).to eq(user) @@ -279,7 +288,7 @@ module HTTP let(:wire_trace) { true } it 'sets the logger on debug_output' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP) .to receive(:set_debug_output).with(logger) subject.transmit(request: request, response: response) diff --git a/hearth/spec/hearth/http/error_parser_spec.rb b/hearth/spec/hearth/http/error_parser_spec.rb index b1d9f27c0..6ba65c952 100644 --- a/hearth/spec/hearth/http/error_parser_spec.rb +++ b/hearth/spec/hearth/http/error_parser_spec.rb @@ -14,6 +14,8 @@ def initialize(location:, **kwargs) @location = location super(**kwargs) end + + attr_reader :location end class ApiClientError < ApiError; end @@ -29,7 +31,8 @@ class TestModeledError < ApiClientError; end let(:errors) { [TestErrors::TestModeledError] } let(:resp_status) { 200 } - let(:http_resp) { Response.new(status: resp_status) } + let(:fields) { Fields.new } + let(:http_resp) { Response.new(status: resp_status, fields: fields) } let(:metadata) { { key: 'value' } } subject do @@ -60,11 +63,19 @@ class TestModeledError < ApiClientError; end end context 'error response: 3XX code' do + let(:field) { Field.new('Location', 'http://example.com') } + let(:fields) { Fields.new([field]) } + let(:resp_status) { 300 } it 'returns an APIRedirectError' do error = subject.parse(http_resp, metadata) expect(error).to be_a(TestErrors::ApiRedirectError) end + + it 'populates a location' do + error = subject.parse(http_resp, metadata) + expect(error.location).to eq('http://example.com') + end end context 'error response: 4XX code' do diff --git a/hearth/spec/hearth/http/field_spec.rb b/hearth/spec/hearth/http/field_spec.rb new file mode 100644 index 000000000..a0563794b --- /dev/null +++ b/hearth/spec/hearth/http/field_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + describe Field do + let(:header) { Field.new('X-Header', 'foo') } + let(:trailer) { Field.new('X-Trailer', 'bar', kind: :trailer) } + + describe '#initialize' do + it 'raises when name is nil' do + expect { Field.new(nil) }.to raise_error(ArgumentError) + end + + it 'raises when name is empty' do + expect { Field.new('') }.to raise_error(ArgumentError) + end + + it 'defaults to header kind' do + expect(Field.new('header').kind).to eq(:header) + end + end + + it 'is immutable' do + expect { header.name = 'X-Header-2' }.to raise_error(NoMethodError) + expect { header.value = 'bar' }.to raise_error(NoMethodError) + expect { header.kind = :trailer }.to raise_error(NoMethodError) + end + + describe '#value' do + let(:time) { Time.now } + + context 'value is a Scalar type' do + let(:header_int) { Field.new('X-HeaderInt', 42) } + let(:header_float) { Field.new('X-HeaderFloat', 420.69) } + let(:header_time) { Field.new('X-HeaderTime', time) } + + it 'returns the value as a String' do + expect(header.value).to eq('foo') + expect(header_int.value).to eq('42') + expect(header_float.value).to eq('420.69') + expect(header_time.value).to eq(time.to_s) + end + end + + context 'value is an Array' do + let(:header_list_scalar) do + Field.new('X-HeaderList', ['foo', 42, 420.69, time]) + end + let(:header_list_escape) do + Field.new('X-HeaderList', ['bar, baz', '"quoted"']) + end + + it 'returns the value as a String' do + expect(header_list_scalar.value) + .to eq("foo, 42, 420.69, #{time}") + expect(header_list_escape.value) + .to eq('"bar, baz", "\"quoted\""') + end + end + + context 'encoding' do + it 'allows for different encoding' do + expect(header.value('UTF-16').encoding).to eq(Encoding::UTF_16) + end + end + end + + describe '#header?' do + it 'returns true when kind is :header' do + expect(header.header?).to eq(true) + expect(trailer.header?).to eq(false) + end + end + + describe '#trailer?' do + it 'returns true when kind is :trailer' do + expect(header.trailer?).to eq(false) + expect(trailer.trailer?).to eq(true) + end + end + + describe '#to_h' do + it 'returns a hash with the field name as key and value as value' do + # ensure value method is called + expect(header).to receive(:value).and_call_original + expect(header.to_h).to eq('X-Header' => 'foo') + end + end + end + end +end diff --git a/hearth/spec/hearth/http/fields_spec.rb b/hearth/spec/hearth/http/fields_spec.rb new file mode 100644 index 000000000..025a47a80 --- /dev/null +++ b/hearth/spec/hearth/http/fields_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + describe Fields do + let(:header_field) { Field.new('X-Header', 'foo') } + let(:trailer_field) { Field.new('X-Trailer', 'bar', kind: :trailer) } + + let(:fields) { Fields.new([header_field, trailer_field]) } + subject { fields } + + describe '#initialize' do + it 'defaults encoding to UTF-8' do + expect(Fields.new.encoding).to eq('utf-8') + end + + it 'raises when fields is not an Array' do + expect { Fields.new(nil) }.to raise_error(ArgumentError) + expect { Fields.new('not an array') }.to raise_error(ArgumentError) + end + + it 'defaults to empty' do + expect(Fields.new.size).to eq(0) + end + + it 'initializes with fields' do + expect(Fields.new([header_field]).size).to eq(1) + end + end + + describe '#[]' do + it 'returns the field' do + expect(subject['x-header']).to eq(header_field) + end + end + + describe '#[]=' do + it 'raises when field is not a Field' do + expect { subject['x-header'] = 'not a field' } + .to raise_error(ArgumentError) + end + + it 'sets the field' do + subject['x-header'] = Field.new('X-Header', 'bar') + expect(subject['x-header'].value).to eq('bar') + end + end + + describe '#key?' do + it 'returns true if the field exists' do + expect(subject.key?('x-header')).to eq(true) + expect(subject.key?('x-trailer')).to eq(true) + expect(subject.key?('x-foo')).to eq(false) + end + end + + describe '#delete' do + it 'deletes the field' do + field = subject['x-header'] + deleted = subject.delete('x-header') + expect(deleted).to eq(field) + expect(subject.key?('x-header')).to eq(false) + expect(subject['x-header']).to eq(nil) + end + end + + describe '#each' do + it 'includes Enumerable' do + expect(subject).to be_a(Enumerable) + end + + it 'enumerates over its contents' do + subject.each { |k, v| expect(subject[k]).to eq(v) } + end + end + + describe '#size' do + it 'returns the number of fields' do + expect(subject.size).to eq(2) + end + end + + describe '#clear' do + it 'clears the fields' do + subject.clear + expect(subject.size).to eq(0) + end + end + + describe Fields::Proxy do + subject { Fields::Proxy.new(fields, :header) } + + describe '#[]' do + it 'returns the field value' do + expect(subject['x-header']).to eq(header_field.value) + end + + it 'returns nil if the kind of field does not exist' do + expect(subject['x-trailer']).to be_nil + end + + context 'encoding' do + let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } + + it 'applies the encoding' do + expect(subject['x-header'].encoding).to eq(Encoding::UTF_16) + end + end + end + + describe '#[]=' do + it 'sets the field value and kind' do + subject['x-foo'] = 'bar' + expect(fields['x-foo']).to be_a(Field) + expect(fields['x-foo'].value).to eq('bar') + expect(fields['x-foo'].kind).to eq(:header) + end + end + + describe '#key?' do + it 'returns true if the field of that kind exists' do + expect(subject.key?('x-header')).to eq(true) + expect(subject.key?('x-trailer')).to eq(false) + end + end + + describe '#each' do + it 'includes Enumerable' do + expect(subject).to be_a(Enumerable) + end + + it 'returns the Field name and value for each field kind' do + # not downcased header name + expect(subject.each.to_h).to eq('X-Header' => 'foo') + end + + context 'encoding' do + let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } + + it 'applies the encoding' do + expect(subject.each.to_h['X-Header'].encoding) + .to eq(Encoding::UTF_16) + end + end + end + end + end + end +end diff --git a/hearth/spec/hearth/http/headers_spec.rb b/hearth/spec/hearth/http/headers_spec.rb deleted file mode 100644 index 1447088e5..000000000 --- a/hearth/spec/hearth/http/headers_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module HTTP - describe Headers do - let(:header1) { 'test-header' } - let(:header1_normalized) { 'Test-Header' } - let(:value1) { 'test header value' } - - let(:header2) { 'X-thing-Mixed' } - let(:header2_normalized) { 'X-Thing-Mixed' } - let(:value2) { 'mixed value' } - - let(:headers_hash) { { header1 => value1, header2 => value2 } } - - subject { Headers.new(headers_hash) } - - describe '#initialize' do - it 'sets and normalizes the headers' do - expect(subject.size).to eq(headers_hash.size) - expect(subject.keys) - .to include(header1_normalized, header2_normalized) - end - end - - describe '#[]' do - it 'normalizes the key' do - expect(subject[header1]).to eq(value1) - expect(subject[header1_normalized]).to eq(value1) - end - end - - describe '#[]=' do - let(:new_value) { 'new value' } - let(:integer_value) { 1 } - - it 'normalizes the key and sets the value' do - subject[header1] = new_value - expect(subject[header1_normalized]).to eq(new_value) - end - - it 'converts values to string' do - subject[header1] = integer_value - expect(subject[header1]).to eq(integer_value.to_s) - end - end - - describe '#key?' do - it 'normalizes the key' do - expect(subject.key?(header1)).to be(true) - end - - it 'returns false when the key does not exist' do - expect(subject.key?('not-found')).to be(false) - end - end - - describe '#delete' do - it 'deletes the normalized key' do - subject.delete(header1) - expect(subject.keys).not_to include(header1_normalized) - end - end - - describe '#each' do - it 'enumerates over its contents' do - subject.each { |k, v| expect(subject[k]).to eq(v) } - end - end - - describe '#size' do - it 'returns the size' do - expect(subject.size).to eq(headers_hash.size) - end - end - - describe '#update' do - it 'accepts a hash, updating self' do - subject.update(:abc => 123, 'xyz' => '234', header2 => 'new') - expect(subject['abc']).to eq('123') - expect(subject['xyz']).to eq('234') - expect(subject[header1]).to eq(value1) - expect(subject[header2]).to eq('new') - end - end - - describe '#clear' do - it 'clears the headers' do - subject.clear - expect(subject.size).to eq(0) - end - end - end - end -end diff --git a/hearth/spec/hearth/http/middleware/content_length_spec.rb b/hearth/spec/hearth/http/middleware/content_length_spec.rb index f807e2695..839231e1f 100644 --- a/hearth/spec/hearth/http/middleware/content_length_spec.rb +++ b/hearth/spec/hearth/http/middleware/content_length_spec.rb @@ -14,8 +14,7 @@ module Middleware let(:request) do Request.new( - http_method: :get, - url: 'http://example.com', + http_method: 'GET', body: body ) end @@ -47,7 +46,8 @@ module Middleware expect(app).to receive(:call).with(input, context) resp = subject.call(input, context) - expect(request.headers['Content-Length'].to_i).to eq(body.size) + expect(request.headers['Content-Length']) + .to eq(body.size.to_s) expect(resp).to be output end end diff --git a/hearth/spec/hearth/http/middleware/content_md5_spec.rb b/hearth/spec/hearth/http/middleware/content_md5_spec.rb index 6b8700df1..23173a48b 100644 --- a/hearth/spec/hearth/http/middleware/content_md5_spec.rb +++ b/hearth/spec/hearth/http/middleware/content_md5_spec.rb @@ -3,7 +3,7 @@ module Hearth module HTTP module Middleware - describe ContentLength do + describe ContentMD5 do let(:app) { double('app', call: output) } subject { ContentMD5.new(app) } @@ -16,8 +16,7 @@ module Middleware let(:request) do Request.new( - http_method: :get, - url: 'http://example.com', + http_method: 'GET', body: body ) end diff --git a/hearth/spec/hearth/http/request_spec.rb b/hearth/spec/hearth/http/request_spec.rb index b484bc3ea..f88afbcc0 100644 --- a/hearth/spec/hearth/http/request_spec.rb +++ b/hearth/spec/hearth/http/request_spec.rb @@ -4,15 +4,15 @@ module Hearth module HTTP describe Request do let(:http_method) { :get } - let(:url) { 'http://example.com' } - let(:headers) { Headers.new({ 'key' => 'value' }) } + let(:uri) { URI('http://example.com') } + let(:fields) { Fields.new } let(:body) { 'body' } subject do Request.new( http_method: http_method, - url: url, - headers: headers, + uri: uri, + fields: fields, body: body ) end @@ -21,82 +21,109 @@ module HTTP it 'sets empty defaults' do request = Request.new expect(request.http_method).to be_nil - expect(request.url).to be_nil - expect(request.headers).to be_a Headers - expect(request.body).to be_a StringIO + expect(request.fields).to be_a(Fields) + expect(request.body).to be_a(StringIO) + expect(request.uri).to be_a(URI) + end + end + + describe '#headers' do + it 'allows setting of headers' do + request = Request.new + request.headers['name'] = 'value' + expect(request.fields['name'].value).to eq('value') + expect(request.fields['name'].kind).to eq(:header) + end + + it 'lets you get a hash of only the headers' do + request = Request.new + request.headers['name'] = 'value' + request.trailers['trailer'] = 'trailer-value' + expect(request.headers.to_h).to eq('name' => 'value') + end + end + + describe '#trailers' do + it 'allows setting of trailers' do + request = Request.new + request.trailers['name'] = 'value' + expect(request.fields['name'].value).to eq('value') + expect(request.fields['name'].kind).to eq(:trailer) + end + + it 'lets you get a hash of only the trailers' do + request = Request.new + request.trailers['name'] = 'value' + request.headers['header'] = 'header-value' + expect(request.trailers.to_h).to eq('name' => 'value') end end describe '#append_path' do - it 'appends to the url' do + it 'appends to the uri' do subject.append_path('test') - expect(subject.url).to eq('http://example.com/test') + expect(subject.uri.to_s).to eq('http://example.com/test') end it 'removes trailing slash' do - subject.url += '/' + subject.uri += '/' subject.append_path('test') - expect(subject.url).to eq('http://example.com/test') + expect(subject.uri.to_s).to eq('http://example.com/test') end it 'removes prefix slash' do subject.append_path('/test') - expect(subject.url).to eq('http://example.com/test') + expect(subject.uri.to_s).to eq('http://example.com/test') end end describe '#append_query_param' do it 'appends a single value' do subject.append_query_param('test') - expect(subject.url).to eq('http://example.com?test') + expect(subject.uri.to_s).to eq('http://example.com?test') end it 'appends a pair of values' do subject.append_query_param('test', 'value') - expect(subject.url).to eq('http://example.com?test=value') + expect(subject.uri.to_s).to eq('http://example.com?test=value') end it 'raises an ArgumentError for invalid number of arguments' do - expect { subject.append_query_param('test', 'value', 'invlaid') } + expect { subject.append_query_param('test', 'value', 'invalid') } .to raise_error(ArgumentError) end it 'appends to existing query params' do subject.append_query_param('test', 'value') subject.append_query_param('test2') - expect(subject.url).to eq('http://example.com?test=value&test2') + expect(subject.uri.to_s).to eq('http://example.com?test=value&test2') end - it 'url escapes parameters and values' do + it 'uri escapes parameters and values' do subject.append_query_param('test space', 'test/value') - expect(subject.url) + expect(subject.uri.to_s) .to eq('http://example.com?test%20space=test%2Fvalue') end end - describe '#append_query_params' do + describe '#append_query_param_list' do it 'appends a param list' do - params = Hearth::Query::ParamList.new - params['key 1'] = nil - params['key 2'] = 'value 2' - subject.append_query_params(params) - expect(subject.url).to eq('http://example.com?key%201=&key%202=value%202') - end - - it 'appends to existing query params' do subject.append_query_param('original') params = Hearth::Query::ParamList.new params['key 1'] = nil - params['key 2'] = 'value 2' - subject.append_query_params(params) - expect(subject.url).to eq('http://example.com?original&key%201=&key%202=value%202') + params['key 2'] = '' + params['key 3'] = 'value' + params['key 4'] = %w[value value2] + subject.append_query_param_list(params) + expect(subject.uri.to_s) + .to eq('http://example.com?original&key%201&key%202=&key%203=value&key%204=value&key%204=value2') end end describe '#prefix_host' do it 'prefixes the host' do subject.prefix_host('data.') - expect(subject.url).to eq('http://data.example.com') + expect(subject.uri.to_s).to eq('http://data.example.com') end end end diff --git a/hearth/spec/hearth/http/response_spec.rb b/hearth/spec/hearth/http/response_spec.rb index aa459712e..7c6efc286 100644 --- a/hearth/spec/hearth/http/response_spec.rb +++ b/hearth/spec/hearth/http/response_spec.rb @@ -6,8 +6,9 @@ module HTTP describe '#initialize' do it 'sets empty defaults' do response = Response.new + expect(response.body).to be_a(StringIO) expect(response.status).to eq(0) - expect(response.headers).to be_a(Headers) + expect(response.fields).to be_a(Fields) expect(response.body).to be_a(StringIO) end end @@ -15,13 +16,16 @@ module HTTP describe '#reset' do it 'resets to defaults' do response = Response.new( + reason: 'Because', status: 200, - headers: Headers.new({ 'key' => 'value' }) + fields: Fields.new([Field.new('key', 'value')]) ) + response.headers['key'] = 'value' response.body << 'foo bar' # frozen string literal, cannot pass in response.reset expect(response.status).to eq(0) - expect(response.headers.size).to eq(0) + expect(response.fields.size).to eq(0) + expect(response.reason).to be_nil response.body.rewind # ensure nothing is there when we read expect(response.body.read).to eq('') end diff --git a/hearth/spec/hearth/middleware/host_prefix_spec.rb b/hearth/spec/hearth/middleware/host_prefix_spec.rb index b9bc2986d..aeda0e416 100644 --- a/hearth/spec/hearth/middleware/host_prefix_spec.rb +++ b/hearth/spec/hearth/middleware/host_prefix_spec.rb @@ -19,8 +19,8 @@ module Middleware let(:input) { struct.new } let(:output) { double('output') } - let(:url) { 'https://example.com' } - let(:request) { Hearth::HTTP::Request.new(url: url) } + let(:uri) { URI('https://example.com') } + let(:request) { Hearth::HTTP::Request.new(uri: uri) } let(:response) { double('response') } let(:context) do Context.new( @@ -38,7 +38,7 @@ module Middleware expect(app).to receive(:call).with(input, context).ordered resp = subject.call(input, context) - expect(request.url).to eq('https://foo.example.com') + expect(request.uri.to_s).to eq('https://foo.example.com') expect(resp).to be output end @@ -50,7 +50,7 @@ module Middleware expect(app).to receive(:call).with(input, context) resp = subject.call(input, context) - expect(request.url).to eq('https://bar.example.com') + expect(request.uri.to_s).to eq('https://bar.example.com') expect(resp).to be output end @@ -83,7 +83,7 @@ module Middleware expect(app).to receive(:call).with(input, context) resp = subject.call(input, context) - expect(request.url).to eq(url) + expect(request.uri).to eq(uri) expect(resp).to be output end end diff --git a/hearth/spec/hearth/middleware/retry_spec.rb b/hearth/spec/hearth/middleware/retry_spec.rb index 2a1d86122..57cf734c4 100644 --- a/hearth/spec/hearth/middleware/retry_spec.rb +++ b/hearth/spec/hearth/middleware/retry_spec.rb @@ -102,6 +102,7 @@ module Middleware metadata: {} ) end + let(:request) { Hearth::HTTP::Request.new } let(:response) { Hearth::HTTP::Response.new } let(:context) do diff --git a/hearth/spec/hearth/query/param_spec.rb b/hearth/spec/hearth/query/param_spec.rb index 31ec5edd1..efb5913f8 100644 --- a/hearth/spec/hearth/query/param_spec.rb +++ b/hearth/spec/hearth/query/param_spec.rb @@ -27,12 +27,12 @@ module Query it 'leaves the trailing = when value is nil' do param = Param.new('key') - expect(param.to_s).to eq('key=') + expect(param.to_s).to eq('key') end it 'can handle arrays' do param = Param.new('foo', ['1', nil, '3']) - expect(param.to_s).to eq('foo=1&foo=&foo=3') + expect(param.to_s).to eq('foo=1&foo&foo=3') end end diff --git a/hearth/spec/hearth/request_spec.rb b/hearth/spec/hearth/request_spec.rb new file mode 100644 index 000000000..6493ee6f1 --- /dev/null +++ b/hearth/spec/hearth/request_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + describe Request do + let(:uri) { URI('http://example.com') } + let(:body) { 'body' } + + subject { Request.new(uri: uri, body: body) } + + describe '#initialize' do + it 'sets empty defaults' do + request = Request.new + expect(request.body).to be_a(StringIO) + expect(request.uri).to be_a(URI) + end + end + end +end diff --git a/hearth/spec/hearth/response_spec.rb b/hearth/spec/hearth/response_spec.rb new file mode 100644 index 000000000..dfee87380 --- /dev/null +++ b/hearth/spec/hearth/response_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Hearth + describe Response do + describe '#initialize' do + it 'sets empty defaults' do + response = Response.new + expect(response.body).to be_a(StringIO) + end + end + end +end diff --git a/hearth/spec/hearth/retry/error_inspector_spec.rb b/hearth/spec/hearth/retry/error_inspector_spec.rb index d53854e90..982213b58 100644 --- a/hearth/spec/hearth/retry/error_inspector_spec.rb +++ b/hearth/spec/hearth/retry/error_inspector_spec.rb @@ -6,13 +6,11 @@ module Retry subject { ErrorInspector.new(error, http_status) } let(:http_status) { 404 } - let(:http_headers) { Hearth::HTTP::Headers.new } - let(:http_body) { 'body' } + let(:http_fields) { Hearth::HTTP::Fields.new } let(:http_resp) do Hearth::HTTP::Response.new( status: http_status, - headers: http_headers, - body: http_body + fields: http_fields ) end let(:message) { 'message' } From ce65e8d4cab9dc1281541cdbe05daec6a64fbf79 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Mon, 17 Apr 2023 13:09:39 -0400 Subject: [PATCH 11/22] Add generated changes to protocol spec --- codegen/projections/rails_json/spec/protocol_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/codegen/projections/rails_json/spec/protocol_spec.rb b/codegen/projections/rails_json/spec/protocol_spec.rb index edf3c7631..9bd5f237c 100644 --- a/codegen/projections/rails_json/spec/protocol_spec.rb +++ b/codegen/projections/rails_json/spec/protocol_spec.rb @@ -3796,9 +3796,8 @@ module RailsJson it 'rails_json_serializes_fractional_timestamp_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"timestamp":"2000-01-02T20:34:56.123Z"}')) @@ -4391,7 +4390,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"timestamp":"2000-01-02T20:34:56.123Z"}') response.body.rewind Hearth::Output.new @@ -4442,7 +4441,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) + response.headers['Content-Type'] = 'application/json' response.body.write('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.123 GMT"}') response.body.rewind Hearth::Output.new From 85d60b22958f7de1fe098fd7a81967a5dec4eeed Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 21 Apr 2023 07:44:34 -0700 Subject: [PATCH 12/22] Add indirect dependencies - don't require/depend on dependencies that are already covered. (#129) --- .../amazon/smithy/ruby/codegen/RubyDependency.java | 9 +++++++++ .../ruby/codegen/generators/GemspecGenerator.java | 11 ++++++++++- .../ruby/codegen/generators/ModuleGenerator.java | 13 ++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java index b605f1e8f..47a34c95f 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java @@ -168,6 +168,15 @@ public List getDependencies() { return new ArrayList<>(symbolDependencySet); } + /** + * Get all the Ruby dependencies. + * + * @return list of RubyDependency + */ + public Set getRubyDependencies() { + return dependencies; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java index f41638ecf..533094471 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java @@ -29,6 +29,8 @@ package software.amazon.smithy.ruby.codegen.generators; +import java.util.HashSet; +import java.util.Set; import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.RubyCodeWriter; @@ -69,8 +71,15 @@ public void render() { .write("spec.files = Dir['lib/**/*.rb']") .write("") .call(() -> { + // determine set of indirect dependencies - covered by requiring another + Set indirectDependencies = new HashSet<>(); + context.getRubyDependencies().forEach(rubyDependency -> { + indirectDependencies.addAll(rubyDependency.getRubyDependencies()); + }); + context.getRubyDependencies().forEach((rubyDependency -> { - if (rubyDependency.getType() != RubyDependency.Type.STANDARD_LIBRARY) { + if (rubyDependency.getType() != RubyDependency.Type.STANDARD_LIBRARY + && !indirectDependencies.contains(rubyDependency)) { writer.write("spec.add_runtime_dependency '$L', '$L'", rubyDependency.getGemName(), rubyDependency.getVersion()); } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java index 68148751b..088e380c9 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java @@ -16,10 +16,13 @@ package software.amazon.smithy.ruby.codegen.generators; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.logging.Logger; import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubyDependency; import software.amazon.smithy.ruby.codegen.RubySettings; import software.amazon.smithy.utils.SmithyInternalApi; @@ -52,8 +55,16 @@ public void render() { context.writerDelegator().useFileWriter(fileName, settings.getModule(), writer -> { writer.includePreamble().includeRequires(); + // determine set of indirect dependencies - covered by requiring another + Set indirectDependencies = new HashSet<>(); + context.getRubyDependencies().forEach(rubyDependency -> { + indirectDependencies.addAll(rubyDependency.getRubyDependencies()); + }); + context.getRubyDependencies().forEach((rubyDependency -> { - writer.write("require '$L'", rubyDependency.getImportPath()); + if (!indirectDependencies.contains(rubyDependency)) { + writer.write("require '$L'", rubyDependency.getImportPath()); + } })); writer.write("\n"); From f91ed26d8148a44dc00cf226af799dd22ccc2a07 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 21 Apr 2023 12:51:11 -0700 Subject: [PATCH 13/22] Add support for specifying relative middleware ordering (#130) * Add support for specifying relative middleware ordering * Use visited to detect ciruclar dependencies --- .../ruby/codegen/middleware/Middleware.java | 50 +++++++++++++++ .../codegen/middleware/MiddlewareBuilder.java | 62 +++++++++++++++++-- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java index 3baafe51a..39e2c5941 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.CodegenException; @@ -54,6 +55,8 @@ public final class Middleware { private final String klass; private final MiddlewareStackStep step; private final byte order; + + private final Optional relative; private final Set clientConfig; private final OperationParams operationParams; private final Map additionalParams; @@ -68,6 +71,7 @@ private Middleware(Builder builder) { this.klass = builder.klass; this.step = builder.step; this.order = builder.order; + this.relative = builder.relative; this.clientConfig = builder.clientConfig; this.operationParams = builder.operationParams; this.additionalParams = builder.additionalParams; @@ -98,6 +102,13 @@ public byte getOrder() { return order; } + /** + * @return relative order within stack step + */ + public Optional getRelative() { + return relative; + } + /** * @return clientConfig to be added to the client to support this middleware. */ @@ -236,6 +247,7 @@ public static class Builder implements SmithyBuilder { } }; private byte order = 0; + private Optional relative = Optional.empty(); private String klass; private MiddlewareStackStep step; private Set clientConfig = new HashSet<>(); @@ -263,10 +275,21 @@ public Builder klass(String klass) { * @return Returns the builder */ public Builder order(byte order) { + if (relative.isPresent()) { + throw new IllegalArgumentException("Cannot combine relative ordering with explicit order value."); + } this.order = order; return this; } + public Builder relative(Relative relative) { + if (order != 0) { + throw new IllegalArgumentException("Cannot combine relative ordering with explicit order value."); + } + this.relative = Optional.of(relative); + return this; + } + /** * @param step The step to apply the middleware to. * @return Returns the builder @@ -467,4 +490,31 @@ public Middleware build() { return new Middleware(this); } } + + public static class Relative { + private final Type type; + private final String to; + + public Relative(Type type, String to) { + this.type = type; + this.to = to; + } + + public Type getType() { + return type; + } + + public String getTo() { + return to; + } + + public String toString() { + return type + " " + to; + } + + public enum Type { + BEFORE, + AFTER + } + } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java index 8aac09903..ba6313595 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.SymbolProvider; @@ -100,11 +101,7 @@ public void render(RubyCodeWriter writer, GenerationContext context, ServiceShape service = context.service(); for (MiddlewareStackStep step : MiddlewareStackStep.values()) { - List orderedStepMiddleware = middlewares.get(step) - .stream() - .filter((m) -> m.includeFor(model, service, operation)) - .sorted(Comparator.comparing(Middleware::getOrder)) - .collect(Collectors.toList()); + List orderedStepMiddleware = resolveAndFilter(step, model, service, operation); for (Middleware middleware : orderedStepMiddleware) { middleware.renderAdd(writer, context, operation); @@ -112,6 +109,61 @@ public void render(RubyCodeWriter writer, GenerationContext context, } } + private List resolveAndFilter(MiddlewareStackStep step, Model model, ServiceShape service, + OperationShape operation) { + Set resolved = new HashSet<>(); + Set visiting = new HashSet<>(); + Map order = new HashMap<>(); + Map klassToMiddlewareMap = new HashMap<>(); + + + List filteredMiddleware = middlewares.get(step) + .stream().filter((m) -> m.includeFor(model, service, operation)) + .collect(Collectors.toList()); + + filteredMiddleware.forEach((m) -> klassToMiddlewareMap.put(m.getKlass(), m)); + + for (Middleware middleware : filteredMiddleware) { + resolve(middleware, resolved, visiting, order, klassToMiddlewareMap); + } + + return filteredMiddleware.stream() + .sorted(Comparator.comparingInt(order::get)) + .collect(Collectors.toList()); + } + + private void resolve(Middleware middleware, Set resolved, Set visiting, + Map order, Map klassToMiddlewareMap) { + + if (visiting.contains(middleware)) { + throw new IllegalArgumentException("Circular dependency detected when resolving order for middleware: " + + middleware.getKlass()); + } + // skip if its already been resolved + if (!resolved.contains(middleware)) { + visiting.add(middleware); + if (middleware.getRelative().isPresent()) { + Middleware relativeTo = Objects.requireNonNull( + klassToMiddlewareMap.get(middleware.getRelative().get().getTo()), + middleware.getKlass() + " relative references a middleware class (" + + middleware.getRelative().get().getTo() + ") that is not available in the stack."); + //recursively resolve the relative middleware + resolve(relativeTo, resolved, visiting, order, klassToMiddlewareMap); + // the order of relativeTo should now be set + if (middleware.getRelative().get().getType().equals(Middleware.Relative.Type.BEFORE)) { + order.put(middleware, order.get(relativeTo) - 1); + } else { + order.put(middleware, order.get(relativeTo) + 1); + } + } else { + // Base case - middleware is not relative to anything else, use its default order. + order.put(middleware, (int) middleware.getOrder()); + } + visiting.remove(middleware); + resolved.add(middleware); + } + } + public void addDefaultMiddleware(GenerationContext context) { ApplicationTransport transport = context.applicationTransport(); SymbolProvider symbolProvider = context.symbolProvider(); From 33f0555cce240befa853b7348dc24e7b60f1bb63 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 24 Apr 2023 10:39:35 -0700 Subject: [PATCH 14/22] Fix bug in handling of renamed shapes (#131) --- .../rails_json/lib/rails_json/builders.rb | 11 ++++++++ .../rails_json/lib/rails_json/client.rb | 7 ++++- .../rails_json/lib/rails_json/params.rb | 15 +++++++++- .../rails_json/lib/rails_json/parsers.rb | 11 ++++++++ .../rails_json/lib/rails_json/stubs.rb | 20 +++++++++++++ .../rails_json/lib/rails_json/types.rb | 21 ++++++++++++++ .../rails_json/lib/rails_json/validators.rb | 15 ++++++++++ .../rails_json/sig/rails_json/types.rbs | 6 ++++ .../rails_json/spec/protocol_spec.rb | 26 +++++++++++++++++ .../smithy/ruby/codegen/CodegenUtils.java | 2 +- .../model/high-score-service.smithy | 2 +- .../model/protocol-test/main.smithy | 4 +++ .../model/protocol-test/unions.smithy | 28 +++++++++++++++++++ 13 files changed, 164 insertions(+), 4 deletions(-) diff --git a/codegen/projections/rails_json/lib/rails_json/builders.rb b/codegen/projections/rails_json/lib/rails_json/builders.rb index e89e70e72..4e814bb50 100644 --- a/codegen/projections/rails_json/lib/rails_json/builders.rb +++ b/codegen/projections/rails_json/lib/rails_json/builders.rb @@ -294,6 +294,15 @@ def self.build(input) end end + # Structure Builder for GreetingStruct + class RenamedGreeting + def self.build(input) + data = {} + data[:salutation] = input[:salutation] unless input[:salutation].nil? + data + end + end + # Structure Builder for GreetingStruct class GreetingStruct def self.build(input) @@ -849,6 +858,8 @@ def self.build(input) data[:map_value] = (Builders::StringMap.build(input) unless input.nil?) when Types::MyUnion::StructureValue data[:structure_value] = (Builders::GreetingStruct.build(input) unless input.nil?) + when Types::MyUnion::RenamedStructureValue + data[:renamed_structure_value] = (Builders::RenamedGreeting.build(input) unless input.nil?) else raise ArgumentError, "Expected input to be one of the subclasses of Types::MyUnion" diff --git a/codegen/projections/rails_json/lib/rails_json/client.rb b/codegen/projections/rails_json/lib/rails_json/client.rb index cb3ad19f8..211707bc5 100644 --- a/codegen/projections/rails_json/lib/rails_json/client.rb +++ b/codegen/projections/rails_json/lib/rails_json/client.rb @@ -1784,6 +1784,9 @@ def json_maps(params = {}, options = {}, &block) # }, # structure_value: { # hi: 'hi' + # }, + # renamed_structure_value: { + # salutation: 'salutation' # } # } # ) @@ -1791,7 +1794,7 @@ def json_maps(params = {}, options = {}, &block) # @example Response structure # # resp.data #=> Types::JsonUnionsOutput - # resp.data.contents #=> Types::MyUnion, one of [StringValue, BooleanValue, NumberValue, BlobValue, TimestampValue, EnumValue, ListValue, MapValue, StructureValue] + # resp.data.contents #=> Types::MyUnion, one of [StringValue, BooleanValue, NumberValue, BlobValue, TimestampValue, EnumValue, ListValue, MapValue, StructureValue, RenamedStructureValue] # resp.data.contents.string_value #=> String # resp.data.contents.boolean_value #=> Boolean # resp.data.contents.number_value #=> Integer @@ -1804,6 +1807,8 @@ def json_maps(params = {}, options = {}, &block) # resp.data.contents.map_value['key'] #=> String # resp.data.contents.structure_value #=> Types::GreetingStruct # resp.data.contents.structure_value.hi #=> String + # resp.data.contents.renamed_structure_value #=> Types::RenamedGreeting + # resp.data.contents.renamed_structure_value.salutation #=> String # def json_unions(params = {}, options = {}, &block) stack = Hearth::MiddlewareStack.new diff --git a/codegen/projections/rails_json/lib/rails_json/params.rb b/codegen/projections/rails_json/lib/rails_json/params.rb index e85b599d9..879fbb93a 100644 --- a/codegen/projections/rails_json/lib/rails_json/params.rb +++ b/codegen/projections/rails_json/lib/rails_json/params.rb @@ -339,6 +339,15 @@ def self.build(params, context: '') end end + module RenamedGreeting + def self.build(params, context: '') + Hearth::Validator.validate_types!(params, ::Hash, Types::RenamedGreeting, context: context) + type = Types::RenamedGreeting.new + type.salutation = params[:salutation] + type + end + end + module GreetingWithErrorsInput def self.build(params, context: '') Hearth::Validator.validate_types!(params, ::Hash, Types::GreetingWithErrorsInput, context: context) @@ -1005,9 +1014,13 @@ def self.build(params, context: '') Types::MyUnion::StructureValue.new( (GreetingStruct.build(params[:structure_value], context: "#{context}[:structure_value]") unless params[:structure_value].nil?) ) + when :renamed_structure_value + Types::MyUnion::RenamedStructureValue.new( + (RenamedGreeting.build(params[:renamed_structure_value], context: "#{context}[:renamed_structure_value]") unless params[:renamed_structure_value].nil?) + ) else raise ArgumentError, - "Expected #{context} to have one of :string_value, :boolean_value, :number_value, :blob_value, :timestamp_value, :enum_value, :list_value, :map_value, :structure_value set" + "Expected #{context} to have one of :string_value, :boolean_value, :number_value, :blob_value, :timestamp_value, :enum_value, :list_value, :map_value, :structure_value, :renamed_structure_value set" end end end diff --git a/codegen/projections/rails_json/lib/rails_json/parsers.rb b/codegen/projections/rails_json/lib/rails_json/parsers.rb index 3a703b3ab..001c03a81 100644 --- a/codegen/projections/rails_json/lib/rails_json/parsers.rb +++ b/codegen/projections/rails_json/lib/rails_json/parsers.rb @@ -223,6 +223,14 @@ def self.parse(list) end end + class RenamedGreeting + def self.parse(map) + data = Types::RenamedGreeting.new + data.salutation = map['salutation'] + return data + end + end + class GreetingStruct def self.parse(map) data = Types::GreetingStruct.new @@ -669,6 +677,9 @@ def self.parse(map) when 'structure_value' value = (Parsers::GreetingStruct.parse(value) unless value.nil?) Types::MyUnion::StructureValue.new(value) if value + when 'renamed_structure_value' + value = (Parsers::RenamedGreeting.parse(value) unless value.nil?) + Types::MyUnion::RenamedStructureValue.new(value) if value else Types::MyUnion::Unknown.new({name: key, value: value}) end diff --git a/codegen/projections/rails_json/lib/rails_json/stubs.rb b/codegen/projections/rails_json/lib/rails_json/stubs.rb index cbc32d9cb..35d7f4c19 100644 --- a/codegen/projections/rails_json/lib/rails_json/stubs.rb +++ b/codegen/projections/rails_json/lib/rails_json/stubs.rb @@ -335,6 +335,24 @@ def self.stub(stub) end end + # Structure Stubber for GreetingStruct + class RenamedGreeting + def self.default(visited=[]) + return nil if visited.include?('RenamedGreeting') + visited = visited + ['RenamedGreeting'] + { + salutation: 'salutation', + } + end + + def self.stub(stub) + stub ||= Types::RenamedGreeting.new + data = {} + data[:salutation] = stub[:salutation] unless stub[:salutation].nil? + data + end + end + # Structure Stubber for GreetingStruct class GreetingStruct def self.default(visited=[]) @@ -1095,6 +1113,8 @@ def self.stub(stub) data[:map_value] = (Stubs::StringMap.stub(stub.__getobj__) unless stub.__getobj__.nil?) when Types::MyUnion::StructureValue data[:structure_value] = (Stubs::GreetingStruct.stub(stub.__getobj__) unless stub.__getobj__.nil?) + when Types::MyUnion::RenamedStructureValue + data[:renamed_structure_value] = (Stubs::RenamedGreeting.stub(stub.__getobj__) unless stub.__getobj__.nil?) else raise ArgumentError, "Expected input to be one of the subclasses of Types::MyUnion" diff --git a/codegen/projections/rails_json/lib/rails_json/types.rb b/codegen/projections/rails_json/lib/rails_json/types.rb index 421cff161..a05b0e78f 100644 --- a/codegen/projections/rails_json/lib/rails_json/types.rb +++ b/codegen/projections/rails_json/lib/rails_json/types.rb @@ -1650,6 +1650,16 @@ def to_s end end + class RenamedStructureValue < MyUnion + def to_h + { renamed_structure_value: super(__getobj__) } + end + + def to_s + "#" + end + end + # Handles unknown future members # class Unknown < MyUnion @@ -1871,6 +1881,17 @@ def to_s include Hearth::Structure end + # @!attribute salutation + # + # @return [String] + # + RenamedGreeting = ::Struct.new( + :salutation, + keyword_init: true + ) do + include Hearth::Structure + end + # @!attribute value # # @return [String] diff --git a/codegen/projections/rails_json/lib/rails_json/validators.rb b/codegen/projections/rails_json/lib/rails_json/validators.rb index 10ce50555..f61b596f8 100644 --- a/codegen/projections/rails_json/lib/rails_json/validators.rb +++ b/codegen/projections/rails_json/lib/rails_json/validators.rb @@ -299,6 +299,13 @@ def self.validate!(input, context:) end end + class RenamedGreeting + def self.validate!(input, context:) + Hearth::Validator.validate_types!(input, Types::RenamedGreeting, context: context) + Hearth::Validator.validate_types!(input[:salutation], ::String, context: "#{context}[:salutation]") + end + end + class GreetingWithErrorsInput def self.validate!(input, context:) Hearth::Validator.validate_types!(input, Types::GreetingWithErrorsInput, context: context) @@ -864,6 +871,8 @@ def self.validate!(input, context:) StringMap.validate!(input.__getobj__, context: context) unless input.__getobj__.nil? when Types::MyUnion::StructureValue GreetingStruct.validate!(input.__getobj__, context: context) unless input.__getobj__.nil? + when Types::MyUnion::RenamedStructureValue + RenamedGreeting.validate!(input.__getobj__, context: context) unless input.__getobj__.nil? else raise ArgumentError, "Expected #{context} to be a union member of "\ @@ -924,6 +933,12 @@ def self.validate!(input, context:) Validators::GreetingStruct.validate!(input, context: context) unless input.nil? end end + + class RenamedStructureValue + def self.validate!(input, context:) + Validators::RenamedGreeting.validate!(input, context: context) unless input.nil? + end + end end class NestedAttributesOperationInput diff --git a/codegen/projections/rails_json/sig/rails_json/types.rbs b/codegen/projections/rails_json/sig/rails_json/types.rbs index 3118283a3..db92aa455 100644 --- a/codegen/projections/rails_json/sig/rails_json/types.rbs +++ b/codegen/projections/rails_json/sig/rails_json/types.rbs @@ -179,6 +179,10 @@ module RailsJson def to_h: () -> { structure_value: Hash[Symbol,MyUnion] } end + class RenamedStructureValue < MyUnion + def to_h: () -> { renamed_structure_value: Hash[Symbol,MyUnion] } + end + class Unknown < MyUnion def to_h: () -> { unknown: Hash[Symbol,MyUnion] } end @@ -214,6 +218,8 @@ module RailsJson QueryParamsAsStringListMapOutput: untyped + RenamedGreeting: untyped + SimpleStruct: untyped StreamingOperationInput: untyped diff --git a/codegen/projections/rails_json/spec/protocol_spec.rb b/codegen/projections/rails_json/spec/protocol_spec.rb index 9bd5f237c..47522510d 100644 --- a/codegen/projections/rails_json/spec/protocol_spec.rb +++ b/codegen/projections/rails_json/spec/protocol_spec.rb @@ -3192,6 +3192,32 @@ module RailsJson } }, **opts) end + # Serializes a renamed structure union value + # + it 'RailsJsonSerializeRenamedStructureUnionValue' do + middleware = Hearth::MiddlewareBuilder.before_send do |input, context| + request = context.request + expect(request.http_method).to eq('POST') + expect(request.uri.path).to eq('/jsonunions') + { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } + expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ + "contents": { + "renamed_structure_value": { + "salutation": "hello!" + } + } + }')) + Hearth::Output.new + end + opts = {middleware: middleware} + client.json_unions({ + contents: { + renamed_structure_value: { + salutation: "hello!" + } + } + }, **opts) + end end describe 'responses' do diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java index 65705d3f6..aa6995111 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java @@ -61,6 +61,6 @@ public static boolean isStubSyntheticClone(Shape shape) { * @return TreeSet sorted by shape name in alphabetical order */ public static TreeSet getAlphabeticalOrderedShapesSet() { - return new TreeSet<>(Comparator.comparing(o -> o.getId().getName())); + return new TreeSet<>(Comparator.comparing(o -> o.getId().getName() + " " + o.getId())); } } diff --git a/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy b/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy index 147fc73b4..2cfb28813 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy @@ -10,7 +10,7 @@ use smithy.ruby.protocols#UnprocessableEntityError @title("High Score Sample Rails Service") service HighScoreService { version: "2021-02-15", - resources: [HighScore], + resources: [HighScore] } /// Rails default scaffold operations diff --git a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy index 3b42e6730..94b004ccc 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy @@ -11,6 +11,10 @@ use smithy.test#httpResponseTests @title("RailsJson Protocol Test Service") service RailsJson { version: "2018-01-01", + // Ensure that generators are able to handle renames. + rename: { + "aws.protocoltests.restjson.nested#GreetingStruct": "RenamedGreeting", + }, operations: [ KitchenSinkOperation, EndpointOperation, diff --git a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy index 35dc52376..5630b2493 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy @@ -37,6 +37,10 @@ union MyUnion { listValue: StringList, mapValue: StringMap, structureValue: GreetingStruct, + + // Note that this uses a conflicting structure name with + // GreetingStruct, so it must be renamed in the service. + renamedStructureValue: aws.protocoltests.restjson.nested#GreetingStruct, } apply JsonUnions @httpRequestTests([ @@ -248,6 +252,30 @@ apply JsonUnions @httpRequestTests([ } } }, + { + id: "RailsJsonSerializeRenamedStructureUnionValue", + documentation: "Serializes a renamed structure union value", + protocol: railsJson, + method: "POST", + uri: "/jsonunions", + body: """ + { + "contents": { + "renamed_structure_value": { + "salutation": "hello!" + } + } + }""", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + contents: { + renamedStructureValue: { + salutation: "hello!", + } + } + } + }, ]) apply JsonUnions @httpResponseTests([ From b224ff7cbdc56962521c3be66b15f2ccd4762e39 Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:03:44 -0400 Subject: [PATCH 15/22] DNS host resolver and host address (#128) --- hearth/Gemfile | 3 +- hearth/lib/hearth.rb | 5 +- hearth/lib/hearth/dns.rb | 46 ++++++++ hearth/lib/hearth/dns/host_address.rb | 23 ++++ hearth/lib/hearth/dns/host_resolver.rb | 91 +++++++++++++++ hearth/lib/hearth/http/client.rb | 38 +++++-- hearth/lib/hearth/http/fields.rb | 6 + hearth/lib/hearth/retry.rb | 5 - hearth/spec/hearth/dns/host_address_spec.rb | 15 +++ hearth/spec/hearth/dns/host_resolver_spec.rb | 114 +++++++++++++++++++ hearth/spec/hearth/dns_spec.rb | 110 ++++++++++++++++++ hearth/spec/hearth/http/client_spec.rb | 17 ++- hearth/spec/hearth/http/fields_spec.rb | 80 ++++++++----- 13 files changed, 509 insertions(+), 44 deletions(-) create mode 100644 hearth/lib/hearth/dns.rb create mode 100644 hearth/lib/hearth/dns/host_address.rb create mode 100644 hearth/lib/hearth/dns/host_resolver.rb delete mode 100644 hearth/lib/hearth/retry.rb create mode 100644 hearth/spec/hearth/dns/host_address_spec.rb create mode 100644 hearth/spec/hearth/dns/host_resolver_spec.rb create mode 100644 hearth/spec/hearth/dns_spec.rb diff --git a/hearth/Gemfile b/hearth/Gemfile index 09933658b..3803f65de 100644 --- a/hearth/Gemfile +++ b/hearth/Gemfile @@ -13,7 +13,8 @@ group :test do end group :development do - gem 'rbs', '~>2' + gem 'parallel', '1.22.1' # 1.23.0 broke steep, temporary + gem 'rbs', '~> 2' gem 'rubocop' gem 'steep' end diff --git a/hearth/lib/hearth.rb b/hearth/lib/hearth.rb index a79346a9f..e37947657 100755 --- a/hearth/lib/hearth.rb +++ b/hearth/lib/hearth.rb @@ -7,6 +7,7 @@ require_relative 'hearth/config/env_provider' require_relative 'hearth/config/resolver' require_relative 'hearth/context' +require_relative 'hearth/dns' # must be required before http require_relative 'hearth/request' @@ -21,7 +22,9 @@ require_relative 'hearth/output' require_relative 'hearth/query/param' require_relative 'hearth/query/param_list' -require_relative 'hearth/retry' +require_relative 'hearth/retry/client_rate_limiter' +require_relative 'hearth/retry/error_inspector' +require_relative 'hearth/retry/retry_quota' require_relative 'hearth/structure' require_relative 'hearth/stubbing/client_stubs' require_relative 'hearth/stubbing/stubs' diff --git a/hearth/lib/hearth/dns.rb b/hearth/lib/hearth/dns.rb new file mode 100644 index 000000000..1ad367892 --- /dev/null +++ b/hearth/lib/hearth/dns.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'socket' + +require_relative 'dns/host_address' +require_relative 'dns/host_resolver' + +# These patches are based on resolv-replace +# https://github.com/ruby/ruby/blob/master/lib/resolv-replace.rb +# We cannot require resolv-replace because it would change DNS resolution +# globally. When opening an HTTP request, we will set a thread local variable +# to enable custom DNS resolution, and then disable it after the request is +# complete. When the thread local variable is not set, we will use the default +# Ruby DNS resolution, which may be Resolv or the system resolver. + +# Patch IPSocket +class << IPSocket + alias original_hearth_getaddress getaddress + + def getaddress(host) + unless (resolver = Thread.current[:net_http_hearth_dns_resolver]) + return original_hearth_getaddress(host) + end + + ipv6, ipv4 = resolver.resolve_address(nodename: host) + return ipv6.address if ipv6 + + ipv4.address + end +end + +# Patch TCPSocket +class TCPSocket < IPSocket + alias original_hearth_initialize initialize + + # rubocop:disable Lint/MissingSuper + def initialize(host, serv, *rest) + if Thread.current[:net_http_hearth_dns_resolver] + rest[0] = IPSocket.getaddress(rest[0]) if rest[0] + original_hearth_initialize(IPSocket.getaddress(host), serv, *rest) + else + original_hearth_initialize(host, serv, *rest) + end + end + # rubocop:enable Lint/MissingSuper +end diff --git a/hearth/lib/hearth/dns/host_address.rb b/hearth/lib/hearth/dns/host_address.rb new file mode 100644 index 000000000..817fa2871 --- /dev/null +++ b/hearth/lib/hearth/dns/host_address.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Hearth + module DNS + # Address results from a DNS lookup in {HostResolver}. + class HostAddress + def initialize(address_type:, address:, hostname:) + @address_type = address_type + @address = address + @hostname = hostname + end + + # @return [Symbol] + attr_reader :address_type + + # @return [String] + attr_reader :address + + # @return [String] + attr_reader :hostname + end + end +end diff --git a/hearth/lib/hearth/dns/host_resolver.rb b/hearth/lib/hearth/dns/host_resolver.rb new file mode 100644 index 000000000..ab709a82c --- /dev/null +++ b/hearth/lib/hearth/dns/host_resolver.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Hearth + module DNS + # Resolves a host name and service to an IP address. Can be used with + # {Hearth::HTTP::Client} host_resolver option. This implementation uses + # {Addrinfo#getaddrinfo} to resolve the host name. + # @see https://ruby-doc.org/stdlib-3.0.2/libdoc/socket/rdoc/Addrinfo.html + class HostResolver + # @param [Integer] service (443) + # @param [Integer] family (nil) + # @param [Symbol] socktype (:SOCK_STREAM) + # @param [Integer] protocol (nil) + # @param [Integer] flags (nil) + def initialize(service: 443, family: nil, socktype: :SOCK_STREAM, + protocol: nil, flags: nil) + @service = service + @family = family + @socktype = socktype + @protocol = protocol + @flags = flags + end + + # @return [Integer] + attr_reader :service + + # @return [Integer] + attr_reader :family + + # @return [Symbol] + attr_reader :socktype + + # @return [Integer] + attr_reader :protocol + + # @return [Integer] + attr_reader :flags + + # @param [String] nodename + # @param (see Hearth::DNS::HostResolver#initialize) + def resolve_address(nodename:, **kwargs) + options = kwargs.merge(nodename: nodename) + addrinfo_list = addrinfo(options) + ipv6 = ipv6_addr(addrinfo_list, options) if use_ipv6? + ipv4 = ipv4_addr(addrinfo_list, options) + [ipv6, ipv4] + end + + private + + def addrinfo(options) + Addrinfo.getaddrinfo( + options[:nodename], + options.fetch(:service, @service), + options.fetch(:family, @family), + options.fetch(:socktype, @socktype), + options.fetch(:protocol, @protocol), + options.fetch(:flags, @flags) + ) + end + + def ipv4_addr(addrinfo_list, options) + addr = addrinfo_list.find(&:ipv4?) + return unless addr + + HostAddress.new( + address_type: :A, + address: addr.ip_address, + hostname: options[:nodename] + ) + end + + def ipv6_addr(addrinfo_list, options) + addr = addrinfo_list.find(&:ipv6?) + return unless addr + + HostAddress.new( + address_type: :AAAA, + address: addr.ip_address, + hostname: options[:nodename] + ) + end + + def use_ipv6? + Socket.ip_address_list.any? do |a| + a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? + end + end + end + end +end diff --git a/hearth/lib/hearth/http/client.rb b/hearth/lib/hearth/http/client.rb index a2977d348..6ac4a0772 100644 --- a/hearth/lib/hearth/http/client.rb +++ b/hearth/lib/hearth/http/client.rb @@ -18,7 +18,7 @@ class Client # # @option options [Logger] :logger A logger where debug output is sent. # - # @option options [URI::HTTP,String] :http_proxy A proxy to send + # @option options [String] :http_proxy A proxy to send # requests through. Formatted like 'http://proxy.com:123'. # # @option options [Boolean] :ssl_verify_peer (true) When `true`, @@ -36,15 +36,26 @@ class Client # authority files for verifying peer certificates. If you do # not pass `:ssl_ca_bundle` or `:ssl_ca_directory` the # system default will be used if available. + # + # @option options [OpenSSL::X509::Store] :ssl_ca_store An OpenSSL X509 + # certificate store that contains the SSL certificate authority. + # + # @option options [#resolve_address] (nil) :host_resolver + # An object, such as {Hearth::DNS::HostResolver} that responds to + # `#resolve_address`, returning an array of up to two IP addresses for + # the given hostname, one IPv6 and one IPv4, in that order. + # `#resolve_address` should take a nodename keyword argument and + # optionally other keyword args similar to {Addrinfo#getaddrinfo}'s + # positional parameters. def initialize(options = {}) @http_wire_trace = options[:http_wire_trace] @logger = options[:logger] - @http_proxy = options[:http_proxy] - @http_proxy = URI.parse(@http_proxy.to_s) if @http_proxy + @http_proxy = URI(options[:http_proxy]) if options[:http_proxy] @ssl_verify_peer = options[:ssl_verify_peer] @ssl_ca_bundle = options[:ssl_ca_bundle] @ssl_ca_directory = options[:ssl_ca_directory] @ssl_ca_store = options[:ssl_ca_store] + @host_resolver = options[:host_resolver] end # @param [Request] request @@ -73,15 +84,24 @@ def transmit(request:, response:) private def _transmit(http, request, response) + # Inform monkey patch to use our DNS resolver + Thread.current[:net_http_hearth_dns_resolver] = @host_resolver http.start do |conn| conn.request(build_net_request(request)) do |net_resp| - response.status = net_resp.code.to_i - net_resp.each_header { |k, v| response.headers[k] = v } - net_resp.read_body do |chunk| - response.body.write(chunk) - end + unpack_response(net_resp, response) end end + ensure + # Restore the default DNS resolver + Thread.current[:net_http_hearth_dns_resolver] = nil + end + + def unpack_response(net_resp, response) + response.status = net_resp.code.to_i + net_resp.each_header { |k, v| response.headers[k] = v } + net_resp.read_body do |chunk| + response.body.write(chunk) + end end # Creates an HTTP connection to the endpoint @@ -150,7 +170,7 @@ def net_http_request_class(request) end # Extract the parts of the http_proxy URI - # @return [Array(String)] + # @return [Array] def http_proxy_parts [ @http_proxy.host, diff --git a/hearth/lib/hearth/http/fields.rb b/hearth/lib/hearth/http/fields.rb index 1edeef74b..e98929573 100644 --- a/hearth/lib/hearth/http/fields.rb +++ b/hearth/lib/hearth/http/fields.rb @@ -90,6 +90,12 @@ def key?(key) @fields.key?(key) && @fields[key].kind == @kind end + # @param [String] key + # @return [Field, nil] Returns the value for the deleted Field key. + def delete(key) + @fields.delete(key).value(@fields.encoding) if key?(key) + end + # @return [Enumerable] def each(&block) @fields.filter { |_k, v| v.kind == @kind } diff --git a/hearth/lib/hearth/retry.rb b/hearth/lib/hearth/retry.rb deleted file mode 100644 index 44981d16a..000000000 --- a/hearth/lib/hearth/retry.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -require_relative 'retry/client_rate_limiter' -require_relative 'retry/error_inspector' -require_relative 'retry/retry_quota' diff --git a/hearth/spec/hearth/dns/host_address_spec.rb b/hearth/spec/hearth/dns/host_address_spec.rb new file mode 100644 index 000000000..e87f46852 --- /dev/null +++ b/hearth/spec/hearth/dns/host_address_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Hearth + module DNS + describe HostAddress do + it 'can be initialized' do + HostAddress.new( + address_type: :A, + address: '123.123.123.123', + hostname: 'example.com' + ) + end + end + end +end diff --git a/hearth/spec/hearth/dns/host_resolver_spec.rb b/hearth/spec/hearth/dns/host_resolver_spec.rb new file mode 100644 index 000000000..61ec80caa --- /dev/null +++ b/hearth/spec/hearth/dns/host_resolver_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Hearth + module DNS + describe HostResolver do + let(:nodename) { 'example.com' } + let(:service) { 443 } + let(:family) { :INET } + let(:socktype) { :SOCK_STREAM } + let(:protocol) { 0 } + let(:flags) { Socket::AI_ALL | Socket::AI_V4MAPPED } + + let(:addrinfo_ipv4) do + double(Addrinfo, ipv4?: true, ipv6?: false, ip_address: '127.0.0.1') + end + let(:addrinfo_ipv6) do + double(Addrinfo, ipv4?: false, ipv6?: true, ip_address: '::1') + end + let(:addrinfo_list) { [addrinfo_ipv4, addrinfo_ipv6] } + + subject do + HostResolver.new( + service: service, + family: family, + socktype: socktype, + protocol: protocol, + flags: flags + ) + end + + describe '#initialize' do + it 'sets empty defaults' do + host_resolver = HostResolver.new + expect(host_resolver.service).to eq(443) + expect(host_resolver.family).to be_nil + expect(host_resolver.socktype).to eq(:SOCK_STREAM) + expect(host_resolver.protocol).to be_nil + expect(host_resolver.flags).to be_nil + end + end + + describe '#resolve_address' do + it 'uses instance defaults' do + expect(Addrinfo).to receive(:getaddrinfo).with( + nodename, + service, + family, + socktype, + protocol, + flags + ).and_return(addrinfo_list) + subject.resolve_address(nodename: nodename) + end + + it 'uses passed in options' do + service = 80 + family = :INET6 + socktype = :SOCK_DGRAM + protocol = nil + flags = nil + + expect(Addrinfo).to receive(:getaddrinfo).with( + nodename, + service, + family, + socktype, + protocol, + flags + ).and_return(addrinfo_list) + subject.resolve_address( + nodename: nodename, + service: service, + family: family, + socktype: socktype, + protocol: protocol, + flags: flags + ) + end + + it 'returns host address objects' do + expect(Addrinfo).to receive(:getaddrinfo).and_return(addrinfo_list) + _ipv6, ipv4 = subject.resolve_address(nodename: nodename) + expect(ipv4).to be_a(Hearth::DNS::HostAddress) + end + + context 'ipv6 is not available' do + before do + allow(subject).to receive(:use_ipv6?).and_return(false) + end + + it 'returns host addresses' do + expect(Addrinfo).to receive(:getaddrinfo).and_return(addrinfo_list) + ipv6, ipv4 = subject.resolve_address(nodename: nodename) + expect(ipv6).to be_nil + expect(ipv4.address).to eq(addrinfo_ipv4.ip_address) + end + end + + context 'ipv6 is available' do + before do + allow(subject).to receive(:use_ipv6?).and_return(true) + end + + it 'returns host addresses' do + expect(Addrinfo).to receive(:getaddrinfo).and_return(addrinfo_list) + ipv6, ipv4 = subject.resolve_address(nodename: nodename) + expect(ipv6.address).to eq(addrinfo_ipv6.ip_address) + expect(ipv4.address).to eq(addrinfo_ipv4.ip_address) + end + end + end + end + end +end diff --git a/hearth/spec/hearth/dns_spec.rb b/hearth/spec/hearth/dns_spec.rb new file mode 100644 index 000000000..f4c12b608 --- /dev/null +++ b/hearth/spec/hearth/dns_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module Hearth + module DNS + context 'DNS resolution patch' do + let(:host_resolver) { Hearth::DNS::HostResolver.new } + let(:hostname) { 'example.com' } + + let(:ipv6_host_address) do + Hearth::DNS::HostAddress.new( + address: '::1', + address_type: :AAAA, + hostname: hostname + ) + end + let(:ipv4_host_address) do + Hearth::DNS::HostAddress.new( + address: '127.0.0.1', + address_type: :A, + hostname: hostname + ) + end + + context 'IPSocket' do + context 'configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = host_resolver + end + + it 'prefers ipv6' do + expect(host_resolver).to receive(:resolve_address) + .with(nodename: 'example.com') + .and_return([ipv6_host_address, ipv4_host_address]) + addr = IPSocket.getaddress('example.com') + expect(addr).to eq(ipv6_host_address.address) + end + + it 'falls back to ipv4' do + expect(host_resolver).to receive(:resolve_address) + .with(nodename: 'example.com') + .and_return([nil, ipv4_host_address]) + addr = IPSocket.getaddress('example.com') + expect(addr).to eq(ipv4_host_address.address) + end + end + + context 'no configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = nil + end + + it 'uses stdlib if no resolver is set' do + expect(IPSocket).to receive(:original_hearth_getaddress) + .with(hostname) # aliased original method + expect(host_resolver).not_to receive(:resolve_address) + IPSocket.getaddress('example.com') + end + end + end + + context 'TCPSocket' do + let(:service) { 443 } + let(:proxy_host) { 'proxy.example.com' } + let(:proxy_addr) { '123.123.123.123' } + let(:proxy_port) { 8080 } + + context 'configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = host_resolver + end + + it 'calls original with patched IPSocket' do + address = ipv4_host_address.address + expect(IPSocket).to receive(:getaddress) + .with(hostname).and_return(address) + expect_any_instance_of(TCPSocket) + .to receive(:original_hearth_initialize).with(address, service) + TCPSocket.new(hostname, service) + end + + it 'calls original with patched IPSocket and proxy' do + address = ipv4_host_address.address + expect(IPSocket).to receive(:getaddress) + .with(hostname).and_return(address) + expect(IPSocket).to receive(:getaddress) + .with(proxy_host).and_return(proxy_addr) + expect_any_instance_of(TCPSocket) + .to receive(:original_hearth_initialize) + .with(address, service, proxy_addr, proxy_port) + TCPSocket.new(hostname, service, proxy_host, proxy_port) + end + end + + context 'no configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = nil + end + + it 'uses stdlib if no resolver is set' do + expect_any_instance_of(TCPSocket) + .to receive(:original_hearth_initialize) + .with(hostname, service, proxy_host, proxy_port) + expect(IPSocket).to_not receive(:getaddress) + TCPSocket.new(hostname, service, proxy_host, proxy_port) + end + end + end + end + end +end diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index 00d0ba13d..7c952ffaf 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -14,6 +14,7 @@ module HTTP let(:ssl_ca_bundle) { nil } let(:ssl_ca_directory) { nil } let(:ssl_ca_store) { nil } + let(:host_resolver) { nil } subject do Client.new( @@ -22,7 +23,8 @@ module HTTP ssl_verify_peer: ssl_verify_peer, ssl_ca_bundle: ssl_ca_bundle, ssl_ca_directory: ssl_ca_directory, - ssl_ca_store: ssl_ca_store + ssl_ca_store: ssl_ca_store, + host_resolver: host_resolver ) end @@ -294,6 +296,19 @@ module HTTP subject.transmit(request: request, response: response) end end + + context 'DNS resolution' do + let(:host_resolver) { Hearth::DNS::HostResolver.new } + + it 'sets the custom dns resolver as a thread local variable' do + expect(Thread.current).to receive(:[]=) + .with(:net_http_hearth_dns_resolver, host_resolver) + expect(Thread.current).to receive(:[]=) + .with(:net_http_hearth_dns_resolver, nil) + stub_request(:any, uri.to_s) + subject.transmit(request: request, response: response) + end + end end end end diff --git a/hearth/spec/hearth/http/fields_spec.rb b/hearth/spec/hearth/http/fields_spec.rb index 025a47a80..e51f876bc 100644 --- a/hearth/spec/hearth/http/fields_spec.rb +++ b/hearth/spec/hearth/http/fields_spec.rb @@ -7,7 +7,6 @@ module HTTP let(:trailer_field) { Field.new('X-Trailer', 'bar', kind: :trailer) } let(:fields) { Fields.new([header_field, trailer_field]) } - subject { fields } describe '#initialize' do it 'defaults encoding to UTF-8' do @@ -30,87 +29,90 @@ module HTTP describe '#[]' do it 'returns the field' do - expect(subject['x-header']).to eq(header_field) + expect(fields['x-header']).to eq(header_field) end end describe '#[]=' do it 'raises when field is not a Field' do - expect { subject['x-header'] = 'not a field' } + expect { fields['x-header'] = 'not a field' } .to raise_error(ArgumentError) end it 'sets the field' do - subject['x-header'] = Field.new('X-Header', 'bar') - expect(subject['x-header'].value).to eq('bar') + fields['x-header'] = Field.new('X-Header', 'bar') + expect(fields['x-header'].value).to eq('bar') end end describe '#key?' do it 'returns true if the field exists' do - expect(subject.key?('x-header')).to eq(true) - expect(subject.key?('x-trailer')).to eq(true) - expect(subject.key?('x-foo')).to eq(false) + expect(fields.key?('x-header')).to eq(true) + expect(fields.key?('x-trailer')).to eq(true) + expect(fields.key?('x-foo')).to eq(false) end end describe '#delete' do it 'deletes the field' do - field = subject['x-header'] - deleted = subject.delete('x-header') + fields.delete('x-header') + expect(fields.key?('x-header')).to eq(false) + end + + it 'returns the deleted field' do + field = fields['x-header'] + deleted = fields.delete('x-header') expect(deleted).to eq(field) - expect(subject.key?('x-header')).to eq(false) - expect(subject['x-header']).to eq(nil) end end describe '#each' do it 'includes Enumerable' do - expect(subject).to be_a(Enumerable) + expect(fields).to be_a(Enumerable) end it 'enumerates over its contents' do - subject.each { |k, v| expect(subject[k]).to eq(v) } + fields.each { |k, v| expect(fields[k]).to eq(v) } end end describe '#size' do it 'returns the number of fields' do - expect(subject.size).to eq(2) + expect(fields.size).to eq(2) end end describe '#clear' do it 'clears the fields' do - subject.clear - expect(subject.size).to eq(0) + fields.clear + expect(fields.size).to eq(0) end end describe Fields::Proxy do - subject { Fields::Proxy.new(fields, :header) } + let(:proxy) { Fields::Proxy.new(fields, :header) } describe '#[]' do it 'returns the field value' do - expect(subject['x-header']).to eq(header_field.value) + expect(proxy['x-header']).to eq(header_field.value) end it 'returns nil if the kind of field does not exist' do - expect(subject['x-trailer']).to be_nil + expect(proxy['x-trailer']).to be_nil end context 'encoding' do let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } it 'applies the encoding' do - expect(subject['x-header'].encoding).to eq(Encoding::UTF_16) + expect(proxy['x-header'].encoding).to eq(Encoding::UTF_16) end end end describe '#[]=' do it 'sets the field value and kind' do - subject['x-foo'] = 'bar' + proxy['x-foo'] = 'bar' expect(fields['x-foo']).to be_a(Field) expect(fields['x-foo'].value).to eq('bar') expect(fields['x-foo'].kind).to eq(:header) @@ -119,26 +121,50 @@ module HTTP describe '#key?' do it 'returns true if the field of that kind exists' do - expect(subject.key?('x-header')).to eq(true) - expect(subject.key?('x-trailer')).to eq(false) + expect(proxy.key?('x-header')).to eq(true) + expect(proxy.key?('x-trailer')).to eq(false) + end + end + + describe '#delete' do + it 'deletes the kind of field' do + proxy.delete('x-header') + expect(fields.key?('x-header')).to eq(false) + proxy.delete('x-trailer') + expect(fields.key?('x-trailer')).to eq(true) + end + + it 'returns the value of the deleted field' do + header = proxy['x-header'] + deleted = proxy.delete('x-header') + expect(deleted).to eq(header) + end + + context 'encoding' do + let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } + + it 'applies the encoding' do + expect(proxy.delete('x-header').encoding) + .to eq(Encoding::UTF_16) + end end end describe '#each' do it 'includes Enumerable' do - expect(subject).to be_a(Enumerable) + expect(proxy).to be_a(Enumerable) end it 'returns the Field name and value for each field kind' do # not downcased header name - expect(subject.each.to_h).to eq('X-Header' => 'foo') + expect(proxy.each.to_h).to eq('X-Header' => 'foo') end context 'encoding' do let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } it 'applies the encoding' do - expect(subject.each.to_h['X-Header'].encoding) + expect(proxy.each.to_h['X-Header'].encoding) .to eq(Encoding::UTF_16) end end From a60552d0a1bb9eebbbc4e51ad1562ddfb9eea781 Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Wed, 10 May 2023 12:00:58 -0400 Subject: [PATCH 16/22] SRA: Retry Strategy classes (#132) --- .../lib/high_score_service/client.rb | 42 +-- .../lib/high_score_service/config.rb | 38 +-- .../rails_json/lib/rails_json/client.rb | 290 +++++------------- .../rails_json/lib/rails_json/config.rb | 38 +-- .../projections/weather/lib/weather/client.rb | 50 +-- .../projections/weather/lib/weather/config.rb | 38 +-- .../white_label/lib/white_label/client.rb | 90 ++---- .../white_label/lib/white_label/config.rb | 38 +-- .../white_label/spec/client_spec.rb | 10 +- .../white_label/spec/config_spec.rb | 4 +- .../white_label/spec/retry_spec.rb | 4 +- .../integration-specs/client_spec.rb | 10 +- .../integration-specs/config_spec.rb | 4 +- .../integration-specs/retry_spec.rb | 4 +- .../ruby/codegen/ApplicationTransport.java | 7 + .../amazon/smithy/ruby/codegen/Hearth.java | 15 +- .../codegen/generators/ClientGenerator.java | 2 - .../generators/ErrorsGeneratorBase.java | 4 +- .../generators/HttpProtocolTestGenerator.java | 2 +- .../codegen/middleware/MiddlewareBuilder.java | 53 +--- hearth/Gemfile | 2 +- hearth/lib/hearth.rb | 4 +- hearth/lib/hearth/http.rb | 1 + hearth/lib/hearth/http/error_inspector.rb | 85 +++++ hearth/lib/hearth/middleware/retry.rb | 118 ++----- hearth/lib/hearth/retry.rb | 15 + hearth/lib/hearth/retry/adaptive.rb | 60 ++++ hearth/lib/hearth/retry/error_inspector.rb | 58 ---- .../lib/hearth/retry/exponential_backoff.rb | 16 + hearth/lib/hearth/retry/retry_quota.rb | 13 +- hearth/lib/hearth/retry/standard.rb | 46 +++ hearth/lib/hearth/retry/strategy.rb | 20 ++ .../{retry => http}/error_inspector_spec.rb | 50 ++- hearth/spec/hearth/middleware/retry_spec.rb | 56 ++-- 34 files changed, 563 insertions(+), 724 deletions(-) create mode 100644 hearth/lib/hearth/http/error_inspector.rb create mode 100644 hearth/lib/hearth/retry.rb create mode 100644 hearth/lib/hearth/retry/adaptive.rb delete mode 100644 hearth/lib/hearth/retry/error_inspector.rb create mode 100644 hearth/lib/hearth/retry/exponential_backoff.rb create mode 100644 hearth/lib/hearth/retry/standard.rb create mode 100644 hearth/lib/hearth/retry/strategy.rb rename hearth/spec/hearth/{retry => http}/error_inspector_spec.rb (68%) diff --git a/codegen/projections/high_score_service/lib/high_score_service/client.rb b/codegen/projections/high_score_service/lib/high_score_service/client.rb index 7a55e8ff9..a01e9024b 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/client.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/client.rb @@ -32,8 +32,6 @@ def initialize(config = HighScoreService::Config.new, options = {}) @config = config @middleware = Hearth::MiddlewareBuilder.new(options[:middleware]) @stubs = Hearth::Stubbing::Stubs.new - @retry_quota = Hearth::Retry::RetryQuota.new - @client_rate_limiter = Hearth::Retry::ClientRateLimiter.new end # Create a new high score @@ -79,12 +77,8 @@ def create_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 201, errors: [Errors::UnprocessableEntityError]), @@ -147,12 +141,8 @@ def delete_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -221,12 +211,8 @@ def get_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -291,12 +277,8 @@ def list_high_scores(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -372,12 +354,8 @@ def update_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::UnprocessableEntityError]), diff --git a/codegen/projections/high_score_service/lib/high_score_service/config.rb b/codegen/projections/high_score_service/lib/high_score_service/config.rb index 69d4b4a0e..4f88d4b80 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/config.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/config.rb @@ -9,9 +9,6 @@ module HighScoreService # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # @@ -27,13 +24,12 @@ module HighScoreService # @option args [Logger] :logger ($stdout) # Logger to use for output # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. - # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,9 +37,6 @@ module HighScoreService # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # @@ -59,11 +52,8 @@ module HighScoreService # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module HighScoreService # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, :http_wire_trace, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, keyword_init: true @@ -89,28 +77,24 @@ module HighScoreService private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], http_wire_trace: [false], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], validate_input: [true] }.freeze diff --git a/codegen/projections/rails_json/lib/rails_json/client.rb b/codegen/projections/rails_json/lib/rails_json/client.rb index 211707bc5..3f3793ab6 100644 --- a/codegen/projections/rails_json/lib/rails_json/client.rb +++ b/codegen/projections/rails_json/lib/rails_json/client.rb @@ -31,8 +31,6 @@ def initialize(config = RailsJson::Config.new, options = {}) @config = config @middleware = Hearth::MiddlewareBuilder.new(options[:middleware]) @stubs = Hearth::Stubbing::Stubs.new - @retry_quota = Hearth::Retry::RetryQuota.new - @client_rate_limiter = Hearth::Retry::ClientRateLimiter.new end # This example uses all query string types. @@ -101,12 +99,8 @@ def all_query_string_types(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -169,12 +163,8 @@ def constant_and_variable_query_string(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -237,12 +227,8 @@ def constant_query_string(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -312,12 +298,8 @@ def document_type(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -385,12 +367,8 @@ def document_type_as_payload(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -446,12 +424,8 @@ def empty_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -511,12 +485,8 @@ def endpoint_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -578,12 +548,8 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -649,12 +615,8 @@ def greeting_with_errors(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::InvalidGreeting, Errors::ComplexError]), @@ -720,12 +682,8 @@ def http_payload_traits(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -789,12 +747,8 @@ def http_payload_traits_with_media_type(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -863,12 +817,8 @@ def http_payload_with_structure(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -936,12 +886,8 @@ def http_prefix_headers(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1001,12 +947,8 @@ def http_prefix_headers_in_response(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1065,12 +1007,8 @@ def http_request_with_float_labels(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1129,12 +1067,8 @@ def http_request_with_greedy_label_in_path(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1208,12 +1142,8 @@ def http_request_with_labels(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1280,12 +1210,8 @@ def http_request_with_labels_and_timestamp_format(params = {}, options = {}, &bl ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1342,12 +1268,8 @@ def http_response_code(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1408,12 +1330,8 @@ def ignore_query_params_in_response(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1523,12 +1441,8 @@ def input_and_output_with_headers(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1608,12 +1522,8 @@ def json_enums(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1720,12 +1630,8 @@ def json_maps(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1823,12 +1729,8 @@ def json_unions(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1992,12 +1894,8 @@ def kitchen_sink_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::ErrorWithMembers, Errors::ErrorWithoutMembers]), @@ -2058,12 +1956,8 @@ def media_type_header(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2124,12 +2018,8 @@ def nested_attributes_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2199,12 +2089,8 @@ def null_and_empty_headers_client(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2273,12 +2159,8 @@ def null_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2339,12 +2221,8 @@ def omits_null_serializes_empty_string(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2403,12 +2281,8 @@ def operation_with_optional_input_output(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2470,12 +2344,8 @@ def query_idempotency_token_auto_fill(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2538,12 +2408,8 @@ def query_params_as_string_list_map(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2601,12 +2467,8 @@ def streaming_operation(params = {}, options = {}, &block) builder: Builders::StreamingOperation ) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2679,12 +2541,8 @@ def timestamp_format_headers(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2747,12 +2605,8 @@ def operation____789_bad_name(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), diff --git a/codegen/projections/rails_json/lib/rails_json/config.rb b/codegen/projections/rails_json/lib/rails_json/config.rb index 12645d4a5..979331707 100644 --- a/codegen/projections/rails_json/lib/rails_json/config.rb +++ b/codegen/projections/rails_json/lib/rails_json/config.rb @@ -9,9 +9,6 @@ module RailsJson # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # @@ -27,13 +24,12 @@ module RailsJson # @option args [Logger] :logger ($stdout) # Logger to use for output # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. - # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,9 +37,6 @@ module RailsJson # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # @@ -59,11 +52,8 @@ module RailsJson # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module RailsJson # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, :http_wire_trace, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, keyword_init: true @@ -89,28 +77,24 @@ module RailsJson private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], http_wire_trace: [false], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], validate_input: [true] }.freeze diff --git a/codegen/projections/weather/lib/weather/client.rb b/codegen/projections/weather/lib/weather/client.rb index a8e342480..a39fca11e 100644 --- a/codegen/projections/weather/lib/weather/client.rb +++ b/codegen/projections/weather/lib/weather/client.rb @@ -30,8 +30,6 @@ def initialize(config = Weather::Config.new, options = {}) @config = config @middleware = Hearth::MiddlewareBuilder.new(options[:middleware]) @stubs = Hearth::Stubbing::Stubs.new - @retry_quota = Hearth::Retry::RetryQuota.new - @client_rate_limiter = Hearth::Retry::ClientRateLimiter.new end # @param [Hash] params @@ -71,12 +69,8 @@ def get_city(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), @@ -143,12 +137,8 @@ def get_city_image(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), @@ -204,12 +194,8 @@ def get_current_time(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -282,12 +268,8 @@ def get_forecast(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -365,12 +347,8 @@ def list_cities(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -433,12 +411,8 @@ def operation____789_bad_name(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), diff --git a/codegen/projections/weather/lib/weather/config.rb b/codegen/projections/weather/lib/weather/config.rb index c7cafac68..36dc2d597 100644 --- a/codegen/projections/weather/lib/weather/config.rb +++ b/codegen/projections/weather/lib/weather/config.rb @@ -9,9 +9,6 @@ module Weather # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # @@ -27,13 +24,12 @@ module Weather # @option args [Logger] :logger ($stdout) # Logger to use for output # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. - # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,9 +37,6 @@ module Weather # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # @@ -59,11 +52,8 @@ module Weather # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module Weather # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, :http_wire_trace, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, keyword_init: true @@ -89,28 +77,24 @@ module Weather private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], http_wire_trace: [false], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], validate_input: [true] }.freeze diff --git a/codegen/projections/white_label/lib/white_label/client.rb b/codegen/projections/white_label/lib/white_label/client.rb index 5ed2de95c..7e5894011 100644 --- a/codegen/projections/white_label/lib/white_label/client.rb +++ b/codegen/projections/white_label/lib/white_label/client.rb @@ -48,8 +48,6 @@ def initialize(config = WhiteLabel::Config.new, options = {}) @config = config @middleware = Hearth::MiddlewareBuilder.new(options[:middleware]) @stubs = Hearth::Stubbing::Stubs.new - @retry_quota = Hearth::Retry::RetryQuota.new - @client_rate_limiter = Hearth::Retry::ClientRateLimiter.new end # @param [Hash] params @@ -133,12 +131,8 @@ def defaults_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -197,12 +191,8 @@ def endpoint_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -263,12 +253,8 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -490,12 +476,8 @@ def kitchen_sink(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::ClientError, Errors::ServerError]), @@ -554,12 +536,8 @@ def mixin_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -619,12 +597,8 @@ def paginators_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -684,12 +658,8 @@ def paginators_test_with_items(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -746,12 +716,8 @@ def streaming_operation(params = {}, options = {}, &block) builder: Builders::StreamingOperation ) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -808,12 +774,8 @@ def streaming_with_length(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -871,12 +833,8 @@ def waiters_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -937,12 +895,8 @@ def operation____paginators_test_with_bad_names(params = {}, options = {}, &bloc ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), diff --git a/codegen/projections/white_label/lib/white_label/config.rb b/codegen/projections/white_label/lib/white_label/config.rb index 3fbb9e45e..61f18e2e5 100644 --- a/codegen/projections/white_label/lib/white_label/config.rb +++ b/codegen/projections/white_label/lib/white_label/config.rb @@ -9,9 +9,6 @@ module WhiteLabel # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # @@ -27,13 +24,12 @@ module WhiteLabel # @option args [Logger] :logger ($stdout) # Logger to use for output # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. - # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,9 +37,6 @@ module WhiteLabel # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # @@ -59,11 +52,8 @@ module WhiteLabel # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module WhiteLabel # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, :http_wire_trace, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, keyword_init: true @@ -89,28 +77,24 @@ module WhiteLabel private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], http_wire_trace: [false], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], validate_input: [true] }.freeze diff --git a/codegen/projections/white_label/spec/client_spec.rb b/codegen/projections/white_label/spec/client_spec.rb index 7734cefcd..ee2ceaef5 100644 --- a/codegen/projections/white_label/spec/client_spec.rb +++ b/codegen/projections/white_label/spec/client_spec.rb @@ -17,16 +17,12 @@ module WhiteLabel client.kitchen_sink end - it 'uses retry_mode, max_attempts, and adaptive_retry_wait_to_fill' do + it 'uses retry_strategy' do expect(Hearth::Middleware::Retry) .to receive(:new) .with(anything, - retry_mode: config.retry_mode, - max_attempts: config.max_attempts, - adaptive_retry_wait_to_fill: config.adaptive_retry_wait_to_fill, - error_inspector_class: anything, - client_rate_limiter: anything, - retry_quota: anything) + retry_strategy: config.retry_strategy, + error_inspector_class: anything) .and_call_original client.kitchen_sink diff --git a/codegen/projections/white_label/spec/config_spec.rb b/codegen/projections/white_label/spec/config_spec.rb index 8806a1169..a3da78ac9 100644 --- a/codegen/projections/white_label/spec/config_spec.rb +++ b/codegen/projections/white_label/spec/config_spec.rb @@ -7,14 +7,12 @@ module WhiteLabel describe '#build' do it 'sets member values' do config_keys = { - adaptive_retry_wait_to_fill: false, disable_host_prefix: true, endpoint: 'test', http_wire_trace: true, log_level: :debug, logger: Logger.new($stdout, level: :debug), - max_attempts: 0, - retry_mode: 'adaptive', + retry_strategy: Hearth::Retry::Adaptive.new, stub_responses: false, validate_input: false } diff --git a/codegen/projections/white_label/spec/retry_spec.rb b/codegen/projections/white_label/spec/retry_spec.rb index da933cb08..cbcf41f2b 100644 --- a/codegen/projections/white_label/spec/retry_spec.rb +++ b/codegen/projections/white_label/spec/retry_spec.rb @@ -15,8 +15,10 @@ module WhiteLabel # first return error, then some data client.stub_responses(:kitchen_sink, [error, { string: "ok" }]) + expect_any_instance_of(Hearth::Retry::Standard).to receive(:acquire_initial_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:refresh_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:record_success).and_call_original expect(Kernel).to receive(:sleep).once - expect_any_instance_of(Hearth::Middleware::Retry).to receive(:call).twice.and_call_original client.kitchen_sink end diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb index 7734cefcd..ee2ceaef5 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb @@ -17,16 +17,12 @@ module WhiteLabel client.kitchen_sink end - it 'uses retry_mode, max_attempts, and adaptive_retry_wait_to_fill' do + it 'uses retry_strategy' do expect(Hearth::Middleware::Retry) .to receive(:new) .with(anything, - retry_mode: config.retry_mode, - max_attempts: config.max_attempts, - adaptive_retry_wait_to_fill: config.adaptive_retry_wait_to_fill, - error_inspector_class: anything, - client_rate_limiter: anything, - retry_quota: anything) + retry_strategy: config.retry_strategy, + error_inspector_class: anything) .and_call_original client.kitchen_sink diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb index 8806a1169..a3da78ac9 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb @@ -7,14 +7,12 @@ module WhiteLabel describe '#build' do it 'sets member values' do config_keys = { - adaptive_retry_wait_to_fill: false, disable_host_prefix: true, endpoint: 'test', http_wire_trace: true, log_level: :debug, logger: Logger.new($stdout, level: :debug), - max_attempts: 0, - retry_mode: 'adaptive', + retry_strategy: Hearth::Retry::Adaptive.new, stub_responses: false, validate_input: false } diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb index da933cb08..cbcf41f2b 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb @@ -15,8 +15,10 @@ module WhiteLabel # first return error, then some data client.stub_responses(:kitchen_sink, [error, { string: "ok" }]) + expect_any_instance_of(Hearth::Retry::Standard).to receive(:acquire_initial_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:refresh_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:record_success).and_call_original expect(Kernel).to receive(:sleep).once - expect_any_instance_of(Hearth::Middleware::Retry).to receive(:call).twice.and_call_original client.kitchen_sink end diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java index 09d636e79..ecd83bfae 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java @@ -254,6 +254,13 @@ public ClientFragment getTransportClient() { return transportClient; } + /** + * @return the error inspector used for HTTP errors. + */ + public String getErrorInspector() { + return Hearth.HTTP_ERROR_INSPECTOR.toString(); + } + /** * @param context generation context * @return list of default middleware to support this transport. diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java index 3c76544ea..8ac4cc3ed 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java @@ -62,19 +62,14 @@ public final class Hearth { .name("Stubs") .build(); - public static final Symbol RETRY_QUOTA = Symbol.builder() - .namespace("Hearth::Retry", "::") - .name("RetryQuota") - .build(); - - public static final Symbol CLIENT_RATE_LIMITER = Symbol.builder() - .namespace("Hearth::Retry", "::") - .name("ClientRateLimiter") + public static final Symbol HTTP_API_ERROR = Symbol.builder() + .namespace("Hearth::HTTP", "::") + .name("ApiError") .build(); - public static final Symbol API_ERROR = Symbol.builder() + public static final Symbol HTTP_ERROR_INSPECTOR = Symbol.builder() .namespace("Hearth::HTTP", "::") - .name("ApiError") + .name("ErrorInspector") .build(); public static final Symbol XML = Symbol.builder() diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java index df2fff3ed..93aea74bc 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java @@ -151,8 +151,6 @@ private void renderInitializeMethod(RubyCodeWriter writer) { .write("@config = config") .write("@middleware = $T.new(options[:middleware])", Hearth.MIDDLEWARE_BUILDER) .write("@stubs = $T.new", Hearth.STUBS) - .write("@retry_quota = $T.new", Hearth.RETRY_QUOTA) - .write("@client_rate_limiter = $T.new", Hearth.CLIENT_RATE_LIMITER) .closeBlock("end"); } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java index 91881e93b..5bdaa3477 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java @@ -157,7 +157,7 @@ public void renderRbs(FileManifest fileManifest) { private void renderBaseErrors() { writer .write("\n# Base class for all errors returned by this service") - .write("class ApiError < $T; end", Hearth.API_ERROR) + .write("class ApiError < $T; end", Hearth.HTTP_API_ERROR) .write("\n# Base class for all errors returned where the client is at fault.") .write("# These are generally errors with 4XX HTTP status codes.") .write("class ApiClientError < ApiError; end") @@ -179,7 +179,7 @@ private void renderBaseErrors() { private void renderRbsBaseErrors() { rbsWriter - .write("\nclass ApiError < $T", Hearth.API_ERROR) + .write("\nclass ApiError < $T", Hearth.HTTP_API_ERROR) .write("def initialize: (request_id: untyped request_id, **untyped kwargs) -> void\n") .write("attr_reader request_id: untyped") .write("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java index 80d05f4d7..6eefd874c 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java @@ -380,7 +380,7 @@ private void renderRequestMiddlewareBody(Optional body, Optional if (body.get().length() > 0) { writer .write("expect($T.parse(request.body.read)).to " - + "match_xml_node(Hearth::XML.parse('$L'))", + + "match_xml_node($T.parse('$L'))", Hearth.XML, body.get()) .addUseImports(RubyDependency.HEARTH_XML_MATCHER); } else { diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java index ba6313595..0ad2741ac 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java @@ -230,55 +230,30 @@ public void addDefaultMiddleware(GenerationContext context) { "Enable response stubbing for testing. See {Hearth::ClientStubs#stub_responses}.") .build(); - ClientConfig maxAttempts = (new ClientConfig.Builder()) - .name("max_attempts") - .type("Integer") - .defaultValue("3") + ClientConfig retryStrategy = (new ClientConfig.Builder()) + .name("retry_strategy") + .type("Hearth::Retry::Strategy") + .documentationDefaultValue("Hearth::Retry::Standard.new") + .defaultValue("Hearth::Retry::Standard.new") .documentation( - "An integer representing the maximum number of attempts that will be made for a " - + "single request, including the initial attempt." - ) - .build(); - - ClientConfig retryMode = (new ClientConfig.Builder()) - .name("retry_mode") - .type("String") - .defaultValue("'standard'") - .documentation( - "Specifies which retry algorithm to use. Values are: \n" - + - " * `standard` - A standardized set of retry rules across the AWS SDKs. " - + - "This includes support" - + - " for retry quotas, which limit the number of unsuccessful retries a client can make.\n" - + " * `adaptive` - An experimental retry mode that includes all the" + "Specifies which retry strategy class to use. Strategy classes\n" + + " may have additional options, such as max_retries and backoff strategies.\n" + + " Available options are: \n" + + " * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. " + + "This includes support for retry quotas, which limit the number of" + + " unsuccessful retries a client can make.\n" + + " * `Retry::Adaptive` - An experimental retry mode that includes all the" + " functionality of `standard` mode along with automatic client side" + " throttling. This is a provisional mode that may change behavior" + " in the future." ) .build(); - ClientConfig adaptiveRetryWaitToFill = (new ClientConfig.Builder()) - .name("adaptive_retry_wait_to_fill") - .type("Boolean") - .defaultValue("true") - .documentation( - "Used only in `adaptive` retry mode. When true, the request will sleep until there is" - + " sufficient client side capacity to retry the request. When false, the request will" - + " raise a `CapacityNotAvailableError` and will not retry instead of sleeping." - ) - .build(); - Middleware retry = (new Middleware.Builder()) .klass("Hearth::Middleware::Retry") .step(MiddlewareStackStep.RETRY) - .addConfig(maxAttempts) - .addConfig(retryMode) - .addConfig(adaptiveRetryWaitToFill) - .addParam("error_inspector_class", "Hearth::Retry::ErrorInspector") - .addParam("retry_quota", "@retry_quota") - .addParam("client_rate_limiter", "@client_rate_limiter") + .addConfig(retryStrategy) + .addParam("error_inspector_class", transport.getErrorInspector()) .build(); Middleware send = (new Middleware.Builder()) diff --git a/hearth/Gemfile b/hearth/Gemfile index 3803f65de..b8863b4b0 100644 --- a/hearth/Gemfile +++ b/hearth/Gemfile @@ -16,5 +16,5 @@ group :development do gem 'parallel', '1.22.1' # 1.23.0 broke steep, temporary gem 'rbs', '~> 2' gem 'rubocop' - gem 'steep' + gem 'steep', '1.3.2' end diff --git a/hearth/lib/hearth.rb b/hearth/lib/hearth.rb index e37947657..6c78380ed 100755 --- a/hearth/lib/hearth.rb +++ b/hearth/lib/hearth.rb @@ -22,9 +22,7 @@ require_relative 'hearth/output' require_relative 'hearth/query/param' require_relative 'hearth/query/param_list' -require_relative 'hearth/retry/client_rate_limiter' -require_relative 'hearth/retry/error_inspector' -require_relative 'hearth/retry/retry_quota' +require_relative 'hearth/retry' require_relative 'hearth/structure' require_relative 'hearth/stubbing/client_stubs' require_relative 'hearth/stubbing/stubs' diff --git a/hearth/lib/hearth/http.rb b/hearth/lib/hearth/http.rb index 22e95ac6b..aa572a0f8 100755 --- a/hearth/lib/hearth/http.rb +++ b/hearth/lib/hearth/http.rb @@ -3,6 +3,7 @@ require 'cgi' require_relative 'http/api_error' require_relative 'http/client' +require_relative 'http/error_inspector' require_relative 'http/error_parser' require_relative 'http/field' require_relative 'http/fields' diff --git a/hearth/lib/hearth/http/error_inspector.rb b/hearth/lib/hearth/http/error_inspector.rb new file mode 100644 index 000000000..b65d590b8 --- /dev/null +++ b/hearth/lib/hearth/http/error_inspector.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'time' + +module Hearth + module HTTP + # An HTTP error inspector, using hints from status code and headers. + # @api private + class ErrorInspector + def initialize(error, http_response) + @error = error + @http_response = http_response + end + + def retryable? + (modeled_retryable? || + throttling? || + transient? || + server?) && + @http_response.body.respond_to?(:truncate) + end + + def error_type + if transient? + 'Transient' + elsif throttling? + 'Throttling' + elsif server? + 'ServerError' + elsif client? + 'ClientError' + else + 'Unknown' + end + end + + def hints + hints = {} + if (retry_after = retry_after_hint) + hints[:retry_after_hint] = retry_after + end + hints + end + + private + + def transient? + @error.is_a?(Hearth::HTTP::NetworkingError) + end + + def throttling? + @http_response.status == 429 || modeled_throttling? + end + + def server? + (500..599).cover?(@http_response.status) + end + + def client? + (400..499).cover?(@http_response.status) + end + + def modeled_retryable? + @error.is_a?(Hearth::ApiError) && @error.retryable? + end + + def modeled_throttling? + modeled_retryable? && @error.throttling? + end + + def retry_after_hint + retry_after = @http_response.headers['retry-after'] + Integer(retry_after) + rescue ArgumentError # string is present, assume it is a date + begin + Time.parse(retry_after) - Time.now + rescue ArgumentError # empty string, somehow + nil + end + rescue TypeError # header is not prseent + nil + end + end + end +end diff --git a/hearth/lib/hearth/middleware/retry.rb b/hearth/lib/hearth/middleware/retry.rb index 272773eae..9d3f772ce 100755 --- a/hearth/lib/hearth/middleware/retry.rb +++ b/hearth/lib/hearth/middleware/retry.rb @@ -2,114 +2,62 @@ module Hearth module Middleware - # A middleware that retries the request. + # A middleware that retries the request using a retry strategy. # @api private class Retry - # Max backoff (in seconds) - MAX_BACKOFF = 20 - # @param [Class] app The next middleware in the stack. - # @param [Boolean] retry_mode Specifies which retry algorithm to use. - # Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. - # This includes support for retry quotas, which limit the number of - # unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the - # functionality of `standard` mode along with automatic client side - # throttling. This is a provisional mode that may change behavior - # in the future. - # @param [String] max_attempts An integer representing the maximum number - # of attempts that will be made for a single request, including the - # initial attempt. - # @param [Boolean] adaptive_retry_wait_to_fill Used only in `adaptive` - # retry mode. When true, the request will sleep until there is - # sufficient client side capacity to retry the request. When false, - # the request will raise a `CapacityNotAvailableError` and will not - # retry instead of sleeping. - def initialize(app, retry_mode:, max_attempts:, - adaptive_retry_wait_to_fill:, error_inspector_class:, - retry_quota:, client_rate_limiter:) + # @param [Standard|Adaptive] retry_strategy (Standard) The retry strategy + # to use. Hearth has two built in classes, Standard and Adaptive. + # * `Retry::Standard` - A standardized set of retry rules across + # the AWS SDKs. This includes support for retry quotas, which limit + # the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes + # all the functionality of `standard` mode along with automatic + # client side throttling. This is a provisional mode that may change + # behavior in the future. + def initialize(app, retry_strategy:, error_inspector_class:) @app = app - # public config - @retry_mode = retry_mode - @max_attempts = max_attempts - @adaptive_retry_wait_to_fill = adaptive_retry_wait_to_fill - - # undocumented options + @retry_strategy = retry_strategy + # undocumented - protocol specific @error_inspector_class = error_inspector_class - @retry_quota = retry_quota - @client_rate_limiter = client_rate_limiter - # instance state - @capacity_amount = nil @retries = 0 end - # @param input - # @param context - # @return [Output] - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics def call(input, context) - acquire_token - output = @app.call(input, context) - error_inspector = @error_inspector_class.new( - output.error, context.response.status + token = @retry_strategy.acquire_initial_retry_token( + context.metadata[:retry_token_scope] ) - request_bookkeeping(output, error_inspector) - return output unless retryable?(context, output, error_inspector) + output = nil + loop do + output = @app.call(input, context) - return output if @retries >= @max_attempts - 1 + if (error = output.error) + error_info = @error_inspector_class.new(error, context.response) + token = @retry_strategy.refresh_retry_token(token, error_info) - @capacity_amount = @retry_quota.checkout_capacity(error_inspector) - return output unless @capacity_amount.positive? + Kernel.sleep(token.retry_delay) if token + else + @retry_strategy.record_success(token) + end + break unless token && output.error - delay = [Kernel.rand * (2**@retries), MAX_BACKOFF].min - Kernel.sleep(delay) - retry_request(input, context, output) + reset_request(context, output) + @retries += 1 + end + output end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics private - def acquire_token - return if @retry_mode == 'standard' - - # either fail fast or block until a token becomes available - # must be configurable - # need a maximum rate at which we can send requests (max_send_rate) - # is unset until a throttle is seen - @client_rate_limiter.token_bucket_acquire( - 1, wait_to_fill: @adaptive_retry_wait_to_fill - ) - end - - # max_send_rate is updated if on adaptive mode and based on response - # retry quota is updated if the request is successful (both modes) - def request_bookkeeping(output, error_inspector) - @retry_quota.release(@capacity_amount) unless output.error - - return unless @retry_mode == 'adaptive' - - @client_rate_limiter.update_sending_rate( - error_inspector.error_type == 'Throttling' - ) - end - - def retryable?(context, output, error_inspector) - return false unless output.error - - error_inspector.retryable? && - context.response.body.respond_to?(:truncate) - end - - def retry_request(input, context, output) - @retries += 1 + def reset_request(context, output) context.request.body.rewind context.response.reset output.error = nil - call(input, context) end end end diff --git a/hearth/lib/hearth/retry.rb b/hearth/lib/hearth/retry.rb new file mode 100644 index 000000000..22682064a --- /dev/null +++ b/hearth/lib/hearth/retry.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative 'retry/strategy' + +require_relative 'retry/adaptive' +require_relative 'retry/client_rate_limiter' +require_relative 'retry/exponential_backoff' +require_relative 'retry/retry_quota' +require_relative 'retry/standard' + +module Hearth + module Retry + Token = Struct.new(:retry_count, :retry_delay, keyword_init: true) + end +end diff --git a/hearth/lib/hearth/retry/adaptive.rb b/hearth/lib/hearth/retry/adaptive.rb new file mode 100644 index 000000000..0f7612196 --- /dev/null +++ b/hearth/lib/hearth/retry/adaptive.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # Adaptive retry strategy for retrying requests. + class Adaptive < Strategy + # @param [#call] backoff (ExponentialBackoff) A callable object that + # calculates a backoff delay for a retry attempt. + # @param [Integer] max_attempts (3) The maximum number of attempts that + # will be made for a single request, including the initial attempt. + # @param [Boolean] wait_to_fill When true, the request will sleep until + # there is sufficient client side capacity to retry the request. When + # false, the request will raise a `CapacityNotAvailableError` and will + # not retry instead of sleeping. + def initialize(backoff: ExponentialBackoff.new, max_attempts: 3, + wait_to_fill: true) + super() + @backoff = backoff + @max_attempts = max_attempts + @wait_to_fill = wait_to_fill + + # instance state + @client_rate_limiter = ClientRateLimiter.new + @retry_quota = RetryQuota.new + @capacity_amount = nil + end + + def acquire_initial_retry_token(_token_scope = nil) + @client_rate_limiter.token_bucket_acquire( + 1, wait_to_fill: @wait_to_fill + ) + Token.new(retry_count: 0) + end + + def refresh_retry_token(retry_token, error_info) + return unless error_info.retryable? + + @client_rate_limiter.update_sending_rate( + error_info.error_type == 'Throttling' + ) + return if retry_token.retry_count >= @max_attempts - 1 + + @capacity_amount = @retry_quota.checkout_capacity(error_info) + return unless @capacity_amount.positive? + + delay = error_info.hints[:retry_after_hint] + delay ||= @backoff.call(retry_token.retry_count) + retry_token.retry_count += 1 + retry_token.retry_delay = delay + retry_token + end + + def record_success(retry_token) + @client_rate_limiter.update_sending_rate(false) + @retry_quota.release(@capacity_amount) + retry_token + end + end + end +end diff --git a/hearth/lib/hearth/retry/error_inspector.rb b/hearth/lib/hearth/retry/error_inspector.rb deleted file mode 100644 index af56da3bb..000000000 --- a/hearth/lib/hearth/retry/error_inspector.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module Retry - # @api private - class ErrorInspector - def initialize(error, http_status) - @error = error - @http_status = http_status - end - - def retryable? - modeled_retryable? || - throttling? || - networking? || - server? - end - - def error_type - if networking? - 'Transient' - elsif throttling? - 'Throttling' - elsif server? - 'ServerError' - elsif client? - 'ClientError' - else - 'Unknown' - end - end - - def throttling? - @http_status == 429 || modeled_throttling? - end - - def networking? - @error.is_a?(Hearth::HTTP::NetworkingError) - end - - def server? - (500..599).cover?(@http_status) - end - - def client? - (400..499).cover?(@http_status) - end - - def modeled_retryable? - @error.is_a?(Hearth::ApiError) && @error.retryable? - end - - def modeled_throttling? - @error.is_a?(Hearth::ApiError) && @error.throttling? - end - end - end -end diff --git a/hearth/lib/hearth/retry/exponential_backoff.rb b/hearth/lib/hearth/retry/exponential_backoff.rb new file mode 100644 index 000000000..22a331f9e --- /dev/null +++ b/hearth/lib/hearth/retry/exponential_backoff.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # @api private + # Computes an exponential backoff delay for a retry attempt. + class ExponentialBackoff + # Max backoff (in seconds) + MAX_BACKOFF = 20 + + def call(attempts) + [Kernel.rand * (2**attempts), MAX_BACKOFF].min + end + end + end +end diff --git a/hearth/lib/hearth/retry/retry_quota.rb b/hearth/lib/hearth/retry/retry_quota.rb index 631380213..3b0a5527f 100644 --- a/hearth/lib/hearth/retry/retry_quota.rb +++ b/hearth/lib/hearth/retry/retry_quota.rb @@ -19,13 +19,14 @@ def initialize # Check if there is sufficient capacity to retry and return it. # If there is insufficient capacity, return 0 # @return [Integer] The amount of capacity checked out - def checkout_capacity(error_inspector) + def checkout_capacity(error_info) @mutex.synchronize do - capacity_amount = if error_inspector.error_type == 'Transient' - TIMEOUT_RETRY_COST - else - RETRY_COST - end + capacity_amount = + if error_info.error_type == 'Transient' + TIMEOUT_RETRY_COST + else + RETRY_COST + end # unable to acquire capacity return 0 if capacity_amount > @available_capacity diff --git a/hearth/lib/hearth/retry/standard.rb b/hearth/lib/hearth/retry/standard.rb new file mode 100644 index 000000000..3903d196d --- /dev/null +++ b/hearth/lib/hearth/retry/standard.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # Standard retry strategy for retrying requests. + class Standard < Strategy + # @param [#call] backoff (ExponentialBackoff) A callable object that + # calculates a backoff delay for a retry attempt. + # @param [Integer] max_attempts (3) The maximum number of attempts that + # will be made for a single request, including the initial attempt. + def initialize(backoff: ExponentialBackoff.new, max_attempts: 3) + super() + @backoff = backoff + @max_attempts = max_attempts + + # instance state + @retry_quota = RetryQuota.new + @capacity_amount = nil + end + + def acquire_initial_retry_token(_token_scope = nil) + Token.new(retry_count: 0) + end + + def refresh_retry_token(retry_token, error_info) + return unless error_info.retryable? + + return if retry_token.retry_count >= @max_attempts - 1 + + @capacity_amount = @retry_quota.checkout_capacity(error_info) + return unless @capacity_amount.positive? + + delay = error_info.hints[:retry_after_hint] + delay ||= @backoff.call(retry_token.retry_count) + retry_token.retry_count += 1 + retry_token.retry_delay = delay + retry_token + end + + def record_success(retry_token) + @retry_quota.release(@capacity_amount) + retry_token + end + end + end +end diff --git a/hearth/lib/hearth/retry/strategy.rb b/hearth/lib/hearth/retry/strategy.rb new file mode 100644 index 000000000..7ab221f52 --- /dev/null +++ b/hearth/lib/hearth/retry/strategy.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # Interface for retry strategies. + class Strategy + def acquire_initial_retry_token(_token_scope = nil) + raise NotImplementedError + end + + def refresh_retry_token(_retry_token, _error_info) + raise NotImplementedError + end + + def record_success(_retry_token) + raise NotImplementedError + end + end + end +end diff --git a/hearth/spec/hearth/retry/error_inspector_spec.rb b/hearth/spec/hearth/http/error_inspector_spec.rb similarity index 68% rename from hearth/spec/hearth/retry/error_inspector_spec.rb rename to hearth/spec/hearth/http/error_inspector_spec.rb index 982213b58..8830555e3 100644 --- a/hearth/spec/hearth/retry/error_inspector_spec.rb +++ b/hearth/spec/hearth/http/error_inspector_spec.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true module Hearth - module Retry + module HTTP describe ErrorInspector do - subject { ErrorInspector.new(error, http_status) } + subject { ErrorInspector.new(error, http_resp) } let(:http_status) { 404 } - let(:http_fields) { Hearth::HTTP::Fields.new } let(:http_resp) do - Hearth::HTTP::Response.new( - status: http_status, - fields: http_fields - ) + Hearth::HTTP::Response.new(status: http_status) end let(:message) { 'message' } @@ -64,7 +60,7 @@ module Retry end end - context 'error is networking' do + context 'error is transient' do let(:error) { Hearth::HTTP::NetworkingError.new(StandardError.new) } it 'returns true' do @@ -74,7 +70,7 @@ module Retry end describe '#error_type' do - context 'networking error' do + context 'transient error' do let(:error) { Hearth::HTTP::NetworkingError.new(StandardError.new) } it 'returns Transient' do @@ -127,6 +123,42 @@ module Retry end end end + + describe '#hints' do + context 'retry_after_hint' do + context 'header is an integer' do + it 'hint returns an integer delay' do + http_resp.headers['retry-after'] = '123' + expect(subject.hints[:retry_after_hint]).to eq(123) + end + end + + context 'header is a date' do + let(:time) { Time.new(2023, 1, 24) } + let(:retry_after_time) { (time + 123).httpdate } + + it 'hint returns an integer delay' do + http_resp.headers['retry-after'] = retry_after_time + allow(Time).to receive(:now).and_return(time) + expect(subject.hints[:retry_after_hint]).to eq(123) + end + end + + context 'header is nil' do + it 'no hint' do + http_resp.headers['retry-after'] = nil + expect(subject.hints.key?(:retry_after_hint)).to eq(false) + end + end + + context 'header is an empty string' do + it 'no hint' do + http_resp.headers['retry-after'] = '' + expect(subject.hints.key?(:retry_after_hint)).to eq(false) + end + end + end + end end end end diff --git a/hearth/spec/hearth/middleware/retry_spec.rb b/hearth/spec/hearth/middleware/retry_spec.rb index 57cf734c4..b9e00b52c 100644 --- a/hearth/spec/hearth/middleware/retry_spec.rb +++ b/hearth/spec/hearth/middleware/retry_spec.rb @@ -31,12 +31,8 @@ def handle_with_retry(test_cases, middleware_args = {}) subject = Hearth::Middleware::Retry.new( app, - retry_mode: middleware_args[:retry_mode], - max_attempts: middleware_args[:max_attempts], - adaptive_retry_wait_to_fill: middleware_args[:adaptive_retry_wait_to_fill], - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: retry_quota, - client_rate_limiter: client_rate_limiter + error_inspector_class: Hearth::HTTP::ErrorInspector, + retry_strategy: middleware_args[:retry_strategy] ) subject.call(input, context) @@ -56,6 +52,11 @@ def apply_expectations(retry_class, test_case) # Don't actually sleep allow(Kernel).to receive(:sleep) + retry_strategy = retry_class.instance_variable_get(:@retry_strategy) + retry_quota = retry_strategy.instance_variable_get(:@retry_quota) + client_rate_limiter = + retry_strategy.instance_variable_get(:@client_rate_limiter) + if expected[:retries] expect(retry_class.instance_variable_get(:@retries)) .to eq(expected[:retries]) @@ -92,10 +93,8 @@ def setup_next_response(context, test_case) module Hearth module Middleware describe Retry do - let(:retry_quota) { Hearth::Retry::RetryQuota.new } - let(:client_rate_limiter) { Hearth::Retry::ClientRateLimiter.new } - let(:input) { double('Type::OperationInput') } + let(:error) do Hearth::ApiError.new( error_code: 'error_code', @@ -112,13 +111,16 @@ module Middleware ) end + before { allow(error).to receive(:retryable?).and_return(true) } + context 'standard mode' do + let(:retry_strategy) { Hearth::Retry::Standard.new } + let(:retry_quota) do + retry_strategy.instance_variable_get(:@retry_quota) + end + let(:middleware_args) do - { - retry_mode: 'standard', - max_attempts: 3, - adaptive_retry_wait_to_fill: true - } + { retry_strategy: retry_strategy } end before do @@ -217,11 +219,14 @@ module Middleware } ] - handle_with_retry(test_cases, middleware_args.merge(max_attempts: 5)) + args = middleware_args.merge( + retry_strategy: Hearth::Retry::Standard.new(max_attempts: 5) + ) + handle_with_retry(test_cases, args) end it 'does not exceed the max backoff time' do - stub_const('Hearth::Middleware::Retry::MAX_BACKOFF', 3) + stub_const('Hearth::Retry::ExponentialBackoff::MAX_BACKOFF', 3) test_cases = [ { @@ -246,17 +251,24 @@ module Middleware } ] - handle_with_retry(test_cases, middleware_args.merge(max_attempts: 5)) + args = middleware_args.merge( + retry_strategy: Hearth::Retry::Standard.new(max_attempts: 5) + ) + handle_with_retry(test_cases, args) end end context 'adaptive mode' do + let(:retry_strategy) { Hearth::Retry::Adaptive.new } + let(:retry_quota) do + retry_strategy.instance_variable_get(:@retry_quota) + end + let(:client_rate_limiter) do + retry_strategy.instance_variable_get(:@client_rate_limiter) + end + let(:middleware_args) do - { - retry_mode: 'adaptive', - max_attempts: 3, - adaptive_retry_wait_to_fill: true - } + { retry_strategy: retry_strategy } end it 'verifies cubic calculations for successes' do From 2743c9cce8a24b1480f0fbc6a7d68ea040095e0c Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:59:47 -0400 Subject: [PATCH 17/22] Fix NetworkingError to be retryable (#133) --- hearth/lib/hearth.rb | 1 + hearth/lib/hearth/http/client.rb | 3 +-- hearth/lib/hearth/http/networking_error.rb | 15 +------------- hearth/lib/hearth/middleware/send.rb | 12 ++++++----- hearth/lib/hearth/networking_error.rb | 18 +++++++++++++++++ hearth/spec/hearth/http/client_spec.rb | 7 +++---- .../spec/hearth/http/networking_error_spec.rb | 20 ------------------- hearth/spec/hearth/middleware/send_spec.rb | 19 ++++++++++++++---- hearth/spec/hearth/networking_error_spec.rb | 18 +++++++++++++++++ 9 files changed, 64 insertions(+), 49 deletions(-) create mode 100644 hearth/lib/hearth/networking_error.rb delete mode 100644 hearth/spec/hearth/http/networking_error_spec.rb create mode 100644 hearth/spec/hearth/networking_error_spec.rb diff --git a/hearth/lib/hearth.rb b/hearth/lib/hearth.rb index 6c78380ed..86c1a2138 100755 --- a/hearth/lib/hearth.rb +++ b/hearth/lib/hearth.rb @@ -10,6 +10,7 @@ require_relative 'hearth/dns' # must be required before http +require_relative 'hearth/networking_error' require_relative 'hearth/request' require_relative 'hearth/response' diff --git a/hearth/lib/hearth/http/client.rb b/hearth/lib/hearth/http/client.rb index 6ac4a0772..d080851a1 100644 --- a/hearth/lib/hearth/http/client.rb +++ b/hearth/lib/hearth/http/client.rb @@ -6,7 +6,6 @@ module Hearth module HTTP - # Transmits an HTTP {Request} object, returning an HTTP {Response}. # @api private class Client # Initialize an instance of this HTTP client. @@ -78,7 +77,7 @@ def transmit(request:, response:) # Invalid verb, ArgumentError is a StandardError raise e rescue StandardError => e - raise Hearth::HTTP::NetworkingError, e + Hearth::HTTP::NetworkingError.new(e) end private diff --git a/hearth/lib/hearth/http/networking_error.rb b/hearth/lib/hearth/http/networking_error.rb index d282e1807..7f1ab6866 100755 --- a/hearth/lib/hearth/http/networking_error.rb +++ b/hearth/lib/hearth/http/networking_error.rb @@ -2,19 +2,6 @@ module Hearth module HTTP - # Thrown by a Client when encountering a networking error while transmitting - # a request or receiving a response. You can access the original error - # by calling {#original_error}. - class NetworkingError < StandardError - MSG = 'Encountered an error while transmitting the request: %s' - - def initialize(original_error) - @original_error = original_error - super(format(MSG, message: original_error.message)) - end - - # @return [StandardError] - attr_reader :original_error - end + class NetworkingError < Hearth::NetworkingError; end end end diff --git a/hearth/lib/hearth/middleware/send.rb b/hearth/lib/hearth/middleware/send.rb index 4ff651173..81efb40a9 100755 --- a/hearth/lib/hearth/middleware/send.rb +++ b/hearth/lib/hearth/middleware/send.rb @@ -27,27 +27,29 @@ def initialize(_app, client:, stub_responses:, # @param input # @param context # @return [Output] + # rubocop:disable Metrics/MethodLength def call(input, context) + output = Output.new if @stub_responses stub = @stubs.next(context.operation_name) - output = Output.new apply_stub(stub, input, context, output) if context.response.body.respond_to?(:rewind) context.response.body.rewind end - output else - @client.transmit( + resp_or_error = @client.transmit( request: context.request, response: context.response ) - Output.new + if resp_or_error.is_a?(Hearth::NetworkingError) + output.error = resp_or_error + end end + output end private - # rubocop:disable Metrics/MethodLength def apply_stub(stub, input, context, output) case stub when Proc diff --git a/hearth/lib/hearth/networking_error.rb b/hearth/lib/hearth/networking_error.rb new file mode 100644 index 000000000..780a40d07 --- /dev/null +++ b/hearth/lib/hearth/networking_error.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + # Thrown by a Client when encountering a networking error while transmitting + # a request or receiving a response. You can access the original error + # by calling {#original_error}. + class NetworkingError < StandardError + MSG = 'Encountered an error while transmitting the request: %s' + + def initialize(original_error) + @original_error = original_error + super(format(MSG, message: original_error.message)) + end + + # @return [StandardError] + attr_reader :original_error + end +end diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index 7c952ffaf..a286e4404 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -163,11 +163,10 @@ module HTTP end.to raise_error(ArgumentError) end - it 'rescues StandardError and converts to a NetworkingError' do + it 'rescues StandardError and returns an HTTP::NetworkingError' do stub_request(:any, uri.to_s).to_raise(StandardError) - expect do - subject.transmit(request: request, response: response) - end.to raise_error(NetworkingError) + resp_or_error = subject.transmit(request: request, response: response) + expect(resp_or_error).to be_a(NetworkingError) end context 'https' do diff --git a/hearth/spec/hearth/http/networking_error_spec.rb b/hearth/spec/hearth/http/networking_error_spec.rb deleted file mode 100644 index 5b47efa68..000000000 --- a/hearth/spec/hearth/http/networking_error_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module HTTP - describe NetworkingError do - let(:original_message) { 'ORIG-MESSAGE' } - let(:original_error) { StandardError.new(original_message) } - - subject { NetworkingError.new(original_error) } - - it 'subclasses StandardError' do - expect(subject).to be_a StandardError - end - - it 'adds to the original errors message' do - expect(subject.message).to include(original_message) - end - end - end -end diff --git a/hearth/spec/hearth/middleware/send_spec.rb b/hearth/spec/hearth/middleware/send_spec.rb index 937215962..9e73b9ee4 100644 --- a/hearth/spec/hearth/middleware/send_spec.rb +++ b/hearth/spec/hearth/middleware/send_spec.rb @@ -44,11 +44,22 @@ module Middleware expect(client).to receive(:transmit).with( request: request, response: response - ) + ).and_return(response) - expect( - subject.call(input, context) - ).to be_a Hearth::Output + output = subject.call(input, context) + expect(output).to be_a(Hearth::Output) + end + + it 'sets output error to NetworkingError if the request fails' do + error = Hearth::HTTP::NetworkingError.new(StandardError.new) + expect(client).to receive(:transmit).with( + request: request, + response: response + ).and_return(error) + + output = subject.call(input, context) + expect(output).to be_a(Hearth::Output) + expect(output.error).to be_a(Hearth::NetworkingError) end context 'stub_responses is true' do diff --git a/hearth/spec/hearth/networking_error_spec.rb b/hearth/spec/hearth/networking_error_spec.rb new file mode 100644 index 000000000..bee211a60 --- /dev/null +++ b/hearth/spec/hearth/networking_error_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + describe NetworkingError do + let(:original_message) { 'ORIG-MESSAGE' } + let(:original_error) { StandardError.new(original_message) } + + subject { NetworkingError.new(original_error) } + + it 'subclasses StandardError' do + expect(subject).to be_a StandardError + end + + it 'adds to the original errors message' do + expect(subject.message).to include(original_message) + end + end +end From 58d3a42501135f4cba730ce9701e80e36becc5d7 Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:29:52 -0400 Subject: [PATCH 18/22] Refactor http client and allow it on config (#134) --- .../lib/high_score_service/client.rb | 10 +- .../lib/high_score_service/config.rb | 20 ++-- .../rails_json/lib/rails_json/client.rb | 72 ++++++------- .../rails_json/lib/rails_json/config.rb | 20 ++-- .../projections/weather/lib/weather/client.rb | 12 +-- .../projections/weather/lib/weather/config.rb | 20 ++-- .../white_label/lib/white_label/client.rb | 22 ++-- .../white_label/lib/white_label/config.rb | 20 ++-- .../white_label/spec/client_spec.rb | 23 ---- .../white_label/spec/config_spec.rb | 2 +- .../integration-specs/client_spec.rb | 23 ---- .../integration-specs/config_spec.rb | 2 +- .../ruby/codegen/ApplicationTransport.java | 42 ++------ .../codegen/middleware/MiddlewareBuilder.java | 26 +++++ hearth/.rubocop.yml | 12 ++- hearth/lib/hearth/dns/host_resolver.rb | 2 +- hearth/lib/hearth/http.rb | 6 +- hearth/lib/hearth/http/client.rb | 86 +++++++++------ hearth/lib/hearth/http/middleware.rb | 11 ++ hearth/lib/hearth/middleware/retry.rb | 2 - hearth/lib/hearth/middleware/send.rb | 5 +- hearth/lib/hearth/request.rb | 2 +- .../lib/hearth/retry/client_rate_limiter.rb | 2 - hearth/sig/lib/hearth/http/client.rbs | 100 ++++++++++++++++++ hearth/spec/hearth/http/client_spec.rb | 62 ++++++----- hearth/spec/hearth/middleware/send_spec.rb | 10 +- 26 files changed, 356 insertions(+), 258 deletions(-) create mode 100644 hearth/lib/hearth/http/middleware.rb create mode 100644 hearth/sig/lib/hearth/http/client.rbs diff --git a/codegen/projections/high_score_service/lib/high_score_service/client.rb b/codegen/projections/high_score_service/lib/high_score_service/client.rb index a01e9024b..c04569018 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/client.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/client.rb @@ -87,7 +87,7 @@ def create_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::CreateHighScore, stubs: @stubs, params_class: Params::CreateHighScoreOutput @@ -151,7 +151,7 @@ def delete_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DeleteHighScore, stubs: @stubs, params_class: Params::DeleteHighScoreOutput @@ -221,7 +221,7 @@ def get_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetHighScore, stubs: @stubs, params_class: Params::GetHighScoreOutput @@ -287,7 +287,7 @@ def list_high_scores(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ListHighScores, stubs: @stubs, params_class: Params::ListHighScoresOutput @@ -364,7 +364,7 @@ def update_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::UpdateHighScore, stubs: @stubs, params_class: Params::UpdateHighScoreOutput diff --git a/codegen/projections/high_score_service/lib/high_score_service/config.rb b/codegen/projections/high_score_service/lib/high_score_service/config.rb index 4f88d4b80..1a37189d4 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/config.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/config.rb @@ -15,14 +15,14 @@ module HighScoreService # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use + # The default log level to use with the Logger. # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) # Specifies which retry strategy class to use. Strategy classes @@ -43,8 +43,8 @@ module HighScoreService # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -64,7 +64,7 @@ module HighScoreService Config = ::Struct.new( :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, :retry_strategy, @@ -79,7 +79,7 @@ module HighScoreService def validate! Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [Hearth::HTTP::Client.new], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/projections/rails_json/lib/rails_json/client.rb b/codegen/projections/rails_json/lib/rails_json/client.rb index 3f3793ab6..31351e254 100644 --- a/codegen/projections/rails_json/lib/rails_json/client.rb +++ b/codegen/projections/rails_json/lib/rails_json/client.rb @@ -109,7 +109,7 @@ def all_query_string_types(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::AllQueryStringTypes, stubs: @stubs, params_class: Params::AllQueryStringTypesOutput @@ -173,7 +173,7 @@ def constant_and_variable_query_string(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ConstantAndVariableQueryString, stubs: @stubs, params_class: Params::ConstantAndVariableQueryStringOutput @@ -237,7 +237,7 @@ def constant_query_string(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ConstantQueryString, stubs: @stubs, params_class: Params::ConstantQueryStringOutput @@ -308,7 +308,7 @@ def document_type(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DocumentType, stubs: @stubs, params_class: Params::DocumentTypeOutput @@ -377,7 +377,7 @@ def document_type_as_payload(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DocumentTypeAsPayload, stubs: @stubs, params_class: Params::DocumentTypeAsPayloadOutput @@ -434,7 +434,7 @@ def empty_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EmptyOperation, stubs: @stubs, params_class: Params::EmptyOperationOutput @@ -495,7 +495,7 @@ def endpoint_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointOperation, stubs: @stubs, params_class: Params::EndpointOperationOutput @@ -558,7 +558,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointWithHostLabelOperation, stubs: @stubs, params_class: Params::EndpointWithHostLabelOperationOutput @@ -625,7 +625,7 @@ def greeting_with_errors(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GreetingWithErrors, stubs: @stubs, params_class: Params::GreetingWithErrorsOutput @@ -692,7 +692,7 @@ def http_payload_traits(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPayloadTraits, stubs: @stubs, params_class: Params::HttpPayloadTraitsOutput @@ -757,7 +757,7 @@ def http_payload_traits_with_media_type(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPayloadTraitsWithMediaType, stubs: @stubs, params_class: Params::HttpPayloadTraitsWithMediaTypeOutput @@ -827,7 +827,7 @@ def http_payload_with_structure(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPayloadWithStructure, stubs: @stubs, params_class: Params::HttpPayloadWithStructureOutput @@ -896,7 +896,7 @@ def http_prefix_headers(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPrefixHeaders, stubs: @stubs, params_class: Params::HttpPrefixHeadersOutput @@ -957,7 +957,7 @@ def http_prefix_headers_in_response(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPrefixHeadersInResponse, stubs: @stubs, params_class: Params::HttpPrefixHeadersInResponseOutput @@ -1017,7 +1017,7 @@ def http_request_with_float_labels(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithFloatLabels, stubs: @stubs, params_class: Params::HttpRequestWithFloatLabelsOutput @@ -1077,7 +1077,7 @@ def http_request_with_greedy_label_in_path(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithGreedyLabelInPath, stubs: @stubs, params_class: Params::HttpRequestWithGreedyLabelInPathOutput @@ -1152,7 +1152,7 @@ def http_request_with_labels(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithLabels, stubs: @stubs, params_class: Params::HttpRequestWithLabelsOutput @@ -1220,7 +1220,7 @@ def http_request_with_labels_and_timestamp_format(params = {}, options = {}, &bl stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithLabelsAndTimestampFormat, stubs: @stubs, params_class: Params::HttpRequestWithLabelsAndTimestampFormatOutput @@ -1278,7 +1278,7 @@ def http_response_code(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpResponseCode, stubs: @stubs, params_class: Params::HttpResponseCodeOutput @@ -1340,7 +1340,7 @@ def ignore_query_params_in_response(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::IgnoreQueryParamsInResponse, stubs: @stubs, params_class: Params::IgnoreQueryParamsInResponseOutput @@ -1451,7 +1451,7 @@ def input_and_output_with_headers(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::InputAndOutputWithHeaders, stubs: @stubs, params_class: Params::InputAndOutputWithHeadersOutput @@ -1532,7 +1532,7 @@ def json_enums(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::JsonEnums, stubs: @stubs, params_class: Params::JsonEnumsOutput @@ -1640,7 +1640,7 @@ def json_maps(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::JsonMaps, stubs: @stubs, params_class: Params::JsonMapsOutput @@ -1739,7 +1739,7 @@ def json_unions(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::JsonUnions, stubs: @stubs, params_class: Params::JsonUnionsOutput @@ -1904,7 +1904,7 @@ def kitchen_sink_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::KitchenSinkOperation, stubs: @stubs, params_class: Params::KitchenSinkOperationOutput @@ -1966,7 +1966,7 @@ def media_type_header(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::MediaTypeHeader, stubs: @stubs, params_class: Params::MediaTypeHeaderOutput @@ -2028,7 +2028,7 @@ def nested_attributes_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::NestedAttributesOperation, stubs: @stubs, params_class: Params::NestedAttributesOperationOutput @@ -2099,7 +2099,7 @@ def null_and_empty_headers_client(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::NullAndEmptyHeadersClient, stubs: @stubs, params_class: Params::NullAndEmptyHeadersClientOutput @@ -2169,7 +2169,7 @@ def null_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::NullOperation, stubs: @stubs, params_class: Params::NullOperationOutput @@ -2231,7 +2231,7 @@ def omits_null_serializes_empty_string(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::OmitsNullSerializesEmptyString, stubs: @stubs, params_class: Params::OmitsNullSerializesEmptyStringOutput @@ -2291,7 +2291,7 @@ def operation_with_optional_input_output(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::OperationWithOptionalInputOutput, stubs: @stubs, params_class: Params::OperationWithOptionalInputOutputOutput @@ -2354,7 +2354,7 @@ def query_idempotency_token_auto_fill(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::QueryIdempotencyTokenAutoFill, stubs: @stubs, params_class: Params::QueryIdempotencyTokenAutoFillOutput @@ -2418,7 +2418,7 @@ def query_params_as_string_list_map(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::QueryParamsAsStringListMap, stubs: @stubs, params_class: Params::QueryParamsAsStringListMapOutput @@ -2477,7 +2477,7 @@ def streaming_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::StreamingOperation, stubs: @stubs, params_class: Params::StreamingOperationOutput @@ -2551,7 +2551,7 @@ def timestamp_format_headers(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::TimestampFormatHeaders, stubs: @stubs, params_class: Params::TimestampFormatHeadersOutput @@ -2615,7 +2615,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::Operation____789BadName, stubs: @stubs, params_class: Params::Struct____789BadNameOutput diff --git a/codegen/projections/rails_json/lib/rails_json/config.rb b/codegen/projections/rails_json/lib/rails_json/config.rb index 979331707..9ab157c76 100644 --- a/codegen/projections/rails_json/lib/rails_json/config.rb +++ b/codegen/projections/rails_json/lib/rails_json/config.rb @@ -15,14 +15,14 @@ module RailsJson # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use + # The default log level to use with the Logger. # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) # Specifies which retry strategy class to use. Strategy classes @@ -43,8 +43,8 @@ module RailsJson # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -64,7 +64,7 @@ module RailsJson Config = ::Struct.new( :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, :retry_strategy, @@ -79,7 +79,7 @@ module RailsJson def validate! Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [Hearth::HTTP::Client.new], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/projections/weather/lib/weather/client.rb b/codegen/projections/weather/lib/weather/client.rb index a39fca11e..ecf81e8c4 100644 --- a/codegen/projections/weather/lib/weather/client.rb +++ b/codegen/projections/weather/lib/weather/client.rb @@ -78,7 +78,7 @@ def get_city(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetCity, stubs: @stubs, params_class: Params::GetCityOutput @@ -146,7 +146,7 @@ def get_city_image(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetCityImage, stubs: @stubs, params_class: Params::GetCityImageOutput @@ -203,7 +203,7 @@ def get_current_time(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetCurrentTime, stubs: @stubs, params_class: Params::GetCurrentTimeOutput @@ -277,7 +277,7 @@ def get_forecast(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetForecast, stubs: @stubs, params_class: Params::GetForecastOutput @@ -356,7 +356,7 @@ def list_cities(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ListCities, stubs: @stubs, params_class: Params::ListCitiesOutput @@ -420,7 +420,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::Operation____789BadName, stubs: @stubs, params_class: Params::Struct____789BadNameOutput diff --git a/codegen/projections/weather/lib/weather/config.rb b/codegen/projections/weather/lib/weather/config.rb index 36dc2d597..92ebf8b58 100644 --- a/codegen/projections/weather/lib/weather/config.rb +++ b/codegen/projections/weather/lib/weather/config.rb @@ -15,14 +15,14 @@ module Weather # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use + # The default log level to use with the Logger. # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) # Specifies which retry strategy class to use. Strategy classes @@ -43,8 +43,8 @@ module Weather # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -64,7 +64,7 @@ module Weather Config = ::Struct.new( :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, :retry_strategy, @@ -79,7 +79,7 @@ module Weather def validate! Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [Hearth::HTTP::Client.new], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/projections/white_label/lib/white_label/client.rb b/codegen/projections/white_label/lib/white_label/client.rb index 7e5894011..741c1dea4 100644 --- a/codegen/projections/white_label/lib/white_label/client.rb +++ b/codegen/projections/white_label/lib/white_label/client.rb @@ -140,7 +140,7 @@ def defaults_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DefaultsTest, stubs: @stubs, params_class: Params::DefaultsTestOutput @@ -200,7 +200,7 @@ def endpoint_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointOperation, stubs: @stubs, params_class: Params::EndpointOperationOutput @@ -262,7 +262,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointWithHostLabelOperation, stubs: @stubs, params_class: Params::EndpointWithHostLabelOperationOutput @@ -485,7 +485,7 @@ def kitchen_sink(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::KitchenSink, stubs: @stubs, params_class: Params::KitchenSinkOutput @@ -545,7 +545,7 @@ def mixin_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::MixinTest, stubs: @stubs, params_class: Params::MixinTestOutput @@ -606,7 +606,7 @@ def paginators_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::PaginatorsTest, stubs: @stubs, params_class: Params::PaginatorsTestOperationOutput @@ -667,7 +667,7 @@ def paginators_test_with_items(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::PaginatorsTestWithItems, stubs: @stubs, params_class: Params::PaginatorsTestWithItemsOutput @@ -725,7 +725,7 @@ def streaming_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::StreamingOperation, stubs: @stubs, params_class: Params::StreamingOperationOutput @@ -783,7 +783,7 @@ def streaming_with_length(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::StreamingWithLength, stubs: @stubs, params_class: Params::StreamingWithLengthOutput @@ -842,7 +842,7 @@ def waiters_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::WaitersTest, stubs: @stubs, params_class: Params::WaitersTestOutput @@ -904,7 +904,7 @@ def operation____paginators_test_with_bad_names(params = {}, options = {}, &bloc ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::Operation____PaginatorsTestWithBadNames, stubs: @stubs, params_class: Params::Struct____PaginatorsTestWithBadNamesOutput diff --git a/codegen/projections/white_label/lib/white_label/config.rb b/codegen/projections/white_label/lib/white_label/config.rb index 61f18e2e5..da43895b2 100644 --- a/codegen/projections/white_label/lib/white_label/config.rb +++ b/codegen/projections/white_label/lib/white_label/config.rb @@ -15,14 +15,14 @@ module WhiteLabel # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use + # The default log level to use with the Logger. # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) # Specifies which retry strategy class to use. Strategy classes @@ -43,8 +43,8 @@ module WhiteLabel # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -64,7 +64,7 @@ module WhiteLabel Config = ::Struct.new( :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, :retry_strategy, @@ -79,7 +79,7 @@ module WhiteLabel def validate! Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [Hearth::HTTP::Client.new], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/projections/white_label/spec/client_spec.rb b/codegen/projections/white_label/spec/client_spec.rb index ee2ceaef5..eac22740a 100644 --- a/codegen/projections/white_label/spec/client_spec.rb +++ b/codegen/projections/white_label/spec/client_spec.rb @@ -29,11 +29,6 @@ module WhiteLabel end it 'uses logger' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(logger: config.logger)) - .and_call_original - expect(Hearth::Context) .to receive(:new) .with(hash_including(logger: config.logger)) @@ -42,24 +37,6 @@ module WhiteLabel client.kitchen_sink end - it 'uses http_wire_trace from config' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: config.http_wire_trace)) - .and_call_original - - client.kitchen_sink - end - - it 'uses http_wire_trace from options' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: true)) - .and_call_original - - client.kitchen_sink({}, http_wire_trace: true) - end - it 'uses endpoint from config' do expect(Hearth::HTTP::Request) .to receive(:new) diff --git a/codegen/projections/white_label/spec/config_spec.rb b/codegen/projections/white_label/spec/config_spec.rb index a3da78ac9..9bfe7302a 100644 --- a/codegen/projections/white_label/spec/config_spec.rb +++ b/codegen/projections/white_label/spec/config_spec.rb @@ -9,7 +9,7 @@ module WhiteLabel config_keys = { disable_host_prefix: true, endpoint: 'test', - http_wire_trace: true, + http_client: Hearth::HTTP::Client.new, log_level: :debug, logger: Logger.new($stdout, level: :debug), retry_strategy: Hearth::Retry::Adaptive.new, diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb index ee2ceaef5..eac22740a 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb @@ -29,11 +29,6 @@ module WhiteLabel end it 'uses logger' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(logger: config.logger)) - .and_call_original - expect(Hearth::Context) .to receive(:new) .with(hash_including(logger: config.logger)) @@ -42,24 +37,6 @@ module WhiteLabel client.kitchen_sink end - it 'uses http_wire_trace from config' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: config.http_wire_trace)) - .and_call_original - - client.kitchen_sink - end - - it 'uses http_wire_trace from options' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: true)) - .and_call_original - - client.kitchen_sink({}, http_wire_trace: true) - end - it 'uses endpoint from config' do expect(Hearth::HTTP::Request) .to receive(:new) diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb index a3da78ac9..9bfe7302a 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb @@ -9,7 +9,7 @@ module WhiteLabel config_keys = { disable_host_prefix: true, endpoint: 'test', - http_wire_trace: true, + http_client: Hearth::HTTP::Client.new, log_level: :debug, logger: Logger.new($stdout, level: :debug), retry_strategy: Hearth::Retry::Adaptive.new, diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java index ecd83bfae..bb5a7d48b 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java @@ -101,39 +101,21 @@ public static ApplicationTransport createDefaultHttpApplicationTransport() { .render((self, ctx) -> "Hearth::HTTP::Response.new(body: response_body)") .build(); - ClientConfig wireTrace = (new ClientConfig.Builder()) - .name("http_wire_trace") - .type("Boolean") - .defaultValue("false") - .documentation("Enable debug wire trace on http requests.") + ClientConfig httpClient = (new ClientConfig.Builder()) + .name("http_client") + .type("Hearth::HTTP::Client") + .documentation("The HTTP Client to use for request transport.") + .documentationDefaultValue("Hearth::HTTP::Client.new") .allowOperationOverride() - .build(); - - ClientConfig logger = (new ClientConfig.Builder()) - .name("logger") - .type("Logger") - .documentationDefaultValue("$stdout") .defaults(new ConfigProviderChain.Builder() - .dynamicProvider("proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ") + .staticProvider("Hearth::HTTP::Client.new") .build() ) - .documentation("Logger to use for output") - .build(); - - ClientConfig logLevel = (new ClientConfig.Builder()) - .name("log_level") - .type("Symbol") - .defaultValue(":info") - .documentation("Default log level to use") .build(); ClientFragment client = (new ClientFragment.Builder()) - .addConfig(wireTrace) - .addConfig(logger) - .addConfig(logLevel) - .render((self, ctx) -> "Hearth::HTTP::Client.new(logger: " + logger.renderGetConfigValue() - + ", http_wire_trace: " - + wireTrace.renderGetConfigValue() + ")") + .addConfig(httpClient) + .render((self, ctx) -> httpClient.renderGetConfigValue()) .build(); MiddlewareList defaultMiddleware = (transport, context) -> { @@ -155,12 +137,8 @@ public static ApplicationTransport createDefaultHttpApplicationTransport() { .klass("Hearth::HTTP::Middleware::ContentLength") .operationPredicate( (model, service, operation) -> - !Streaming.isNonFiniteStreaming(model, - model.expectShape( - operation.getInputShape(), - StructureShape.class - ) - ) + !Streaming.isNonFiniteStreaming( + model, model.expectShape(operation.getInputShape(), StructureShape.class)) ) .step(MiddlewareStackStep.BUILD) .build() diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java index 0ad2741ac..caf721934 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java @@ -39,6 +39,7 @@ import software.amazon.smithy.ruby.codegen.RubyCodeWriter; import software.amazon.smithy.ruby.codegen.RubySymbolProvider; import software.amazon.smithy.ruby.codegen.config.ClientConfig; +import software.amazon.smithy.ruby.codegen.config.ConfigProviderChain; import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi @@ -82,6 +83,9 @@ public Set getClientConfig(GenerationContext context) { .collect(Collectors.toSet()); config.addAll(stepConfig); } + + config.addAll(getDefaultClientConfig()); + return config; } @@ -279,4 +283,26 @@ public void addDefaultMiddleware(GenerationContext context) { register(transport.defaultMiddleware(context)); } + + private Collection getDefaultClientConfig() { + ClientConfig logger = (new ClientConfig.Builder()) + .name("logger") + .type("Logger") + .documentationDefaultValue("Logger.new($stdout, level: cfg.log_level)") + .defaults(new ConfigProviderChain.Builder() + .dynamicProvider("proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ") + .build() + ) + .documentation("The Logger instance to use for logging.") + .build(); + + ClientConfig logLevel = (new ClientConfig.Builder()) + .name("log_level") + .type("Symbol") + .defaultValue(":info") + .documentation("The default log level to use with the Logger.") + .build(); + + return Arrays.asList(logger, logLevel); + } } diff --git a/hearth/.rubocop.yml b/hearth/.rubocop.yml index 455add302..dc804101e 100644 --- a/hearth/.rubocop.yml +++ b/hearth/.rubocop.yml @@ -6,7 +6,11 @@ Metrics: Exclude: - 'spec/**/*.rb' -# For some reason, Metrics disable doesn't cover this +Metrics/AbcSize: + Exclude: + - 'spec/**/*.rb' + - 'lib/hearth/http/client.rb' + Metrics/BlockLength: Exclude: - 'spec/**/*.rb' @@ -19,7 +23,10 @@ Layout/LineLength: Max: 80 Metrics/MethodLength: - Max: 15 + Max: 20 + Exclude: + - 'spec/**/*.rb' + - 'lib/hearth/middleware/send.rb' Metrics/ClassLength: Exclude: @@ -27,7 +34,6 @@ Metrics/ClassLength: Metrics/ParameterLists: Exclude: - - 'lib/hearth/middleware/retry.rb' - 'lib/hearth/middleware/send.rb' Style/Documentation: diff --git a/hearth/lib/hearth/dns/host_resolver.rb b/hearth/lib/hearth/dns/host_resolver.rb index ab709a82c..7429fe2fa 100644 --- a/hearth/lib/hearth/dns/host_resolver.rb +++ b/hearth/lib/hearth/dns/host_resolver.rb @@ -4,7 +4,7 @@ module Hearth module DNS # Resolves a host name and service to an IP address. Can be used with # {Hearth::HTTP::Client} host_resolver option. This implementation uses - # {Addrinfo#getaddrinfo} to resolve the host name. + # Addrinfo.getaddrinfo to resolve the host name. # @see https://ruby-doc.org/stdlib-3.0.2/libdoc/socket/rdoc/Addrinfo.html class HostResolver # @param [Integer] service (443) diff --git a/hearth/lib/hearth/http.rb b/hearth/lib/hearth/http.rb index aa572a0f8..2fa1965a4 100755 --- a/hearth/lib/hearth/http.rb +++ b/hearth/lib/hearth/http.rb @@ -7,8 +7,7 @@ require_relative 'http/error_parser' require_relative 'http/field' require_relative 'http/fields' -require_relative 'http/middleware/content_length' -require_relative 'http/middleware/content_md5' +require_relative 'http/middleware' require_relative 'http/networking_error' require_relative 'http/request' require_relative 'http/response' @@ -16,7 +15,6 @@ module Hearth # HTTP namespace for HTTP specific functionality. Also includes utility # methods for URI escaping. - # @api private module HTTP class << self # URI escapes the given value. @@ -26,6 +24,7 @@ class << self # # @param [String] value # @return [String] URI encoded value except for '+' and '~'. + # @api private def uri_escape(value) CGI.escape(value.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~') end @@ -37,6 +36,7 @@ def uri_escape(value) # # @param [String] path # @return [String] URI encoded path except for '+' and '~'. + # @api private def uri_escape_path(path) path.gsub(%r{[^/]+}) { |part| uri_escape(part) } end diff --git a/hearth/lib/hearth/http/client.rb b/hearth/lib/hearth/http/client.rb index d080851a1..4017957c5 100644 --- a/hearth/lib/hearth/http/client.rb +++ b/hearth/lib/hearth/http/client.rb @@ -6,37 +6,52 @@ module Hearth module HTTP - # @api private + # An HTTP client that uses Net::HTTP to send requests. class Client # Initialize an instance of this HTTP client. # # @param [Hash] options The options for this HTTP Client # - # @option options [Boolean] :http_wire_trace (false) When `true`, - # HTTP debug output will be sent to the `:logger`. + # @option options [Boolean] :debug_output (false) When `true`, + # sets an output stream to the configured Logger for debugging. # - # @option options [Logger] :logger A logger where debug output is sent. - # - # @option options [String] :http_proxy A proxy to send + # @option options [String] :proxy A proxy to send # requests through. Formatted like 'http://proxy.com:123'. # - # @option options [Boolean] :ssl_verify_peer (true) When `true`, + # @option options [Float] :open_timeout Number of seconds to + # wait for the connection to open. + # + # @option options [Float] :read_timeout Number of seconds to wait + # for one block to be read (via one read(2) call). + # + # @option options [Float] :keep_alive_timeout Seconds to reuse the + # connection of the previous request. + # + # @option options [Float] :continue_timeout Seconds to wait for + # 100 Continue response. + # + # @option options [Float] :write_timeout Number of seconds to wait + # for one block to be written (via one write(2) call). + # + # @option options [Float] :ssl_timeout Sets the SSL timeout seconds. + # + # @option options [Boolean] :verify_peer (true) When `true`, # SSL peer certificates are verified when establishing a # connection. # - # @option options [String] :ssl_ca_bundle Full path to the SSL + # @option options [String] :ca_file Full path to the SSL # certificate authority bundle file that should be used when # verifying peer certificates. If you do not pass - # `:ssl_ca_bundle` or `:ssl_ca_directory` the system default + # `:ca_file` or `:ca_path` the system default # will be used if available. # - # @option options [String] :ssl_ca_directory Full path of the + # @option options [String] :ca_path Full path of the # directory that contains the unbundled SSL certificate # authority files for verifying peer certificates. If you do - # not pass `:ssl_ca_bundle` or `:ssl_ca_directory` the + # not pass `:ca_file` or `:ca_path` the # system default will be used if available. # - # @option options [OpenSSL::X509::Store] :ssl_ca_store An OpenSSL X509 + # @option options [OpenSSL::X509::Store] :cert_store An OpenSSL X509 # certificate store that contains the SSL certificate authority. # # @option options [#resolve_address] (nil) :host_resolver @@ -44,25 +59,30 @@ class Client # `#resolve_address`, returning an array of up to two IP addresses for # the given hostname, one IPv6 and one IPv4, in that order. # `#resolve_address` should take a nodename keyword argument and - # optionally other keyword args similar to {Addrinfo#getaddrinfo}'s + # optionally other keyword args similar to Addrinfo.getaddrinfo's # positional parameters. def initialize(options = {}) - @http_wire_trace = options[:http_wire_trace] - @logger = options[:logger] - @http_proxy = URI(options[:http_proxy]) if options[:http_proxy] - @ssl_verify_peer = options[:ssl_verify_peer] - @ssl_ca_bundle = options[:ssl_ca_bundle] - @ssl_ca_directory = options[:ssl_ca_directory] - @ssl_ca_store = options[:ssl_ca_store] + @debug_output = options[:debug_output] + @proxy = URI(options[:proxy]) if options[:proxy] + @open_timeout = options[:open_timeout] + @read_timeout = options[:read_timeout] + @keep_alive_timeout = options[:keep_alive_timeout] + @continue_timeout = options[:continue_timeout] + @write_timeout = options[:write_timeout] + @ssl_timeout = options[:ssl_timeout] + @verify_peer = options[:verify_peer] + @ca_file = options[:ca_file] + @ca_path = options[:ca_path] + @cert_store = options[:cert_store] @host_resolver = options[:host_resolver] end # @param [Request] request # @param [Response] response # @return [Response] - def transmit(request:, response:) + def transmit(request:, response:, **options) http = create_http(request.uri) - http.set_debug_output(@logger) if @http_wire_trace + http.set_debug_output(options[:logger]) if @debug_output if request.uri.scheme == 'https' configure_ssl(http) @@ -109,7 +129,7 @@ def create_http(endpoint) args = [] args << endpoint.host args << endpoint.port - args += http_proxy_parts if @http_proxy + args += proxy_parts if @proxy # Net::HTTP.new uses positional arguments: host, port, proxy_args.... Net::HTTP.new(*args.compact) end @@ -117,11 +137,11 @@ def create_http(endpoint) # applies ssl settings to the HTTP object def configure_ssl(http) http.use_ssl = true - if @ssl_verify_peer + if @verify_peer http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.ca_file = @ssl_ca_bundle if @ssl_ca_bundle - http.ca_path = @ssl_ca_directory if @ssl_ca_directory - http.cert_store = @ssl_ca_store if @ssl_ca_store + http.ca_file = @ca_file if @ca_file + http.ca_path = @ca_path if @ca_path + http.cert_store = @cert_store if @cert_store else http.verify_mode = OpenSSL::SSL::VERIFY_NONE end @@ -168,14 +188,14 @@ def net_http_request_class(request) raise ArgumentError, msg end - # Extract the parts of the http_proxy URI + # Extract the parts of the proxy URI # @return [Array] - def http_proxy_parts + def proxy_parts [ - @http_proxy.host, - @http_proxy.port, - (@http_proxy.user && CGI.unescape(@http_proxy.user)), - (@http_proxy.password && CGI.unescape(@http_proxy.password)) + @proxy.host, + @proxy.port, + (@proxy.user && CGI.unescape(@proxy.user)), + (@proxy.password && CGI.unescape(@proxy.password)) ] end end diff --git a/hearth/lib/hearth/http/middleware.rb b/hearth/lib/hearth/http/middleware.rb new file mode 100644 index 000000000..1b4596d1d --- /dev/null +++ b/hearth/lib/hearth/http/middleware.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative 'middleware/content_length' +require_relative 'middleware/content_md5' + +module Hearth + module HTTP + # @api private + module Middleware; end + end +end diff --git a/hearth/lib/hearth/middleware/retry.rb b/hearth/lib/hearth/middleware/retry.rb index 9d3f772ce..fbd997c2c 100755 --- a/hearth/lib/hearth/middleware/retry.rb +++ b/hearth/lib/hearth/middleware/retry.rb @@ -25,7 +25,6 @@ def initialize(app, retry_strategy:, error_inspector_class:) @retries = 0 end - # rubocop:disable Metrics def call(input, context) token = @retry_strategy.acquire_initial_retry_token( context.metadata[:retry_token_scope] @@ -50,7 +49,6 @@ def call(input, context) end output end - # rubocop:enable Metrics private diff --git a/hearth/lib/hearth/middleware/send.rb b/hearth/lib/hearth/middleware/send.rb index 81efb40a9..95f18fd35 100755 --- a/hearth/lib/hearth/middleware/send.rb +++ b/hearth/lib/hearth/middleware/send.rb @@ -27,7 +27,6 @@ def initialize(_app, client:, stub_responses:, # @param input # @param context # @return [Output] - # rubocop:disable Metrics/MethodLength def call(input, context) output = Output.new if @stub_responses @@ -39,7 +38,8 @@ def call(input, context) else resp_or_error = @client.transmit( request: context.request, - response: context.response + response: context.response, + logger: context.logger ) if resp_or_error.is_a?(Hearth::NetworkingError) output.error = resp_or_error @@ -79,7 +79,6 @@ def apply_stub(stub, input, context, output) raise ArgumentError, 'Unsupported stub type' end end - # rubocop:enable Metrics/MethodLength end end end diff --git a/hearth/lib/hearth/request.rb b/hearth/lib/hearth/request.rb index f5111d729..9b87cc029 100644 --- a/hearth/lib/hearth/request.rb +++ b/hearth/lib/hearth/request.rb @@ -8,7 +8,7 @@ module Hearth # @api private class Request # @param [URI] uri (URI('')) - # @param [IO] (StringIO.new) body + # @param [IO] body (StringIO.new) def initialize(uri: URI(''), body: StringIO.new) @uri = uri @body = body diff --git a/hearth/lib/hearth/retry/client_rate_limiter.rb b/hearth/lib/hearth/retry/client_rate_limiter.rb index 938b4ea5c..4fbf74d8e 100644 --- a/hearth/lib/hearth/retry/client_rate_limiter.rb +++ b/hearth/lib/hearth/retry/client_rate_limiter.rb @@ -50,7 +50,6 @@ def token_bucket_acquire(amount, wait_to_fill: true) end end - # rubocop:disable Metrics/MethodLength def update_sending_rate(is_throttling_error) @mutex.synchronize do update_measured_rate @@ -77,7 +76,6 @@ def update_sending_rate(is_throttling_error) token_bucket_update_rate(new_rate) end end - # rubocop:enable Metrics/MethodLength private diff --git a/hearth/sig/lib/hearth/http/client.rbs b/hearth/sig/lib/hearth/http/client.rbs new file mode 100644 index 000000000..33392d642 --- /dev/null +++ b/hearth/sig/lib/hearth/http/client.rbs @@ -0,0 +1,100 @@ +module Hearth + module HTTP + # An HTTP client that uses Net::HTTP to send requests. + class Client + # Initialize an instance of this HTTP client. + # + # @param [Hash] options The options for this HTTP Client + # + # @option options [Boolean] :debug_output (false) When `true`, + # sets an output stream to the configured Logger for debugging. + # + # @option options [String] :proxy A proxy to send + # requests through. Formatted like 'http://proxy.com:123'. + # + # @option options [Float] :open_timeout Number of seconds to + # wait for the connection to open. + # + # @option options [Float] :read_timeout Number of seconds to wait + # for one block to be read (via one read(2) call). + # + # @option options [Float] :keep_alive_timeout Seconds to reuse the + # connection of the previous request. + # + # @option options [Float] :continue_timeout Seconds to wait for + # 100 Continue response. + # + # @option options [Float] :write_timeout Number of seconds to wait + # for one block to be written (via one write(2) call). + # + # @option options [Float] :ssl_timeout Sets the SSL timeout seconds. + # + # @option options [Boolean] :verify_peer (true) When `true`, + # SSL peer certificates are verified when establishing a + # connection. + # + # @option options [String] :ca_file Full path to the SSL + # certificate authority bundle file that should be used when + # verifying peer certificates. If you do not pass + # `:ca_file` or `:ca_path` the system default + # will be used if available. + # + # @option options [String] :ca_path Full path of the + # directory that contains the unbundled SSL certificate + # authority files for verifying peer certificates. If you do + # not pass `:ca_file` or `:ca_path` the + # system default will be used if available. + # + # @option options [OpenSSL::X509::Store] :cert_store An OpenSSL X509 + # certificate store that contains the SSL certificate authority. + # + # @option options [#resolve_address] (nil) :host_resolver + # An object, such as {Hearth::DNS::HostResolver} that responds to + # `#resolve_address`, returning an array of up to two IP addresses for + # the given hostname, one IPv6 and one IPv4, in that order. + # `#resolve_address` should take a nodename keyword argument and + # optionally other keyword args similar to {Addrinfo#getaddrinfo}'s + # positional parameters. + def initialize: (?::Hash[untyped, untyped] options) -> void + + # @param [Request] request + # @param [Response] response + # @return [Response] + def transmit: (request: untyped, response: untyped, **untyped options) -> untyped + + private + + def _transmit: (untyped http, untyped request, untyped response) -> untyped + + def unpack_response: (untyped net_resp, untyped response) -> untyped + + # Creates an HTTP connection to the endpoint + # Applies proxy if set + def create_http: (untyped endpoint) -> untyped + + # applies ssl settings to the HTTP object + def configure_ssl: (untyped http) -> untyped + + # Constructs and returns a Net::HTTP::Request object from + # a {Http::Request}. + # @param [Http::Request] request + # @return [Net::HTTP::Request] + def build_net_request: (untyped request) -> untyped + + # Validate that fields are not trailers and return a hash of headers. + # @param [HTTP::Request] request + # @return [Hash] + def net_headers_for: (untyped request) -> untyped + + # @param [Http::Request] request + # @raise [InvalidHttpVerbError] + # @return Returns a base `Net::HTTP::Request` class, e.g., + # `Net::HTTP::Get`, `Net::HTTP::Post`, etc. + def net_http_request_class: (untyped request) -> untyped + + # Extract the parts of the proxy URI + # @return [Array] + def proxy_parts: () -> ::Array[untyped] + end + end +end diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index a286e4404..ce4bd1911 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -7,23 +7,23 @@ module HTTP describe Client do before { WebMock.disable_net_connect! } - let(:wire_trace) { false } + let(:debug_output) { false } let(:logger) { double('logger') } - let(:http_proxy) { nil } - let(:ssl_verify_peer) { true } - let(:ssl_ca_bundle) { nil } - let(:ssl_ca_directory) { nil } - let(:ssl_ca_store) { nil } + let(:proxy) { nil } + let(:verify_peer) { true } + let(:ca_file) { nil } + let(:ca_path) { nil } + let(:cert_store) { nil } let(:host_resolver) { nil } subject do Client.new( - http_wire_trace: wire_trace, logger: logger, - http_proxy: http_proxy, - ssl_verify_peer: ssl_verify_peer, - ssl_ca_bundle: ssl_ca_bundle, - ssl_ca_directory: ssl_ca_directory, - ssl_ca_store: ssl_ca_store, + debug_output: debug_output, + proxy: proxy, + verify_peer: verify_peer, + ca_file: ca_file, + ca_path: ca_path, + cert_store: cert_store, host_resolver: host_resolver ) end @@ -182,8 +182,8 @@ module HTTP subject.transmit(request: request, response: response) end - context 'ssl_verify_peer: false' do - let(:ssl_verify_peer) { false } + context 'verify_peer: false' do + let(:verify_peer) { false } it 'sets verify_peer to NONE' do stub_request(:any, uri.to_s) @@ -196,8 +196,8 @@ module HTTP end end - context 'ssl_verify_peer: true' do - let(:ssl_verify_peer) { true } + context 'verify_peer: true' do + let(:verify_peer) { true } it 'sets verify_peer to VERIFY_PEER' do stub_request(:any, uri.to_s) @@ -209,8 +209,8 @@ module HTTP subject.transmit(request: request, response: response) end - context 'ssl_ca_bundle' do - let(:ssl_ca_bundle) { 'ca_bundle' } + context 'ca_file' do + let(:ca_file) { 'ca_bundle' } it 'sets ca_file' do stub_request(:any, uri.to_s) @@ -223,8 +223,8 @@ module HTTP end end - context 'ssl_ca_directory' do - let(:ssl_ca_directory) { 'ca_directory' } + context 'ca_path' do + let(:ca_path) { 'ca_directory' } it 'sets ca_path' do stub_request(:any, uri.to_s) @@ -237,13 +237,13 @@ module HTTP end end - context 'ssl_ca_store' do - let(:ssl_ca_store) { 'ca_store' } + context 'cert_store' do + let(:cert_store) { 'cert_store' } it 'sets cert_store' do stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| - expect(http.cert_store).to eq 'ca_store' + expect(http.cert_store).to eq 'cert_store' http end @@ -253,8 +253,8 @@ module HTTP end end - context 'http_proxy set' do - let(:http_proxy) { 'http://my-proxy-host.com:88' } + context 'proxy set' do + let(:proxy) { 'http://my-proxy-host.com:88' } it 'sets the http proxy' do stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| @@ -268,7 +268,7 @@ module HTTP context 'user and password set on proxy' do let(:password) { 'pass/word' } let(:user) { 'my user' } - let(:http_proxy) do + let(:proxy) do "http://#{CGI.escape(user)}:#{CGI.escape(password)}@my-proxy-host.com:88" end @@ -285,14 +285,18 @@ module HTTP end end - context 'http_wire_trace: true' do - let(:wire_trace) { true } + context 'debug_output: true' do + let(:debug_output) { true } it 'sets the logger on debug_output' do stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP) .to receive(:set_debug_output).with(logger) - subject.transmit(request: request, response: response) + subject.transmit( + request: request, + response: response, + logger: logger + ) end end diff --git a/hearth/spec/hearth/middleware/send_spec.rb b/hearth/spec/hearth/middleware/send_spec.rb index 9e73b9ee4..d2b5aaebf 100644 --- a/hearth/spec/hearth/middleware/send_spec.rb +++ b/hearth/spec/hearth/middleware/send_spec.rb @@ -13,6 +13,7 @@ module Middleware let(:stub_class) { double('stub_class') } let(:params_class) { double('params_class') } let(:stubs) { Hearth::Stubbing::Stubs.new } + let(:logger) { double('Logger') } subject do Send.new( @@ -36,14 +37,16 @@ module Middleware Hearth::Context.new( request: request, response: response, - operation_name: operation + operation_name: operation, + logger: logger ) end it 'sends the request and returns an output object' do expect(client).to receive(:transmit).with( request: request, - response: response + response: response, + logger: logger ).and_return(response) output = subject.call(input, context) @@ -54,7 +57,8 @@ module Middleware error = Hearth::HTTP::NetworkingError.new(StandardError.new) expect(client).to receive(:transmit).with( request: request, - response: response + response: response, + logger: logger ).and_return(error) output = subject.call(input, context) From 83dce07887f71765d75d318d3126d674f02ce76e Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Thu, 8 Jun 2023 16:06:31 -0400 Subject: [PATCH 19/22] Use timeouts in http client --- hearth/lib/hearth/http/client.rb | 10 +++++++++ hearth/spec/hearth/http/client_spec.rb | 29 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/hearth/lib/hearth/http/client.rb b/hearth/lib/hearth/http/client.rb index 4017957c5..17e7cd596 100644 --- a/hearth/lib/hearth/http/client.rb +++ b/hearth/lib/hearth/http/client.rb @@ -83,6 +83,7 @@ def initialize(options = {}) def transmit(request:, response:, **options) http = create_http(request.uri) http.set_debug_output(options[:logger]) if @debug_output + configure_timeouts(http) if request.uri.scheme == 'https' configure_ssl(http) @@ -115,6 +116,14 @@ def _transmit(http, request, response) Thread.current[:net_http_hearth_dns_resolver] = nil end + def configure_timeouts(http) + http.open_timeout = @open_timeout if @open_timeout + http.keep_alive_timeout = @keep_alive_timeout if @keep_alive_timeout + http.read_timeout = @read_timeout if @read_timeout + http.continue_timeout = @continue_timeout if @continue_timeout + http.write_timeout = @write_timeout if @write_timeout + end + def unpack_response(net_resp, response) response.status = net_resp.code.to_i net_resp.each_header { |k, v| response.headers[k] = v } @@ -137,6 +146,7 @@ def create_http(endpoint) # applies ssl settings to the HTTP object def configure_ssl(http) http.use_ssl = true + http.ssl_timeout = @ssl_timeout if @ssl_timeout if @verify_peer http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.ca_file = @ca_file if @ca_file diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index ce4bd1911..cf4f26f08 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -10,6 +10,7 @@ module HTTP let(:debug_output) { false } let(:logger) { double('logger') } let(:proxy) { nil } + let(:ssl_timeout) { nil } let(:verify_peer) { true } let(:ca_file) { nil } let(:ca_path) { nil } @@ -20,6 +21,12 @@ module HTTP Client.new( debug_output: debug_output, proxy: proxy, + read_timeout: 1, + open_timeout: 1, + write_timeout: 1, + keep_alive_timeout: 1, + continue_timeout: 1, + ssl_timeout: ssl_timeout, verify_peer: verify_peer, ca_file: ca_file, ca_path: ca_path, @@ -169,6 +176,17 @@ module HTTP expect(resp_or_error).to be_a(NetworkingError) end + it 'configures timeouts' do + stub_request(:any, uri.to_s) + expect_any_instance_of(Net::HTTP).to receive(:open_timeout=).with(1) + expect_any_instance_of(Net::HTTP).to receive(:read_timeout=).with(1) + expect_any_instance_of(Net::HTTP).to receive(:write_timeout=).with(1) + expect_any_instance_of(Net::HTTP).to receive(:continue_timeout=).with(1) + expect_any_instance_of(Net::HTTP).to receive(:keep_alive_timeout=).with(1) + + subject.transmit(request: request, response: response) + end + context 'https' do let(:uri) { URI('https://example.com') } @@ -209,6 +227,17 @@ module HTTP subject.transmit(request: request, response: response) end + context 'ssl_timeout' do + let(:ssl_timeout) { 1 } + + it 'sets ssl_timeout' do + stub_request(:any, uri.to_s) + expect_any_instance_of(Net::HTTP).to receive(:ssl_timeout=).with(1) + + subject.transmit(request: request, response: response) + end + end + context 'ca_file' do let(:ca_file) { 'ca_bundle' } From 0ccc0829561da3a660f12996134b1be3532ee622 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Thu, 8 Jun 2023 16:08:42 -0400 Subject: [PATCH 20/22] Satisfy rubocop --- hearth/spec/hearth/http/client_spec.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index cf4f26f08..2e202f95f 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -181,8 +181,10 @@ module HTTP expect_any_instance_of(Net::HTTP).to receive(:open_timeout=).with(1) expect_any_instance_of(Net::HTTP).to receive(:read_timeout=).with(1) expect_any_instance_of(Net::HTTP).to receive(:write_timeout=).with(1) - expect_any_instance_of(Net::HTTP).to receive(:continue_timeout=).with(1) - expect_any_instance_of(Net::HTTP).to receive(:keep_alive_timeout=).with(1) + expect_any_instance_of(Net::HTTP) + .to receive(:continue_timeout=).with(1) + expect_any_instance_of(Net::HTTP) + .to receive(:keep_alive_timeout=).with(1) subject.transmit(request: request, response: response) end @@ -232,7 +234,8 @@ module HTTP it 'sets ssl_timeout' do stub_request(:any, uri.to_s) - expect_any_instance_of(Net::HTTP).to receive(:ssl_timeout=).with(1) + expect_any_instance_of(Net::HTTP) + .to receive(:ssl_timeout=).with(1) subject.transmit(request: request, response: response) end From c5a765680b9f5b23a691b01948837af3ccf1a374 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Thu, 8 Jun 2023 16:11:01 -0400 Subject: [PATCH 21/22] Satisfy rubocop (again) --- hearth/.rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/hearth/.rubocop.yml b/hearth/.rubocop.yml index dc804101e..d7819a24a 100644 --- a/hearth/.rubocop.yml +++ b/hearth/.rubocop.yml @@ -30,6 +30,7 @@ Metrics/MethodLength: Metrics/ClassLength: Exclude: + - 'lib/hearth/http/client.rb' - 'lib/hearth/middleware_builder.rb' Metrics/ParameterLists: From 1c7c684cce809f0c06b2088cbd4a15632345c600 Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Thu, 15 Jun 2023 14:10:33 -0400 Subject: [PATCH 22/22] Connection Pooling (#135) --- .../lib/high_score_service/config.rb | 2 +- .../rails_json/lib/rails_json/config.rb | 2 +- .../projections/weather/lib/weather/config.rb | 2 +- .../white_label/lib/white_label/config.rb | 2 +- .../ruby/codegen/ApplicationTransport.java | 2 +- hearth/lib/hearth.rb | 1 + hearth/lib/hearth/connection_pool.rb | 92 ++++++++ hearth/lib/hearth/http/client.rb | 199 +++++++++++++----- hearth/lib/hearth/middleware/send.rb | 1 + hearth/spec/hearth/connection_pool_spec.rb | 102 +++++++++ hearth/spec/hearth/http/client_spec.rb | 107 +++++++++- hearth/spec/hearth/retry/strategy_spec.rb | 18 ++ sample-service/.ruby-version | 1 + sample-service/Gemfile | 2 +- 14 files changed, 463 insertions(+), 70 deletions(-) create mode 100644 hearth/lib/hearth/connection_pool.rb create mode 100644 hearth/spec/hearth/connection_pool_spec.rb create mode 100644 hearth/spec/hearth/retry/strategy_spec.rb create mode 100644 sample-service/.ruby-version diff --git a/codegen/projections/high_score_service/lib/high_score_service/config.rb b/codegen/projections/high_score_service/lib/high_score_service/config.rb index 1a37189d4..380f87c94 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/config.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/config.rb @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_client: [Hearth::HTTP::Client.new], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/projections/rails_json/lib/rails_json/config.rb b/codegen/projections/rails_json/lib/rails_json/config.rb index 9ab157c76..bd0b1e4ac 100644 --- a/codegen/projections/rails_json/lib/rails_json/config.rb +++ b/codegen/projections/rails_json/lib/rails_json/config.rb @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_client: [Hearth::HTTP::Client.new], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/projections/weather/lib/weather/config.rb b/codegen/projections/weather/lib/weather/config.rb index 92ebf8b58..48663c02c 100644 --- a/codegen/projections/weather/lib/weather/config.rb +++ b/codegen/projections/weather/lib/weather/config.rb @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_client: [Hearth::HTTP::Client.new], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/projections/white_label/lib/white_label/config.rb b/codegen/projections/white_label/lib/white_label/config.rb index da43895b2..90abffd38 100644 --- a/codegen/projections/white_label/lib/white_label/config.rb +++ b/codegen/projections/white_label/lib/white_label/config.rb @@ -91,7 +91,7 @@ def self.defaults @defaults ||= { disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_client: [Hearth::HTTP::Client.new], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], retry_strategy: [Hearth::Retry::Standard.new], diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java index bb5a7d48b..7a4898409 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java @@ -108,7 +108,7 @@ public static ApplicationTransport createDefaultHttpApplicationTransport() { .documentationDefaultValue("Hearth::HTTP::Client.new") .allowOperationOverride() .defaults(new ConfigProviderChain.Builder() - .staticProvider("Hearth::HTTP::Client.new") + .dynamicProvider("proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }") .build() ) .build(); diff --git a/hearth/lib/hearth.rb b/hearth/lib/hearth.rb index 86c1a2138..28e2f0b18 100755 --- a/hearth/lib/hearth.rb +++ b/hearth/lib/hearth.rb @@ -6,6 +6,7 @@ require_relative 'hearth/configuration' require_relative 'hearth/config/env_provider' require_relative 'hearth/config/resolver' +require_relative 'hearth/connection_pool' require_relative 'hearth/context' require_relative 'hearth/dns' diff --git a/hearth/lib/hearth/connection_pool.rb b/hearth/lib/hearth/connection_pool.rb new file mode 100644 index 000000000..9dc9a0d37 --- /dev/null +++ b/hearth/lib/hearth/connection_pool.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Hearth + # @api private + class ConnectionPool + @pools_mutex = Mutex.new + @pools = {} + + class << self + # @return [ConnectionPool] + def for(config = {}) + @pools_mutex.synchronize do + @pools[config] ||= new + end + end + + # @return [Array] Returns a list of the + # constructed connection pools. + def pools + @pools_mutex.synchronize do + @pools.values + end + end + end + + # @api private + def initialize + @pool_mutex = Mutex.new + @pool = {} + end + + # @param [URI::HTTP, URI::HTTPS] endpoint The HTTP(S) endpoint + # to connect to (e.g. 'https://domain.com'). + # @param [Proc] block A block that returns a new connection if + # there are no connections present. + # @return [Connection, nil] + def connection_for(endpoint, &block) + connection = nil + endpoint = remove_path_and_query(endpoint) + # attempt to recycle an already open connection + @pool_mutex.synchronize do + clean + connection = @pool[endpoint].shift if @pool.key?(endpoint) + end + connection || (block.call if block_given?) + end + + # @param [URI::HTTP, URI::HTTPS] endpoint The HTTP(S) endpoint + # @param [Object] connection The connection to check back into the pool. + # @return [nil] + def offer(endpoint, connection) + endpoint = remove_path_and_query(endpoint) + @pool_mutex.synchronize do + @pool[endpoint] = [] unless @pool.key?(endpoint) + @pool[endpoint] << connection + end + end + + # Closes and removes all connections from the pool. + # If empty! is called while there are outstanding requests they may + # get checked back into the pool, leaving the pool in a non-empty + # state. + # @return [nil] + def empty! + @pool_mutex.synchronize do + @pool.each_pair do |_endpoint, connections| + connections.each(&:finish) + end + @pool.clear + end + nil + end + + private + + # Removes stale connections from the pool. This method *must* be called + # @note **Must** be called behind a `@pool_mutex` synchronize block. + def clean + @pool.each_pair do |_endpoint, connections| + connections.delete_if(&:stale?) + end + end + + # Connection pools should be keyed by endpoint and port. + def remove_path_and_query(endpoint) + endpoint.dup.tap do |e| + e.path = '' + e.query = nil + end.to_s + end + end +end diff --git a/hearth/lib/hearth/http/client.rb b/hearth/lib/hearth/http/client.rb index 17e7cd596..b69df27ba 100644 --- a/hearth/lib/hearth/http/client.rb +++ b/hearth/lib/hearth/http/client.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'delegate' require 'net/http' require 'logger' require 'openssl' @@ -8,26 +9,48 @@ module Hearth module HTTP # An HTTP client that uses Net::HTTP to send requests. class Client + # @api private + OPTIONS = { + logger: Logger.new($stdout), + debug_output: nil, + proxy: nil, + open_timeout: 15, + read_timeout: nil, + keep_alive_timeout: 5, + continue_timeout: 1, + write_timeout: nil, + ssl_timeout: nil, + verify_peer: true, + ca_file: nil, + ca_path: nil, + cert_store: nil, + host_resolver: nil + }.freeze + # Initialize an instance of this HTTP client. # # @param [Hash] options The options for this HTTP Client # + # @param options [Logger] (Logger.new($stdout)) :logger A logger + # used to log Net::HTTP requests and responses when `:debug_output` + # is enabled. + # # @option options [Boolean] :debug_output (false) When `true`, # sets an output stream to the configured Logger for debugging. # - # @option options [String] :proxy A proxy to send + # @option options [String, URI] :proxy A proxy to send # requests through. Formatted like 'http://proxy.com:123'. # - # @option options [Float] :open_timeout Number of seconds to + # @option options [Float] :open_timeout (15) Number of seconds to # wait for the connection to open. # # @option options [Float] :read_timeout Number of seconds to wait # for one block to be read (via one read(2) call). # - # @option options [Float] :keep_alive_timeout Seconds to reuse the + # @option options [Float] :keep_alive_timeout (5) Seconds to reuse the # connection of the previous request. # - # @option options [Float] :continue_timeout Seconds to wait for + # @option options [Float] :continue_timeout (1) Seconds to wait for # 100 Continue response. # # @option options [Float] :write_timeout Number of seconds to wait @@ -54,7 +77,7 @@ class Client # @option options [OpenSSL::X509::Store] :cert_store An OpenSSL X509 # certificate store that contains the SSL certificate authority. # - # @option options [#resolve_address] (nil) :host_resolver + # @option options [#resolve_address] :host_resolver # An object, such as {Hearth::DNS::HostResolver} that responds to # `#resolve_address`, returning an array of up to two IP addresses for # the given hostname, one IPv6 and one IPv4, in that order. @@ -62,36 +85,25 @@ class Client # optionally other keyword args similar to Addrinfo.getaddrinfo's # positional parameters. def initialize(options = {}) - @debug_output = options[:debug_output] - @proxy = URI(options[:proxy]) if options[:proxy] - @open_timeout = options[:open_timeout] - @read_timeout = options[:read_timeout] - @keep_alive_timeout = options[:keep_alive_timeout] - @continue_timeout = options[:continue_timeout] - @write_timeout = options[:write_timeout] - @ssl_timeout = options[:ssl_timeout] - @verify_peer = options[:verify_peer] - @ca_file = options[:ca_file] - @ca_path = options[:ca_path] - @cert_store = options[:cert_store] - @host_resolver = options[:host_resolver] + OPTIONS.each_pair do |opt_name, default_value| + value = options.key?(opt_name) ? options[opt_name] : default_value + instance_variable_set("@#{opt_name}", value) + end + end + + OPTIONS.each_key do |attr_name| + attr_reader(attr_name) end # @param [Request] request # @param [Response] response + # @param [Logger] (nil) logger # @return [Response] - def transmit(request:, response:, **options) - http = create_http(request.uri) - http.set_debug_output(options[:logger]) if @debug_output - configure_timeouts(http) - - if request.uri.scheme == 'https' - configure_ssl(http) - else - http.use_ssl = false + def transmit(request:, response:, logger: nil) + net_request = build_net_request(request) + with_connection_pool(request.uri, logger) do |connection| + _transmit(connection, net_request, response) end - - _transmit(http, request, response) response.body.rewind if response.body.respond_to?(:rewind) response rescue ArgumentError => e @@ -103,27 +115,47 @@ def transmit(request:, response:, **options) private - def _transmit(http, request, response) + def with_connection_pool(endpoint, logger) + pool = ConnectionPool.for(pool_config) + connection = pool.connection_for(endpoint) do + new_connection(endpoint, logger) + end + yield connection + pool.offer(endpoint, connection) + rescue StandardError => e + connection&.finish + raise e + end + + # Starts and returns a new HTTP connection. + # @param [URI] endpoint + # @return [Net::HTTP] + def new_connection(endpoint, logger) + http = create_http(endpoint) + http.set_debug_output(logger || @logger) if @debug_output + configure_timeouts(http) + + if endpoint.scheme == 'https' + configure_ssl(http) + else + http.use_ssl = false + end + + http.start + http + end + + def _transmit(http, net_request, response) # Inform monkey patch to use our DNS resolver Thread.current[:net_http_hearth_dns_resolver] = @host_resolver - http.start do |conn| - conn.request(build_net_request(request)) do |net_resp| - unpack_response(net_resp, response) - end + http.request(net_request) do |net_resp| + unpack_response(net_resp, response) end ensure # Restore the default DNS resolver Thread.current[:net_http_hearth_dns_resolver] = nil end - def configure_timeouts(http) - http.open_timeout = @open_timeout if @open_timeout - http.keep_alive_timeout = @keep_alive_timeout if @keep_alive_timeout - http.read_timeout = @read_timeout if @read_timeout - http.continue_timeout = @continue_timeout if @continue_timeout - http.write_timeout = @write_timeout if @write_timeout - end - def unpack_response(net_resp, response) response.status = net_resp.code.to_i net_resp.each_header { |k, v| response.headers[k] = v } @@ -132,26 +164,34 @@ def unpack_response(net_resp, response) end end - # Creates an HTTP connection to the endpoint - # Applies proxy if set + # Creates an HTTP connection to the endpoint. + # Applies proxy if set. def create_http(endpoint) args = [] args << endpoint.host args << endpoint.port args += proxy_parts if @proxy # Net::HTTP.new uses positional arguments: host, port, proxy_args.... - Net::HTTP.new(*args.compact) + HTTP.new(Net::HTTP.new(*args.compact)) + end + + def configure_timeouts(http) + http.open_timeout = @open_timeout + http.keep_alive_timeout = @keep_alive_timeout + http.read_timeout = @read_timeout + http.continue_timeout = @continue_timeout + http.write_timeout = @write_timeout end # applies ssl settings to the HTTP object def configure_ssl(http) http.use_ssl = true - http.ssl_timeout = @ssl_timeout if @ssl_timeout + http.ssl_timeout = @ssl_timeout if @verify_peer http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.ca_file = @ca_file if @ca_file - http.ca_path = @ca_path if @ca_path - http.cert_store = @cert_store if @cert_store + http.ca_file = @ca_file + http.ca_path = @ca_path + http.cert_store = @cert_store else http.verify_mode = OpenSSL::SSL::VERIFY_NONE end @@ -201,13 +241,66 @@ def net_http_request_class(request) # Extract the parts of the proxy URI # @return [Array] def proxy_parts + proxy = URI(@proxy) [ - @proxy.host, - @proxy.port, - (@proxy.user && CGI.unescape(@proxy.user)), - (@proxy.password && CGI.unescape(@proxy.password)) + proxy.host, + proxy.port, + (proxy.user && CGI.unescape(proxy.user)), + (proxy.password && CGI.unescape(proxy.password)) ] end + + # Config options for the HTTP client used for connection pooling + # @return [Hash] + def pool_config + OPTIONS.each_key.with_object({}) do |option_name, hash| + hash[option_name] = instance_variable_get("@#{option_name}") + end + end + + # Helper methods extended onto Net::HTTP objects. + # @api private + class HTTP < Delegator + def initialize(http) + super(http) + @http = http + end + + # @return [Integer, nil] + attr_reader :last_used + + def __getobj__ + @http + end + + def __setobj__(obj) + @http = obj + end + + # Sends the request and tracks that this connection has been used. + def request(...) + @http.request(...) + @last_used = monotonic_milliseconds + end + + def stale? + @last_used.nil? || + (monotonic_milliseconds - @last_used) > keep_alive_timeout * 1000 + end + + # Attempts to close/finish the connection without raising an error. + def finish + @http.finish + rescue IOError + nil + end + + private + + def monotonic_milliseconds + Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + end + end end end end diff --git a/hearth/lib/hearth/middleware/send.rb b/hearth/lib/hearth/middleware/send.rb index 95f18fd35..35c56d7a8 100755 --- a/hearth/lib/hearth/middleware/send.rb +++ b/hearth/lib/hearth/middleware/send.rb @@ -36,6 +36,7 @@ def call(input, context) context.response.body.rewind end else + # TODO: should this instead raise NetworkingError? resp_or_error = @client.transmit( request: context.request, response: context.response, diff --git a/hearth/spec/hearth/connection_pool_spec.rb b/hearth/spec/hearth/connection_pool_spec.rb new file mode 100644 index 000000000..f49b4abcc --- /dev/null +++ b/hearth/spec/hearth/connection_pool_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Hearth + describe ConnectionPool do + # Set instance variable instead of calling empty! + # to avoid re-use of double rspec error. + before do + ConnectionPool.instance_variable_set(:@pools, {}) + end + + let(:config) { { timeout: 1 } } + let(:pool) { ConnectionPool.for(config) } + + let(:endpoint) { URI('https://example.com') } + let(:endpoint2) { URI('https://example.org') } + let(:endpoint_path_query) { URI('https://example.com/path?query') } + let(:connection) { double('connection', stale?: false, finish: nil) } + let(:connection2) { double('connection2', stale?: false, finish: nil) } + let(:stale_connection) { double('stale_connection', stale?: true) } + + describe '.for' do + it 'returns a connection pool' do + expect(ConnectionPool.for(config)).to be_a(ConnectionPool) + end + + it 'returns the same connection pool for the same config' do + expect(ConnectionPool.for(config)).to eq(ConnectionPool.for(config)) + end + + it 'returns a different connection pool for a different config' do + expect(ConnectionPool.for(config)).not_to eq(ConnectionPool.for({})) + end + end + + describe '.pools' do + it 'returns a list of the constructed connection pools' do + expect(ConnectionPool.pools).to be_a(Array) + end + + it 'returns the same list of constructed connection pools' do + ConnectionPool.for(config) + expect(ConnectionPool.pools).to eq(ConnectionPool.pools) + end + end + + describe '#offer / #connection_for' do + it 'returns a new default connection using a block' do + actual = pool.connection_for(endpoint) { connection } + expect(actual).to eq(connection) + end + + it 'ignores the block if there is a connection' do + pool.offer(endpoint, connection) + actual = pool.connection_for(endpoint) { connection2 } + expect(actual).to eq(connection) + end + + it 'is keyed by endpoint' do + pool.offer(endpoint, connection) + pool.offer(endpoint2, connection2) + actual = pool.connection_for(endpoint) + actual2 = pool.connection_for(endpoint2) + expect(actual).to eq(connection) + expect(actual2).to eq(connection2) + end + + it 'uses FIFO order' do + pool.offer(endpoint, connection) + pool.offer(endpoint, connection2) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection2) + end + + it 'removes stale connections' do + pool.offer(endpoint, stale_connection) + pool.offer(endpoint, connection) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection) + end + + it 'uses the same endpoint without path and query' do + pool.offer(endpoint_path_query, connection) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection) + end + end + + describe '#empty!' do + it 'closes and removes all sessions from the pool' do + pool.offer(endpoint, connection) + pool.offer(endpoint2, connection2) + expect(connection).to receive(:finish) + expect(connection2).to receive(:finish) + pool.empty! + expect(pool.connection_for(endpoint)).to be_nil + expect(pool.connection_for(endpoint2)).to be_nil + end + end + end +end diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index 2e202f95f..af31667dd 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -6,12 +6,15 @@ module Hearth module HTTP describe Client do before { WebMock.disable_net_connect! } + before do + ConnectionPool.pools.each(&:empty!) + end let(:debug_output) { false } let(:logger) { double('logger') } let(:proxy) { nil } let(:ssl_timeout) { nil } - let(:verify_peer) { true } + let(:verify_peer) { false } let(:ca_file) { nil } let(:ca_path) { nil } let(:cert_store) { nil } @@ -19,6 +22,7 @@ module HTTP subject do Client.new( + logger: logger, debug_output: debug_output, proxy: proxy, read_timeout: 1, @@ -178,13 +182,13 @@ module HTTP it 'configures timeouts' do stub_request(:any, uri.to_s) - expect_any_instance_of(Net::HTTP).to receive(:open_timeout=).with(1) - expect_any_instance_of(Net::HTTP).to receive(:read_timeout=).with(1) - expect_any_instance_of(Net::HTTP).to receive(:write_timeout=).with(1) - expect_any_instance_of(Net::HTTP) - .to receive(:continue_timeout=).with(1) - expect_any_instance_of(Net::HTTP) - .to receive(:keep_alive_timeout=).with(1) + expect_any_instance_of(Net::HTTP).to receive(:start) do |http| + expect(http.open_timeout).to eq(1) + expect(http.read_timeout).to eq(1) + expect(http.write_timeout).to eq(1) + expect(http.continue_timeout).to eq(1) + expect(http.keep_alive_timeout).to eq(1) + end subject.transmit(request: request, response: response) end @@ -234,8 +238,9 @@ module HTTP it 'sets ssl_timeout' do stub_request(:any, uri.to_s) - expect_any_instance_of(Net::HTTP) - .to receive(:ssl_timeout=).with(1) + expect_any_instance_of(Net::HTTP).to receive(:start) do |http| + expect(http.ssl_timeout).to eq 1 + end subject.transmit(request: request, response: response) end @@ -319,15 +324,26 @@ module HTTP context 'debug_output: true' do let(:debug_output) { true } + let(:request_logger) { double('request_logger') } it 'sets the logger on debug_output' do stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP) .to receive(:set_debug_output).with(logger) + subject.transmit( + request: request, + response: response + ) + end + + it 'allows logger per request' do + stub_request(:any, uri.to_s) + expect_any_instance_of(Net::HTTP) + .to receive(:set_debug_output).with(request_logger) subject.transmit( request: request, response: response, - logger: logger + logger: request_logger ) end end @@ -344,6 +360,75 @@ module HTTP subject.transmit(request: request, response: response) end end + + context 'connection pooling' do + it 'gets a connection from the pool' do + stub_request(http_method, uri.to_s) + expect(ConnectionPool).to receive(:for).and_call_original + expect_any_instance_of(ConnectionPool).to receive(:connection_for) + .and_call_original + subject.transmit(request: request, response: response) + end + + it 'offers the connection back to the pool' do + stub_request(http_method, uri.to_s) + expect_any_instance_of(ConnectionPool).to receive(:offer) + .with(uri, an_instance_of(Hearth::HTTP::Client::HTTP)) + .and_call_original + subject.transmit(request: request, response: response) + end + + it 'finishes the connection if there is a networking error' do + stub_request(http_method, uri.to_s) + original_error = StandardError.new('failed') + error = Hearth::HTTP::NetworkingError.new(original_error) + expect_any_instance_of(Net::HTTP) + .to receive(:start).and_raise(original_error) + resp = subject.transmit(request: request, response: response) + expect(resp).to eq(error) + end + end + end + + describe HTTP do + let(:http) { Hearth::HTTP::Client::HTTP.new(net_http) } + let(:net_http) { Net::HTTP.new(request.uri.host, request.uri.port) } + + it 'delegates to Net::HTTP' do + expect(http).to be_a(Delegator) + expect(http.__getobj__).to be(net_http) + end + + describe '#stale?' do + let(:base_time_ms) { 0 } + let(:fresh_time_ms) { 1000 } + let(:stale_time_ms) { 3000 } + + before do + net_http.keep_alive_timeout = 2 + allow(net_http).to receive(:request) + end + + it 'uses last used time to determine staleness' do + expect(Process).to receive(:clock_gettime).and_return(base_time_ms) + http.request(request) + expect(Process).to receive(:clock_gettime).and_return(fresh_time_ms) + expect(http.stale?).to be(false) + expect(Process).to receive(:clock_gettime).and_return(stale_time_ms) + expect(http.stale?).to be(true) + end + + it 'is stale if not used' do + expect(http.stale?).to be(true) + end + end + + describe '#finish' do + it 'closes the connection without errors' do + expect(net_http).to receive(:finish).and_raise(IOError) + expect { http.finish }.not_to raise_error + end + end end end end diff --git a/hearth/spec/hearth/retry/strategy_spec.rb b/hearth/spec/hearth/retry/strategy_spec.rb new file mode 100644 index 000000000..887e9c24c --- /dev/null +++ b/hearth/spec/hearth/retry/strategy_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + module Retry + describe Strategy do + subject { Strategy.new } + + it 'defines the interface' do + expect { subject.acquire_initial_retry_token } + .to raise_error(NotImplementedError) + expect { subject.refresh_retry_token(nil, nil) } + .to raise_error(NotImplementedError) + expect { subject.record_success(nil) } + .to raise_error(NotImplementedError) + end + end + end +end diff --git a/sample-service/.ruby-version b/sample-service/.ruby-version new file mode 100644 index 000000000..818bd47ab --- /dev/null +++ b/sample-service/.ruby-version @@ -0,0 +1 @@ +3.0.6 diff --git a/sample-service/Gemfile b/sample-service/Gemfile index eebaa7247..c561435ed 100644 --- a/sample-service/Gemfile +++ b/sample-service/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.0.2' +ruby '3.0.6' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' gem 'rails', '~> 6.1.4', '>= 6.1.4.1'