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

Support RBS for aws-sdk-core and service gems #2961

Merged
merged 40 commits into from Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
17320ef
Add RBS signature files to support static type checking
ksss Nov 30, 2023
adab340
Introduce RBS generator
ksss Dec 12, 2023
87548aa
Build with RBS generator
ksss Dec 12, 2023
bda2499
Add CHANGELOG
ksss Dec 12, 2023
469ccd5
Fix typo
ksss Dec 14, 2023
2e266d8
Fixed to not use Client instance
ksss Dec 14, 2023
a412cf1
Direct reference type
ksss Dec 17, 2023
76d5786
Return Success or Error interface on operation
ksss Dec 20, 2023
4baf332
Remove unneeded argument
ksss Dec 20, 2023
9cbacaa
Add rbs:test task
ksss Dec 20, 2023
c51f7b1
Adjust format
ksss Dec 20, 2023
ab5dc94
Fix core types
ksss Dec 21, 2023
c8f6f06
Error response should not include overloads
ksss Dec 21, 2023
be2e16e
Currently S3 is sufficient
ksss Dec 21, 2023
2fab3e2
Add rspec tag `rbs_test: :skip`
ksss Dec 21, 2023
df16406
Add more method documentation
ksss Dec 21, 2023
0e52449
Fix testing CI for RBS
ksss Dec 21, 2023
60ff211
Regenerate sample result
ksss Dec 21, 2023
a65d569
Fix type
ksss Dec 21, 2023
109ac5d
Fix skip by aws-sdk-amplifybackend
ksss Dec 22, 2023
4b8bd59
Merge branch 'version-3' into rbs-aws-sdk-core
mullermp Dec 26, 2023
47206a5
Add `rbs_type` attribute for PluginOption
ksss Jan 11, 2024
7834758
Fix typo
ksss Jan 11, 2024
238c219
Fix skip members
ksss Jan 11, 2024
af94b5c
Support empty identifiers
ksss Jan 11, 2024
3d7c881
Avoid duplicate methods
ksss Jan 11, 2024
9a1fe40
Allows some option
ksss Jan 11, 2024
4b3ec10
Validate all at once
ksss Jan 11, 2024
44907a8
Regenerate lambda
ksss Jan 11, 2024
0d15fbb
Support Ruby v2.5~
ksss Jan 11, 2024
c54efd3
Merge branch 'version-3' into rbs-aws-sdk-core
ksss Jan 11, 2024
a67d217
Merge branch 'version-3' into rbs-aws-sdk-core
mullermp Jan 17, 2024
6a5f0ba
Fix region spec
mullermp Jan 17, 2024
9bbf038
Revert sample signatures for aws-sdk-lambda
ksss Jan 18, 2024
83dfdf1
Revert generated code
ksss Jan 18, 2024
c07478f
Update ruby
ksss Jan 18, 2024
7e99658
Set RBS type for undefined class
ksss Jan 18, 2024
335a613
Merge branch 'version-3' into rbs-aws-sdk-core
mullermp Jan 18, 2024
8b2a736
Undo more codegen and prepare version bump
mullermp Jan 18, 2024
637a5a0
Merge branch 'version-3' into rbs-aws-sdk-core
mullermp Jan 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -13,6 +13,32 @@ permissions:
contents: read

jobs:
sig:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: [3.3]

steps:
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}

- uses: actions/checkout@v4

- name: Install gems
run: |
bundle config set --local with 'build signature'
bundle install

- name: SDK Build
run: bundle exec rake build

- name: rbs testing
run: bundle exec rake rbs:test

test:
runs-on: ubuntu-latest
strategy:
Expand Down
4 changes: 4 additions & 0 deletions Gemfile
Expand Up @@ -58,3 +58,7 @@ group :benchmark do
gem 'benchmark'
gem 'memory_profiler'
end

group :signature do
gem 'rbs', platforms: :ruby
end
17 changes: 17 additions & 0 deletions build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator.rb
Expand Up @@ -75,6 +75,23 @@
require_relative 'aws-sdk-code-generator/code_builder'
require_relative 'aws-sdk-code-generator/gem_builder'

# RBS
require_relative 'aws-sdk-code-generator/rbs'
require_relative 'aws-sdk-code-generator/rbs/error_list'
require_relative 'aws-sdk-code-generator/rbs/method_signature'
require_relative 'aws-sdk-code-generator/rbs/keyword_argument_builder'
require_relative 'aws-sdk-code-generator/rbs/resource_action'
require_relative 'aws-sdk-code-generator/rbs/resource_association'
require_relative 'aws-sdk-code-generator/rbs/resource_batch_action'
require_relative 'aws-sdk-code-generator/rbs/resource_client_request'
require_relative 'aws-sdk-code-generator/rbs/waiter'
require_relative 'aws-sdk-code-generator/views/rbs/client_class'
require_relative 'aws-sdk-code-generator/views/rbs/errors_module'
require_relative 'aws-sdk-code-generator/views/rbs/resource_class'
require_relative 'aws-sdk-code-generator/views/rbs/root_resource_class'
require_relative 'aws-sdk-code-generator/views/rbs/types_module'
require_relative 'aws-sdk-code-generator/views/rbs/waiters_module'

module AwsSdkCodeGenerator

GENERATED_SRC_WARNING = <<-WARNING_TXT
Expand Down
Expand Up @@ -109,6 +109,58 @@ def spec_files(options = {})
end
end

# @return [Enumerable<String<path>, String<code>>]
def rbs_files(options = {})
Enumerator.new do |y|
prefix = options.fetch(:prefix, '')
codegenerated_plugins = codegen_plugins(prefix)
client_class = Views::RBS::ClientClass.new(
service_name: @service.name,
codegenerated_plugins: codegenerated_plugins,
aws_sdk_core_lib_path: @aws_sdk_core_lib_path,
legacy_endpoints: @service.legacy_endpoints?,
signature_version: @service.signature_version,
api: @service.api,
waiters: @service.waiters,
protocol: @service.protocol,
add_plugins: @service.add_plugins,
remove_plugins: @service.remove_plugins,
)
y.yield("#{prefix}/client.rbs", client_class.render)
y.yield("#{prefix}/errors.rbs", Views::RBS::ErrorsModule.new(
service: @service
).render)
y.yield("#{prefix}/resource.rbs", Views::RBS::RootResourceClass.new(
service_name: @service.name,
client_class: client_class,
api: @service.api,
resources: @service.resources,
paginators: @service.paginators,
).render)
y.yield("#{prefix}/waiters.rbs", Views::RBS::WaitersModule.new(
service_name: @service.name,
api: @service.api,
waiters: @service.waiters,
).render)
y.yield("#{prefix}/types.rbs", Views::RBS::TypesModule.new(
service: @service
).render)
if @resources
@resources['resources'].keys.sort.each do |class_name|
path = "#{prefix}/#{Underscore.underscore(class_name)}.rbs"
code = Views::RBS::ResourceClass.new(
service_name: @service.name,
class_name: class_name,
resource: @resources['resources'][class_name],
api: @service.api,
paginators: @service.paginators,
).render
y.yield(path, code)
end
end
end
end

private

def service_module(prefix, codegenerated_plugins)
Expand Down
Expand Up @@ -36,6 +36,9 @@ def each(&block)
code.spec_files.each do |path, code|
y.yield("spec/#{path}", code)
end
code.rbs_files.each do |path, code|
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you want to check @service.support_rbs? here, similar to if @service.smoke_tests above, since rbs_files method entirely relies on it being enabled.

y.yield("sig/#{path}", code)
end
end.each(&block)
end

Expand Down
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module AwsSdkCodeGenerator
module RBS
class << self
def to_type(shape_ref, api)
_, shape = Api.resolve(shape_ref, api)
case shape['type']
when 'blob' then Api.streaming?(shape_ref, api) ? '::IO' : '::String'
when 'boolean' then 'bool'
when 'byte' then '::Integer'
when 'character' then '::String'
when 'double' then '::Float'
when 'float' then '::Float'
when 'integer' then '::Integer'
when 'list' then "::Array[#{to_type(shape['member'], api)}]"
when 'long' then '::Integer'
when 'map' then "::Hash[#{to_type(shape['key'], api)}, #{to_type(shape['value'], api)}]"
when 'string'
if shape['enum']
"(#{shape['enum'].map { |e| "\"#{e}\"" }.join(" | ")})"
elsif Api.streaming?(shape_ref, api)
'::IO'
else
'::String'
end
when 'structure'
if shape['document']
'untyped'
else
"Types::#{shape_ref['shape']}"
end
when 'timestamp' then '::Time'
else
raise "unhandled type #{shape['type'].inspect}"
end
end
end
end
end
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module AwsSdkCodeGenerator
module RBS
class ErrorList
include Enumerable

def initialize(api:, module_name:)
@api = api
@module_name = module_name
@errors = @api['shapes'].inject([]) do |es, (name, shape)|
if error_struct?(shape)
members = shape["members"].map do |member_name, member_body|
MethodSignature.new(
method_name: Underscore.underscore(member_name),
overloads: ["() -> #{Docstring.ucfirst(member_body['type'] ||'::String')}"]
)
end
es << {
name: name,
members: members,
}
end
es
end
end

def error_struct?(shape)
shape['type'] == 'structure' && !!!shape['event'] &&
(shape['error'] || shape['exception'])
end

def to_a
@errors
end
end
end
end
@@ -0,0 +1,159 @@
# frozen_string_literal: true

module AwsSdkCodeGenerator
module RBS
# similar to SyntaxExampleHash
class KeywordArgumentBuilder
include Helper

attr_reader :newline

def initialize(api:, shape:, newline:)
@api = api
@shape = shape
@newline = newline
end

def format(indent: '')
members_str = struct_members(@shape, indent, [], keyword: true)
result = []
result << '' if newline
result << members_str if !members_str.empty?
result << indent if newline
result.join(joint)
end

def struct(struct_shape, i, visited)
members_str = struct_members(struct_shape, i, visited, keyword: false)
result = ["{"]
result << members_str if struct_shape['members']&.empty?&.!
result << "#{i}}"
result.join(joint)
end

def struct_members(struct_shape, i, visited, keyword:)
lines = []
unless struct_shape['members'].nil?
n = 0
struct_shape['members'].each do |member_name, member_ref|
next if member_ref['documented'] === false
more_indent = newline ? " " : ""
if @api['shapes'][member_ref['shape']]['eventstream'] === true
# FIXME: "input_event_stream_hander: EventStreams::#{member_ref['shape']}.new"
lines << "#{i}#{more_indent}input_event_stream_hander: untyped,"
else
lines << "#{i}#{more_indent}#{struct_member(struct_shape, member_name, member_ref, i, visited, keyword: keyword)}"
end
end
end
if lines.empty?
""
else
lines.join(joint).chomp(",")
end
end

def struct_member(struct, member_name, member_ref, i, visited, keyword:)
required = (struct['required'] || []).include?(member_name)
if keyword
"#{required ? '' : '?'}#{underscore(member_name)}: #{ref_value(member_ref, i + more_indent, visited)},"
else
"#{underscore(member_name)}: #{ref_value(member_ref, i + more_indent, visited)}#{required ? '' : '?'},"
end
end

def ref_value(ref, i, visited)
if visited.include?(ref['shape'])
return "untyped"
else
visited = visited + [ref['shape']]
end

s = shape(ref)
case s['type']
when 'structure'
if ref['shape'] == 'AttributeValue'
'untyped'
else
struct(s, i, visited)
end
when 'blob'
if ref['streaming']
"::String | ::StringIO | ::File" # input only
else
"::String"
end
when 'list' then list(s, i, visited)
when 'map' then map(s, i, visited)
when 'boolean' then "bool"
when 'integer', 'long' then '::Integer'
when 'float', 'double' then '::Float'
when 'byte' then '::Integer'
when 'character' then '::String'
when 'string' then string(ref)
when 'timestamp' then '::Time'
else raise "unsupported shape #{s['type'].inspect}"
end
end

def list(list_shape, i, visited)
member_ref = list_shape['member']
if complex?(member_ref)
complex_list(member_ref, i, visited)
else
scalar_list(member_ref, i, visited)
end
end

def scalar_list(member_ref, i, visited)
"Array[#{ref_value(member_ref, i, visited)}]"
end

def complex_list(member_ref, i, visited)
newline_indent = newline ? "\n#{i}" : ""
"Array[#{newline_indent}#{more_indent}#{ref_value(member_ref, i + more_indent, visited)},#{newline_indent}]"
end

def complex?(ref)
s = shape(ref)
if s['type'] == 'structure'
!ddb_av?(ref)
else
s['type'] == 'list' || s['type'] == 'map'
end
end

def ddb_av?(ref)
s = shape(ref)
case s['type']
when 'list' then ddb_av?(s['member'])
when 'structure' then ref['shape'] == 'AttributeValue'
else false
end
end

def map(map_shape, i, visited)
key = string(map_shape['key'])
value = ref_value(map_shape['value'], i + more_indent, visited)
"Hash[#{key}, #{value}]"
end

def string(ref)
string_shape = shape(ref)
if string_shape['enum']
"(#{string_shape['enum'].map { |s| "\"#{s}\"" }.join(" | ")})"
else ref['shape']
"::String"
end
end

def more_indent
newline ? " " : ""
end

def joint
newline ? "\n" : " "
end
end
end
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module AwsSdkCodeGenerator
module RBS
class MethodSignature < Struct.new(:method_name, :overloads, keyword_init: true)
def signature
"def #{method_name}: #{overloads.join("\n #{" " * method_name.length}| ")}"
end
end
end
end