Commit 12c3a0f1 authored by Roman Alifanov's avatar Roman Alifanov

Add recursive nested class support for json.unmarshal/marshal

parent 1d92a91e
......@@ -892,16 +892,41 @@ print (user.name) # Alice
print (user.age) # 30
```
Создаёт экземпляр указанного класса и заполняет поля из JSON. Второй аргумент — имя класса (не строка, а идентификатор). Работает только со скалярными полями (string, int, float, bool).
Создаёт экземпляр указанного класса и заполняет поля из JSON. Второй аргумент — имя класса (не строка, а идентификатор).
**Вложенные классы:**
```
class Chat {
id: string = ""
username: string = ""
}
class Message {
chat: Chat = ""
text: string = ""
}
json_str = '\{"chat":\{"id":"456","username":"alice"\},"text":"hello"\}'
msg = json.unmarshal (json_str, Message)
print (msg.text) # hello
print (msg.chat.id) # 456
print (msg.chat.username) # alice
```
Если поле класса имеет тип другого класса, unmarshal рекурсивно создаёт вложенные объекты. Поддерживается произвольная глубина вложенности.
**json.marshal — объект класса в JSON:**
```
output = json.marshal (user)
print (output) # {"name":"Alice","age":30,"active":true}
output = json.marshal (msg)
print (output) # {"chat":{"id":"456","username":"alice"},"text":"hello"}
```
Сериализует поля объекта в JSON-строку. Числовые и булевы типы выводятся без кавычек, строки — в кавычках.
Сериализует поля объекта в JSON-строку. Числовые и булевы типы выводятся без кавычек, строки — в кавычках. Вложенные объекты сериализуются рекурсивно.
**Пример: Telegram бот**
......@@ -958,7 +983,7 @@ obj = reflect.create ("User")
| `reflect.class_name (obj)` | Возвращает имя класса объекта |
| `reflect.create (class_name)` | Создаёт новый экземпляр класса по строковому имени |
**Ограничения:** работает только со скалярными полями (string, int, float, bool). Массивы и вложенные объекты не поддерживаются.
**Ограничения:** массивы в полях не поддерживаются.
### logger
......
......@@ -210,21 +210,27 @@ evens = numbers.filter (x => x % 2 == 0) # [2, 4]
### JSON Marshal/Unmarshal
Go-style JSON serialization with classes:
Go-style JSON serialization with classes, including nested class support:
```
class User {
name: string = ""
age: int = 0
class Chat {
id: string = ""
username: string = ""
}
class Message {
chat: Chat = ""
text: string = ""
}
# JSON → class instance
user = json.unmarshal ('\{"name":"Alice","age":30\}', User)
print (user.name) # Alice
# JSON → class instance (recursive for nested classes)
msg = json.unmarshal ('\{"chat":\{"id":"456","username":"alice"\},"text":"hello"\}', Message)
print (msg.text) # hello
print (msg.chat.id) # 456
# Class instance → JSON
output = json.marshal (user)
print (output) # {"name":"Alice","age":30}
# Class instance → JSON (recursive)
output = json.marshal (msg)
print (output) # {"chat":{"id":"456","username":"alice"},"text":"hello"}
```
### Reflect (Runtime Introspection)
......@@ -469,12 +475,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo
```
Features:
- `Message` class for structured message handling
- Nested classes (`Update``TgMessage``Chat`/`User`) with recursive `json.unmarshal()`
- User decorators for command registration (`@bot.command("start")`)
- Callbacks for message handlers
- `json.marshal()` for JSON logging of incoming messages
- `json.marshal()` for recursive JSON serialization of nested objects
- `reflect` for runtime introspection (`/info` command)
- `json.get()` for parsing Telegram API responses
- `str.urlencode()` for UTF-8 URL encoding
## Documentation
......
......@@ -210,21 +210,27 @@ evens = numbers.filter (x => x % 2 == 0) # [2, 4]
### JSON Marshal/Unmarshal
Сериализация JSON в стиле Go через классы:
Сериализация JSON в стиле Go через классы с поддержкой вложенных классов:
```
class User {
name: string = ""
age: int = 0
class Chat {
id: string = ""
username: string = ""
}
class Message {
chat: Chat = ""
text: string = ""
}
# JSON → экземпляр класса
user = json.unmarshal ('\{"name":"Alice","age":30\}', User)
print (user.name) # Alice
# JSON → экземпляр класса (рекурсивно для вложенных классов)
msg = json.unmarshal ('\{"chat":\{"id":"456","username":"alice"\},"text":"hello"\}', Message)
print (msg.text) # hello
print (msg.chat.id) # 456
# Экземпляр класса → JSON
output = json.marshal (user)
print (output) # {"name":"Alice","age":30}
# Экземпляр класса → JSON (рекурсивно)
output = json.marshal (msg)
print (output) # {"chat":{"id":"456","username":"alice"},"text":"hello"}
```
### Reflect (интроспекция)
......@@ -461,12 +467,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo
```
Возможности:
- Класс `Message` для структурированной обработки сообщений
- Вложенные классы (`Update``TgMessage``Chat`/`User`) с рекурсивным `json.unmarshal()`
- Пользовательские декораторы для регистрации команд (`@bot.command("start")`)
- Колбеки для обработчиков сообщений
- `json.marshal()` для JSON-логирования входящих сообщений
- `json.marshal()` для рекурсивной JSON-сериализации вложенных объектов
- `reflect` для интроспекции в рантайме (команда `/info`)
- `json.get()` для парсинга ответов Telegram API
- `str.urlencode()` для UTF-8 URL-кодирования
## Документация
......
......@@ -39,6 +39,7 @@ class ClassMixin:
self.class_field_types[(cls.name, field_name)] = "dict"
elif type_annotation.name in self.classes:
self.class_field_types[(cls.name, field_name)] = "object"
self.class_field_class[(cls.name, field_name)] = type_annotation.name
else:
self.class_field_types[(cls.name, field_name)] = "scalar"
elif isinstance(default_value, ArrayLiteral):
......@@ -151,6 +152,8 @@ class ClassMixin:
field_types[field_name] = type_annotation.name
elif ft == "scalar":
field_types[field_name] = "string"
elif ft == "object" and type_annotation and type_annotation.name in self.classes:
field_types[field_name] = type_annotation.name
else:
field_types[field_name] = ft
......
......@@ -56,6 +56,7 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.callback_vars: Set[str] = set() # vars that hold function names (callbacks)
self.instance_vars: Dict[str, str] = {} # var_name -> class_name
self.class_field_types: Dict[tuple, str] = {}
self.class_field_class: Dict[tuple, str] = {}
self.func_param_types: Dict[tuple, str] = {} # (func_name, param_name) -> "array"/"dict"
self.local_vars: Set[str] = set()
......
......@@ -74,10 +74,14 @@ class UsageAnalyzer:
if isinstance(field, ClassField):
field_name = field.name
default_value = field.default
type_annotation = field.type_annotation
else:
field_name, default_value = field
type_annotation = None
field_class = None
if default_value:
if type_annotation and type_annotation.name in self.defined_classes:
field_class = type_annotation.name
elif default_value:
if isinstance(default_value, NewExpr):
field_class = default_value.class_name
elif isinstance(default_value, CallExpr) and isinstance(default_value.callee, Identifier):
......
......@@ -334,6 +334,27 @@ class DispatchMixin:
self.dict_vars.add(target)
return
is_object_field = any(
self.class_field_types.get((cls, field_name)) == "object"
for cls in self.classes
)
if is_object_field:
obj_class = None
if isinstance(stmt.value.object, Identifier) and stmt.value.object.name in self.instance_vars:
parent_class = self.instance_vars[stmt.value.object.name]
obj_class = self.class_field_class.get((parent_class, field_name))
if not obj_class:
for cls in self.classes:
obj_class = self.class_field_class.get((cls, field_name))
if obj_class:
break
value = self.generate_expr(stmt.value)
self.emit_var_assign(target, value)
self.object_vars.add(target)
if obj_class:
self.instance_vars[target] = obj_class
return
if isinstance(stmt.value, BinaryOp) and stmt.value.operator in ("==", "!=", "<", ">", "<=", ">=", "&&", "||"):
cond = self.generate_condition(stmt.value)
if self.in_function and target not in self.local_vars and target not in self.global_vars and '.' not in target and '[' not in target:
......
......@@ -173,15 +173,29 @@ class StdlibMixin:
self.emit('local __cls="${__ct_obj_class[$__obj]}"')
self.emit('local -n __fields="__ct_class_meta_${__cls}_fields"')
self.emit('local -n __types="__ct_class_meta_${__cls}_types"')
self.emit('local __f')
self.emit('for __f in "${__fields[@]}"; do')
with self.indented():
self.emit('local __t="${__types[$__f]}"')
self.emit('local __val')
self.emit('__val="$(echo "$__json" | jq -r --arg f "$__f" \'.[$f] // empty\' 2>/dev/null)"')
self.emit('if [[ -n "$__val" ]]; then')
self.emit('if declare -p "__ct_class_meta_${__t}_fields" &>/dev/null; then')
with self.indented():
self.emit('__val="$(echo "$__json" | jq -c --arg f "$__f" \'.[$f] // empty\' 2>/dev/null)"')
self.emit('if [[ -n "$__val" && "$__val" != "null" ]]; then')
with self.indented():
self.emit('__ct_json_unmarshal "$__val" "$__t"')
self.emit('__CT_OBJ["$__obj.$__f"]="$__ct_last_instance"')
self.emit('fi')
self.emit('else')
with self.indented():
self.emit('__CT_OBJ["$__obj.$__f"]="$__val"')
self.emit('__val="$(echo "$__json" | jq -r --arg f "$__f" \'.[$f] // empty\' 2>/dev/null)"')
self.emit('if [[ -n "$__val" ]]; then')
with self.indented():
self.emit('__CT_OBJ["$__obj.$__f"]="$__val"')
self.emit('fi')
self.emit('fi')
self.emit('done')
self.emit('__ct_last_instance="$__obj"')
self.emit("}")
self.emit()
......@@ -199,7 +213,10 @@ class StdlibMixin:
self.emit('printf "\\"%s\\":" "$__f"')
self.emit('local __v="${__CT_OBJ["$__obj.$__f"]}"')
self.emit('local __t="${__types[$__f]}"')
self.emit('if [[ "$__t" == "int" || "$__t" == "float" || "$__t" == "bool" ]]; then')
self.emit('if [[ -n "${__ct_obj_class[$__v]+x}" ]]; then')
with self.indented():
self.emit('__ct_json_marshal "$__v"')
self.emit('elif [[ "$__t" == "int" || "$__t" == "float" || "$__t" == "bool" ]]; then')
with self.indented():
self.emit('printf "%s" "$__v"')
self.emit('else')
......@@ -207,7 +224,7 @@ class StdlibMixin:
self.emit('printf "\\"%s\\"" "$__v"')
self.emit('fi')
self.emit('done')
self.emit('printf "}\\n"')
self.emit('printf "}"')
self.emit("}")
self.emit()
......
......@@ -7,20 +7,20 @@ if token == "" {
bot = new TelegramBot (token)
@bot.command ("start")
func handle_start (msg: Message, arg) {
chat_id = msg.chat_id
func handle_start (update: Update, arg) {
chat_id = update.message.chat.id
bot.send (chat_id, "Welcome! Commands:\n/help - Show help\n/echo <text> - Echo text\n/json - Message as JSON\n/info - Reflect message fields")
}
@bot.command ("help")
func handle_help (msg: Message, arg) {
chat_id = msg.chat_id
func handle_help (update: Update, arg) {
chat_id = update.message.chat.id
bot.send (chat_id, "Available commands:\n/start - Start bot\n/help - This help\n/echo <text> - Echo back text\n/json - Show message as JSON (json.marshal)\n/info - Show message fields (reflect)")
}
@bot.command ("echo")
func handle_echo (msg: Message, arg) {
chat_id = msg.chat_id
func handle_echo (update: Update, arg) {
chat_id = update.message.chat.id
if arg == "" {
bot.send (chat_id, "Usage: /echo <text>")
} else {
......@@ -29,15 +29,16 @@ func handle_echo (msg: Message, arg) {
}
@bot.command ("json")
func handle_json (msg: Message, arg) {
chat_id = msg.chat_id
result = json.marshal (msg)
func handle_json (update: Update, arg) {
chat_id = update.message.chat.id
result = json.marshal (update)
bot.send (chat_id, result)
}
@bot.command ("info")
func handle_info (msg: Message, arg) {
chat_id = msg.chat_id
func handle_info (update: Update, arg) {
chat_id = update.message.chat.id
msg = update.message
cls = reflect.class_name (msg)
fields = reflect.fields (msg)
field_list = fields.join (", ")
......@@ -51,9 +52,9 @@ func handle_info (msg: Message, arg) {
}
@bot.on_message ()
func handle_message (msg: Message, arg) {
chat_id = msg.chat_id
text = msg.text
func handle_message (update: Update, arg) {
chat_id = update.message.chat.id
text = update.message.text
bot.send (chat_id, text)
}
......
class Message {
chat_id: string = ""
from_name: string = ""
class Chat {
id: string = ""
first_name: string = ""
last_name: string = ""
username: string = ""
type: string = ""
}
class User {
first_name: string = ""
last_name: string = ""
username: string = ""
}
class TgMessage {
chat: Chat = ""
from: User = ""
text: string = ""
}
class Update {
update_id: string = "0"
message: TgMessage = ""
}
class TelegramBot {
......@@ -54,20 +72,17 @@ class TelegramBot {
if count != "0" {
i = 0
while i < count {
msg = new Message ()
msg.update_id = json.get (response, ".result[{i}].update_id")
msg.chat_id = json.get (response, ".result[{i}].message.chat.id")
msg.text = json.get (response, ".result[{i}].message.text")
msg.from_name = json.get (response, ".result[{i}].message.from.first_name")
update_json = json.get (response, ".result[{i}]")
update = json.unmarshal (update_json, Update)
update_id = msg.update_id
offset = update_id + 1
offset = update.update_id + 1
msg = update.message
text = msg.text
if text != "null" {
log = json.marshal (msg)
log = json.marshal (update)
print (log)
this._dispatch (msg)
this._dispatch (update)
}
i = i + 1
......@@ -82,9 +97,10 @@ class TelegramBot {
return response
}
func _dispatch (msg: Message) {
func _dispatch (update: Update) {
msg = update.message
text = msg.text
chat_id = msg.chat_id
chat_id = msg.chat.id
if text.starts ("/") {
space_idx = text.index (" ")
......@@ -98,17 +114,17 @@ class TelegramBot {
if this.commands.has (cmd) {
handler = this.commands.get (cmd)
this._invoke (handler, msg, arg)
this._invoke (handler, update, arg)
return
}
}
if this.message_handler != "" {
this._invoke (this.message_handler, msg, "")
this._invoke (this.message_handler, update, "")
}
}
func _invoke (handler, msg, arg) {
handler (msg, arg)
func _invoke (handler, update, arg) {
handler (update, arg)
}
}
......@@ -433,3 +433,119 @@ foreach f in fields {
assert code == 0
assert "name:string=Bob" in stdout
assert "age:int=25" in stdout
class TestNestedClasses:
def test_nested_unmarshal(self):
code, stdout, _ = run_ct(r'''
class Inner {
value: string = ""
}
class Outer {
name: string = ""
inner: Inner = ""
}
json_str = "\{\"name\":\"test\",\"inner\":\{\"value\":\"hello\"\}\}"
obj = json.unmarshal(json_str, Outer)
print(obj.name)
print(obj.inner.value)
''')
assert code == 0
lines = stdout.strip().split('\n')
assert lines[0] == "test"
assert lines[1] == "hello"
def test_nested_marshal(self):
code, stdout, _ = run_ct(r'''
class Point {
x: int = 0
y: int = 0
}
class Shape {
name: string = ""
origin: Point = ""
}
json_str = "\{\"name\":\"rect\",\"origin\":\{\"x\":10,\"y\":20\}\}"
s = json.unmarshal(json_str, Shape)
output = json.marshal(s)
print(output)
''')
assert code == 0
assert '"name":"rect"' in stdout
assert '"origin":{' in stdout
assert '"x":10' in stdout
assert '"y":20' in stdout
def test_deep_nesting(self):
code, stdout, _ = run_ct(r'''
class C {
val: string = ""
}
class B {
c: C = ""
}
class A {
b: B = ""
name: string = ""
}
json_str = "\{\"name\":\"top\",\"b\":\{\"c\":\{\"val\":\"deep\"\}\}\}"
a = json.unmarshal(json_str, A)
print(a.name)
print(a.b.c.val)
''')
assert code == 0
lines = stdout.strip().split('\n')
assert lines[0] == "top"
assert lines[1] == "deep"
def test_nested_type_propagation(self):
code, stdout, _ = run_ct(r'''
class Chat {
id: string = ""
}
class Message {
chat: Chat = ""
text: string = ""
}
json_str = "\{\"chat\":\{\"id\":\"456\"\},\"text\":\"hello\"\}"
msg = json.unmarshal(json_str, Message)
chat = msg.chat
print(chat.id)
print(msg.text)
''')
assert code == 0
lines = stdout.strip().split('\n')
assert lines[0] == "456"
assert lines[1] == "hello"
def test_nested_roundtrip(self):
code, stdout, _ = run_ct(r'''
class Inner {
x: int = 0
name: string = ""
}
class Outer {
child: Inner = ""
label: string = ""
}
json_str = "\{\"label\":\"parent\",\"child\":\{\"x\":42,\"name\":\"inner\"\}\}"
obj = json.unmarshal(json_str, Outer)
output = json.marshal(obj)
print(output)
''')
assert code == 0
assert '"label":"parent"' in stdout
assert '"child":{' in stdout
assert '"x":42' in stdout
assert '"name":"inner"' in stdout
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment