smithy-schema 1.0.0.pre0 → 1.0.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dc2a4e23db98951101c73d858d031c3a1e2b757c49d891c448bd3e049a6917d
4
- data.tar.gz: 888f46534f8c174d00f6a09470ce69ac183ab68dd9d9c43e55f9e4ec51b3cf07
3
+ metadata.gz: 7969227c178251f03785e6acea6cec078f3a5b58f266442dc3d980e6d4b91df5
4
+ data.tar.gz: 9308246739c8b7f192dce70a2dc7dcd7a26aa8446f8e0be1da54c724de30f4ec
5
5
  SHA512:
6
- metadata.gz: 949146a02a4357e994ad85028d06cb198bb20dbb208c5d8d001762ff7653b7c9c03ca2f7aa313aa85f2b6051798184dc24b3c2547a9e034cab818c3bb6b0edef
7
- data.tar.gz: f63565e8f4297c5c9e20720d89ac5f316f8ac590572f4d600196d2aa15cc82c9720496c26559a300806d1346bb1cd611ee00f7caa06047c96b60b8add5a8676c
6
+ metadata.gz: 8098ac1b23fade8569d4e5bf57f645ec5cf7d2783740327dcad4564b19b6f86e442a5d7b8af092170e581ca2948f6ba4ca47d2e9874b58ba333d08a9826a52cb
7
+ data.tar.gz: '03649f8030eea28a80d0a7f37d306f6ca33933437e56c7acb4a579f9ea4423d3b0c01d819c685096392792031e1a6630e09b0085351baf18dea748203dda37ad'
data/CHANGELOG.md CHANGED
@@ -1,2 +1,4 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
+
4
+ * Feature - Initial version of this gem.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0.pre0
1
+ 1.0.0.pre1
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'document_utils/deserializer'
4
+ require_relative 'document_utils/serializer'
5
+ require 'delegate'
6
+
7
+ module Smithy
8
+ module Schema
9
+ # A Smithy document, representing typed or untyped data from the Smithy data model.
10
+ # The Document class delegates to the underlying data object while providing additional
11
+ # document-specific functionality. The document will represent protocol-agnostic
12
+ # data structures in the Smithy data model.
13
+ #
14
+ # This class includes capabilities for:
15
+ #
16
+ # - Serialization and deserialization of document data
17
+ # - Type-aware data handling
18
+ # - Support for JSON document format
19
+ #
20
+ # To create a Document using various input formats, use {Document.create}
21
+ # @example Basic usage with a document
22
+ # document = Document.new(name: "document")
23
+ # document # => { "name" => "document" }
24
+ class Document < ::SimpleDelegator
25
+ # A Smithy document, representing typed or untyped data from the Smithy data model.
26
+ # This class delegates to the underlying data object while providing additional
27
+ # document-specific functionality.
28
+ # @param [Object] data document data
29
+ # @param [Hash] options
30
+ # @option options [String] :discriminator This value is used to identify a specific
31
+ # shape. This is equivalent of a Smithy shape ID.
32
+ def initialize(data, options = {})
33
+ @data = data
34
+ @discriminator = options[:discriminator]
35
+ super(@data)
36
+ end
37
+
38
+ # Returns the discriminator value for the document.
39
+ #
40
+ # @return [String, nil] discriminator
41
+ attr_reader :discriminator
42
+
43
+ # Serializes a {Document} with optional formatting.
44
+ #
45
+ # @param [TypeRegistry] type_registry Registry is required for identifying
46
+ # and validating typed documents
47
+ # @param [Hash] opts Formatting options
48
+ # @option opts [Boolean] :timestamp_format Whether to use the `timestampFormat`
49
+ # trait or ignore it. The `timestampFormat` trait is ignored by default.
50
+ # @option opts [Boolean] :json_name Whether to use `jsonName` trait
51
+ # or just member name. The `jsonName` trait is ignored by default.
52
+ def serialize(type_registry, opts = {})
53
+ validate_document(type_registry)
54
+
55
+ opts[:type_registry] = type_registry
56
+ opts[:json] = true
57
+ serializer = DocumentUtils::Serializer.new(opts)
58
+ serializer.format_document_data(type_registry[@discriminator], @data)
59
+ end
60
+
61
+ # Deserializes a {Document} into a type.
62
+ #
63
+ # @param [TypeRegistry, nil] type_registry Registry is required for
64
+ # identifying and deserializing typed documents. Either this or shape
65
+ # must be provided.
66
+ # @param [StructureShape, nil] shape shape to use for deserialization.
67
+ # If provided, this shape takes precedence over the document's discriminator.
68
+ # The shape must have a type.
69
+ def deserialize(type_registry: nil, shape: nil)
70
+ msg = 'either a type registry or a structure shape must be provided to deserialize'
71
+ raise ArgumentError, msg unless type_registry || shape
72
+
73
+ type_registry.nil? ? validate_shape(shape) : validate_document(type_registry)
74
+
75
+ shape ||= type_registry[@discriminator]
76
+ deserializer = DocumentUtils::Deserializer.new(type_registry: type_registry)
77
+ deserializer.deserialize(@data, shape, shape.type.new)
78
+ end
79
+
80
+ private
81
+
82
+ def validate_document(type_registry)
83
+ msg = 'unable validate typed document - must have a discriminator'
84
+ raise ArgumentError, msg unless @discriminator
85
+
86
+ msg = 'document discriminator not found in type registry'
87
+ raise ArgumentError, msg unless type_registry.key?(@discriminator)
88
+ end
89
+
90
+ def validate_shape(shape)
91
+ msg = 'invalid shape - must be a structure shape with type'
92
+ raise ArgumentError, msg unless shape.is_a?(Shapes::StructureShape) && shape.type
93
+ end
94
+
95
+ class << self
96
+ # Create a {Document} from various input formats.
97
+ #
98
+ # @param [Object] data Input data could be one of the following: a Ruby object,
99
+ # a Struct type, or a parsed JSON with type discriminator key.
100
+ # @param [TypeRegistry, nil] type_registry Type Registry is required for
101
+ # identifying and serializing typed documents. Option for untyped documents.
102
+ # @return [Document] document
103
+ #
104
+ # @example Ruby Object as input
105
+ # # creating an untyped document
106
+ # document = Smithy::Schema::Document.create(foo: "bar")
107
+ # # => {"foo" => "bar"}
108
+ # @example Structure type as input
109
+ # structure = some_structure.type.new(some_data)
110
+ # # => #<struct SampleService::Types::Structure ...>
111
+ #
112
+ # # Type Registry is required to properly serialize
113
+ # document = Smithy::Schema::Document.create(structure, type_registry)
114
+ # # => #<Smithy::Schema::Document ...>
115
+ # @example JSON data
116
+ # # given the following json data
117
+ # parsed_json = {
118
+ # "__type" => "smithy.ruby.tests#Structure",
119
+ # "string" => "hello"
120
+ # }
121
+ #
122
+ # document = serializer.create(parsed_json, type_registry)
123
+ # # => an instance of Smithy::Schema::Document
124
+ # document.discriminator
125
+ # # => "smithy.ruby.tests#Structure"
126
+ def create(data, type_registry = nil)
127
+ raise ArgumentError, 'invalid data - document cannot be nil' if data.nil?
128
+
129
+ return untyped_document(data) if type_registry.nil?
130
+
131
+ validate_typed_data(data, type_registry)
132
+ typed_document(data, type_registry)
133
+ end
134
+
135
+ private
136
+
137
+ def discriminator?(data)
138
+ data.is_a?(Hash) && data.key?('__type')
139
+ end
140
+
141
+ def untyped_document(data)
142
+ serializer = DocumentUtils::Serializer.new
143
+ new(serializer.serialize_untyped(data))
144
+ end
145
+
146
+ def typed_document(data, type_registry)
147
+ opts = { type_registry: type_registry }
148
+ case data
149
+ when Structure
150
+ shape = type_registry.shape_by_type(data.class)
151
+ else
152
+ opts = opts.merge(json: true, json_name: true)
153
+ shape = type_registry[data['__type']]
154
+ end
155
+ serializer = DocumentUtils::Serializer.new(opts)
156
+ new(serializer.format_document_data(shape, data), discriminator: shape.id)
157
+ end
158
+
159
+ def validate_typed_data(data, type_registry)
160
+ case data
161
+ when Structure
162
+ msg = 'given type class not found in type registry'
163
+ raise ArgumentError, msg unless type_registry.shape_by_type?(data.class)
164
+ else
165
+ msg = 'document discriminator not found in type registry'
166
+ raise ArgumentError, msg if discriminator?(data) && !type_registry.key?(data['__type'])
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Schema
5
+ module DocumentUtils
6
+ # Deserializes document data into a type.
7
+ # @api private
8
+ class Deserializer
9
+ include Shapes
10
+
11
+ def initialize(options = {})
12
+ @type_registry = options[:type_registry]
13
+ end
14
+
15
+ def deserialize(data, shape, target)
16
+ ref = shape.is_a?(ShapeRef) ? shape : ShapeRef.new(shape: shape)
17
+ shape(ref, data, target)
18
+ end
19
+
20
+ private
21
+
22
+ def shape(ref, value, target = nil) # rubocop:disable Metrics/CyclomaticComplexity
23
+ case ref.shape
24
+ when BlobShape then Base64.strict_decode64(value)
25
+ when DocumentShape then document(value)
26
+ when FloatShape then float(value)
27
+ when ListShape then list(ref, value, target)
28
+ when MapShape then map(ref, value, target)
29
+ when StructureShape then structure(ref, value, target)
30
+ when TimestampShape then timestamp(value)
31
+ when UnionShape then union(ref, value, target)
32
+ else value
33
+ end
34
+ end
35
+
36
+ def document(values)
37
+ return values unless values.is_a?(Hash) && values.key?('__type')
38
+
39
+ msg = 'invalid document - document discriminator not found in type registry'
40
+ raise ArgumentError, msg unless @type_registry.key?(values['__type'])
41
+
42
+ shape(ShapeRef.new(shape: @type_registry[values['__type']]), values)
43
+ end
44
+
45
+ def float(value)
46
+ case value
47
+ when 'Infinity' then ::Float::INFINITY
48
+ when '-Infinity' then -::Float::INFINITY
49
+ when 'NaN' then ::Float::NAN
50
+ when nil then nil
51
+ else value.to_f
52
+ end
53
+ end
54
+
55
+ def list(ref, values, target = nil)
56
+ return if values.nil?
57
+
58
+ target = [] if target.nil?
59
+ values.each do |value|
60
+ target << shape(ref.shape.member, value) unless value.nil?
61
+ end
62
+ target
63
+ end
64
+
65
+ def map(ref, values, target = nil)
66
+ return if values.nil?
67
+
68
+ target = {} if target.nil?
69
+ values.each do |key, value|
70
+ target[key] = shape(ref.shape.value, value) unless value.nil?
71
+ end
72
+ target
73
+ end
74
+
75
+ def structure(ref, values, target = nil)
76
+ return if values.nil?
77
+
78
+ target = ref.shape.type.new if target.nil?
79
+ ref.shape.members.each do |member_name, member_ref|
80
+ value = values[location_name(member_ref)]
81
+ target[member_name] = shape(member_ref, value) unless value.nil?
82
+ end
83
+ target
84
+ end
85
+
86
+ def timestamp(value)
87
+ case value
88
+ when nil then nil
89
+ when Numeric
90
+ Time.at(value).utc
91
+ when /^[\d.]+$/
92
+ Time.at(value.to_f).utc
93
+ else
94
+ begin
95
+ fractional_time = Time.parse(value).to_f
96
+ Time.at(fractional_time).utc
97
+ rescue ArgumentError
98
+ raise "unhandled timestamp format `#{value}'"
99
+ end
100
+ end
101
+ end
102
+
103
+ def union(ref, values, target = nil) # rubocop:disable Metrics/AbcSize
104
+ ref.shape.members.each do |member_name, member_ref|
105
+ value = values[location_name(member_ref)]
106
+ next if value.nil?
107
+
108
+ target = ref.shape.member_type(member_name) if target.nil?
109
+ return target.new(member_name => shape(member_ref, value))
110
+ end
111
+
112
+ values.delete('__type')
113
+ key, value = values.first
114
+ ref.shape.member_type(:unknown).new(key, value)
115
+ end
116
+
117
+ def location_name(ref)
118
+ return ref.member_name unless @json_name
119
+
120
+ ref.traits['smithy.api#jsonName'] || ref.member_name
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'time'
5
+
6
+ module Smithy
7
+ module Schema
8
+ module DocumentUtils
9
+ # Serializes data into a document data.
10
+ # @api private
11
+ class Serializer
12
+ include Shapes
13
+
14
+ def initialize(options = {})
15
+ @type_registry = options[:type_registry]
16
+ @json = options[:json] || false
17
+ @json_name = options[:json_name] || false
18
+ @timestamp_format = options[:timestamp_format] || false
19
+ end
20
+
21
+ def format_document_data(shape, data)
22
+ ref = shape.is_a?(ShapeRef) ? shape : ShapeRef.new(shape: shape)
23
+ document_data = shape(ref, data)
24
+ document_data['__type'] = shape.id
25
+ document_data
26
+ end
27
+
28
+ def serialize_untyped(values)
29
+ return if values.nil?
30
+
31
+ case values
32
+ when Time then values.utc.to_i # timestamp format is "epoch-seconds" by default
33
+ when Hash
34
+ values.each_with_object({}) do |(k, v), h|
35
+ h[k.to_s] = serialize_untyped(v)
36
+ end
37
+ when Array then values.map { |d| serialize_untyped(d) }
38
+ else values
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def shape(ref, values) # rubocop:disable Metrics/CyclomaticComplexity
45
+ case ref.shape
46
+ when BlobShape then blob(values)
47
+ when DocumentShape then document(values)
48
+ when FloatShape then float(values)
49
+ when ListShape then list(ref, values)
50
+ when MapShape then map(ref, values)
51
+ when StructureShape then structure(ref, values)
52
+ when TimestampShape then timestamp(ref, values)
53
+ when UnionShape then union(ref, values)
54
+ else values
55
+ end
56
+ end
57
+
58
+ def blob(value)
59
+ return value if @json # blob is already encoded
60
+
61
+ Base64.strict_encode64(value.respond_to?(:read) ? value.read : value)
62
+ end
63
+
64
+ def document(values)
65
+ shape = document_shape(values)
66
+ return values unless shape
67
+
68
+ format_document_data(shape, values)
69
+ end
70
+
71
+ def document_shape(values)
72
+ case values
73
+ when Structure
74
+ @type_registry.shape_by_type(values.class)
75
+ when Hash
76
+ @type_registry[values['__type']]
77
+ end
78
+ end
79
+
80
+ def float(value)
81
+ if value == ::Float::INFINITY
82
+ 'Infinity'
83
+ elsif value == -::Float::INFINITY
84
+ '-Infinity'
85
+ elsif value.to_f.nan?
86
+ 'NaN'
87
+ else
88
+ value.to_f
89
+ end
90
+ end
91
+
92
+ def list(ref, values)
93
+ return if values.nil?
94
+
95
+ shape = ref.shape
96
+ values.collect do |value|
97
+ shape(shape.member, value)
98
+ end
99
+ end
100
+
101
+ def map(ref, values)
102
+ return if values.nil?
103
+
104
+ shape = ref.shape
105
+ values.each.with_object({}) do |(key, value), data|
106
+ data[key.to_s] = shape(shape.value, value)
107
+ end
108
+ end
109
+
110
+ def structure(ref, values)
111
+ return if values.nil?
112
+
113
+ ref.shape.members.each_with_object({}) do |(member_name, member_ref), data|
114
+ value = resolve_value(member_name, member_ref, values.to_h)
115
+ data[location_name(member_ref)] = shape(member_ref, value) unless value.nil?
116
+ end
117
+ end
118
+
119
+ def timestamp(ref, value)
120
+ value = normalize_timestamp_value(value)
121
+ return value.to_i unless @timestamp_format
122
+
123
+ trait = 'smithy.api#timestampFormat'
124
+ case ref.traits[trait] || ref.shape.traits[trait]
125
+ when 'date-time' then value.utc.iso8601
126
+ when 'http-date' then value.utc.httpdate
127
+ else
128
+ # default to epoch-seconds
129
+ value.to_i
130
+ end
131
+ end
132
+
133
+ def union(ref, values)
134
+ return if values.nil?
135
+
136
+ data = {}
137
+ if values.is_a?(Union)
138
+ _name, member_ref = ref.shape.member_by_type(values.class)
139
+ data[location_name(member_ref)] = shape(member_ref, values)
140
+ else
141
+ key, value = values.first
142
+ if (member_ref = resolve_member_ref(ref, key))
143
+ data[location_name(member_ref)] = shape(member_ref, value)
144
+ end
145
+ end
146
+ data
147
+ end
148
+
149
+ def location_name(ref)
150
+ return ref.member_name unless @json_name
151
+
152
+ ref.traits['smithy.api#jsonName'] || ref.member_name
153
+ end
154
+
155
+ def normalize_timestamp_value(value)
156
+ case value
157
+ when Time then value
158
+ when Numeric then Time.at(value)
159
+ else Time.parse(value)
160
+ end
161
+ end
162
+
163
+ def resolve_member_ref(ref, name)
164
+ return ref.shape.member(name) if ref.shape.member?(name)
165
+
166
+ ref.shape.members.values.find do |member_ref|
167
+ member_ref.traits['smithy.api#jsonName'] == name || member_ref.member_name == name
168
+ end
169
+ end
170
+
171
+ def resolve_value(member_name, member_ref, values)
172
+ if (json_name = member_ref.traits['smithy.api#jsonName'])
173
+ value = values[json_name]
174
+ return value unless value.nil?
175
+ end
176
+ values[member_name] || values[member_ref.member_name]
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Schema
5
+ # An empty Struct that includes the {Schema::Structure} module.
6
+ class EmptyStructure < Struct.new(nil) # rubocop:disable Style/StructInheritance
7
+ include Smithy::Schema::Structure
8
+ end
9
+ end
10
+ end
@@ -9,6 +9,7 @@ module Smithy
9
9
  def initialize(options = {})
10
10
  @id = options[:id]
11
11
  @traits = options[:traits] || {}
12
+ @metadata = {}
12
13
  end
13
14
 
14
15
  # @return [String, nil] Absolute shape ID from model
@@ -16,53 +17,46 @@ module Smithy
16
17
 
17
18
  # @return [Hash<String, Object>]
18
19
  attr_accessor :traits
19
- end
20
20
 
21
- # Represents an aggregate shape that has members.
22
- class Structure < Shape
23
- def initialize(options = {})
24
- super
25
- @members = {}
26
- @names_by_member_name = {}
21
+ # @return [Object]
22
+ def [](key)
23
+ @metadata[key]
27
24
  end
28
25
 
29
- # @return [Hash<Symbol, MemberShape>]
30
- attr_accessor :members
31
-
32
- # @return [Hash<String, Symbol>]
33
- attr_accessor :names_by_member_name
34
-
35
- # @return [Class, nil]
36
- attr_accessor :type
37
-
38
- # @return [MemberShape]
39
- def add_member(name, member_name, shape, traits: {})
40
- @names_by_member_name[member_name] = name
41
- @members[name] = MemberShape.new(member_name, shape, traits: traits)
26
+ # @param [Symbol] key
27
+ # @param [Object] value
28
+ def []=(key, value)
29
+ @metadata[key] = value
42
30
  end
31
+ end
43
32
 
44
- # @param [Symbol] name
45
- # @return [Boolean]
46
- def member?(name)
47
- @members.key?(name)
33
+ # A reference to a shape.
34
+ class ShapeRef
35
+ def initialize(options = {})
36
+ @shape = options[:shape]
37
+ @member_name = options[:member_name]
38
+ @traits = options[:traits] || {}
39
+ @metadata = {}
48
40
  end
49
41
 
50
- # @param [Symbol] name
51
- # @return [MemberShape, nil]
52
- def member(name)
53
- @members[name]
54
- end
42
+ # @return [Shape]
43
+ attr_reader :shape
55
44
 
56
- # @param [String] member_name
57
- # @return [Boolean]
58
- def name_by_member_name?(member_name)
59
- @names_by_member_name.key?(member_name)
45
+ # @return [String, nil]
46
+ attr_reader :member_name
47
+
48
+ # @return [Hash<String, Object>]
49
+ attr_reader :traits
50
+
51
+ # @return [Object]
52
+ def [](key)
53
+ @metadata[key]
60
54
  end
61
55
 
62
- # @param [String] member_name
63
- # @return [Symbol, nil]
64
- def name_by_member_name(member_name)
65
- @names_by_member_name[member_name]
56
+ # @param [Symbol] key
57
+ # @param [Object] value
58
+ def []=(key, value)
59
+ @metadata[key] = value
66
60
  end
67
61
  end
68
62
 
@@ -78,7 +72,7 @@ module Smithy
78
72
  yield self if block_given?
79
73
  end
80
74
 
81
- # @return [String, nil] Service name
75
+ # @return [String]
82
76
  attr_accessor :name
83
77
 
84
78
  # @return [String, nil]
@@ -122,16 +116,16 @@ module Smithy
122
116
  yield self if block_given?
123
117
  end
124
118
 
125
- # @return [String, nil] Operation name
119
+ # @return [String]
126
120
  attr_accessor :name
127
121
 
128
- # @return [StructureShape, nil]
122
+ # @return [ShapeRef]
129
123
  attr_accessor :input
130
124
 
131
- # @return [StructureShape, nil]
125
+ # @return [ShapeRef]
132
126
  attr_accessor :output
133
127
 
134
- # @return [Array<StructureShape>]
128
+ # @return [Array<ShapeRef>]
135
129
  attr_accessor :errors
136
130
  end
137
131
 
@@ -148,87 +142,157 @@ module Smithy
148
142
  class DocumentShape < Shape; end
149
143
 
150
144
  # Represents an Enum shape.
151
- class EnumShape < Structure; end
145
+ class EnumShape < Shape
146
+ def initialize(options = {})
147
+ super
148
+ @members = {}
149
+ end
150
+
151
+ # @return [Hash<Symbol, ShapeRef>]
152
+ attr_accessor :members
153
+
154
+ # @return [ShapeRef]
155
+ def add_member(name, shape_ref)
156
+ @members[name] = shape_ref
157
+ end
158
+
159
+ # @param [Symbol] name
160
+ # @return [Boolean]
161
+ def member?(name)
162
+ @members.key?(name)
163
+ end
164
+
165
+ # @param [Symbol] name
166
+ # @return [ShapeRef, nil]
167
+ def member(name)
168
+ @members[name]
169
+ end
170
+ end
152
171
 
153
172
  # Represents the following shapes: Byte, Short, Integer, Long, BigInteger.
154
173
  class IntegerShape < Shape; end
155
174
 
156
175
  # Represents an IntEnum shape.
157
- class IntEnumShape < Structure; end
176
+ class IntEnumShape < Shape
177
+ def initialize(options = {})
178
+ super
179
+ @members = {}
180
+ end
181
+
182
+ # @return [Hash<Symbol, ShapeRef>]
183
+ attr_accessor :members
184
+
185
+ # @return [ShapeRef]
186
+ def add_member(name, shape_ref)
187
+ @members[name] = shape_ref
188
+ end
189
+
190
+ # @param [Symbol] name
191
+ # @return [Boolean]
192
+ def member?(name)
193
+ @members.key?(name)
194
+ end
195
+
196
+ # @param [Symbol] name
197
+ # @return [ShapeRef, nil]
198
+ def member(name)
199
+ @members[name]
200
+ end
201
+ end
158
202
 
159
203
  # Represents both Float and Double shapes.
160
204
  class FloatShape < Shape; end
161
205
 
162
206
  # Represents a List shape.
163
207
  class ListShape < Shape
164
- def initialize(options = {})
165
- super
166
- @member = nil
167
- end
168
-
169
- # @return [MemberShape, nil]
208
+ # @return [ShapeRef]
170
209
  attr_accessor :member
171
-
172
- def set_member(shape, traits: {})
173
- @member = MemberShape.new('member', shape, traits: traits)
174
- end
175
210
  end
176
211
 
177
212
  # Represents a Map shape.
178
213
  class MapShape < Shape
214
+ # @return [ShapeRef]
215
+ attr_accessor :key
216
+
217
+ # @return [ShapeRef]
218
+ attr_accessor :value
219
+ end
220
+
221
+ # Represents a String shape.
222
+ class StringShape < Shape; end
223
+
224
+ # Represents a Structure shape.
225
+ class StructureShape < Shape
179
226
  def initialize(options = {})
180
227
  super
181
- @key = nil
182
- @value = nil
228
+ @members = {}
183
229
  end
184
230
 
185
- # @return [MemberShape, nil]
186
- attr_accessor :key
231
+ # @return [Hash<Symbol, ShapeRef>]
232
+ attr_accessor :members
187
233
 
188
- # @return [MemberShape, nil]
189
- attr_accessor :value
234
+ # @return [Class]
235
+ attr_accessor :type
190
236
 
191
- def set_key(shape, traits: {})
192
- @key = MemberShape.new('key', shape, traits: traits)
237
+ # @return [ShapeRef]
238
+ def add_member(name, shape_ref)
239
+ @members[name] = shape_ref
193
240
  end
194
241
 
195
- def set_value(shape, traits: {})
196
- @value = MemberShape.new('value', shape, traits: traits)
242
+ # @param [Symbol] name
243
+ # @return [Boolean]
244
+ def member?(name)
245
+ @members.key?(name)
197
246
  end
198
- end
199
-
200
- # Represents a String shape.
201
- class StringShape < Shape; end
202
247
 
203
- # Represents a Structure shape.
204
- class StructureShape < Structure
248
+ # @param [Symbol] name
249
+ # @return [ShapeRef, nil]
250
+ def member(name)
251
+ @members[name]
252
+ end
205
253
  end
206
254
 
207
255
  # Represents a Timestamp shape.
208
256
  class TimestampShape < Shape; end
209
257
 
210
258
  # Represents both Union and EventStream shapes.
211
- class UnionShape < Structure
259
+ class UnionShape < Shape
212
260
  def initialize(options = {})
213
261
  super
262
+ @members = {}
214
263
  @member_types = {}
215
264
  @members_by_type = {}
216
265
  end
217
266
 
267
+ # @return [Hash<Symbol, ShapeRef>]
268
+ attr_accessor :members
269
+
218
270
  # @return [Hash<Symbol, Class>]
219
- attr_accessor :member_types
271
+ attr_reader :member_types
272
+
273
+ # @return [Hash<Class, [String, ShapeRef]>]
274
+ attr_reader :members_by_type
220
275
 
221
- # @return [Hash<Class, MemberShape>]
222
- attr_accessor :members_by_type
276
+ # @return [Class]
277
+ attr_accessor :type
223
278
 
224
- # @return [MemberShape]
225
- def add_member(name, member_name, shape, type, traits: {})
226
- member = MemberShape.new(member_name, shape, traits: traits)
227
- @members[name] = member
228
- @names_by_member_name[member_name] = name
279
+ # @return [ShapeRef]
280
+ def add_member(name, type, shape_ref)
229
281
  @member_types[name] = type
230
- @members_by_type[type] = member
231
- member
282
+ @members_by_type[type] = [name, shape_ref]
283
+ @members[name] = shape_ref
284
+ end
285
+
286
+ # @param [Symbol] name
287
+ # @return [Boolean]
288
+ def member?(name)
289
+ @members.key?(name)
290
+ end
291
+
292
+ # @param [Symbol] name
293
+ # @return [ShapeRef, nil]
294
+ def member(name)
295
+ @members[name]
232
296
  end
233
297
 
234
298
  # @param [Symbol] name
@@ -250,30 +314,12 @@ module Smithy
250
314
  end
251
315
 
252
316
  # @param [Class] type
253
- # @return [MemberShape, nil]
317
+ # @return [ShapeRef, nil]
254
318
  def member_by_type(type)
255
319
  @members_by_type[type]
256
320
  end
257
321
  end
258
322
 
259
- # Represents a member shape.
260
- class MemberShape
261
- def initialize(name, shape, traits: {})
262
- @name = name
263
- @shape = shape
264
- @traits = traits
265
- end
266
-
267
- # @return [String] Member name
268
- attr_accessor :name
269
-
270
- # @return [Shape] Referenced shape
271
- attr_accessor :shape
272
-
273
- # @return [Hash<String, Object>]
274
- attr_accessor :traits
275
- end
276
-
277
323
  # Prelude shape definitions.
278
324
  module Prelude
279
325
  BigDecimal = BigDecimalShape.new(id: 'smithy.api#BigDecimal')
@@ -321,6 +367,7 @@ module Smithy
321
367
  id: 'smithy.api#Unit',
322
368
  traits: { 'smithy.api#unitType' => {} }
323
369
  )
370
+ Unit.type = Schema::EmptyStructure
324
371
  end
325
372
  end
326
373
  end
@@ -2,15 +2,13 @@
2
2
 
3
3
  module Smithy
4
4
  module Schema
5
- # A module mixed into Structs that provides utility methods.
5
+ # A module mixed into Structs that provides utility methods for Structure shapes.
6
6
  module Structure
7
7
  # Deeply converts the Struct into a hash. Structure members that
8
8
  # are `nil` are omitted from the resultant hash.
9
9
  # @return [Hash, Structure]
10
10
  def to_h(obj = self)
11
11
  case obj
12
- when Union
13
- obj.to_h
14
12
  when Structure
15
13
  _to_h_structure(obj)
16
14
  when Hash
@@ -42,10 +40,5 @@ module Smithy
42
40
  obj.collect { |value| to_hash(value) }
43
41
  end
44
42
  end
45
-
46
- # An empty Struct that includes the {Client::Structure} module.
47
- EmptyStructure = Struct.new do
48
- include Smithy::Schema::Structure
49
- end
50
43
  end
51
44
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Schema
5
+ # A registry that contains a map of Smithy shape ID to its shape defined in a schema.
6
+ # The registered shapes are limited to {Shapes::StructureShape} with a type.
7
+ #
8
+ # This registry has the following functionalities:
9
+ #
10
+ # * Access shape by shape ID
11
+ # * Access shape by its type
12
+ # * Register shape to the Registry
13
+ # * Supports enumeration of registered shapes
14
+ #
15
+ # You could also combine multiple registries into one {TypeRegistry}.
16
+ #
17
+ # @example Creating a new Registry
18
+ # # accepts an array of structure shapes
19
+ # registry = TypeRegistry.new(StructureShape1, StructureShape2)
20
+ #
21
+ # @example Shape Lookup
22
+ # # Find shape by its id
23
+ # registry["someId"]
24
+ # # => #<Smithy::Schema::Shapes::StructureShape...>
25
+ #
26
+ # # Find shape by its type
27
+ # registry.shape_by_type(ExampleService::Types::Structure)
28
+ # # => #<Smithy::Schema::Shapes::StructureShape...>
29
+ #
30
+ # @example Combining multiple registries
31
+ # registry.concat(registry1, registry2)
32
+ # # => #<Smithy::Schema::TypeRegistry...>
33
+ class TypeRegistry
34
+ include Enumerable
35
+
36
+ # @param [Array<Shapes::StructureShape>] shapes
37
+ def initialize(shapes = [])
38
+ @registry = {}
39
+ @shapes_by_type = {}
40
+ shapes.each do |shape|
41
+ self[shape.id] = shape
42
+ end
43
+ end
44
+
45
+ def each(&)
46
+ @registry.each(&)
47
+ end
48
+
49
+ # @param [String] id
50
+ # @return [Shapes::StructureShape, nil]
51
+ def [](id)
52
+ @registry[id]
53
+ end
54
+
55
+ # @param [String] id
56
+ # @param [Shapes::StructureShape] shape
57
+ def []=(id, shape)
58
+ validate_shape(shape)
59
+ @registry[id] = shape
60
+ @shapes_by_type[shape.type] = shape
61
+ end
62
+
63
+ # @return [Boolean]
64
+ def empty?
65
+ @registry.empty?
66
+ end
67
+
68
+ # @param [String] id
69
+ def key?(id)
70
+ @registry.key?(id)
71
+ end
72
+ alias include? key?
73
+
74
+ # @return [Array<String>]
75
+ def keys
76
+ @registry.keys
77
+ end
78
+
79
+ # @param [Class] type
80
+ def shape_by_type?(type)
81
+ @shapes_by_type.key?(type)
82
+ end
83
+
84
+ # @param [Class] type
85
+ # @return [Shapes::StructureShape, nil]
86
+ def shape_by_type(type)
87
+ @shapes_by_type[type]
88
+ end
89
+
90
+ # @return [Array<Shape::StructureShape>]
91
+ def values
92
+ @registry.values
93
+ end
94
+
95
+ # Merges multiple type registries into a new registry.
96
+ #
97
+ # @param [Array<TypeRegistry>] type_registries
98
+ # @return [TypeRegistry]
99
+ def merge(*type_registries)
100
+ registry = TypeRegistry.new
101
+ @registry.each do |shape_id, shape|
102
+ registry[shape_id] = shape
103
+ end
104
+ type_registries.each do |type_registry|
105
+ unless type_registry.is_a?(TypeRegistry)
106
+ raise ArgumentError, "expected TypeRegistry, got #{type_registry.class}"
107
+ end
108
+
109
+ type_registry.each do |shape_id, shape|
110
+ registry[shape_id] = shape
111
+ end
112
+ end
113
+ registry
114
+ end
115
+
116
+ private
117
+
118
+ def validate_shape(shape)
119
+ return if shape.is_a?(Shapes::StructureShape) && shape.type
120
+
121
+ raise ArgumentError, "expected a StructureShape with a type, got: #{shape.class}"
122
+ end
123
+ end
124
+ end
125
+ end
@@ -4,16 +4,16 @@ require 'delegate'
4
4
 
5
5
  module Smithy
6
6
  module Schema
7
- # Top level class for all generated Union types
8
- class Union < ::SimpleDelegator
7
+ # A module mixed into Structs that provides utility methods for Union shapes.
8
+ module Union
9
9
  include Structure
10
10
 
11
- def to_s
12
- "#<#{self.class.name} #{__getobj__ || 'nil'}>"
11
+ def member
12
+ members.find { |m| !self[m].nil? }
13
13
  end
14
14
 
15
15
  def value
16
- __getobj__
16
+ self[member] if member
17
17
  end
18
18
  end
19
19
  end
data/lib/smithy-schema.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'smithy-schema/shapes'
4
3
  require_relative 'smithy-schema/structure'
4
+ require_relative 'smithy-schema/empty_structure'
5
5
  require_relative 'smithy-schema/union'
6
6
 
7
+ require_relative 'smithy-schema/shapes'
8
+ require_relative 'smithy-schema/document'
9
+ require_relative 'smithy-schema/type_registry'
10
+
7
11
  module Smithy
8
- # Base module for Smithy model classes.
9
- module Model
12
+ # Base module for Smithy schema classes.
13
+ module Schema
10
14
  VERSION = File.read(File.expand_path('../VERSION', __dir__.to_s)).strip
11
15
  end
12
16
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smithy-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre0
4
+ version: 1.0.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amazon Web Services
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-06 00:00:00.000000000 Z
10
+ date: 2025-06-26 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Smithy is a code generation toolkit for creating Client and Server SDKs
13
13
  from Smithy models.
@@ -18,8 +18,13 @@ files:
18
18
  - CHANGELOG.md
19
19
  - VERSION
20
20
  - lib/smithy-schema.rb
21
+ - lib/smithy-schema/document.rb
22
+ - lib/smithy-schema/document_utils/deserializer.rb
23
+ - lib/smithy-schema/document_utils/serializer.rb
24
+ - lib/smithy-schema/empty_structure.rb
21
25
  - lib/smithy-schema/shapes.rb
22
26
  - lib/smithy-schema/structure.rb
27
+ - lib/smithy-schema/type_registry.rb
23
28
  - lib/smithy-schema/union.rb
24
29
  homepage: https://github.com/smithy-lang/smithy-ruby
25
30
  licenses: