Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement protocol plugin and rpcv2 protocol #264

Merged
merged 75 commits into from
Feb 22, 2025
Merged
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
a57acd6
Add protocol base class and draft rpcv2 protocol implementation
jterapin Feb 3, 2025
4d3a7f4
Add example implementation of codegenerated plugin
jterapin Feb 3, 2025
036d2d8
Update smithy model to use rpcv2 trait for testing
jterapin Feb 3, 2025
fb17908
Add projections
jterapin Feb 3, 2025
c7c672c
Merge decaf into branch
jterapin Feb 4, 2025
787f763
Update RPCV2 protocol
jterapin Feb 5, 2025
37c2508
Add protocols weld
jterapin Feb 6, 2025
578478e
Update protocol weld
jterapin Feb 6, 2025
c168be0
Update protocol weld to include a TODO
jterapin Feb 6, 2025
2d098e4
Add more TODO guidance
jterapin Feb 6, 2025
885ae8b
Update protocol plugin
jterapin Feb 6, 2025
20abd06
Update protocol plugin and other components
jterapin Feb 6, 2025
aa367c8
Update handler syntax
jterapin Feb 6, 2025
589feaa
Appease specs
jterapin Feb 6, 2025
8e4c0f3
Update projections
jterapin Feb 6, 2025
03e7699
Remove base class
jterapin Feb 6, 2025
318b0cd
Remove unnecessary method
jterapin Feb 6, 2025
c29eb20
Merge branch 'decaf' into add_cbor_protocol
jterapin Feb 7, 2025
d5413e6
Update protocol modules
jterapin Feb 7, 2025
68713d1
Update documentation
jterapin Feb 7, 2025
5418ba0
Update endpoint specs to handle protocol plugin
jterapin Feb 7, 2025
1f475c2
Move handler classes to smithy-client
jterapin Feb 10, 2025
42114fa
Updates based on feedbacks
jterapin Feb 10, 2025
2656ba7
Sync fixtures
jterapin Feb 10, 2025
86c777d
Update handlers
jterapin Feb 10, 2025
2014c1c
Add rbs file
jterapin Feb 10, 2025
1a26319
Revert "Sync fixtures"
jterapin Feb 10, 2025
1ed8b04
Add protocol plugin specs
jterapin Feb 10, 2025
f7922a9
Update fixture model to recongize config file
jterapin Feb 10, 2025
a4ef87c
update projections
jterapin Feb 10, 2025
f4cb90d
Revert "Update fixture model to recongize config file"
jterapin Feb 10, 2025
fd361e9
Fix config flag for fixtures
jterapin Feb 10, 2025
659a1ea
Fix documentation
jterapin Feb 11, 2025
f3360c3
Merge decaf into branch
jterapin Feb 11, 2025
7952f0e
Merge branch 'decaf' into add_cbor_protocol
jterapin Feb 11, 2025
b4354a9
Update plugin docs
jterapin Feb 11, 2025
44f1898
Fix config documentation
jterapin Feb 11, 2025
4b1ed95
Fix specs based on merged changes
jterapin Feb 11, 2025
15cf0aa
Handle endpoints
jterapin Feb 11, 2025
9a9afb6
Resolve failures relating to content-type and accept headers
jterapin Feb 11, 2025
3f35c47
Add shapes change coverage
jterapin Feb 11, 2025
69208d9
Merge branch 'decaf' into add_cbor_protocol
jterapin Feb 11, 2025
0edce81
Fix codec deserialization
jterapin Feb 11, 2025
046227d
Add possible rbs fix
jterapin Feb 11, 2025
0687fd4
Add header fixes
jterapin Feb 11, 2025
7a9a8f3
Update rbs
jterapin Feb 11, 2025
e05f941
Merge branch 'decaf' into add_cbor_protocol
jterapin Feb 14, 2025
3bc1683
Merge branch 'decaf' into add_cbor_protocol
jterapin Feb 17, 2025
8e201a6
Merge decaf into branch
jterapin Feb 17, 2025
0ae5b87
Adding member names to shape
jterapin Feb 17, 2025
36fe4a5
Update based on refactor
jterapin Feb 17, 2025
c175478
Merge decaf into branch
jterapin Feb 17, 2025
936d4ec
Temp fix
jterapin Feb 17, 2025
d3875d1
Add to reference service shape name
jterapin Feb 18, 2025
eb3255a
Fix failures related to protocol tests
jterapin Feb 18, 2025
c5adb00
Fix top model changes
jterapin Feb 18, 2025
a65325d
Fix rbs
jterapin Feb 18, 2025
cebe5c4
Appease rubocop
jterapin Feb 18, 2025
70dd666
Update syntax
jterapin Feb 20, 2025
52a8d15
Update helpful commands
jterapin Feb 20, 2025
fcb34b1
Update schema and its shapes
jterapin Feb 20, 2025
bd0d912
Update cbor
jterapin Feb 20, 2025
f7358e5
Merge branch 'decaf' into add_cbor_protocol
jterapin Feb 21, 2025
402f07a
Clean up README
jterapin Feb 21, 2025
86995e0
Update Protocol Plugin to hold generic handlers
jterapin Feb 21, 2025
8a29c33
Update based on feedbacks
jterapin Feb 21, 2025
668d9b0
Add DummyWeldProtocol
jterapin Feb 21, 2025
cafe0a9
Fix failure
jterapin Feb 21, 2025
37ca72d
Update existing spec const
jterapin Feb 21, 2025
7d590d8
Fix protocol test failures
jterapin Feb 21, 2025
3ec2dd0
Add members by name to shapes with members
jterapin Feb 22, 2025
75837df
Remove dup method
jterapin Feb 22, 2025
7ba096f
Refactor protocol weld tests
mullermp Feb 22, 2025
8ea5ea0
Refactor weld testing with protocol welds
mullermp Feb 22, 2025
0ab8632
Fix build
mullermp Feb 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,9 @@ For previous pre-release, Java based Smithy-Ruby, see: [smithy-ruby/main](https:

[apache-badge]: https://img.shields.io/badge/License-Apache%202.0-blue.svg


## Helpful Commands

Run `smithy` gem tests:
```
bundle exec rake smithy:spec
```

Run `smithy-client` gem tests:
```
bundle exec rake smithy-client:spec
```

### Smithy Build
local build using smithy cli
```
bundle exec smithy build --debug model/weather.smithy
Expand All @@ -34,25 +24,62 @@ export SMITHY_PLUGIN_DIR=build/smithy/source/smithy-ruby
bundle exec smithy-ruby smith client --gem-name weather --gem-version 1.0.0 --destination-root projections/weather <<< $(smithy ast model/weather.smithy)
```

### IRB
IRB on `weather` gem:
```
irb -I projections/weather/lib -I gems/smithy-client/lib -r weather
irb -I projections/weather/lib -I gems/smithy-client/lib -I gems/smithy-model/lib -r weather
```

Create a Weather client:
```
client = Weather::Client.new(endpoint: 'https://example.com')
protocol = Smithy::Client::Protocols::RPCv2.new
client = Weather::Client.new(endpoint: 'https://example.com', protocol: protocol)
client.get_city(city_id: '1')
client.get_current_time
```

### Fixtures

Build a fixture
```
export SMITHY_PLUGIN_DIR=build/smithy/source/smithy-ruby
bundle exec smithy-ruby smith client --gem-name fixture --gem-version 1.0.0 <<< $(cat gems/smithy/spec/fixtures/endpoints/default-values/model.json)
```

Running RBS validations and tests:
Sync and validate fixtures on smithy:
```
bundle exec rake smithy:sync-fixtures
bundle exec rake smithy:validate-fixtures
```

### Running tests on gems

To run tests on smithy gem:
```
bundle exec rake smithy:spec
```

To run tests on smithy-model gem:
```
bundle exec rake smithy-model:spec
```

To run tests on smithy-client gem:
```
bundle exec rake smithy-client:spec
```

To run RBS validation/tests on smithy gem:
```
bundle exec rake smithy-client:rbs
bundle exec rake smithy:rbs
```

To run RBS validation/tests on smithy-model gem:
```
bundle exec rake smithy-model:rbs
```

To run RBS validation/tests on smithy-client gem:
```
bundle exec rake smithy-client:rbs
```
5 changes: 4 additions & 1 deletion gems/smithy-client/lib/smithy-client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
# codecs

require_relative 'smithy-client/cbor'
require_relative 'smithy-client/codecs/cbor'
require_relative 'smithy-client/codecs'

# protocols
require_relative 'smithy-client/protocols'

module Smithy
# Base module for a generated Smithy gem.
Expand Down
70 changes: 52 additions & 18 deletions gems/smithy-client/lib/smithy-client/codecs/cbor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@ def serialize(data, shape)
# @param [Struct] type
# @return [Object, Hash]
def deserialize(bytes, shape, type = nil)
return {} if bytes.empty?
return {} if bytes.empty? || shape == Prelude::Unit

parse_data(Client::CBOR.decode(bytes), shape, type)
end

private

def sparse?(shape)
shape.traits.keys.include?('smithy.api#sparse')
end

def format_blob(value)
(value.is_a?(::String) ? value : value.read).force_encoding(Encoding::BINARY)
(value.is_a?(String) ? value : value.read).force_encoding(Encoding::BINARY)
end

def format_data(value, shape)
Expand All @@ -52,20 +56,35 @@ def format_data(value, shape)
end

def format_list(values, shape)
values.collect { |value| format_data(value, shape.member.shape) }
values.collect do |value|
next if value.nil? && !sparse?(shape)

if value.nil? && sparse?(shape)
nil
else
format_data(value, shape.member.shape)
end
end
end

def format_map(values, shape)
values.each.with_object({}) do |(key, value), data|
data[key] = format_data(value, shape.value.shape)
next if value.nil? && !sparse?(shape)

data[key] =
if value.nil? && sparse?(shape)
nil
else
format_data(value, shape.value.shape)
end
end
end

def format_structure(values, shape)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to implement the (de)serialization of union shapes in a separate PR since I have some questions before implementing. Will be a small follow-up PR most likely.

values.each_pair.with_object({}) do |(key, value), data|
if shape.member?(key) && !value.nil?
member = shape.member(key)
data[key] = format_data(value, member.shape)
data[member.name] = format_data(value, member.shape)
end
end
end
Expand All @@ -81,30 +100,45 @@ def parse_data(value, shape, type = nil)
end
end

def parse_list(values, shape, target = nil)
target = [] if target.nil?
def parse_list(values, shape, type = nil)
type = [] if type.nil?
values.each do |value|
target << parse_data(value, shape.member.shape)
next if value.nil? && !sparse?(shape)

type <<
if value.nil?
nil
else
parse_data(value, shape.member.shape)
end
end
target
type
end

def parse_map(values, shape, target = nil)
target = {} if target.nil?
def parse_map(values, shape, type = nil)
type = {} if type.nil?
values.each do |key, value|
target[key] = parse_data(value, shape.value.shape) unless value.nil?
next if value.nil? && !sparse?(shape)

type[key] =
if value.nil?
nil
else
parse_data(value, shape.value.shape)
end
end
target
type
end

def parse_structure(values, shape, target = nil)
target = shape.type.new if target.nil?
def parse_structure(values, shape, type = nil)
type = shape.type.new if type.nil?
values.each do |key, value|
if (member = shape.member(key.to_sym))
target[key] = parse_data(value, member.shape)
if (member = shape.member(key))
member_name = shape.members_by_name[member.name]
type[member_name] = parse_data(value, member.shape)
end
end
target
type
end
end
end
Expand Down
1 change: 1 addition & 0 deletions gems/smithy-client/lib/smithy-client/param_converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def structure(shape, values)
if values.is_a?(::Struct) || values.is_a?(Hash)
values.each_pair do |k, v|
next if v.nil?

next unless shape.member?(k)

values[k] = member(shape.member(k), v)
Expand Down
28 changes: 28 additions & 0 deletions gems/smithy-client/lib/smithy-client/protocols.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require_relative 'protocols/rpc_v2'

module Smithy
module Client
# In Smithy, a protocol is a named set of rules that defines the
# syntax and semantics of how a client and server communicate. To
# use a {Smithy::Client}, a protocol is required to be set on
# {Configuration}.
#
# We currently support the following protocols:
#
# - {RPCv2}, a RPC-based protocol over HTTP that sends requests
# and responses with CBOR payloads.
#
# You could also create a custom protocol to pass into the client
# configuration. The given protocol must provide the following
# functionalities:
#
# - `build` - builds the request
# - `parse` - parse the response
# - `error` - extracts the error from response
#
# See {RPCv2} as an example implementation.
module Protocols; end
end
end
82 changes: 82 additions & 0 deletions gems/smithy-client/lib/smithy-client/protocols/rpc_v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

module Smithy
module Client
module Protocols
# A RPC-based protocol over HTTP that sends requests
# and responses with CBOR payloads.
#
# TODO: Refactor methods to handle eventstreams
class RPCv2
# @api private
SHAPE_ID = 'smithy.protocols#rpcv2Cbor'

# @param options [Hash] Protocol options
# @option options [Boolean] :query_compatible (nil)
def initialize(options = {})
@query_compatible = options[:query_compatible]
end

# @api private
def build(context)
codec = Client::Codecs::CBOR.new(setting(context))
context.request.body = codec.serialize(context.params, context.operation.input)
context.request.http_method = 'POST'
apply_headers(context)
build_url(context)
end

# @api private
def parse(context)
output_shape = context.operation.output
codec = Client::Codecs::CBOR.new(setting(context))
codec.deserialize(context.response.body.read, output_shape)
end

# @api private
# TODO: To implement after error handling
def error(_context, _response); end

private

def apply_headers(context)
context.request.headers['X-Amzn-Query-Mode'] = 'true' if query_compatible?(context)
context.request.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
apple_content_type(context)
apply_accept_header(context)
# TODO: Implement Content-Length Plugin/Handler
context.request.headers['Content-Length'] = context.request.body.size
end

def apply_accept_header(context)
# TODO: Needs an update when streaming is handled
context.request.headers['Accept'] = 'application/cbor'
end

def apple_content_type(context)
return if context.operation.input == Model::Shapes::Prelude::Unit

# TODO: Needs an update when streaming is handled
context.request.headers['Content-Type'] = 'application/cbor'
end

def build_url(context)
base = context.request.endpoint
base.path +=
"/service/#{context.config.service.name}/operation/#{context.operation.name}"
end

def setting(context)
{}.tap do |h|
h[:query_compatible] = true if query_compatible?(context)
end
end

def query_compatible?(context)
@query_compatible ||
context.config.service.traits.one? { |k, _v| k == 'aws.protocols#awsQuery' }
end
end
end
end
end
6 changes: 6 additions & 0 deletions gems/smithy-client/sig/interfaces.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ module Smithy
end

type endpoint_url = String | URI::HTTP | URI::HTTPS

interface _Protocol
def build: (HandlerContext context) -> void
def parse: (HandlerContext context) -> void
def error: (HandlerContext context, HTTP::Response response) -> void
end
end
end
11 changes: 11 additions & 0 deletions gems/smithy-client/sig/smithy-client/protocols/rpc_v2.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Smithy
module Client
module Protocols
# A RPC-based protocol over HTTP that sends requests
# and responses with CBOR payloads.
class RPCv2
def initialize: (?::Hash[untyped, untyped] options) -> void
end
end
end
end
6 changes: 3 additions & 3 deletions gems/smithy-client/spec/smithy-client/codecs/cbor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ module Codecs

let(:structure_shape) do
struct = Model::Shapes::StructureShape.new(id: 'structure')
struct.add_member(:s, string_shape)
struct.add_member(:l, list_shape)
struct.add_member(:m, map_shape)
struct.add_member(:s, 's', string_shape)
struct.add_member(:l, 'l', list_shape)
struct.add_member(:m, 'm', map_shape)
struct.type = typed_struct
struct
end
Expand Down
Loading