Skip to content

Commit

Permalink
Support RBS for aws-sdk-core and service gems (#2961)
Browse files Browse the repository at this point in the history
  • Loading branch information
ksss authored Jan 25, 2024
1 parent a3fe800 commit 33e31fe
Show file tree
Hide file tree
Showing 57 changed files with 1,657 additions and 22 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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|
y.yield("sig/#{path}", code)
end
end.each(&block)
end

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 33e31fe

Please sign in to comment.