Commit 1d92a91e authored by Roman Alifanov's avatar Roman Alifanov

Add json.unmarshal/marshal, reflect module, update telegram bot

- json.unmarshal(str, Class) deserializes JSON into class instance - json.marshal(obj) serializes class instance to JSON string - reflect module: fields, get, set, type, class_name, create - Class metadata generation for runtime introspection - Fix DCE namespace detection inside function bodies - Update telegram bot example with Message class, /json, /info commands
parent f11ced15
...@@ -878,6 +878,31 @@ count = json.get (items, ".items | length") # 3 ...@@ -878,6 +878,31 @@ count = json.get (items, ".items | length") # 3
Использует jq под капотом — поддерживает все jq-пути. Использует jq под капотом — поддерживает все jq-пути.
**json.unmarshal — JSON в объект класса:**
```
class User {
name: string = ""
age: int = 0
active: bool = false
}
user = json.unmarshal ('\{"name":"Alice","age":30,"active":true\}', User)
print (user.name) # Alice
print (user.age) # 30
```
Создаёт экземпляр указанного класса и заполняет поля из JSON. Второй аргумент — имя класса (не строка, а идентификатор). Работает только со скалярными полями (string, int, float, bool).
**json.marshal — объект класса в JSON:**
```
output = json.marshal (user)
print (output) # {"name":"Alice","age":30,"active":true}
```
Сериализует поля объекта в JSON-строку. Числовые и булевы типы выводятся без кавычек, строки — в кавычках.
**Пример: Telegram бот** **Пример: Telegram бот**
``` ```
...@@ -891,6 +916,50 @@ http.get ("{base_url}/sendMessage?chat_id={chat_id}&text={encoded}") ...@@ -891,6 +916,50 @@ http.get ("{base_url}/sendMessage?chat_id={chat_id}&text={encoded}")
**Примечание:** фигурные скобки в строках нужно экранировать как `\{` и `\}`. **Примечание:** фигурные скобки в строках нужно экранировать как `\{` и `\}`.
### reflect
Модуль для runtime-интроспекции классов. Предназначен для авторов библиотек, создающих форматы сериализации, ORM и подобные инструменты.
```
class User {
name: string = ""
age: int = 0
}
user = new User ()
user.name = "Alice"
user.age = 30
# Получить список полей класса
fields = reflect.fields (user) # ["name", "age"]
# Получить/установить значение поля по имени
val = reflect.get (user, "name") # "Alice"
reflect.set (user, "name", "Bob")
# Получить тип поля
t = reflect.type (user, "name") # "string"
# Получить имя класса объекта
cls = reflect.class_name (user) # "User"
# Создать новый экземпляр класса по имени
obj = reflect.create ("User")
```
**Методы:**
| Метод | Описание |
|-------|----------|
| `reflect.fields (obj)` | Возвращает массив имён полей класса |
| `reflect.get (obj, field)` | Возвращает значение поля |
| `reflect.set (obj, field, value)` | Устанавливает значение поля |
| `reflect.type (obj, field)` | Возвращает тип поля (`"string"`, `"int"`, `"float"`, `"bool"`) |
| `reflect.class_name (obj)` | Возвращает имя класса объекта |
| `reflect.create (class_name)` | Создаёт новый экземпляр класса по строковому имени |
**Ограничения:** работает только со скалярными полями (string, int, float, bool). Массивы и вложенные объекты не поддерживаются.
### logger ### logger
``` ```
...@@ -1318,7 +1387,8 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list ...@@ -1318,7 +1387,8 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list
- **Stdlib namespaces**: - **Stdlib namespaces**:
- `fs``append`, `exists`, `list`, `mkdir`, `open`, `read`, `remove`, `write` - `fs``append`, `exists`, `list`, `mkdir`, `open`, `read`, `remove`, `write`
- `http``delete`, `get`, `post`, `put` - `http``delete`, `get`, `post`, `put`
- `json``get`, `parse`, `stringify` - `json``get`, `marshal`, `parse`, `stringify`, `unmarshal`
- `reflect``class_name`, `create`, `fields`, `get`, `set`, `type`
- `logger``debug`, `error`, `info`, `warn` - `logger``debug`, `error`, `info`, `warn`
- `regex``extract`, `match` - `regex``extract`, `match`
- `args``count`, `get` - `args``count`, `get`
......
...@@ -208,6 +208,38 @@ squared = numbers.map (x => x * x) # [1, 4, 9, 16, 25] ...@@ -208,6 +208,38 @@ squared = numbers.map (x => x * x) # [1, 4, 9, 16, 25]
evens = numbers.filter (x => x % 2 == 0) # [2, 4] evens = numbers.filter (x => x % 2 == 0) # [2, 4]
``` ```
### JSON Marshal/Unmarshal
Go-style JSON serialization with classes:
```
class User {
name: string = ""
age: int = 0
}
# JSON → class instance
user = json.unmarshal ('\{"name":"Alice","age":30\}', User)
print (user.name) # Alice
# Class instance → JSON
output = json.marshal (user)
print (output) # {"name":"Alice","age":30}
```
### Reflect (Runtime Introspection)
For library authors building serialization, ORM, and similar tools:
```
fields = reflect.fields (user) # ["name", "age"]
val = reflect.get (user, "name") # "Alice"
reflect.set (user, "name", "Bob")
t = reflect.type (user, "name") # "string"
cls = reflect.class_name (user) # "User"
obj = reflect.create ("User") # new User instance
```
### File Handles & Context Managers ### File Handles & Context Managers
``` ```
...@@ -272,7 +304,8 @@ try { ...@@ -272,7 +304,8 @@ try {
| **HTTP** | `http.get/post/put/delete` | | **HTTP** | `http.get/post/put/delete` |
| **Filesystem** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` | | **Filesystem** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **File handles** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` | | **File handles** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
| **JSON** | `json.parse()` → dict, `json.stringify()` → string, `json.get(str, path)` → extract by jq path | | **JSON** | `json.parse()` → dict, `json.stringify()` → string, `json.get(str, path)` → extract by jq path, `json.unmarshal(str, Class)` → class instance, `json.marshal(obj)` → JSON string |
| **Reflect** | `reflect.fields(obj)`, `reflect.get(obj, field)`, `reflect.set(obj, field, value)`, `reflect.type(obj, field)`, `reflect.class_name(obj)`, `reflect.create(name)` |
| **Strings** | `.len()`, `.upper()`, `.lower()`, `.trim()`, `.contains()`, `.replace()`, `.split()`, `.substr()`, `.urlencode()` | | **Strings** | `.len()`, `.upper()`, `.lower()`, `.trim()`, `.contains()`, `.replace()`, `.split()`, `.substr()`, `.urlencode()` |
| **Arrays** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` | | **Arrays** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` |
| **Dicts** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` | | **Dicts** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` |
...@@ -436,8 +469,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo ...@@ -436,8 +469,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo
``` ```
Features: Features:
- `Message` class for structured message handling
- 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
- `reflect` for runtime introspection (`/info` command)
- `json.get()` for parsing Telegram API responses - `json.get()` for parsing Telegram API responses
- `str.urlencode()` for UTF-8 URL encoding - `str.urlencode()` for UTF-8 URL encoding
......
...@@ -208,6 +208,38 @@ squared = numbers.map (x => x * x) # [1, 4, 9, 16, 25] ...@@ -208,6 +208,38 @@ squared = numbers.map (x => x * x) # [1, 4, 9, 16, 25]
evens = numbers.filter (x => x % 2 == 0) # [2, 4] evens = numbers.filter (x => x % 2 == 0) # [2, 4]
``` ```
### JSON Marshal/Unmarshal
Сериализация JSON в стиле Go через классы:
```
class User {
name: string = ""
age: int = 0
}
# JSON → экземпляр класса
user = json.unmarshal ('\{"name":"Alice","age":30\}', User)
print (user.name) # Alice
# Экземпляр класса → JSON
output = json.marshal (user)
print (output) # {"name":"Alice","age":30}
```
### Reflect (интроспекция)
Для авторов библиотек — сериализация, ORM и подобные инструменты:
```
fields = reflect.fields (user) # ["name", "age"]
val = reflect.get (user, "name") # "Alice"
reflect.set (user, "name", "Bob")
t = reflect.type (user, "name") # "string"
cls = reflect.class_name (user) # "User"
obj = reflect.create ("User") # новый экземпляр User
```
### Файловые дескрипторы и контекстные менеджеры ### Файловые дескрипторы и контекстные менеджеры
``` ```
...@@ -272,7 +304,8 @@ try { ...@@ -272,7 +304,8 @@ try {
| **HTTP** | `http.get/post/put/delete` | | **HTTP** | `http.get/post/put/delete` |
| **Файловая система** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` | | **Файловая система** | `fs.read/write/append/exists/remove/mkdir/list`, `fs.open()` |
| **Файловые дескрипторы** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` | | **Файловые дескрипторы** | `f.read()`, `f.readline()`, `f.write()`, `f.writeln()`, `f.close()` |
| **JSON** | `json.parse()` → dict, `json.stringify()` → string, `json.get(str, path)` → извлечь по jq-пути | | **JSON** | `json.parse()` → dict, `json.stringify()` → string, `json.get(str, path)` → извлечь по jq-пути, `json.unmarshal(str, Class)` → экземпляр класса, `json.marshal(obj)` → JSON-строка |
| **Reflect** | `reflect.fields(obj)`, `reflect.get(obj, field)`, `reflect.set(obj, field, value)`, `reflect.type(obj, field)`, `reflect.class_name(obj)`, `reflect.create(name)` |
| **Строки** | `.len()`, `.upper()`, `.lower()`, `.trim()`, `.contains()`, `.replace()`, `.split()`, `.substr()`, `.urlencode()` | | **Строки** | `.len()`, `.upper()`, `.lower()`, `.trim()`, `.contains()`, `.replace()`, `.split()`, `.substr()`, `.urlencode()` |
| **Массивы** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` | | **Массивы** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` |
| **Словари** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` | | **Словари** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` |
...@@ -428,8 +461,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo ...@@ -428,8 +461,11 @@ python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echo
``` ```
Возможности: Возможности:
- Класс `Message` для структурированной обработки сообщений
- Пользовательские декораторы для регистрации команд (`@bot.command("start")`) - Пользовательские декораторы для регистрации команд (`@bot.command("start")`)
- Колбеки для обработчиков сообщений - Колбеки для обработчиков сообщений
- `json.marshal()` для JSON-логирования входящих сообщений
- `reflect` для интроспекции в рантайме (команда `/info`)
- `json.get()` для парсинга ответов Telegram API - `json.get()` для парсинга ответов Telegram API
- `str.urlencode()` для UTF-8 URL-кодирования - `str.urlencode()` для UTF-8 URL-кодирования
......
...@@ -62,6 +62,7 @@ class ClassMixin: ...@@ -62,6 +62,7 @@ class ClassMixin:
self._check_inlineable_method(cls, method) self._check_inlineable_method(cls, method)
self._generate_class_constructor(cls) self._generate_class_constructor(cls)
self._generate_class_metadata(cls)
if cls.constructor: if cls.constructor:
self._generate_construct_method(cls) self._generate_construct_method(cls)
...@@ -139,6 +140,27 @@ class ClassMixin: ...@@ -139,6 +140,27 @@ class ClassMixin:
self.emit("}") self.emit("}")
self.emit() self.emit()
def _generate_class_metadata(self, cls: ClassDecl):
field_names = []
field_types = {}
for field in cls.fields:
field_name, type_annotation, _ = self._get_field_info(field)
field_names.append(field_name)
ft = self.class_field_types.get((cls.name, field_name), "scalar")
if ft == "scalar" and type_annotation:
field_types[field_name] = type_annotation.name
elif ft == "scalar":
field_types[field_name] = "string"
else:
field_types[field_name] = ft
fields_str = " ".join([f'"{f}"' for f in field_names])
self.emit(f'declare -ga __ct_class_meta_{cls.name}_fields=({fields_str})')
types_pairs = " ".join([f'["{f}"]="{t}"' for f, t in field_types.items()])
self.emit(f'declare -gA __ct_class_meta_{cls.name}_types=({types_pairs})')
self.emit()
def _generate_construct_method(self, cls: ClassDecl): def _generate_construct_method(self, cls: ClassDecl):
"""Generate construct method.""" """Generate construct method."""
self.emit(f"__ct_class_{cls.name}_construct () {{") self.emit(f"__ct_class_{cls.name}_construct () {{")
......
...@@ -407,21 +407,18 @@ class UsageAnalyzer: ...@@ -407,21 +407,18 @@ class UsageAnalyzer:
self.used.add('array') self.used.add('array')
elif ns in self.dict_variables: elif ns in self.dict_variables:
self.used.add('dict') self.used.add('dict')
elif hasattr(self, 'current_func_name') and self.current_func_name:
key = (self.current_func_name, ns)
if key in self.func_param_types:
for obj_class in self.func_param_types[key]:
if obj_class not in self.used_methods:
self.used_methods[obj_class] = set()
self.used_methods[obj_class].add(method)
else:
self._check_method(method)
elif ns == 'http': elif ns == 'http':
self.used.add('http') self.used.add('http')
elif ns == 'fs': elif ns == 'fs':
self.used.add('fs') self.used.add('fs')
elif ns == 'json': elif ns == 'json':
self.used.add('json') self.used.add('json')
if method in ('unmarshal',) and len(expr.arguments) >= 2:
arg2 = expr.arguments[1]
if isinstance(arg2, Identifier) and arg2.name in self.defined_classes:
self.has_classes = True
self.used_classes.add(arg2.name)
self.used.add('object')
elif ns == 'logger': elif ns == 'logger':
self.used.add('logger') self.used.add('logger')
elif ns == 'regex': elif ns == 'regex':
...@@ -432,11 +429,31 @@ class UsageAnalyzer: ...@@ -432,11 +429,31 @@ class UsageAnalyzer:
self.used.add('time') self.used.add('time')
elif ns == 'args': elif ns == 'args':
self.used.add('args') self.used.add('args')
elif ns == 'reflect':
self.used.add('reflect')
self.used.add('object')
if method == 'create' and len(expr.arguments) >= 1:
arg1 = expr.arguments[0]
cls_ref = None
if isinstance(arg1, Identifier) and arg1.name in self.defined_classes:
cls_ref = arg1.name
elif isinstance(arg1, StringLiteral) and arg1.value in self.defined_classes:
cls_ref = arg1.value
if cls_ref:
self.has_classes = True
self.used_classes.add(cls_ref)
elif ns == 'shell': elif ns == 'shell':
pass pass
elif hasattr(self, 'current_func_name') and self.current_func_name:
key = (self.current_func_name, ns)
if key in self.func_param_types:
for obj_class in self.func_param_types[key]:
if obj_class not in self.used_methods:
self.used_methods[obj_class] = set()
self.used_methods[obj_class].add(method)
else:
self._check_method(method)
else: else:
# Check if this could be a class method (conservative approach)
# Include method in all classes that define it
found_in_class = False found_in_class = False
for cls_name, cls_decl in self.defined_classes.items(): for cls_name, cls_decl in self.defined_classes.items():
for m in cls_decl.methods: for m in cls_decl.methods:
......
...@@ -250,6 +250,30 @@ class DispatchMixin: ...@@ -250,6 +250,30 @@ class DispatchMixin:
self.emit(f'__ct_json_parse "{args[0]}" "{target}"') self.emit(f'__ct_json_parse "{args[0]}" "{target}"')
self.dict_vars.add(target) self.dict_vars.add(target)
return return
if isinstance(callee.object, Identifier) and callee.object.name == "json" and callee.member == "unmarshal":
args = [self.generate_expr(arg) for arg in stmt.value.arguments]
class_name = stmt.value.arguments[1].name if isinstance(stmt.value.arguments[1], Identifier) else args[1]
self.emit(f'__ct_json_unmarshal "{args[0]}" "{class_name}"')
self.emit_var_assign(target, '$__ct_last_instance')
self.object_vars.add(target)
self.instance_vars[target] = class_name
return
if isinstance(callee.object, Identifier) and callee.object.name == "reflect" and callee.member == "create":
args = [self.generate_expr(arg) for arg in stmt.value.arguments]
self.emit(f'__ct_reflect_create "{args[0]}"')
self.emit_var_assign(target, '$__ct_last_instance')
self.object_vars.add(target)
return
if isinstance(callee.object, Identifier) and callee.object.name == "reflect" and callee.member == "fields":
args = [self._generate_call_arg(arg) for arg in stmt.value.arguments]
self.emit(f'__ct_reflect_fields "{args[0]}"')
if self.in_function and target not in self.local_vars and target not in self.global_vars:
self.local_vars.add(target)
self.emit(f'local -a {target}=("${{{RET_ARR}[@]}}")')
else:
self.emit(f'{target}=("${{{RET_ARR}[@]}}")')
self.array_vars.add(target)
return
if self._generate_method_call_assignment(stmt, target): if self._generate_method_call_assignment(stmt, target):
return return
...@@ -1145,7 +1169,11 @@ class DispatchMixin: ...@@ -1145,7 +1169,11 @@ class DispatchMixin:
elif obj_name == "fs": elif obj_name == "fs":
return f'__ct_fs_{method} {args_str}' return f'__ct_fs_{method} {args_str}'
elif obj_name == "json": elif obj_name == "json":
if method == "marshal":
return f'__ct_json_marshal {args_str}'
return f'__ct_json_{method} {args_str}' return f'__ct_json_{method} {args_str}'
elif obj_name == "reflect":
return f'__ct_reflect_{method} {args_str}'
elif obj_name == "logger" and method in ("info", "warn", "error", "debug"): elif obj_name == "logger" and method in ("info", "warn", "error", "debug"):
return f'__ct_logger_{method} {args_str}' return f'__ct_logger_{method} {args_str}'
elif obj_name == "regex": elif obj_name == "regex":
......
...@@ -12,6 +12,7 @@ from .math import MathMethods ...@@ -12,6 +12,7 @@ from .math import MathMethods
from .time import TimeMethods from .time import TimeMethods
from .args import ArgsMethods from .args import ArgsMethods
from .core import CoreFunctions, AwkBuiltinFunctions from .core import CoreFunctions, AwkBuiltinFunctions
from .reflect import ReflectMethods
STRING_METHODS = collect_methods(StringMethods) STRING_METHODS = collect_methods(StringMethods)
ARRAY_METHODS = collect_methods(ArrayMethods) ARRAY_METHODS = collect_methods(ArrayMethods)
...@@ -27,6 +28,7 @@ TIME_METHODS = collect_methods(TimeMethods) ...@@ -27,6 +28,7 @@ TIME_METHODS = collect_methods(TimeMethods)
ARGS_METHODS = collect_methods(ArgsMethods) ARGS_METHODS = collect_methods(ArgsMethods)
CORE_FUNCTIONS = collect_methods(CoreFunctions) CORE_FUNCTIONS = collect_methods(CoreFunctions)
AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions) AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions)
REFLECT_METHODS = collect_methods(ReflectMethods)
NAMESPACE_REGISTRY = { NAMESPACE_REGISTRY = {
"fs": FS_METHODS, "fs": FS_METHODS,
...@@ -37,6 +39,7 @@ NAMESPACE_REGISTRY = { ...@@ -37,6 +39,7 @@ NAMESPACE_REGISTRY = {
"args": ARGS_METHODS, "args": ARGS_METHODS,
"time": TIME_METHODS, "time": TIME_METHODS,
"math": MATH_METHODS, "math": MATH_METHODS,
"reflect": REFLECT_METHODS,
"shell": {"exec", "capture", "source"}, "shell": {"exec", "capture", "source"},
} }
......
...@@ -20,3 +20,15 @@ class JsonMethods: ...@@ -20,3 +20,15 @@ class JsonMethods:
bash_impl='echo "$1" | jq -r "$2" 2>/dev/null', bash_impl='echo "$1" | jq -r "$2" 2>/dev/null',
min_args=2, max_args=2, min_args=2, max_args=2,
) )
unmarshal = Method(
name="unmarshal",
bash_func="__ct_json_unmarshal",
bash_impl=None,
min_args=2, max_args=2,
)
marshal = Method(
name="marshal",
bash_func="__ct_json_marshal",
bash_impl=None,
min_args=1, max_args=1,
)
from .base import Method
class ReflectMethods:
fields = Method(
name="fields",
bash_func="__ct_reflect_fields",
bash_impl=None,
min_args=1, max_args=1,
returns_array=True,
)
get = Method(
name="get",
bash_func="__ct_reflect_get",
bash_impl=None,
min_args=2, max_args=2,
)
set = Method(
name="set",
bash_func="__ct_reflect_set",
bash_impl=None,
min_args=3, max_args=3,
)
type = Method(
name="type",
bash_func="__ct_reflect_type",
bash_impl=None,
min_args=2, max_args=2,
)
class_name = Method(
name="class_name",
bash_func="__ct_reflect_class_name",
bash_impl=None,
min_args=1, max_args=1,
)
create = Method(
name="create",
bash_func="__ct_reflect_create",
bash_impl=None,
min_args=1, max_args=1,
)
...@@ -65,6 +65,8 @@ class StdlibMixin: ...@@ -65,6 +65,8 @@ class StdlibMixin:
self._emit_math() self._emit_math()
if 'dict' in used_categories: if 'dict' in used_categories:
self._emit_dict() self._emit_dict()
if 'reflect' in used_categories:
self._emit_reflect()
if 'misc' in used_categories or 'time' in used_categories: if 'misc' in used_categories or 'time' in used_categories:
self._emit_misc() self._emit_misc()
if 'test' in used_categories: if 'test' in used_categories:
...@@ -163,6 +165,52 @@ class StdlibMixin: ...@@ -163,6 +165,52 @@ class StdlibMixin:
self.emit("}") self.emit("}")
self.emit() self.emit()
self.emit("__ct_json_unmarshal () {")
with self.indented():
self.emit('local __json="$1" __class="$2"')
self.emit('"$__class"')
self.emit('local __obj="$__ct_last_instance"')
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('for __f in "${__fields[@]}"; do')
with self.indented():
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')
with self.indented():
self.emit('__CT_OBJ["$__obj.$__f"]="$__val"')
self.emit('fi')
self.emit('done')
self.emit("}")
self.emit()
self.emit("__ct_json_marshal () {")
with self.indented():
self.emit('local __obj="$1"')
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 __first=1')
self.emit('printf "{"')
self.emit('for __f in "${__fields[@]}"; do')
with self.indented():
self.emit('[[ $__first -eq 1 ]] && __first=0 || printf ","')
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')
with self.indented():
self.emit('printf "%s" "$__v"')
self.emit('else')
with self.indented():
self.emit('printf "\\"%s\\"" "$__v"')
self.emit('fi')
self.emit('done')
self.emit('printf "}\\n"')
self.emit("}")
self.emit()
self.emit("__ct_json_stringify () {") self.emit("__ct_json_stringify () {")
with self.indented(): with self.indented():
self.emit('local -n __d="$1"') self.emit('local -n __d="$1"')
...@@ -325,6 +373,57 @@ class StdlibMixin: ...@@ -325,6 +373,57 @@ class StdlibMixin:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}") self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit() self.emit()
def _emit_reflect(self):
self.emit("# Reflect functions")
self.emit("__ct_reflect_fields () {")
with self.indented():
self.emit('local __obj="$1"')
self.emit('local __cls="${__ct_obj_class[$__obj]}"')
self.emit('local -n __fields="__ct_class_meta_${__cls}_fields"')
self.emit('__CT_RET_ARR=("${__fields[@]}")')
self.emit("}")
self.emit()
self.emit("__ct_reflect_type () {")
with self.indented():
self.emit('local __obj="$1" __field="$2"')
self.emit('local __cls="${__ct_obj_class[$__obj]}"')
self.emit('local -n __types="__ct_class_meta_${__cls}_types"')
self.emit('__CT_RET="${__types[$__field]}"')
self.emit('echo "${__types[$__field]}"')
self.emit("}")
self.emit()
self.emit("__ct_reflect_get () {")
with self.indented():
self.emit('local __obj="$1" __field="$2"')
self.emit('__CT_RET="${__CT_OBJ["$__obj.$__field"]}"')
self.emit('echo "${__CT_OBJ["$__obj.$__field"]}"')
self.emit("}")
self.emit()
self.emit("__ct_reflect_set () {")
with self.indented():
self.emit('local __obj="$1" __field="$2" __value="$3"')
self.emit('__CT_OBJ["$__obj.$__field"]="$__value"')
self.emit("}")
self.emit()
self.emit("__ct_reflect_class_name () {")
with self.indented():
self.emit('local __obj="$1"')
self.emit('__CT_RET="${__ct_obj_class[$__obj]}"')
self.emit('echo "${__ct_obj_class[$__obj]}"')
self.emit("}")
self.emit()
self.emit("__ct_reflect_create () {")
with self.indented():
self.emit('local __class="$1"; shift')
self.emit('"$__class" "$@"')
self.emit("}")
self.emit()
def _emit_dict(self): def _emit_dict(self):
"""Dict functions from DICT_METHODS.""" """Dict functions from DICT_METHODS."""
self.emit("# Dict functions") self.emit("# Dict functions")
......
...@@ -7,17 +7,20 @@ if token == "" { ...@@ -7,17 +7,20 @@ if token == "" {
bot = new TelegramBot (token) bot = new TelegramBot (token)
@bot.command ("start") @bot.command ("start")
func handle_start (chat_id, text, arg) { func handle_start (msg: Message, arg) {
bot.send (chat_id, "Welcome! Commands:\n/help - Show help\n/echo <text> - Echo text") chat_id = msg.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") @bot.command ("help")
func handle_help (chat_id, text, arg) { func handle_help (msg: Message, arg) {
bot.send (chat_id, "Available commands:\n/start - Start bot\n/help - Show this help\n/echo <text> - Echo back text") chat_id = msg.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") @bot.command ("echo")
func handle_echo (chat_id, text, arg) { func handle_echo (msg: Message, arg) {
chat_id = msg.chat_id
if arg == "" { if arg == "" {
bot.send (chat_id, "Usage: /echo <text>") bot.send (chat_id, "Usage: /echo <text>")
} else { } else {
...@@ -25,8 +28,32 @@ func handle_echo (chat_id, text, arg) { ...@@ -25,8 +28,32 @@ func handle_echo (chat_id, text, arg) {
} }
} }
@bot.command ("json")
func handle_json (msg: Message, arg) {
chat_id = msg.chat_id
result = json.marshal (msg)
bot.send (chat_id, result)
}
@bot.command ("info")
func handle_info (msg: Message, arg) {
chat_id = msg.chat_id
cls = reflect.class_name (msg)
fields = reflect.fields (msg)
field_list = fields.join (", ")
response = "Class: {cls}\nFields: {field_list}"
foreach f in fields {
t = reflect.type (msg, f)
v = reflect.get (msg, f)
response = response .. "\n {f} ({t}) = {v}"
}
bot.send (chat_id, response)
}
@bot.on_message () @bot.on_message ()
func handle_message (chat_id, text, arg) { func handle_message (msg: Message, arg) {
chat_id = msg.chat_id
text = msg.text
bot.send (chat_id, text) bot.send (chat_id, text)
} }
......
class Message {
chat_id: string = ""
from_name: string = ""
text: string = ""
update_id: string = "0"
}
class TelegramBot { class TelegramBot {
token = "" token = ""
base_url = "" base_url = ""
...@@ -47,16 +54,20 @@ class TelegramBot { ...@@ -47,16 +54,20 @@ class TelegramBot {
if count != "0" { if count != "0" {
i = 0 i = 0
while i < count { while i < count {
update_id = json.get (response, ".result[{i}].update_id") msg = new Message ()
chat_id = json.get (response, ".result[{i}].message.chat.id") msg.update_id = json.get (response, ".result[{i}].update_id")
text = json.get (response, ".result[{i}].message.text") msg.chat_id = json.get (response, ".result[{i}].message.chat.id")
from_name = json.get (response, ".result[{i}].message.from.first_name") 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_id + 1 offset = update_id + 1
text = msg.text
if text != "null" { if text != "null" {
print ("{from_name}: {text}") log = json.marshal (msg)
this._dispatch (chat_id, text) print (log)
this._dispatch (msg)
} }
i = i + 1 i = i + 1
...@@ -71,7 +82,10 @@ class TelegramBot { ...@@ -71,7 +82,10 @@ class TelegramBot {
return response return response
} }
func _dispatch (chat_id, text) { func _dispatch (msg: Message) {
text = msg.text
chat_id = msg.chat_id
if text.starts ("/") { if text.starts ("/") {
space_idx = text.index (" ") space_idx = text.index (" ")
if space_idx > 0 { if space_idx > 0 {
...@@ -84,17 +98,17 @@ class TelegramBot { ...@@ -84,17 +98,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, chat_id, text, arg) this._invoke (handler, msg, arg)
return return
} }
} }
if this.message_handler != "" { if this.message_handler != "" {
this._invoke (this.message_handler, chat_id, text, "") this._invoke (this.message_handler, msg, "")
} }
} }
func _invoke (handler, chat_id, text, arg) { func _invoke (handler, msg, arg) {
handler (chat_id, text, arg) handler (msg, arg)
} }
} }
...@@ -267,3 +267,169 @@ test () ...@@ -267,3 +267,169 @@ test ()
lines = stdout.strip().split('\n') lines = stdout.strip().split('\n')
assert "work" in lines[0] assert "work" in lines[0]
assert "cleanup" in lines[1] assert "cleanup" in lines[1]
class TestJsonMarshal:
def test_json_unmarshal_basic(self):
code, stdout, _ = run_ct(r'''
class User {
name: string = ""
age: int = 0
}
json_str = "\{\"name\":\"Alice\",\"age\":30\}"
user = json.unmarshal(json_str, User)
print(user.name)
print(user.age)
''')
assert code == 0
assert "Alice" in stdout
assert "30" in stdout
def test_json_marshal_basic(self):
code, stdout, _ = run_ct(r'''
class Config {
host: string = ""
port: int = 0
}
json_str = "\{\"host\":\"localhost\",\"port\":8080\}"
cfg = json.unmarshal(json_str, Config)
output = json.marshal(cfg)
print(output)
''')
assert code == 0
assert '"host"' in stdout
assert "localhost" in stdout
assert '"port"' in stdout
assert "8080" in stdout
def test_json_marshal_types(self):
code, stdout, _ = run_ct(r'''
class Item {
name: string = ""
count: int = 0
active: bool = false
}
json_str = "\{\"name\":\"widget\",\"count\":5,\"active\":true\}"
item = json.unmarshal(json_str, Item)
output = json.marshal(item)
print(output)
''')
assert code == 0
assert '"name":"widget"' in stdout
assert '"count":5' in stdout
assert '"active":true' in stdout
def test_json_roundtrip(self):
code, stdout, _ = run_ct(r'''
class Point {
x: int = 0
y: int = 0
}
json_str = "\{\"x\":10,\"y\":20\}"
p = json.unmarshal(json_str, Point)
p.x = 100
output = json.marshal(p)
print(output)
''')
assert code == 0
assert '"x":100' in stdout
assert '"y":20' in stdout
class TestReflect:
def test_reflect_fields(self):
code, stdout, _ = run_ct(r'''
class User {
name: string = ""
age: int = 0
}
user = new User()
fields = reflect.fields(user)
print(fields.join(","))
''')
assert code == 0
assert "name" in stdout
assert "age" in stdout
def test_reflect_get_set(self):
code, stdout, _ = run_ct(r'''
class User {
name: string = ""
}
user = new User()
reflect.set(user, "name", "Alice")
val = reflect.get(user, "name")
print(val)
''')
assert code == 0
assert "Alice" in stdout
def test_reflect_type(self):
code, stdout, _ = run_ct(r'''
class Config {
host: string = ""
port: int = 0
debug: bool = false
}
cfg = new Config()
print(reflect.type(cfg, "host"))
print(reflect.type(cfg, "port"))
print(reflect.type(cfg, "debug"))
''')
assert code == 0
lines = stdout.strip().split('\n')
assert "string" in lines[0]
assert "int" in lines[1]
assert "bool" in lines[2]
def test_reflect_class_name(self):
code, stdout, _ = run_ct(r'''
class MyClass {
x: int = 0
}
obj = new MyClass()
print(reflect.class_name(obj))
''')
assert code == 0
assert "MyClass" in stdout
def test_reflect_create(self):
code, stdout, _ = run_ct(r'''
class Item {
name: string = "default"
}
obj = reflect.create("Item")
print(obj.name)
''')
assert code == 0
assert "default" in stdout
def test_reflect_with_json(self):
code, stdout, _ = run_ct(r'''
class User {
name: string = ""
age: int = 0
}
json_str = "\{\"name\":\"Bob\",\"age\":25\}"
user = json.unmarshal(json_str, User)
fields = reflect.fields(user)
foreach f in fields {
val = reflect.get(user, f)
tp = reflect.type(user, f)
print("{f}:{tp}={val}")
}
''')
assert code == 0
assert "name:string=Bob" in stdout
assert "age:int=25" 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