-
Notifications
You must be signed in to change notification settings - Fork 6
Implement Typed Documents and TypeRegistry #282
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
base: decaf
Are you sure you want to change the base?
Changes from all commits
d33fcdf
5ff40cc
66a6285
c5e45ed
2bd15a2
03bfa82
e6435d5
0830827
877654f
a61318f
4edfae3
ff959f1
8b9b560
598db66
3a4c0d1
269b2b5
90c58ce
a1e46cc
8b666cd
2ddf4bd
112ddf4
6283813
efbfa5e
88ff845
66b2cde
22998a0
9afeacd
232844c
e8920fd
ef8c027
3854661
48e1b0f
04d0d5b
b35e09b
66825be
4efe669
73e0e83
60ef29c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'document_utils' | ||
|
||
module Smithy | ||
module Schema | ||
# A Smithy document type, representing typed or untyped data from Smithy data model. | ||
# ## Document types | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could add more details (e.g. code examples) but I might punt that to the Smithy-Ruby Wiki instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think in-code examples/documentation are preferred but it doesn't have to be done this moment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea - I'd agree examples and more detail would be better as in code documentation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add them now - since information is very fresh in my head. |
||
# Document types are protocol-agnostic view of untyped data. They could be combined | ||
# with a shape to serialize its contents. | ||
# | ||
# Smithy-Ruby currently only support JSON documents. | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class Document | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# @param [Object] data document data | ||
# @param [Hash] options | ||
# @option options [Smithy::Schema::Structure] :shape shape to reference when setting | ||
# document data. Only applicable when data param is a type of {Shapes::StructureShape}. | ||
# @option options [Boolean] :use_timestamp_format Whether to use the `timestampFormat` | ||
# trait or ignore it when creating a {Document} with given shape. The `timestampFormat` | ||
# trait is ignored by default. | ||
# @option options [Boolean] :use_json_name Whether to use the `jsonName` trait or ignore | ||
# it when creating a {Document} with given shape. The `jsonName` trait is ignored | ||
# by default. | ||
def initialize(data, options = {}) | ||
@data = set_data(data, options) | ||
@discriminator = extract_discriminator(data, options) | ||
end | ||
|
||
# @return [Object] data | ||
attr_reader :data | ||
|
||
# @return [String] discriminator | ||
attr_reader :discriminator | ||
|
||
# @param [String] key | ||
# @return [Object] | ||
def [](key) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? - I think this class should delegate to the data (perhaps even use Delegator). It should just add some utility to the raw document value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a question about this comment but we could discuss it offline. |
||
return unless @data.is_a?(Hash) && @data.key?(key) | ||
|
||
@data[key] | ||
end | ||
|
||
# @param [Shapes::Structure] shape | ||
# @return [Object] typed shape | ||
def as_typed(shape) | ||
error_message = 'Invalid shape or document data' | ||
raise ArgumentError, error_message unless valid_shape?(shape) && @data.is_a?(Hash) | ||
|
||
type = shape.type.new | ||
DocumentUtils.apply(@data, shape, type) | ||
end | ||
|
||
private | ||
|
||
def discriminator?(data) | ||
data.is_a?(Hash) && data.key?('__type') | ||
end | ||
|
||
def extract_discriminator(data, opts) | ||
return if data.nil? | ||
|
||
return unless discriminator?(data) || (shape = opts[:shape]) | ||
|
||
if discriminator?(data) | ||
data['__type'] | ||
else | ||
error_message = "Expected a structure shape, given #{shape.class} instead" | ||
raise error_message unless valid_shape?(shape) | ||
|
||
shape.id | ||
end | ||
end | ||
|
||
def set_data(data, opts) | ||
return if data.nil? | ||
|
||
case data | ||
when Smithy::Schema::Structure | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this already "done"? If it's a structure then it's already typed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you expand on that? what do you mean by being "done" - I still want to re-format the typed shape into a document format. |
||
shape = opts[:shape] | ||
if shape.nil? || !valid_shape?(shape) | ||
raise ArgumentError, "Unable to create a document with given shape: #{shape}" | ||
end | ||
|
||
opts = opts.except(:shape) | ||
DocumentUtils.extract(data, shape, opts) | ||
else | ||
if discriminator?(data) | ||
data.except('__type') | ||
else | ||
DocumentUtils.format(data) | ||
end | ||
end | ||
end | ||
|
||
def valid_shape?(shape) | ||
shape.is_a?(Shapes::StructureShape) && !shape.type.nil? | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'base64' | ||
require 'time' | ||
|
||
module Smithy | ||
module Schema | ||
# @api private | ||
# Document Utilities to help (de)construct data to/from Smithy document | ||
module DocumentUtils | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class << self | ||
# Used to transform untyped data | ||
def format(data) | ||
return if data.nil? | ||
|
||
case data | ||
when Time | ||
data.to_i # timestamp format is "epoch-seconds" by default | ||
when Hash | ||
data.each_with_object({}) do |(k, v), h| | ||
h[k.to_s] = format(v) | ||
end | ||
when Array | ||
data.map { |d| format(d) } | ||
else | ||
data | ||
end | ||
end | ||
|
||
# Used to apply data to runtime shape | ||
def apply(data, shape, type = nil) | ||
case shape_reference(shape) | ||
when Shapes::StructureShape then apply_structure(data, shape, type) | ||
when Shapes::UnionShape then apply_union(data, shape, type) | ||
when Shapes::ListShape then apply_list(data, shape) | ||
when Shapes::MapShape then apply_map(data, shape) | ||
when Shapes::TimestampShape then apply_timestamp(data, shape) | ||
when Shapes::BlobShape then Base64.decode64(data) | ||
else data | ||
end | ||
end | ||
|
||
# rubocop:disable Metrics/CyclomaticComplexity | ||
def extract(data, shape, opts = {}) | ||
return if data.nil? | ||
|
||
case shape_reference(shape) | ||
when Shapes::StructureShape then extract_structure(data, shape, opts) | ||
when Shapes::UnionShape then extract_union(data, shape, opts) | ||
when Shapes::ListShape then extract_list(data, shape) | ||
when Shapes::MapShape then extract_map(data, shape) | ||
when Shapes::BlobShape then extract_blob(data) | ||
when Shapes::TimestampShape then extract_timestamp(data, shape, opts) | ||
else data | ||
end | ||
end | ||
# rubocop:enable Metrics/CyclomaticComplexity | ||
|
||
private | ||
|
||
def apply_list(data, shape) | ||
shape = shape_reference(shape) | ||
data.map do |v| | ||
next if v.nil? | ||
|
||
apply(v, shape.member) | ||
end | ||
end | ||
|
||
def apply_map(data, shape) | ||
shape = shape_reference(shape) | ||
data.transform_values do |v| | ||
if v.nil? | ||
nil | ||
else | ||
apply(v, shape.value) | ||
end | ||
end | ||
end | ||
|
||
def apply_structure(data, shape, type) | ||
shape = shape_reference(shape) | ||
|
||
type = shape.type.new if type.nil? | ||
data.each do |k, v| | ||
name = | ||
if (member = member_with_json_name(k, shape)) | ||
shape.name_by_member_name(member.name) | ||
else | ||
member_name(shape, k) | ||
end | ||
next if name.nil? | ||
|
||
type[name] = apply(v, shape.member(name)) | ||
end | ||
type | ||
end | ||
|
||
def apply_timestamp(data, shape) | ||
data = data.is_a?(Numeric) ? Time.at(data) : Time.parse(data) | ||
TimeHelper.time(data, timestamp_format(shape)) | ||
end | ||
|
||
def apply_union(data, shape, type) | ||
shape = shape_reference(shape) | ||
key, value = data.flatten | ||
return if key.nil? | ||
|
||
if (member = member_with_json_name(key, shape)) | ||
apply_union_member(member.name, value, shape, type) | ||
elsif shape.name_by_member_name?(key) | ||
apply_union_member(key, value, shape, type) | ||
else | ||
shape.member_type(:unknown).new(key, value) | ||
end | ||
end | ||
|
||
def apply_union_member(key, value, shape, type) | ||
member_name = shape.name_by_member_name(key) | ||
type = shape.member_type(member_name) if type.nil? | ||
type.new(apply(value, shape.member(member_name))) | ||
end | ||
|
||
def extract_blob(data) | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Base64.strict_encode64(data.is_a?(String) ? data : data.read) | ||
end | ||
|
||
def extract_list(data, shape) | ||
shape = shape_reference(shape) | ||
data.collect { |v| extract(v, shape.member) } | ||
end | ||
|
||
def extract_map(data, shape) | ||
shape = shape_reference(shape) | ||
data.each.with_object({}) { |(k, v), h| h[k.to_s] = extract(v, shape.value) } | ||
end | ||
|
||
def extract_structure(data, shape, opts) | ||
shape = shape_reference(shape) | ||
data.to_h.each_with_object({}) do |(k, v), o| | ||
next unless shape.member?(k) | ||
|
||
member_shape = shape.member(k) | ||
member_name = resolve_member_name(member_shape, opts) | ||
pp member_name | ||
o[member_name] = extract(v, member_shape, opts) | ||
end | ||
end | ||
|
||
def extract_timestamp(data, shape, opts) | ||
return unless data.is_a?(Time) | ||
|
||
trait = opts[:use_timestamp_format] ? timestamp_format(shape) : 'epoch-seconds' | ||
TimeHelper.time(data, trait) | ||
end | ||
|
||
# rubocop:disable Metrics/AbcSize | ||
def extract_union(data, shape, opts) | ||
h = {} | ||
shape = shape_reference(shape) | ||
if data.is_a?(Schema::Union) | ||
member_shape = shape.member_by_type(data.class) | ||
member_name = resolve_member_name(member_shape, opts) | ||
h[member_name] = extract(data, member_shape).value | ||
else | ||
key, value = data.first | ||
if shape.member?(key) | ||
member_shape = shape.member(key) | ||
member_name = resolve_member_name(member_shape, opts) | ||
h[member_name] = extract(value, member_shape) | ||
end | ||
end | ||
h | ||
end | ||
# rubocop:enable Metrics/AbcSize | ||
|
||
def member_name(shape, key) | ||
return unless shape.name_by_member_name?(key) || shape.member?(key.to_sym) | ||
|
||
shape.name_by_member_name(key) || key.to_sym | ||
end | ||
|
||
def member_with_json_name(name, shape) | ||
shape.members.values.find do |v| | ||
v.traits['smithy.api#jsonName'] == name if v.traits.include?('smithy.api#jsonName') | ||
end | ||
end | ||
|
||
def resolve_member_name(member_shape, opts) | ||
if opts[:use_json_name] && member_shape.traits['smithy.api#jsonName'] | ||
member_shape.traits['smithy.api#jsonName'] | ||
else | ||
member_shape.name | ||
end | ||
end | ||
|
||
def shape_reference(shape) | ||
shape.is_a?(Shapes::MemberShape) ? shape.shape : shape | ||
end | ||
|
||
# The following steps are taken to determine the format of timestamp: | ||
# Use the timestampFormat trait of the member, if present. | ||
# Use the timestampFormat trait of the shape, if present. | ||
# If none of the above applies, use epoch-seconds as default | ||
def timestamp_format(shape) | ||
if shape.traits['smithy.api#timestampFormat'] | ||
shape.traits['smithy.api#timestampFormat'] | ||
elsif shape.shape.traits['smithy.api#timestampFormat'] | ||
shape.shape.traits['smithy.api#timestampFormat'] | ||
else | ||
'epoch-seconds' | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the intention of placing this on base and why is it needed here? This will add a method to clients. I think we should bias to make this feature for schema gems only for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all gems should have the type register available - though I'm not sure it belongs as a method on client class - maybe as a method on
Schema
module, eg:Schema.type_registery
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need access to TypeRegistry during SERDE process - when it comes to deserializing typed documents. I haven't quite figured out where would be a good "home" for this.
I'm thinking of few options...
1 - Update
Smithy::Client::HandlerContext
to have access to the type registry (when deserializing typed documents - we can find the associated shape) - like:context.type_registry
2 - Keep the reference on the client so we don't need to add another attribute on the
HandlerContext
- we could do:context.client.type_registry
to access during SERDE process3 - Should
ServiceShape
have an attribute calledtype_registry
so we could do:client.service.type_registry
for easily access?3 - Just omit the reference for now until we need it. If you need it for whatever reason, you gotta use this constant:
BirdService::Schema.TYPE_REGISTRY
If we decide to omit this now, when do we need it?
Any JSON-esque protocol since they support
JSON
docsWhat do y'all think?