-
Notifications
You must be signed in to change notification settings - Fork 7
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
Merged
+1,065
−11
Merged
Changes from 22 commits
Commits
Show all changes
85 commits
Select commit
Hold shift + click to select a range
d33fcdf
Add type registry prototype class
jterapin 5ff40cc
Add type registry to codegenerated schema
jterapin 66a6285
Update projections
jterapin c5e45ed
Merge branch 'decaf' into typed_documents
jterapin 2bd15a2
Merge branch 'decaf' into typed_documents
jterapin 03bfa82
Merge branch 'decaf' into typed_documents
jterapin e6435d5
Update requires
jterapin 0830827
Add initial document implementation
jterapin 877654f
Merge decaf into branch
jterapin a61318f
Update to include cbor
jterapin 4edfae3
Expand on typed docs
jterapin ff959f1
Update file names
jterapin 8b9b560
Merge branch 'decaf' into typed_documents
jterapin 598db66
More refactoring
jterapin 3a4c0d1
Merge branch 'decaf' into typed_documents
jterapin 269b2b5
Remove scratches
jterapin 90c58ce
Fix rubocop
jterapin a1e46cc
Clean up document
jterapin 8b666cd
Clean document specs
jterapin 2ddf4bd
Update TypeRegistry
jterapin 112ddf4
Add documentation
jterapin 6283813
Add TypeRegistry specs
jterapin efbfa5e
Merge branch 'decaf' into typed_documents
jterapin 88ff845
Add TypeRegistry tests
jterapin 66b2cde
Update projections
jterapin 22998a0
Update syntax
jterapin 9afeacd
Update projections
jterapin 232844c
Merge branch 'decaf' into typed_documents
jterapin e8920fd
Change schema name to shapes to stay aligned
jterapin ef8c027
Remove as_typed method from TypeRegistry
jterapin 3854661
Create TimeHelper module
jterapin 48e1b0f
Fix timestamp failures
jterapin 04d0d5b
Refactor type registry per feedbacks
jterapin b35e09b
Update 1 projection
jterapin 66825be
Update weather projection
jterapin 4efe669
Use SchemaHelper for testing
jterapin 73e0e83
Update TypeRegistry to use SchemaHelper for testing
jterapin 60ef29c
Add TypeRegistry documentation
jterapin b67cf54
Update example
jterapin ea9b380
Merge decaf into branch
jterapin 381602b
Remove reference to type registry from client
jterapin 6d41f64
Update projections
jterapin 83fa2bb
Update projections
jterapin b23a2e9
Document now inherits SimpleDelegator
jterapin 306a97e
Expand on type registry docs
jterapin 8ef3055
Fix bug in timehelper
jterapin a297963
Slim down the sample shapes
jterapin 8e2d322
Update docs
jterapin 532ba8a
Merge decaf into branch
jterapin cb66cec
Only add structures to type registry
jterapin 9fa984c
Update TypeRegistry to limit to StructureShape
jterapin 166c8b1
Document revamp
jterapin 9cef7ee
Rename document test cases
jterapin d01a780
Merge branch 'decaf' into typed_documents
jterapin e283281
Improve Document Deserializer
jterapin a48f7a1
Update Document Serializer and its specs
jterapin 1913938
Update Document Data class
jterapin 8cc697c
Remove TimeHelper
jterapin 75141d1
Require delegate
jterapin d36a70b
Fix relative ordering
jterapin b2a6998
Fix type registry test
jterapin 9219b13
Remove unnecessary shape
jterapin 1f0b771
Rubocop fix
jterapin 74450a1
More changes
jterapin 68c94c1
Add docs
jterapin 3ae063f
Update Document Data class discriminator
jterapin fcbe801
Remove data method out of Document::Data
jterapin 03a2d67
Update unions for document deserializers
jterapin 252456d
Fix serializer example
jterapin 1c59f21
Update shape test definitions closer to test cases
jterapin d0ef75e
Update schema heler requires
jterapin 669d90a
Update handling of typed documents
jterapin c1ea6c9
Remove unnecessary docs
jterapin fe8271c
Clean up document serializer
jterapin 6b8fa2e
merge from decaf
jterapin dea7d14
Comment out failing test due to synethic shape changes
jterapin 82a54f7
Streamline documents
jterapin cd380ed
Type Registry fixes
jterapin 8bc6e17
Update projections
jterapin 312d94e
Update docs
jterapin 612807a
Merge branch 'decaf' into typed_documents
mullermp 1904852
Dynamic type registry shape population
mullermp d404cbe
Clean up of type registry implementation and spec
mullermp c5ba058
Clean up some docs and implementation
mullermp cc9fdd2
More renaming + doc fixes
mullermp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# 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 schema 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] :schema schema 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 schema. 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 schema. The `jsonName` trait is ignored | ||
# by default. | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 [Object] key | ||
# @return [Object] | ||
def [](key) | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return unless @data.is_a?(Hash) && @data.key?(key) | ||
|
||
@data[key] | ||
end | ||
|
||
# @param [Shapes::Shape] schema | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# @return [Shapes::Structure] typed shape | ||
def as_typed(schema) | ||
error_message = 'Invalid schema or document data' | ||
raise ArgumentError, error_message unless valid_schema?(schema) && @data.is_a?(Hash) | ||
|
||
type = schema.type.new | ||
DocumentUtils.apply(@data, schema, 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) || (schema = opts[:schema]) | ||
|
||
if discriminator?(data) | ||
data['__type'] | ||
else | ||
error_message = "Expected a structure schema, given #{schema.class} instead" | ||
raise error_message unless valid_schema?(schema) | ||
|
||
schema.id | ||
end | ||
end | ||
|
||
def set_data(data, opts) | ||
return if data.nil? | ||
|
||
case data | ||
when Smithy::Schema::Structure | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
schema = opts[:schema] | ||
if schema.nil? || !valid_schema?(schema) | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
raise ArgumentError, "Unable to create a document with given schema: #{schema}" | ||
end | ||
|
||
opts = opts.except(:schema) | ||
# case 1 - extract data from runtime shape, schema is required to know to properly extract | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
DocumentUtils.extract(data, schema, opts) | ||
|
||
else | ||
if discriminator?(data) | ||
# case 2 - extract typed data from parsed JSON | ||
data.except('__type') | ||
else | ||
# case 3 - untyped data, we will need consolidate timestamps and such | ||
DocumentUtils.format(data) | ||
end | ||
end | ||
end | ||
|
||
def valid_schema?(schema) | ||
schema.is_a?(Shapes::StructureShape) && !schema.type.nil? | ||
end | ||
end | ||
end | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
# 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.transform_values { |v| format(v) } | ||
when Array | ||
data.map { |d| format(d) } | ||
else | ||
data | ||
end | ||
end | ||
|
||
# Used to apply data to runtime shape | ||
def apply(data, schema, type = nil) | ||
case shape(schema) | ||
when Shapes::StructureShape then apply_structure(data, schema, type) | ||
when Shapes::UnionShape then apply_union(data, schema, type) | ||
when Shapes::ListShape then apply_list(data, schema) | ||
when Shapes::MapShape then apply_map(data, schema) | ||
when Shapes::TimestampShape then apply_timestamp(data, schema) | ||
when Shapes::BlobShape then Base64.decode64(data) | ||
else data | ||
end | ||
end | ||
|
||
# rubocop:disable Metrics/CyclomaticComplexity | ||
def extract(data, schema, opts = {}) | ||
return if data.nil? | ||
|
||
case shape(schema) | ||
when Shapes::StructureShape then extract_structure(data, schema, opts) | ||
when Shapes::UnionShape then extract_union(data, schema, opts) | ||
when Shapes::ListShape then extract_list(data, schema) | ||
when Shapes::MapShape then extract_map(data, schema) | ||
when Shapes::BlobShape then extract_blob(data) | ||
when Shapes::TimestampShape then extract_timestamp(data, schema, opts) | ||
else data | ||
end | ||
end | ||
# rubocop:enable Metrics/CyclomaticComplexity | ||
|
||
private | ||
|
||
def apply_list(data, schema) | ||
shape = shape(schema) | ||
data.map do |v| | ||
next if v.nil? | ||
|
||
apply(v, shape.member) | ||
end | ||
end | ||
|
||
def apply_map(data, schema) | ||
shape = shape(schema) | ||
data.transform_values do |v| | ||
if v.nil? | ||
nil | ||
else | ||
apply(v, shape.value) | ||
end | ||
end | ||
end | ||
|
||
def apply_structure(data, schema, type) | ||
shape = shape(schema) | ||
|
||
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, schema) | ||
data = data.is_a?(Numeric) ? Time.at(data) : Time.parse(data) | ||
time(data, timestamp_format(schema)) | ||
end | ||
|
||
def apply_union(data, schema, type) | ||
shape = shape(schema) | ||
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, schema) | ||
shape = shape(schema) | ||
data.collect { |v| extract(v, shape.member) } | ||
end | ||
|
||
def extract_map(data, schema) | ||
shape = shape(schema) | ||
data.each.with_object({}) { |(k, v), h| h[k] = extract(v, shape.value) } | ||
end | ||
|
||
def extract_structure(data, schema, opts) | ||
shape = shape(schema) | ||
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, schema, opts) | ||
return unless data.is_a?(Time) | ||
|
||
trait = timestamp_format(schema) if opts[:use_timestamp_format] | ||
time(data, trait) | ||
end | ||
|
||
# rubocop:disable Metrics/AbcSize | ||
def extract_union(data, schema, opts) | ||
h = {} | ||
shape = shape(schema) | ||
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(schema, key) | ||
return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) | ||
|
||
schema.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(schema) | ||
schema.is_a?(Shapes::MemberShape) ? schema.shape : schema | ||
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(schema) | ||
if schema.traits['smithy.api#timestampFormat'] | ||
schema.traits['smithy.api#timestampFormat'] | ||
elsif schema.shape.traits['smithy.api#timestampFormat'] | ||
schema.shape.traits['smithy.api#timestampFormat'] | ||
else | ||
'epoch-seconds' | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
|
||
def time(data, trait = nil) | ||
jterapin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if trait | ||
case trait | ||
when 'http-date' | ||
data.utc.iso8601 | ||
when 'date-time' | ||
data.utc.httpdate | ||
when 'epoch-seconds' | ||
data.utc.to_i | ||
else | ||
raise "unhandled timestamp format `#{value}`" | ||
end | ||
else | ||
data.utc.to_i # default format | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.