Skip to content

Commit 269777a

Browse files
committed
feat(xdr-generator): add SEP-0051 XDR-JSON support.
1 parent 7a51547 commit 269777a

3 files changed

Lines changed: 83 additions & 72 deletions

File tree

xdr-generator/generator/generator.rb

Lines changed: 81 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,87 +1054,102 @@ def render_union_to_json(out, union, union_name, discriminant_name, render_impor
10541054
end
10551055
out.puts "def from_json_dict(cls, json_value: #{union_json_type}) -> #{union_name}:"
10561056
out.indent(2) do
1057-
# String input handling: only valid for void arms
1058-
out.puts "if isinstance(json_value, str):"
1059-
out.indent(2) do
1060-
if has_void_default
1061-
# Void default arm: string input is valid for void arms and default,
1062-
# but must reject non-void arm keys (they require dict form with a value)
1063-
if non_void_keys.any?
1064-
nv_keys_str = non_void_keys.map { |k| "\"#{k}\"" }.join(", ")
1065-
out.puts "if json_value in (#{nv_keys_str},):"
1066-
out.indent(2) do
1067-
out.puts "raise ValueError(f\"'{json_value}' requires a value for #{union_name}, use dict form instead\")"
1068-
end
1069-
end
1070-
if disc_enum
1071-
disc_type_name = name(disc_enum)
1072-
out.puts "#{discriminant_name} = #{disc_type_name}.from_json_dict(json_value)"
1073-
else
1074-
out.puts "#{discriminant_name} = #{non_enum_disc_parse_expr(disc_type_str, 'json_value')}"
1075-
end
1076-
out.puts "return cls(#{discriminant_name}=#{discriminant_name})"
1077-
elsif void_keys.any?
1078-
# Only specific void arms are valid as string input
1079-
keys_str = void_keys.map { |k| "\"#{k}\"" }.join(", ")
1080-
out.puts "if json_value not in (#{keys_str},):"
1057+
if has_void
1058+
if has_non_void
1059+
# Mixed mode: check for string input first (void arms)
1060+
out.puts "if isinstance(json_value, str):"
10811061
out.indent(2) do
1082-
out.puts "raise ValueError(f\"Unexpected string '{json_value}' for #{union_name}, must be one of: #{void_keys.join(', ')}\")"
1062+
render_union_void_from_json(out, union_name, discriminant_name, disc_enum, disc_type_str, void_keys, non_void_keys, has_void_default)
10831063
end
1084-
if disc_enum
1085-
disc_type_name = name(disc_enum)
1086-
out.puts "#{discriminant_name} = #{disc_type_name}.from_json_dict(json_value)"
1087-
else
1088-
out.puts "#{discriminant_name} = #{non_enum_disc_parse_expr(disc_type_str, 'json_value')}"
1089-
end
1090-
out.puts "return cls(#{discriminant_name}=#{discriminant_name})"
10911064
else
1092-
# No void arms at all: string input is never valid
1093-
out.puts "raise ValueError(f\"Unexpected string input for #{union_name}: {json_value}\")"
1065+
# Only void arms: json_value is always str, handle directly
1066+
render_union_void_from_json(out, union_name, discriminant_name, disc_enum, disc_type_str, void_keys, non_void_keys, has_void_default)
10941067
end
10951068
end
10961069

1097-
# Dict input handling: validate single key
1098-
out.puts "if not isinstance(json_value, dict) or len(json_value) != 1:"
1099-
out.indent(2) do
1100-
out.puts "raise ValueError(f\"Expected a single-key object for #{union_name}, got: {json_value}\")"
1101-
end
1070+
if has_non_void
1071+
# Dict input validation
1072+
if has_void
1073+
out.puts "if not isinstance(json_value, dict) or len(json_value) != 1:"
1074+
else
1075+
# Only non-void arms: json_value is always dict, skip isinstance check
1076+
out.puts "if len(json_value) != 1:"
1077+
end
1078+
out.indent(2) do
1079+
out.puts "raise ValueError(f\"Expected a single-key object for #{union_name}, got: {json_value}\")"
1080+
end
11021081

1103-
out.puts "key = next(iter(json_value))"
1104-
if disc_enum
1105-
disc_type_name = name(disc_enum)
1106-
out.puts "#{discriminant_name} = #{disc_type_name}.from_json_dict(key)"
1107-
else
1108-
out.puts "#{discriminant_name} = #{non_enum_disc_parse_expr(disc_type_str, 'key')}"
1109-
end
1082+
out.puts "key = next(iter(json_value))"
1083+
if disc_enum
1084+
disc_type_name = name(disc_enum)
1085+
out.puts "#{discriminant_name} = #{disc_type_name}.from_json_dict(key)"
1086+
else
1087+
out.puts "#{discriminant_name} = #{non_enum_disc_parse_expr(disc_type_str, 'key')}"
1088+
end
11101089

1111-
union.normal_arms.each do |arm|
1112-
next if arm.void?
1113-
arm.cases.each do |union_case|
1114-
json_key = json_key_for_case(union_case, disc_enum)
1115-
arm_name = safe_identifier(arm.name.underscore)
1116-
local_var = safe_local_var(arm_name)
1090+
union.normal_arms.each do |arm|
1091+
next if arm.void?
1092+
arm.cases.each do |union_case|
1093+
json_key = json_key_for_case(union_case, disc_enum)
1094+
arm_name = safe_identifier(arm.name.underscore)
1095+
local_var = safe_local_var(arm_name)
11171096

1118-
out.puts "if key == \"#{json_key}\":"
1119-
out.indent(2) do
1120-
render_import(out, arm.declaration, union_name, track: false) if render_import_in_func
1121-
decode_expr = decode_union_arm_value(arm, "json_value[\"#{json_key}\"]")
1122-
out.puts "#{local_var} = #{decode_expr}"
1123-
out.puts "return cls(#{discriminant_name}=#{discriminant_name}, #{arm_name}=#{local_var})"
1097+
out.puts "if key == \"#{json_key}\":"
1098+
out.indent(2) do
1099+
render_import(out, arm.declaration, union_name, track: false) if render_import_in_func
1100+
decode_expr = decode_union_arm_value(arm, "json_value[\"#{json_key}\"]")
1101+
out.puts "#{local_var} = #{decode_expr}"
1102+
out.puts "return cls(#{discriminant_name}=#{discriminant_name}, #{arm_name}=#{local_var})"
1103+
end
11241104
end
11251105
end
1106+
1107+
if union.default_arm.present? && !union.default_arm.void?
1108+
arm_name = safe_identifier(union.default_arm.name.underscore)
1109+
local_var = safe_local_var(arm_name)
1110+
render_import(out, union.default_arm.declaration, union_name, track: false) if render_import_in_func
1111+
decode_expr = decode_union_arm_value(union.default_arm, "json_value[key]")
1112+
out.puts "#{local_var} = #{decode_expr}"
1113+
out.puts "return cls(#{discriminant_name}=#{discriminant_name}, #{arm_name}=#{local_var})"
1114+
else
1115+
out.puts "raise ValueError(f\"Unknown key '{key}' for #{union_name}\")"
1116+
end
11261117
end
1118+
end
1119+
end
11271120

1128-
if union.default_arm.present? && !union.default_arm.void?
1129-
arm_name = safe_identifier(union.default_arm.name.underscore)
1130-
local_var = safe_local_var(arm_name)
1131-
render_import(out, union.default_arm.declaration, union_name, track: false) if render_import_in_func
1132-
decode_expr = decode_union_arm_value(union.default_arm, "json_value[key]")
1133-
out.puts "#{local_var} = #{decode_expr}"
1134-
out.puts "return cls(#{discriminant_name}=#{discriminant_name}, #{arm_name}=#{local_var})"
1121+
def render_union_void_from_json(out, union_name, discriminant_name, disc_enum, disc_type_str, void_keys, non_void_keys, has_void_default)
1122+
if has_void_default
1123+
# Void default arm: string input is valid for void arms and default,
1124+
# but must reject non-void arm keys (they require dict form with a value)
1125+
if non_void_keys.any?
1126+
nv_keys_str = non_void_keys.map { |k| "\"#{k}\"" }.join(", ")
1127+
out.puts "if json_value in (#{nv_keys_str},):"
1128+
out.indent(2) do
1129+
out.puts "raise ValueError(f\"'{json_value}' requires a value for #{union_name}, use dict form instead\")"
1130+
end
1131+
end
1132+
if disc_enum
1133+
disc_type_name = name(disc_enum)
1134+
out.puts "#{discriminant_name} = #{disc_type_name}.from_json_dict(json_value)"
1135+
else
1136+
out.puts "#{discriminant_name} = #{non_enum_disc_parse_expr(disc_type_str, 'json_value')}"
1137+
end
1138+
out.puts "return cls(#{discriminant_name}=#{discriminant_name})"
1139+
elsif void_keys.any?
1140+
# Only specific void arms are valid as string input
1141+
keys_str = void_keys.map { |k| "\"#{k}\"" }.join(", ")
1142+
out.puts "if json_value not in (#{keys_str},):"
1143+
out.indent(2) do
1144+
out.puts "raise ValueError(f\"Unexpected string '{json_value}' for #{union_name}, must be one of: #{void_keys.join(', ')}\")"
1145+
end
1146+
if disc_enum
1147+
disc_type_name = name(disc_enum)
1148+
out.puts "#{discriminant_name} = #{disc_type_name}.from_json_dict(json_value)"
11351149
else
1136-
out.puts "raise ValueError(f\"Unknown key '{key}' for #{union_name}\")"
1150+
out.puts "#{discriminant_name} = #{non_enum_disc_parse_expr(disc_type_str, 'json_value')}"
11371151
end
1152+
out.puts "return cls(#{discriminant_name}=#{discriminant_name})"
11381153
end
11391154
end
11401155

xdr-generator/test/snapshots/union/int_union.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@ def to_json_dict(self):
110110
raise ValueError(f"Unknown type in IntUnion: {self.type}")
111111
@classmethod
112112
def from_json_dict(cls, json_value: dict) -> IntUnion:
113-
if isinstance(json_value, str):
114-
raise ValueError(f"Unexpected string input for IntUnion: {json_value}")
115-
if not isinstance(json_value, dict) or len(json_value) != 1:
113+
if len(json_value) != 1:
116114
raise ValueError(f"Expected a single-key object for IntUnion, got: {json_value}")
117115
key = next(iter(json_value))
118116
type = int(key[1:])

xdr-generator/test/snapshots/union/my_union.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@ def to_json_dict(self):
112112
raise ValueError(f"Unknown type in MyUnion: {self.type}")
113113
@classmethod
114114
def from_json_dict(cls, json_value: dict) -> MyUnion:
115-
if isinstance(json_value, str):
116-
raise ValueError(f"Unexpected string input for MyUnion: {json_value}")
117-
if not isinstance(json_value, dict) or len(json_value) != 1:
115+
if len(json_value) != 1:
118116
raise ValueError(f"Expected a single-key object for MyUnion, got: {json_value}")
119117
key = next(iter(json_value))
120118
type = UnionKey.from_json_dict(key)

0 commit comments

Comments
 (0)