-
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
b67cf54
ea9b380
381602b
6d41f64
83fa2bb
b23a2e9
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,94 @@ | ||
# 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 | ||
# 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 < ::SimpleDelegator | ||
# @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) | ||
super(@data) | ||
end | ||
|
||
# @return [String] discriminator | ||
attr_reader :discriminator | ||
|
||
def data | ||
__getobj__ # return object we are delegating to, required | ||
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,216 @@ | ||
# 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) | ||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# frozen_string_literal: true | ||
|
||
module Smithy | ||
module Schema | ||
# A module that provides helper methods to convert Time objects | ||
# based on the given TimestampFormat trait. | ||
# @api private | ||
# TODO: need to handle fractional secs | ||
module TimeHelper | ||
class << self | ||
# @param [Time] time | ||
# @param [String] trait TimestampFormat trait value | ||
# @return [Object] The time as TimestampFormat trait format | ||
def time(time, trait) | ||
raise ArgumentError, 'expected Time as input' unless time.is_a?(Time) | ||
|
||
case trait | ||
when 'http-date' | ||
time.utc.iso8601 | ||
when 'date-time' | ||
time.utc.httpdate | ||
when 'epoch-seconds' | ||
time.utc.to_i | ||
else | ||
raise ArgumentError, "unhandled timestamp format `#{trait}`" | ||
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.
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 comment
The 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 comment
The 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 comment
The 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.