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 ...@@ -892,16 +892,41 @@ print (user.name) # Alice
print (user.age) # 30 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:** **json.marshal — объект класса в JSON:**
``` ```
output = json.marshal (user) output = json.marshal (user)
print (output) # {"name":"Alice","age":30,"active":true} print (output) # {"name":"Alice","age":30,"active":true}
output = json.marshal (msg)
print (output) # {"chat":{"id":"456","username":"alice"},"text":"hello"}
``` ```
Сериализует поля объекта в JSON-строку. Числовые и булевы типы выводятся без кавычек, строки — в кавычках. Сериализует поля объекта в JSON-строку. Числовые и булевы типы выводятся без кавычек, строки — в кавычках. Вложенные объекты сериализуются рекурсивно.
**Пример: Telegram бот** **Пример: Telegram бот**
...@@ -958,7 +983,7 @@ obj = reflect.create ("User") ...@@ -958,7 +983,7 @@ obj = reflect.create ("User")
| `reflect.class_name (obj)` | Возвращает имя класса объекта | | `reflect.class_name (obj)` | Возвращает имя класса объекта |
| `reflect.create (class_name)` | Создаёт новый экземпляр класса по строковому имени | | `reflect.create (class_name)` | Создаёт новый экземпляр класса по строковому имени |
**Ограничения:** работает только со скалярными полями (string, int, float, bool). Массивы и вложенные объекты не поддерживаются. **Ограничения:** массивы в полях не поддерживаются.
### logger ### logger
......
...@@ -210,21 +210,27 @@ evens = numbers.filter (x => x % 2 == 0) # [2, 4] ...@@ -210,21 +210,27 @@ evens = numbers.filter (x => x % 2 == 0) # [2, 4]
### JSON Marshal/Unmarshal ### JSON Marshal/Unmarshal
Go-style JSON serialization with classes: Go-style JSON serialization with classes, including nested class support:
``` ```
class User { class Chat {
name: string = "" id: string = ""
age: int = 0 username: string = ""
}
class Message {
chat: Chat = ""
text: string = ""
} }
# JSON → class instance # JSON → class instance (recursive for nested classes)
user = json.unmarshal ('\{"name":"Alice","age":30\}', User) msg = json.unmarshal ('\{"chat":\{"id":"456","username":"alice"\},"text":"hello"\}', Message)
print (user.name) # Alice print (msg.text) # hello
print (msg.chat.id) # 456
# Class instance → JSON # Class instance → JSON (recursive)
output = json.marshal (user) output = json.marshal (msg)
print (output) # {"name":"Alice","age":30} print (output) # {"chat":{"id":"456","username":"alice"},"text":"hello"}
``` ```
### Reflect (Runtime Introspection) ### Reflect (Runtime Introspection)
...@@ -469,12 +475,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo ...@@ -469,12 +475,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo
``` ```
Features: 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")`) - User decorators for command registration (`@bot.command("start")`)
- Callbacks for message handlers - 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) - `reflect` for runtime introspection (`/info` command)
- `json.get()` for parsing Telegram API responses
- `str.urlencode()` for UTF-8 URL encoding - `str.urlencode()` for UTF-8 URL encoding
## Documentation ## Documentation
......
...@@ -210,21 +210,27 @@ evens = numbers.filter (x => x % 2 == 0) # [2, 4] ...@@ -210,21 +210,27 @@ evens = numbers.filter (x => x % 2 == 0) # [2, 4]
### JSON Marshal/Unmarshal ### JSON Marshal/Unmarshal
Сериализация JSON в стиле Go через классы: Сериализация JSON в стиле Go через классы с поддержкой вложенных классов:
``` ```
class User { class Chat {
name: string = "" id: string = ""
age: int = 0 username: string = ""
}
class Message {
chat: Chat = ""
text: string = ""
} }
# JSON → экземпляр класса # JSON → экземпляр класса (рекурсивно для вложенных классов)
user = json.unmarshal ('\{"name":"Alice","age":30\}', User) msg = json.unmarshal ('\{"chat":\{"id":"456","username":"alice"\},"text":"hello"\}', Message)
print (user.name) # Alice print (msg.text) # hello
print (msg.chat.id) # 456
# Экземпляр класса → JSON # Экземпляр класса → JSON (рекурсивно)
output = json.marshal (user) output = json.marshal (msg)
print (output) # {"name":"Alice","age":30} print (output) # {"chat":{"id":"456","username":"alice"},"text":"hello"}
``` ```
### Reflect (интроспекция) ### Reflect (интроспекция)
...@@ -461,12 +467,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo ...@@ -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")`) - Пользовательские декораторы для регистрации команд (`@bot.command("start")`)
- Колбеки для обработчиков сообщений - Колбеки для обработчиков сообщений
- `json.marshal()` для JSON-логирования входящих сообщений - `json.marshal()` для рекурсивной JSON-сериализации вложенных объектов
- `reflect` для интроспекции в рантайме (команда `/info`) - `reflect` для интроспекции в рантайме (команда `/info`)
- `json.get()` для парсинга ответов Telegram API
- `str.urlencode()` для UTF-8 URL-кодирования - `str.urlencode()` для UTF-8 URL-кодирования
## Документация ## Документация
......
...@@ -39,6 +39,7 @@ class ClassMixin: ...@@ -39,6 +39,7 @@ class ClassMixin:
self.class_field_types[(cls.name, field_name)] = "dict" self.class_field_types[(cls.name, field_name)] = "dict"
elif type_annotation.name in self.classes: elif type_annotation.name in self.classes:
self.class_field_types[(cls.name, field_name)] = "object" self.class_field_types[(cls.name, field_name)] = "object"
self.class_field_class[(cls.name, field_name)] = type_annotation.name
else: else:
self.class_field_types[(cls.name, field_name)] = "scalar" self.class_field_types[(cls.name, field_name)] = "scalar"
elif isinstance(default_value, ArrayLiteral): elif isinstance(default_value, ArrayLiteral):
...@@ -151,6 +152,8 @@ class ClassMixin: ...@@ -151,6 +152,8 @@ class ClassMixin:
field_types[field_name] = type_annotation.name field_types[field_name] = type_annotation.name
elif ft == "scalar": elif ft == "scalar":
field_types[field_name] = "string" 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: else:
field_types[field_name] = ft field_types[field_name] = ft
......
...@@ -56,6 +56,7 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin, ...@@ -56,6 +56,7 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.callback_vars: Set[str] = set() # vars that hold function names (callbacks) self.callback_vars: Set[str] = set() # vars that hold function names (callbacks)
self.instance_vars: Dict[str, str] = {} # var_name -> class_name self.instance_vars: Dict[str, str] = {} # var_name -> class_name
self.class_field_types: Dict[tuple, str] = {} 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.func_param_types: Dict[tuple, str] = {} # (func_name, param_name) -> "array"/"dict"
self.local_vars: Set[str] = set() self.local_vars: Set[str] = set()
......
...@@ -74,10 +74,14 @@ class UsageAnalyzer: ...@@ -74,10 +74,14 @@ class UsageAnalyzer:
if isinstance(field, ClassField): if isinstance(field, ClassField):
field_name = field.name field_name = field.name
default_value = field.default default_value = field.default
type_annotation = field.type_annotation
else: else:
field_name, default_value = field field_name, default_value = field
type_annotation = None
field_class = 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): if isinstance(default_value, NewExpr):
field_class = default_value.class_name field_class = default_value.class_name
elif isinstance(default_value, CallExpr) and isinstance(default_value.callee, Identifier): elif isinstance(default_value, CallExpr) and isinstance(default_value.callee, Identifier):
......
...@@ -334,6 +334,27 @@ class DispatchMixin: ...@@ -334,6 +334,27 @@ class DispatchMixin:
self.dict_vars.add(target) self.dict_vars.add(target)
return 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 ("==", "!=", "<", ">", "<=", ">=", "&&", "||"): if isinstance(stmt.value, BinaryOp) and stmt.value.operator in ("==", "!=", "<", ">", "<=", ">=", "&&", "||"):
cond = self.generate_condition(stmt.value) 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: 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: ...@@ -173,15 +173,29 @@ class StdlibMixin:
self.emit('local __cls="${__ct_obj_class[$__obj]}"') self.emit('local __cls="${__ct_obj_class[$__obj]}"')
self.emit('local -n __fields="__ct_class_meta_${__cls}_fields"') self.emit('local -n __fields="__ct_class_meta_${__cls}_fields"')
self.emit('local -n __types="__ct_class_meta_${__cls}_types"') self.emit('local -n __types="__ct_class_meta_${__cls}_types"')
self.emit('local __f')
self.emit('for __f in "${__fields[@]}"; do') self.emit('for __f in "${__fields[@]}"; do')
with self.indented(): with self.indented():
self.emit('local __t="${__types[$__f]}"')
self.emit('local __val') self.emit('local __val')
self.emit('__val="$(echo "$__json" | jq -r --arg f "$__f" \'.[$f] // empty\' 2>/dev/null)"') self.emit('if declare -p "__ct_class_meta_${__t}_fields" &>/dev/null; then')
self.emit('if [[ -n "$__val" ]]; 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(): 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('fi')
self.emit('done') self.emit('done')
self.emit('__ct_last_instance="$__obj"')
self.emit("}") self.emit("}")
self.emit() self.emit()
...@@ -199,7 +213,10 @@ class StdlibMixin: ...@@ -199,7 +213,10 @@ class StdlibMixin:
self.emit('printf "\\"%s\\":" "$__f"') self.emit('printf "\\"%s\\":" "$__f"')
self.emit('local __v="${__CT_OBJ["$__obj.$__f"]}"') self.emit('local __v="${__CT_OBJ["$__obj.$__f"]}"')
self.emit('local __t="${__types[$__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(): with self.indented():
self.emit('printf "%s" "$__v"') self.emit('printf "%s" "$__v"')
self.emit('else') self.emit('else')
...@@ -207,7 +224,7 @@ class StdlibMixin: ...@@ -207,7 +224,7 @@ class StdlibMixin:
self.emit('printf "\\"%s\\"" "$__v"') self.emit('printf "\\"%s\\"" "$__v"')
self.emit('fi') self.emit('fi')
self.emit('done') self.emit('done')
self.emit('printf "}\\n"') self.emit('printf "}"')
self.emit("}") self.emit("}")
self.emit() self.emit()
......
...@@ -7,20 +7,20 @@ if token == "" { ...@@ -7,20 +7,20 @@ if token == "" {
bot = new TelegramBot (token) bot = new TelegramBot (token)
@bot.command ("start") @bot.command ("start")
func handle_start (msg: Message, arg) { func handle_start (update: Update, arg) {
chat_id = msg.chat_id 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.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") @bot.command ("help")
func handle_help (msg: Message, arg) { func handle_help (update: Update, arg) {
chat_id = msg.chat_id 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.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") @bot.command ("echo")
func handle_echo (msg: Message, arg) { func handle_echo (update: Update, arg) {
chat_id = msg.chat_id chat_id = update.message.chat.id
if arg == "" { if arg == "" {
bot.send (chat_id, "Usage: /echo <text>") bot.send (chat_id, "Usage: /echo <text>")
} else { } else {
...@@ -29,15 +29,16 @@ func handle_echo (msg: Message, arg) { ...@@ -29,15 +29,16 @@ func handle_echo (msg: Message, arg) {
} }
@bot.command ("json") @bot.command ("json")
func handle_json (msg: Message, arg) { func handle_json (update: Update, arg) {
chat_id = msg.chat_id chat_id = update.message.chat.id
result = json.marshal (msg) result = json.marshal (update)
bot.send (chat_id, result) bot.send (chat_id, result)
} }
@bot.command ("info") @bot.command ("info")
func handle_info (msg: Message, arg) { func handle_info (update: Update, arg) {
chat_id = msg.chat_id chat_id = update.message.chat.id
msg = update.message
cls = reflect.class_name (msg) cls = reflect.class_name (msg)
fields = reflect.fields (msg) fields = reflect.fields (msg)
field_list = fields.join (", ") field_list = fields.join (", ")
...@@ -51,9 +52,9 @@ func handle_info (msg: Message, arg) { ...@@ -51,9 +52,9 @@ func handle_info (msg: Message, arg) {
} }
@bot.on_message () @bot.on_message ()
func handle_message (msg: Message, arg) { func handle_message (update: Update, arg) {
chat_id = msg.chat_id chat_id = update.message.chat.id
text = msg.text text = update.message.text
bot.send (chat_id, text) bot.send (chat_id, text)
} }
......
class Message { class Chat {
chat_id: string = "" id: string = ""
from_name: 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 = "" text: string = ""
}
class Update {
update_id: string = "0" update_id: string = "0"
message: TgMessage = ""
} }
class TelegramBot { class TelegramBot {
...@@ -54,20 +72,17 @@ class TelegramBot { ...@@ -54,20 +72,17 @@ class TelegramBot {
if count != "0" { if count != "0" {
i = 0 i = 0
while i < count { while i < count {
msg = new Message () update_json = json.get (response, ".result[{i}]")
msg.update_id = json.get (response, ".result[{i}].update_id") update = json.unmarshal (update_json, Update)
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_id = msg.update_id offset = update.update_id + 1
offset = update_id + 1
msg = update.message
text = msg.text text = msg.text
if text != "null" { if text != "null" {
log = json.marshal (msg) log = json.marshal (update)
print (log) print (log)
this._dispatch (msg) this._dispatch (update)
} }
i = i + 1 i = i + 1
...@@ -82,9 +97,10 @@ class TelegramBot { ...@@ -82,9 +97,10 @@ class TelegramBot {
return response return response
} }
func _dispatch (msg: Message) { func _dispatch (update: Update) {
msg = update.message
text = msg.text text = msg.text
chat_id = msg.chat_id chat_id = msg.chat.id
if text.starts ("/") { if text.starts ("/") {
space_idx = text.index (" ") space_idx = text.index (" ")
...@@ -98,17 +114,17 @@ class TelegramBot { ...@@ -98,17 +114,17 @@ class TelegramBot {
if this.commands.has (cmd) { if this.commands.has (cmd) {
handler = this.commands.get (cmd) handler = this.commands.get (cmd)
this._invoke (handler, msg, arg) this._invoke (handler, update, arg)
return return
} }
} }
if this.message_handler != "" { if this.message_handler != "" {
this._invoke (this.message_handler, msg, "") this._invoke (this.message_handler, update, "")
} }
} }
func _invoke (handler, msg, arg) { func _invoke (handler, update, arg) {
handler (msg, arg) handler (update, arg)
} }
} }
...@@ -433,3 +433,119 @@ foreach f in fields { ...@@ -433,3 +433,119 @@ foreach f in fields {
assert code == 0 assert code == 0
assert "name:string=Bob" in stdout assert "name:string=Bob" in stdout
assert "age:int=25" 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