mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-07-01 16:19:16 +00:00
0.3.0
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/target
|
||||
editor/dist
|
||||
editor/node_modules
|
||||
|
||||
@@ -72,6 +72,7 @@ The dexpr language supports:
|
||||
- Line comments (`//`) and block comments (`/* */`)
|
||||
- Lists: `NumberList` and `StringList` types with methods (`sum`, `avg`, `min`, `max`, `first`, `last`, `get`, `join`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `isEmpty`, etc.)
|
||||
- Objects: `Object` type (provided externally via `set_global`) with property access (`obj.field`), nested access (`obj.a.b`), property assignment (`obj.field = value`), and methods (`keys()`, `values()`, `length()`, `contains(key)`, `get(key)`)
|
||||
- Lists: `List` type for heterogeneous arrays (including array of objects), with methods (`length`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `join`, `map(field)`, `filter(field, value?)`, `find(field, value?)`, `sort(field)`). Property projection: `kalemler.tutar` extracts field from each Object element, returning NumberList/StringList/List. `map("field")` also available as explicit alternative
|
||||
|
||||
## Detailed Module Documentation
|
||||
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -304,7 +304,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "dexpr"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"criterion",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dexpr"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
description = "Embeddable expression evaluator and bytecode VM"
|
||||
license = "MIT"
|
||||
|
||||
@@ -88,12 +88,13 @@ struct Spanned<T> { node: T, span: Span }
|
||||
| `NumberList(Vec<Decimal>)` | `Vec<Decimal>` | Sayı listesi |
|
||||
| `StringList(Vec<SmolStr>)` | `Vec<SmolStr>` | String listesi |
|
||||
| `Object(IndexMap<SmolStr, Value>)` | `IndexMap<SmolStr, Value>` | Anahtar-değer nesnesi |
|
||||
| `List(Vec<Value>)` | `Vec<Value>` | Genel liste (object array dahil) |
|
||||
|
||||
### Serileştirme
|
||||
|
||||
Her `Value` bytecode'a gömülebilir. Serileştirme formatı:
|
||||
|
||||
1. **Tip etiketi** (1 byte): `NULL=0x00`, `NUMBER=0x01`, `STRING=0x02`, `BOOLEAN=0x03`, `NUMBER_LIST=0x04`, `STRING_LIST=0x05`, `OBJECT=0x06`
|
||||
1. **Tip etiketi** (1 byte): `NULL=0x00`, `NUMBER=0x01`, `STRING=0x02`, `BOOLEAN=0x03`, `NUMBER_LIST=0x04`, `STRING_LIST=0x05`, `OBJECT=0x06`, `LIST=0x07`
|
||||
2. **Veri:**
|
||||
- Number: 16 byte (Decimal serialization)
|
||||
- String: 2-byte uzunluk + UTF-8 bytes
|
||||
@@ -101,6 +102,7 @@ Her `Value` bytecode'a gömülebilir. Serileştirme formatı:
|
||||
- NumberList: 2-byte count + her sayı için 16 byte
|
||||
- StringList: 2-byte count + her string için (2-byte uzunluk + bytes)
|
||||
- Object: 2-byte entry count + her girdi için (anahtar: 2-byte uzunluk + bytes, değer: rekürsif serialize)
|
||||
- List: 2-byte count + her eleman için rekürsif serialize
|
||||
|
||||
`serialize()` ve `deserialize()` metodları bu dönüşümü gerçekleştirir.
|
||||
|
||||
|
||||
@@ -155,7 +155,9 @@ Bu analiz her autocomplete tetiklendiğinde Lezer tree üzerinde yapılır. Bozu
|
||||
| `x.` (assignment'tan `String` çıkarıldı) | String metodları |
|
||||
| `items.` (config'de `StringList`) | StringList metodları |
|
||||
| `scores.` (config'de `NumberList`) | NumberList metodları |
|
||||
| `obj.` (config'de `Object`) | Object metodları |
|
||||
| `obj.` (config'de `Object`) | Object field'ları + Object metodları |
|
||||
| `kalemler.` (config'de `List`) | Element field'ları (property projection) + List metodları |
|
||||
| `kalemler.tutar.` (List projection → `NumberList`) | NumberList metodları (sum, avg, min, max...) |
|
||||
| `result.` (tip bilinmiyor) | Tüm metodlar |
|
||||
| `42.` | Öneri yok |
|
||||
|
||||
@@ -260,7 +262,7 @@ Eğer host uygulama çalışma sırasında yeni fonksiyon/değişken eklerse, ed
|
||||
| Tip | Açıklama |
|
||||
|-----|----------|
|
||||
| `DexprLanguageInfo` | Metadata arayüzü (JSON yapısı) |
|
||||
| `DexprType` | `"String" \| "Number" \| "Boolean" \| "NumberList" \| "StringList" \| "Object"` |
|
||||
| `DexprType` | `"String" \| "Number" \| "Boolean" \| "NumberList" \| "StringList" \| "Object" \| "List"` |
|
||||
| `FunctionInfo` | Fonksiyon metadata'sı |
|
||||
| `MethodInfo` | Metod metadata'sı |
|
||||
| `VariableInfo` | Değişken metadata'sı |
|
||||
|
||||
@@ -29,7 +29,7 @@ Editör entegrasyonu için dil metadata'sı üretir. Built-in fonksiyonlar, tipe
|
||||
| Alan | Tip | Açıklama |
|
||||
|------|-----|----------|
|
||||
| `name` | `String` | Değişken adı |
|
||||
| `type_name` | `String` | Tip adı: `String`, `Number`, `Boolean`, `NumberList`, `StringList`, `Object` |
|
||||
| `type_name` | `String` | Tip adı: `String`, `Number`, `Boolean`, `NumberList`, `StringList`, `Object`, `List` |
|
||||
| `doc` | `Option<String>` | Opsiyonel açıklama |
|
||||
|
||||
### LanguageInfo
|
||||
@@ -62,6 +62,7 @@ Tüm built-in fonksiyon ve metodları içeren yeni bir `LanguageInfo` oluşturur
|
||||
| `NumberList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `sum`, `avg`, `min`, `max` |
|
||||
| `StringList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `join` |
|
||||
| `Object` | `keys`, `values`, `length`, `len`, `contains`, `get` |
|
||||
| `List` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `join`, `map`, `filter`, `find`, `sort` |
|
||||
|
||||
### `add_function(name, signature, doc)`
|
||||
|
||||
|
||||
@@ -137,17 +137,18 @@ struct VM<'a> {
|
||||
|
||||
### String, Nesne ve Metodlar
|
||||
- **`handle_concat()`** — İki register'ı birleştir (karışık tip dönüşümü destekler: String, Number, Boolean otomatik olarak String'e dönüştürülür)
|
||||
- **`handle_get_property()`** — Object register'ından alan oku, alan yoksa `Null` döndür
|
||||
- **`handle_get_property()`** — Object register'ından alan oku, alan yoksa `Null` döndür. List register'ında property projection yapar: her Object elemanından ilgili alanı çıkarıp NumberList/StringList/List döndürür
|
||||
- **`handle_set_property()`** — Object register'ında alan değerini ayarla
|
||||
- **`handle_method_call()`** — Nesne register'ı, metod adı, argümanlar
|
||||
- **String metodları:** `upper`, `lower`, `trim`, `trimStart`, `trimEnd`, `split(delimiter)`, `replace(old, new)`, `startsWith(prefix)`, `endsWith(suffix)`, `contains(substr)`, `length`, `charAt(index)`, `substring(start, end?)`
|
||||
- **StringList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `join(delimiter?)`
|
||||
- **NumberList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `sum`, `avg`, `min`, `max`
|
||||
- **Object metodları:** `keys()`, `values()`, `length`/`len()`, `contains(key)`, `get(key)`
|
||||
- **List metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `join(delim?)`, `map(field)`, `filter(field, value?)`, `find(field, value?)`, `sort(field)`
|
||||
- **Harici metodlar:** Yukarıdaki built-in metodlar bulunamazsa `external_methods` HashMap'inde aranır
|
||||
|
||||
### Üyelik Testi
|
||||
- **`handle_contains()`** — `in` operatörü: String in StringList, Number in NumberList, String in String (substring), String in Object (anahtar varlığı kontrolü)
|
||||
- **`handle_contains()`** — `in` operatörü: String in StringList, Number in NumberList, String in String (substring), String in Object (anahtar varlığı kontrolü), Value in List
|
||||
|
||||
### Harici Fonksiyonlar ve Sonuç
|
||||
- **`handle_call_external()`** — İsimle harici fonksiyon çağır (HashMap lookup)
|
||||
@@ -203,7 +204,7 @@ vm.register_method("Number", "format", |this, args| {
|
||||
});
|
||||
```
|
||||
|
||||
**Tip isimleri:** `"Number"`, `"String"`, `"Boolean"`, `"NumberList"`, `"StringList"`, `"Object"`, `"Null"`
|
||||
**Tip isimleri:** `"Number"`, `"String"`, `"Boolean"`, `"NumberList"`, `"StringList"`, `"Object"`, `"List"`, `"Null"`
|
||||
|
||||
---
|
||||
|
||||
|
||||
134
editor/bun.lock
134
editor/bun.lock
@@ -15,6 +15,7 @@
|
||||
"codemirror": "^6.0.2",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^4.1.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
@@ -22,6 +23,7 @@
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -40,6 +42,12 @@
|
||||
|
||||
"@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
|
||||
@@ -110,6 +118,42 @@
|
||||
|
||||
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.123.0", "", {}, "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.13", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.13", "", { "os": "linux", "cpu": "arm" }, "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "s390x" }, "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "x64" }, "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.13", "", { "os": "linux", "cpu": "x64" }, "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.13", "", { "os": "none", "cpu": "arm64" }, "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.13", "", { "dependencies": { "@emnapi/core": "1.9.1", "@emnapi/runtime": "1.9.1", "@napi-rs/wasm-runtime": "^1.1.2" }, "cpu": "none" }, "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.13", "", { "os": "win32", "cpu": "x64" }, "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.13", "", {}, "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
|
||||
@@ -160,16 +204,42 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.1.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.3", "@vitest/utils": "4.1.3", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.3", "", { "dependencies": { "@vitest/spy": "4.1.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.3", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.1.3", "", { "dependencies": { "@vitest/utils": "4.1.3", "pathe": "^2.0.3" } }, "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.3", "", { "dependencies": { "@vitest/pretty-format": "4.1.3", "@vitest/utils": "4.1.3", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.1.3", "", {}, "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.1.3", "", { "dependencies": { "@vitest/pretty-format": "4.1.3", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
||||
@@ -180,12 +250,22 @@
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
|
||||
@@ -194,6 +274,30 @@
|
||||
|
||||
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
@@ -208,8 +312,12 @@
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
@@ -220,16 +328,28 @@
|
||||
|
||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.13", "", { "dependencies": { "@oxc-project/types": "=0.123.0", "@rolldown/pluginutils": "1.0.0-rc.13" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.13", "@rolldown/binding-darwin-arm64": "1.0.0-rc.13", "@rolldown/binding-darwin-x64": "1.0.0-rc.13", "@rolldown/binding-freebsd-x64": "1.0.0-rc.13", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw=="],
|
||||
|
||||
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
|
||||
|
||||
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||
|
||||
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
|
||||
@@ -238,20 +358,34 @@
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"vite": ["vite@8.0.6", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.13", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-jeOXoY6N8rOfit/mZADMd0misLqjRdWBB3/S23ZQNuPcbVsfMBJutWD8b4ftdczMOsNyMBnKro0Z1Kt0HIqq5Q=="],
|
||||
|
||||
"vitest": ["vitest@4.1.3", "", { "dependencies": { "@vitest/expect": "4.1.3", "@vitest/mocker": "4.1.3", "@vitest/pretty-format": "4.1.3", "@vitest/runner": "4.1.3", "@vitest/snapshot": "4.1.3", "@vitest/spy": "4.1.3", "@vitest/utils": "4.1.3", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.3", "@vitest/browser-preview": "4.1.3", "@vitest/browser-webdriverio": "4.1.3", "@vitest/coverage-istanbul": "4.1.3", "@vitest/coverage-v8": "4.1.3", "@vitest/ui": "4.1.3", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw=="],
|
||||
|
||||
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||
}
|
||||
}
|
||||
|
||||
15
editor/dist/index.cjs
vendored
15
editor/dist/index.cjs
vendored
@@ -285,6 +285,15 @@ function inferMethodReturnType(method) {
|
||||
// depends on input type
|
||||
case "join":
|
||||
return "String";
|
||||
// List methods
|
||||
case "map":
|
||||
return null;
|
||||
// depends on field type (NumberList, StringList, or List)
|
||||
case "filter":
|
||||
return "List";
|
||||
case "find":
|
||||
return null;
|
||||
// returns single element
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -318,7 +327,7 @@ function dexprCompletion(info) {
|
||||
}
|
||||
const objectFieldCompletions = /* @__PURE__ */ new Map();
|
||||
for (const v of info.variables ?? []) {
|
||||
if (v.type === "Object" && v.fields) {
|
||||
if ((v.type === "Object" || v.type === "List") && v.fields) {
|
||||
const fieldItems = [];
|
||||
for (const f of v.fields) {
|
||||
configVarTypes.set(`${v.name}.${f.name}`, f.type);
|
||||
@@ -392,6 +401,10 @@ function dexprCompletion(info) {
|
||||
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
|
||||
const objMethods = methodsByType["Object"] ?? [];
|
||||
options = [...fieldItems, ...objMethods];
|
||||
} else if (finalType === "List") {
|
||||
const rootVarName = path[0];
|
||||
const listMethods = methodsByType["List"] ?? [];
|
||||
options = [...listMethods];
|
||||
} else if (finalType) {
|
||||
options = methodsByType[finalType] ?? allMethods;
|
||||
} else {
|
||||
|
||||
2
editor/dist/index.d.cts
vendored
2
editor/dist/index.d.cts
vendored
@@ -2,7 +2,7 @@ import { Extension } from '@codemirror/state';
|
||||
import { Completion } from '@codemirror/autocomplete';
|
||||
import { LRLanguage, HighlightStyle } from '@codemirror/language';
|
||||
|
||||
type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object";
|
||||
type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object" | "List";
|
||||
interface FunctionInfo {
|
||||
name: string;
|
||||
signature: string;
|
||||
|
||||
2
editor/dist/index.d.ts
vendored
2
editor/dist/index.d.ts
vendored
@@ -2,7 +2,7 @@ import { Extension } from '@codemirror/state';
|
||||
import { Completion } from '@codemirror/autocomplete';
|
||||
import { LRLanguage, HighlightStyle } from '@codemirror/language';
|
||||
|
||||
type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object";
|
||||
type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object" | "List";
|
||||
interface FunctionInfo {
|
||||
name: string;
|
||||
signature: string;
|
||||
|
||||
15
editor/dist/index.js
vendored
15
editor/dist/index.js
vendored
@@ -258,6 +258,15 @@ function inferMethodReturnType(method) {
|
||||
// depends on input type
|
||||
case "join":
|
||||
return "String";
|
||||
// List methods
|
||||
case "map":
|
||||
return null;
|
||||
// depends on field type (NumberList, StringList, or List)
|
||||
case "filter":
|
||||
return "List";
|
||||
case "find":
|
||||
return null;
|
||||
// returns single element
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -291,7 +300,7 @@ function dexprCompletion(info) {
|
||||
}
|
||||
const objectFieldCompletions = /* @__PURE__ */ new Map();
|
||||
for (const v of info.variables ?? []) {
|
||||
if (v.type === "Object" && v.fields) {
|
||||
if ((v.type === "Object" || v.type === "List") && v.fields) {
|
||||
const fieldItems = [];
|
||||
for (const f of v.fields) {
|
||||
configVarTypes.set(`${v.name}.${f.name}`, f.type);
|
||||
@@ -365,6 +374,10 @@ function dexprCompletion(info) {
|
||||
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
|
||||
const objMethods = methodsByType["Object"] ?? [];
|
||||
options = [...fieldItems, ...objMethods];
|
||||
} else if (finalType === "List") {
|
||||
const rootVarName = path[0];
|
||||
const listMethods = methodsByType["List"] ?? [];
|
||||
options = [...listMethods];
|
||||
} else if (finalType) {
|
||||
options = methodsByType[finalType] ?? allMethods;
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codemirror-lang-dexpr",
|
||||
"version": "0.1.0",
|
||||
"name": "@duhanbalci/codemirror-lang-dexpr",
|
||||
"version": "0.3.0",
|
||||
"description": "CodeMirror 6 language support for dexpr",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
@@ -17,7 +17,8 @@
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --external @codemirror/language --external @codemirror/autocomplete --external @codemirror/state --external @codemirror/view --external @lezer/highlight --external @lezer/lr",
|
||||
"demo": "tsup demo.ts --format iife --outDir dist --no-dts",
|
||||
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
||||
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
@@ -37,7 +38,8 @@
|
||||
"@lezer/lr": "^1.4.8",
|
||||
"codemirror": "^6.0.2",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^4.1.3"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
149
editor/src/completions.test.ts
Normal file
149
editor/src/completions.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { inferMethodReturnType, projectedListType } from "./completions";
|
||||
import type { DexprType } from "./completions";
|
||||
|
||||
// ==================== inferMethodReturnType ====================
|
||||
|
||||
describe("inferMethodReturnType", () => {
|
||||
// String methods → String
|
||||
it.each(["upper", "lower", "trim", "trimStart", "trimEnd", "replace", "charAt", "substring"])(
|
||||
"%s → String",
|
||||
(method) => {
|
||||
expect(inferMethodReturnType(method)).toBe("String");
|
||||
}
|
||||
);
|
||||
|
||||
// String/List methods → Boolean
|
||||
it.each(["contains", "startsWith", "endsWith", "isEmpty"])(
|
||||
"%s → Boolean",
|
||||
(method) => {
|
||||
expect(inferMethodReturnType(method)).toBe("Boolean");
|
||||
}
|
||||
);
|
||||
|
||||
// Methods → Number
|
||||
it.each(["length", "len", "indexOf", "sum", "avg", "min", "max", "first", "last"])(
|
||||
"%s → Number",
|
||||
(method) => {
|
||||
expect(inferMethodReturnType(method)).toBe("Number");
|
||||
}
|
||||
);
|
||||
|
||||
// split → StringList
|
||||
it("split → StringList", () => {
|
||||
expect(inferMethodReturnType("split")).toBe("StringList");
|
||||
});
|
||||
|
||||
// join → String
|
||||
it("join → String", () => {
|
||||
expect(inferMethodReturnType("join")).toBe("String");
|
||||
});
|
||||
|
||||
// filter → List
|
||||
it("filter → List", () => {
|
||||
expect(inferMethodReturnType("filter")).toBe("List");
|
||||
});
|
||||
|
||||
// Methods that depend on input type → null
|
||||
it.each(["reverse", "sort", "slice", "map", "find"])(
|
||||
"%s → null (context-dependent)",
|
||||
(method) => {
|
||||
expect(inferMethodReturnType(method)).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
// Unknown method → null
|
||||
it("unknown method → null", () => {
|
||||
expect(inferMethodReturnType("foobar")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== projectedListType ====================
|
||||
|
||||
describe("projectedListType", () => {
|
||||
it("Number field → NumberList", () => {
|
||||
expect(projectedListType("Number")).toBe("NumberList");
|
||||
});
|
||||
|
||||
it("String field → StringList", () => {
|
||||
expect(projectedListType("String")).toBe("StringList");
|
||||
});
|
||||
|
||||
it("Boolean field → List", () => {
|
||||
expect(projectedListType("Boolean")).toBe("List");
|
||||
});
|
||||
|
||||
it("Object field → List", () => {
|
||||
expect(projectedListType("Object")).toBe("List");
|
||||
});
|
||||
|
||||
it("List field → List", () => {
|
||||
expect(projectedListType("List")).toBe("List");
|
||||
});
|
||||
|
||||
it("null (unknown) field → List", () => {
|
||||
expect(projectedListType(null)).toBe("List");
|
||||
});
|
||||
|
||||
it("NumberList field → List (nested lists stay as List)", () => {
|
||||
expect(projectedListType("NumberList")).toBe("List");
|
||||
});
|
||||
|
||||
it("StringList field → List", () => {
|
||||
expect(projectedListType("StringList")).toBe("List");
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Type flow scenarios ====================
|
||||
|
||||
describe("type flow scenarios", () => {
|
||||
// Simulates what happens in the autocomplete pipeline
|
||||
|
||||
it("kalemler.tutar.sum() — List → NumberList → Number", () => {
|
||||
// Step 1: kalemler is List, tutar field is Number
|
||||
const projectedType = projectedListType("Number");
|
||||
expect(projectedType).toBe("NumberList");
|
||||
|
||||
// Step 2: .sum() on NumberList returns Number
|
||||
const resultType = inferMethodReturnType("sum");
|
||||
expect(resultType).toBe("Number");
|
||||
});
|
||||
|
||||
it("kalemler.adi.join() — List → StringList → String", () => {
|
||||
const projectedType = projectedListType("String");
|
||||
expect(projectedType).toBe("StringList");
|
||||
|
||||
const resultType = inferMethodReturnType("join");
|
||||
expect(resultType).toBe("String");
|
||||
});
|
||||
|
||||
it("kalemler.filter().tutar.sum() — List → List → NumberList → Number", () => {
|
||||
// filter returns List
|
||||
const afterFilter = inferMethodReturnType("filter");
|
||||
expect(afterFilter).toBe("List");
|
||||
|
||||
// .tutar on List with Number field
|
||||
const afterProjection = projectedListType("Number");
|
||||
expect(afterProjection).toBe("NumberList");
|
||||
|
||||
// .sum() on NumberList
|
||||
const result = inferMethodReturnType("sum");
|
||||
expect(result).toBe("Number");
|
||||
});
|
||||
|
||||
it("kalemler.tutar.max() — projection then aggregate", () => {
|
||||
const projected = projectedListType("Number");
|
||||
expect(projected).toBe("NumberList");
|
||||
|
||||
const result = inferMethodReturnType("max");
|
||||
expect(result).toBe("Number");
|
||||
});
|
||||
|
||||
it("kalemler.birim.contains() — StringList method", () => {
|
||||
const projected = projectedListType("String");
|
||||
expect(projected).toBe("StringList");
|
||||
|
||||
const result = inferMethodReturnType("contains");
|
||||
expect(result).toBe("Boolean");
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,8 @@ export type DexprType =
|
||||
| "Boolean"
|
||||
| "NumberList"
|
||||
| "StringList"
|
||||
| "Object";
|
||||
| "Object"
|
||||
| "List";
|
||||
|
||||
export interface FunctionInfo {
|
||||
name: string;
|
||||
@@ -171,15 +172,15 @@ function inferExprType(
|
||||
if (objNode.name === "VariableName") {
|
||||
const varName = doc.sliceString(objNode.from, objNode.to);
|
||||
const fieldName = doc.sliceString(propNode.from, propNode.to);
|
||||
// Look up from objectFieldTypes via the global lookup
|
||||
// (We use knownTypes to check if root is Object, then check field)
|
||||
const rootType = knownTypes.get(varName);
|
||||
if (rootType === "Object") {
|
||||
// Field type needs to come from config — stored as "varName.fieldName" key
|
||||
// We can't access objectFieldTypes here, so use the convention
|
||||
// that knownTypes may contain "varName.fieldName" entries
|
||||
return knownTypes.get(`${varName}.${fieldName}`) ?? null;
|
||||
}
|
||||
if (rootType === "List") {
|
||||
// Property projection: list.field → typed list based on field type
|
||||
const fieldType = knownTypes.get(`${varName}.${fieldName}`) ?? null;
|
||||
return projectedListType(fieldType);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -204,7 +205,7 @@ function findChild(
|
||||
}
|
||||
|
||||
/** Infer return type from known method names */
|
||||
function inferMethodReturnType(method: string): DexprType | null {
|
||||
export function inferMethodReturnType(method: string): DexprType | null {
|
||||
switch (method) {
|
||||
// String -> String
|
||||
case "upper":
|
||||
@@ -245,11 +246,29 @@ function inferMethodReturnType(method: string): DexprType | null {
|
||||
return null; // depends on input type
|
||||
case "join":
|
||||
return "String";
|
||||
// List methods
|
||||
case "map":
|
||||
return null; // depends on field type (NumberList, StringList, or List)
|
||||
case "filter":
|
||||
return "List";
|
||||
case "find":
|
||||
return null; // returns single element
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a field type from an Object element within a List,
|
||||
* return the projected list type after property access.
|
||||
* e.g. List with Number field "tutar" → kalemler.tutar → NumberList
|
||||
*/
|
||||
export function projectedListType(fieldType: DexprType | null): DexprType {
|
||||
if (fieldType === "Number") return "NumberList";
|
||||
if (fieldType === "String") return "StringList";
|
||||
return "List";
|
||||
}
|
||||
|
||||
// --- Autocomplete ---
|
||||
|
||||
function dedup(items: Completion[]): Completion[] {
|
||||
@@ -290,7 +309,7 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
|
||||
// and field completions per Object variable
|
||||
const objectFieldCompletions = new Map<string, Completion[]>();
|
||||
for (const v of info.variables ?? []) {
|
||||
if (v.type === "Object" && v.fields) {
|
||||
if ((v.type === "Object" || v.type === "List") && v.fields) {
|
||||
const fieldItems: Completion[] = [];
|
||||
for (const f of v.fields) {
|
||||
// Store "customer.name" → "String" in configVarTypes for type inference
|
||||
@@ -354,13 +373,17 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
|
||||
// e.g. path=["customer","name"] → look up "customer.name" in varTypes
|
||||
let currentType = rootType;
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
if (currentType !== "Object") {
|
||||
if (currentType === "Object") {
|
||||
const key = `${path[i - 1]}.${path[i]}`;
|
||||
currentType = varTypes.get(key) ?? null;
|
||||
} else if (currentType === "List") {
|
||||
// Property projection: list.field → typed list
|
||||
const key = `${path[0]}.${path[i]}`;
|
||||
const fieldType = varTypes.get(key) ?? null;
|
||||
currentType = projectedListType(fieldType);
|
||||
} else {
|
||||
return { type: currentType, path };
|
||||
}
|
||||
// "customer.name" key convention
|
||||
const key = `${path[i - 1]}.${path[i]}`;
|
||||
const fieldType = varTypes.get(key) ?? null;
|
||||
currentType = fieldType;
|
||||
}
|
||||
|
||||
return { type: currentType, path };
|
||||
@@ -408,6 +431,12 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
|
||||
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
|
||||
const objMethods = methodsByType["Object"] ?? [];
|
||||
options = [...fieldItems, ...objMethods];
|
||||
} else if (finalType === "List") {
|
||||
// Show field names (property projection) + List methods
|
||||
const rootVarName = path[0];
|
||||
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
|
||||
const listMethods = methodsByType["List"] ?? [];
|
||||
options = [...fieldItems, ...listMethods];
|
||||
} else if (finalType) {
|
||||
options = methodsByType[finalType] ?? allMethods;
|
||||
} else {
|
||||
|
||||
14
justfile
14
justfile
@@ -14,7 +14,7 @@ run:
|
||||
|
||||
# --- Publish ---
|
||||
|
||||
# Publish to Gitea cargo registry
|
||||
# Publish dexpr to Gitea cargo registry
|
||||
publish:
|
||||
cargo publish --registry gitea --allow-dirty
|
||||
|
||||
@@ -22,11 +22,11 @@ publish:
|
||||
|
||||
# Build wasm package (web target)
|
||||
wasm:
|
||||
cd wasm && wasm-pack build --target web --release
|
||||
cd wasm && wasm-pack build --target web --release --scope duhanbalci
|
||||
|
||||
# Build wasm package (bundler target, for npm)
|
||||
wasm-bundler:
|
||||
cd wasm && wasm-pack build --target bundler --release
|
||||
cd wasm && wasm-pack build --target bundler --release --scope duhanbalci
|
||||
|
||||
# --- Editor ---
|
||||
|
||||
@@ -42,6 +42,14 @@ editor-build: editor-grammar
|
||||
editor-demo: editor-build
|
||||
cd editor && bun run demo
|
||||
|
||||
# Publish editor package to Gitea npm registry
|
||||
editor-publish: editor-build
|
||||
cd editor && npm publish
|
||||
|
||||
# Publish wasm package to Gitea npm registry
|
||||
wasm-publish: wasm-bundler
|
||||
cd wasm/pkg && npm publish
|
||||
|
||||
# --- Combined ---
|
||||
|
||||
# Build everything (wasm + editor)
|
||||
|
||||
@@ -15,6 +15,7 @@ pub enum Value {
|
||||
NumberList(Rc<Vec<Decimal>>),
|
||||
StringList(Rc<Vec<SmolStr>>),
|
||||
Object(Rc<IndexMap<SmolStr, Value>>),
|
||||
List(Rc<Vec<Value>>),
|
||||
}
|
||||
|
||||
/// Type tag constants for serialization
|
||||
@@ -25,6 +26,7 @@ pub const TYPE_BOOLEAN: u8 = 0x03;
|
||||
pub const TYPE_NUMBER_LIST: u8 = 0x04;
|
||||
pub const TYPE_STRING_LIST: u8 = 0x05;
|
||||
pub const TYPE_OBJECT: u8 = 0x06;
|
||||
pub const TYPE_LIST: u8 = 0x07;
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -63,6 +65,16 @@ impl fmt::Display for Value {
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
Value::List(list) => {
|
||||
write!(f, "[")?;
|
||||
for (i, val) in list.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{}", val)?;
|
||||
}
|
||||
write!(f, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +90,7 @@ impl Value {
|
||||
Value::NumberList(_) => TYPE_NUMBER_LIST,
|
||||
Value::StringList(_) => TYPE_STRING_LIST,
|
||||
Value::Object(_) => TYPE_OBJECT,
|
||||
Value::List(_) => TYPE_LIST,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +104,7 @@ impl Value {
|
||||
Value::NumberList(_) => "NumberList",
|
||||
Value::StringList(_) => "StringList",
|
||||
Value::Object(_) => "Object",
|
||||
Value::List(_) => "List",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +164,15 @@ impl Value {
|
||||
bytes.extend_from_slice(&val.serialize());
|
||||
}
|
||||
}
|
||||
Value::List(list) => {
|
||||
// List length (2 bytes)
|
||||
bytes.push((list.len() >> 8) as u8);
|
||||
bytes.push(list.len() as u8);
|
||||
// List items (recursive serialization)
|
||||
for val in list.iter() {
|
||||
bytes.extend_from_slice(&val.serialize());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes
|
||||
@@ -226,6 +249,12 @@ impl From<IndexMap<SmolStr, Value>> for Value {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Value>> for Value {
|
||||
fn from(v: Vec<Value>) -> Self {
|
||||
Value::List(Rc::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Deserialize a value from bytes
|
||||
pub fn deserialize(bytes: &[u8]) -> Result<(Value, usize), String> {
|
||||
@@ -357,6 +386,22 @@ impl Value {
|
||||
|
||||
Ok((Value::Object(Rc::new(map)), pos))
|
||||
}
|
||||
TYPE_LIST => {
|
||||
if bytes.len() < pos + 2 {
|
||||
return Err("Insufficient bytes for List length".to_string());
|
||||
}
|
||||
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
|
||||
let mut list = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
let (val, val_bytes) = Value::deserialize(&bytes[pos..])?;
|
||||
pos += val_bytes;
|
||||
list.push(val);
|
||||
}
|
||||
|
||||
Ok((Value::List(Rc::new(list)), pos))
|
||||
}
|
||||
_ => Err(format!("Unknown type tag: {}", type_tag)),
|
||||
}
|
||||
}
|
||||
@@ -426,7 +471,11 @@ impl Value {
|
||||
.collect();
|
||||
Ok(Value::StringList(Rc::new(strings)))
|
||||
} else {
|
||||
Err("Arrays must contain all numbers or all strings".to_string())
|
||||
let mut items = Vec::with_capacity(arr.len());
|
||||
for item in arr {
|
||||
items.push(Self::from_json_value(item)?);
|
||||
}
|
||||
Ok(Value::List(Rc::new(items)))
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(obj) => {
|
||||
|
||||
@@ -121,6 +121,19 @@ impl LanguageInfo {
|
||||
type_name: v.type_name().to_string(),
|
||||
}).collect())
|
||||
}
|
||||
Value::List(list) => {
|
||||
// For List of Objects, derive fields from the first Object element
|
||||
list.iter().find_map(|item| {
|
||||
if let Value::Object(map) = item {
|
||||
Some(map.iter().map(|(k, v)| FieldInfo {
|
||||
name: k.to_string(),
|
||||
type_name: v.type_name().to_string(),
|
||||
}).collect())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
self.variables.push(VariableInfo {
|
||||
@@ -276,6 +289,23 @@ fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
|
||||
MethodInfo { name: "contains", signature: "(key: String) -> Boolean", doc: Some("Check if key exists") },
|
||||
MethodInfo { name: "get", signature: "(key: String) -> any", doc: Some("Get value by key") },
|
||||
]),
|
||||
("List", vec![
|
||||
MethodInfo { name: "length", signature: "() -> Number", doc: Some("Number of elements") },
|
||||
MethodInfo { name: "len", signature: "() -> Number", doc: None },
|
||||
MethodInfo { name: "isEmpty", signature: "() -> Boolean", doc: None },
|
||||
MethodInfo { name: "first", signature: "() -> any", doc: Some("First element") },
|
||||
MethodInfo { name: "last", signature: "() -> any", doc: Some("Last element") },
|
||||
MethodInfo { name: "get", signature: "(index: Number) -> any", doc: None },
|
||||
MethodInfo { name: "contains", signature: "(value: any) -> Boolean", doc: None },
|
||||
MethodInfo { name: "indexOf", signature: "(value: any) -> Number", doc: None },
|
||||
MethodInfo { name: "slice", signature: "(start: Number, end?: Number) -> List", doc: None },
|
||||
MethodInfo { name: "reverse", signature: "() -> List", doc: None },
|
||||
MethodInfo { name: "join", signature: "(delim?: String) -> String", doc: None },
|
||||
MethodInfo { name: "map", signature: "(field: String) -> NumberList | StringList | List", doc: Some("Extract field from each Object element") },
|
||||
MethodInfo { name: "filter", signature: "(field: String, value?: any) -> List", doc: Some("Filter by field value or truthy field") },
|
||||
MethodInfo { name: "find", signature: "(field: String, value?: any) -> any", doc: Some("Find first element matching field condition") },
|
||||
MethodInfo { name: "sort", signature: "(field: String) -> List", doc: Some("Sort by field value") },
|
||||
]),
|
||||
("StringList", vec![
|
||||
MethodInfo { name: "length", signature: "() -> Number", doc: None },
|
||||
MethodInfo { name: "len", signature: "() -> Number", doc: None },
|
||||
|
||||
@@ -160,6 +160,7 @@ impl<'a> VM<'a> {
|
||||
Value::StringList(l) => Decimal::from(l.len()),
|
||||
Value::NumberList(l) => Decimal::from(l.len()),
|
||||
Value::Object(m) => Decimal::from(m.len()),
|
||||
Value::List(l) => Decimal::from(l.len()),
|
||||
other => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"len() not supported for type {}",
|
||||
|
||||
@@ -26,6 +26,7 @@ impl<'a> VM<'a> {
|
||||
Value::StringList(_) => self.dispatch_string_list_method_inner(&obj_val, method, args),
|
||||
Value::NumberList(_) => self.dispatch_number_list_method_inner(&obj_val, method, args),
|
||||
Value::Object(_) => self.dispatch_object_method_inner(&obj_val, method, args),
|
||||
Value::List(_) => self.dispatch_list_method_inner(&obj_val, method, args),
|
||||
_ => {
|
||||
// Try external methods for any type
|
||||
let type_name: SmolStr = obj_val.type_name().into();
|
||||
@@ -452,9 +453,7 @@ impl<'a> VM<'a> {
|
||||
.collect();
|
||||
Ok(Value::NumberList(Rc::new(numbers)))
|
||||
} else {
|
||||
Err(VMError::RuntimeError(
|
||||
"values() only works when all values are the same type (String or Number)".to_string(),
|
||||
))
|
||||
Ok(Value::List(Rc::new(vals)))
|
||||
}
|
||||
}
|
||||
"length" | "len" => Ok(Value::Number(Decimal::from(map.len()))),
|
||||
@@ -497,4 +496,254 @@ impl<'a> VM<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_list_method_inner(
|
||||
&self,
|
||||
obj_val: &Value,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<Value, VMError> {
|
||||
let list = match obj_val {
|
||||
Value::List(l) => l,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match method {
|
||||
"length" | "len" => Ok(Value::Number(Decimal::from(list.len()))),
|
||||
"isEmpty" => Ok(Value::Boolean(list.is_empty())),
|
||||
"first" => Ok(list.first().cloned().unwrap_or(Value::Null)),
|
||||
"last" => Ok(list.last().cloned().unwrap_or(Value::Null)),
|
||||
"get" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("get() requires an index".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Number(idx) => {
|
||||
let index = idx.to_usize().unwrap_or(usize::MAX);
|
||||
Ok(list.get(index).cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"contains" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
|
||||
}
|
||||
Ok(Value::Boolean(list.contains(&args[0])))
|
||||
}
|
||||
"indexOf" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
|
||||
}
|
||||
let idx = list.iter().position(|item| item == &args[0]);
|
||||
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
|
||||
}
|
||||
"slice" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Number(start_idx) => {
|
||||
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
|
||||
let end = if args.len() > 1 {
|
||||
match &args[1] {
|
||||
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
|
||||
_ => list.len(),
|
||||
}
|
||||
} else {
|
||||
list.len()
|
||||
};
|
||||
Ok(Value::List(Rc::new(list[start..end].to_vec())))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"reverse" => {
|
||||
let mut reversed = list.to_vec();
|
||||
reversed.reverse();
|
||||
Ok(Value::List(Rc::new(reversed)))
|
||||
}
|
||||
"join" => {
|
||||
let delim = if args.is_empty() {
|
||||
""
|
||||
} else {
|
||||
match &args[0] {
|
||||
Value::String(s) => s.as_str(),
|
||||
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
|
||||
}
|
||||
};
|
||||
let strings: Vec<String> = list.iter().map(|v| format!("{}", v)).collect();
|
||||
Ok(Value::String(SmolStr::new(strings.join(delim))))
|
||||
}
|
||||
"map" => {
|
||||
// Property shorthand: list.map("fieldName") extracts a field from each Object element
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("map() requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
let mut values: Vec<Value> = Vec::with_capacity(list.len());
|
||||
for item in list.iter() {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
values.push(map.get(field.as_str()).cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"map() requires all elements to be Objects, got {}",
|
||||
item.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Smart return type: if all values are the same primitive type, return typed list
|
||||
if values.is_empty() {
|
||||
return Ok(Value::List(Rc::new(values)));
|
||||
}
|
||||
if values.iter().all(|v| matches!(v, Value::Number(_))) {
|
||||
let nums: Vec<Decimal> = values
|
||||
.into_iter()
|
||||
.map(|v| match v {
|
||||
Value::Number(n) => n,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
Ok(Value::NumberList(Rc::new(nums)))
|
||||
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
|
||||
let strings: Vec<SmolStr> = values
|
||||
.into_iter()
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
Ok(Value::StringList(Rc::new(strings)))
|
||||
} else {
|
||||
Ok(Value::List(Rc::new(values)))
|
||||
}
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("map() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
"filter" => {
|
||||
// Property shorthand:
|
||||
// list.filter("active") — filter by truthy boolean field
|
||||
// list.filter("field", value) — filter where field == value
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("filter() requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
let filtered: Result<Vec<Value>, VMError> = list
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
|
||||
if args.len() > 1 {
|
||||
// filter("field", value) — equality check
|
||||
if field_val == args[1] {
|
||||
Some(Ok(item.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// filter("field") — truthy check
|
||||
match &field_val {
|
||||
Value::Boolean(b) => if *b { Some(Ok(item.clone())) } else { None },
|
||||
Value::Null => None,
|
||||
_ => Some(Ok(item.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Some(Err(VMError::RuntimeError(format!(
|
||||
"filter() requires all elements to be Objects, got {}",
|
||||
item.type_name()
|
||||
)))),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Value::List(Rc::new(filtered?)))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("filter() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
"find" => {
|
||||
// Property shorthand: list.find("field", value) — first element where field == value
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("find() requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
for item in list.iter() {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
|
||||
if args.len() > 1 {
|
||||
if field_val == args[1] {
|
||||
return Ok(item.clone());
|
||||
}
|
||||
} else {
|
||||
// find("field") — first with truthy field
|
||||
match &field_val {
|
||||
Value::Boolean(b) => if *b { return Ok(item.clone()); },
|
||||
Value::Null => {},
|
||||
_ => return Ok(item.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"find() requires all elements to be Objects, got {}",
|
||||
item.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("find() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
"sort" => {
|
||||
// Property shorthand: list.sort("field") — sort by field value
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("sort() on List requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
let mut sorted = list.to_vec();
|
||||
sorted.sort_by(|a, b| {
|
||||
let a_val = match a {
|
||||
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
|
||||
_ => Value::Null,
|
||||
};
|
||||
let b_val = match b {
|
||||
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
|
||||
_ => Value::Null,
|
||||
};
|
||||
match (&a_val, &b_val) {
|
||||
(Value::Number(a), Value::Number(b)) => a.cmp(b),
|
||||
(Value::String(a), Value::String(b)) => a.cmp(b),
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
}
|
||||
});
|
||||
Ok(Value::List(Rc::new(sorted)))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let key = (SmolStr::new_static("List"), SmolStr::from(method));
|
||||
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||
ext_method(obj_val, args).map_err(VMError::RuntimeError)
|
||||
} else {
|
||||
Err(VMError::MethodNotFound {
|
||||
type_name: "List",
|
||||
method: SmolStr::from(method),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
src/vm/vm.rs
38
src/vm/vm.rs
@@ -597,6 +597,8 @@ impl<'a> VM<'a> {
|
||||
}
|
||||
// String in Object (key check)
|
||||
(Value::String(key), Value::Object(map)) => map.contains_key(key),
|
||||
// Value in List
|
||||
(needle_val, Value::List(list)) => list.contains(needle_val),
|
||||
(needle_val, haystack_val) => {
|
||||
return Err(VMError::InvalidOperation {
|
||||
operation: "in",
|
||||
@@ -684,6 +686,42 @@ impl<'a> VM<'a> {
|
||||
let value = map.get(prop).cloned().unwrap_or(Value::Null);
|
||||
self.registers[dest] = value;
|
||||
}
|
||||
Value::List(list) => {
|
||||
// Property projection: list.field → extract field from each Object element
|
||||
let mut values: Vec<Value> = Vec::with_capacity(list.len());
|
||||
for item in list.iter() {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
values.push(map.get(prop).cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"Cannot access property '{}' on non-Object element in List (got {})",
|
||||
prop,
|
||||
item.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Smart return type: NumberList/StringList when homogeneous
|
||||
if values.is_empty() {
|
||||
self.registers[dest] = Value::List(Rc::new(values));
|
||||
} else if values.iter().all(|v| matches!(v, Value::Number(_))) {
|
||||
let nums: Vec<Decimal> = values.into_iter().map(|v| match v {
|
||||
Value::Number(n) => n,
|
||||
_ => unreachable!(),
|
||||
}).collect();
|
||||
self.registers[dest] = Value::NumberList(Rc::new(nums));
|
||||
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
|
||||
let strings: Vec<SmolStr> = values.into_iter().map(|v| match v {
|
||||
Value::String(s) => s,
|
||||
_ => unreachable!(),
|
||||
}).collect();
|
||||
self.registers[dest] = Value::StringList(Rc::new(strings));
|
||||
} else {
|
||||
self.registers[dest] = Value::List(Rc::new(values));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"Cannot access property '{}' on type {}",
|
||||
|
||||
@@ -1335,3 +1335,608 @@ fn test_from_json_full_workflow() {
|
||||
let r = run_and_get_result_with_globals(code, vec![("customer", customer)]);
|
||||
assert_eq!(r, Value::String("premium".into()));
|
||||
}
|
||||
|
||||
// ==================== LIST (ARRAY OF OBJECTS) TESTS ====================
|
||||
|
||||
/// Build the invoice items test data
|
||||
fn kalemler() -> Value {
|
||||
let items: Vec<Value> = vec![
|
||||
make_kalem(1, "Web Uygulama Gelistirme", 1, "Adet", dec!(45000), dec!(45000)),
|
||||
make_kalem(2, "Mobil Uygulama Gelistirme", 1, "Adet", dec!(35000), dec!(35000)),
|
||||
make_kalem(3, "UI/UX Tasarim Hizmeti", 40, "Saat", dec!(750), dec!(30000)),
|
||||
make_kalem(4, "Sunucu Bakim Sozlesmesi (Yillik)", 1, "Adet", dec!(12000), dec!(12000)),
|
||||
make_kalem(5, "SSL Sertifikasi", 3, "Adet", dec!(500), dec!(1500)),
|
||||
make_kalem(6, "Veritabani Yonetimi", 12, "Ay", dec!(2000), dec!(24000)),
|
||||
make_kalem(7, "API Entegrasyon Hizmeti", 1, "Adet", dec!(18000), dec!(18000)),
|
||||
make_kalem(8, "Bulut Altyapi Kurulumu", 1, "Adet", dec!(8000), dec!(8000)),
|
||||
make_kalem(9, "Siber Guvenlik Danismanligi", 20, "Saat", dec!(900), dec!(18000)),
|
||||
make_kalem(10, "E-posta Sunucu Yapilandirmasi", 1, "Adet", dec!(3500), dec!(3500)),
|
||||
make_kalem(11, "Yedekleme Sistemi Kurulumu", 1, "Adet", dec!(5000), dec!(5000)),
|
||||
make_kalem(12, "SEO Optimizasyonu", 1, "Adet", dec!(7500), dec!(7500)),
|
||||
make_kalem(13, "Egitim ve Dokumantasyon", 8, "Saat", dec!(600), dec!(4800)),
|
||||
make_kalem(14, "Performans Testi ve Raporlama", 1, "Adet", dec!(6000), dec!(6000)),
|
||||
make_kalem(15, "Teknik Destek Paketi (6 Ay)", 1, "Adet", dec!(9000), dec!(9000)),
|
||||
];
|
||||
Value::List(Rc::new(items))
|
||||
}
|
||||
|
||||
fn make_kalem(sira_no: i64, adi: &str, miktar: i64, birim: &str, birim_fiyat: rust_decimal::Decimal, tutar: rust_decimal::Decimal) -> Value {
|
||||
let mut map = IndexMap::new();
|
||||
map.insert(SmolStr::new("siraNo"), Value::Number(rust_decimal::Decimal::from(sira_no)));
|
||||
map.insert(SmolStr::new("adi"), Value::String(SmolStr::new(adi)));
|
||||
map.insert(SmolStr::new("miktar"), Value::Number(rust_decimal::Decimal::from(miktar)));
|
||||
map.insert(SmolStr::new("birim"), Value::String(SmolStr::new(birim)));
|
||||
map.insert(SmolStr::new("birimFiyat"), Value::Number(birim_fiyat));
|
||||
map.insert(SmolStr::new("tutar"), Value::Number(tutar));
|
||||
Value::Object(Rc::new(map))
|
||||
}
|
||||
|
||||
// --- List: length / len ---
|
||||
|
||||
#[test]
|
||||
fn test_list_length() {
|
||||
let result = run_expr_with_globals("kalemler.length()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(15)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_len() {
|
||||
let result = run_expr_with_globals("kalemler.len()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(15)));
|
||||
}
|
||||
|
||||
// --- List: isEmpty ---
|
||||
|
||||
#[test]
|
||||
fn test_list_is_empty_false() {
|
||||
let result = run_expr_with_globals("kalemler.isEmpty()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Boolean(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_is_empty_true() {
|
||||
let empty = Value::List(Rc::new(vec![]));
|
||||
let result = run_expr_with_globals("items.isEmpty()", vec![("items", empty)]);
|
||||
assert_eq!(result, Value::Boolean(true));
|
||||
}
|
||||
|
||||
// --- List: first / last ---
|
||||
|
||||
#[test]
|
||||
fn test_list_first() {
|
||||
let result = run_expr_with_globals("kalemler.first().adi", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::String("Web Uygulama Gelistirme".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_last() {
|
||||
let result = run_expr_with_globals("kalemler.last().adi", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::String("Teknik Destek Paketi (6 Ay)".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_first_empty() {
|
||||
let empty = Value::List(Rc::new(vec![]));
|
||||
let result = run_expr_with_globals("items.first()", vec![("items", empty)]);
|
||||
assert_eq!(result, Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_last_empty() {
|
||||
let empty = Value::List(Rc::new(vec![]));
|
||||
let result = run_expr_with_globals("items.last()", vec![("items", empty)]);
|
||||
assert_eq!(result, Value::Null);
|
||||
}
|
||||
|
||||
// --- List: get ---
|
||||
|
||||
#[test]
|
||||
fn test_list_get() {
|
||||
let result = run_expr_with_globals("kalemler.get(2).adi", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::String("UI/UX Tasarim Hizmeti".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_get_out_of_bounds() {
|
||||
let result = run_expr_with_globals("kalemler.get(100)", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Null);
|
||||
}
|
||||
|
||||
// --- List: contains ---
|
||||
|
||||
#[test]
|
||||
fn test_list_contains() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
Value::Number(dec!(1)),
|
||||
Value::Number(dec!(2)),
|
||||
Value::Number(dec!(3)),
|
||||
]));
|
||||
let result = run_expr_with_globals("items.contains(2)", vec![("items", items)]);
|
||||
assert_eq!(result, Value::Boolean(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_contains_false() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
Value::Number(dec!(1)),
|
||||
Value::Number(dec!(2)),
|
||||
]));
|
||||
let result = run_expr_with_globals("items.contains(5)", vec![("items", items)]);
|
||||
assert_eq!(result, Value::Boolean(false));
|
||||
}
|
||||
|
||||
// --- List: indexOf ---
|
||||
|
||||
#[test]
|
||||
fn test_list_index_of() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
Value::String("a".into()),
|
||||
Value::String("b".into()),
|
||||
Value::String("c".into()),
|
||||
]));
|
||||
let result = run_expr_with_globals(r#"items.indexOf("b")"#, vec![("items", items)]);
|
||||
assert_eq!(result, Value::Number(dec!(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_index_of_not_found() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
Value::String("a".into()),
|
||||
]));
|
||||
let result = run_expr_with_globals(r#"items.indexOf("z")"#, vec![("items", items)]);
|
||||
assert_eq!(result, Value::Number(dec!(-1)));
|
||||
}
|
||||
|
||||
// --- List: slice ---
|
||||
|
||||
#[test]
|
||||
fn test_list_slice() {
|
||||
let result = run_expr_with_globals("kalemler.slice(0, 3).length()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_slice_to_end() {
|
||||
let result = run_expr_with_globals("kalemler.slice(13).length()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(2)));
|
||||
}
|
||||
|
||||
// --- List: reverse ---
|
||||
|
||||
#[test]
|
||||
fn test_list_reverse() {
|
||||
let result = run_expr_with_globals("kalemler.reverse().first().adi", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::String("Teknik Destek Paketi (6 Ay)".into()));
|
||||
}
|
||||
|
||||
// --- List: join ---
|
||||
|
||||
#[test]
|
||||
fn test_list_join() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
Value::Number(dec!(1)),
|
||||
Value::Number(dec!(2)),
|
||||
Value::Number(dec!(3)),
|
||||
]));
|
||||
let result = run_expr_with_globals(r#"items.join(", ")"#, vec![("items", items)]);
|
||||
assert_eq!(result, Value::String("1, 2, 3".into()));
|
||||
}
|
||||
|
||||
// --- List: map (property shorthand) ---
|
||||
|
||||
#[test]
|
||||
fn test_list_map_number_field() {
|
||||
// map("tutar") should return NumberList when all values are Number
|
||||
let result = run_expr_with_globals(r#"kalemler.map("tutar").sum()"#, vec![("kalemler", kalemler())]);
|
||||
// 45000+35000+30000+12000+1500+24000+18000+8000+18000+3500+5000+7500+4800+6000+9000 = 227300
|
||||
assert_eq!(result, Value::Number(dec!(227300)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_map_string_field() {
|
||||
// map("birim") should return StringList when all values are String
|
||||
let result = run_expr_with_globals(r#"kalemler.map("birim").contains("Saat")"#, vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Boolean(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_map_string_field_join() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
make_kalem(1, "A", 1, "X", dec!(10), dec!(10)),
|
||||
make_kalem(2, "B", 1, "Y", dec!(20), dec!(20)),
|
||||
]));
|
||||
let result = run_expr_with_globals(r#"items.map("adi").join(", ")"#, vec![("items", items)]);
|
||||
assert_eq!(result, Value::String("A, B".into()));
|
||||
}
|
||||
|
||||
// --- The main use case: kalemler.map("tutar").sum() * kdvOrani ---
|
||||
|
||||
#[test]
|
||||
fn test_list_map_sum_multiply() {
|
||||
let code = r#"kalemler.map("tutar").sum() * kdvOrani"#;
|
||||
let result = run_expr_with_globals(code, vec![
|
||||
("kalemler", kalemler()),
|
||||
("kdvOrani", Value::Number(dec!(0.20))),
|
||||
]);
|
||||
// 227300 * 0.20 = 45460
|
||||
assert_eq!(result, Value::Number(dec!(45460.00)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_map_avg() {
|
||||
let result = run_expr_with_globals(r#"kalemler.map("tutar").avg()"#, vec![("kalemler", kalemler())]);
|
||||
// 227300 / 15 = 15153.333...
|
||||
let avg = result.clone();
|
||||
match avg {
|
||||
Value::Number(n) => {
|
||||
let rounded = n.round_dp(2);
|
||||
assert_eq!(rounded, dec!(15153.33));
|
||||
}
|
||||
_ => panic!("Expected Number, got {:?}", avg),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_map_min() {
|
||||
let result = run_expr_with_globals(r#"kalemler.map("tutar").min()"#, vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(1500)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_map_max() {
|
||||
let result = run_expr_with_globals(r#"kalemler.map("tutar").max()"#, vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(45000)));
|
||||
}
|
||||
|
||||
// --- List: filter ---
|
||||
|
||||
#[test]
|
||||
fn test_list_filter_by_value() {
|
||||
let result = run_expr_with_globals(
|
||||
r#"kalemler.filter("birim", "Saat").length()"#,
|
||||
vec![("kalemler", kalemler())],
|
||||
);
|
||||
// Items 3, 9, 13 have birim="Saat"
|
||||
assert_eq!(result, Value::Number(dec!(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_filter_by_value_sum() {
|
||||
let result = run_expr_with_globals(
|
||||
r#"kalemler.filter("birim", "Saat").map("tutar").sum()"#,
|
||||
vec![("kalemler", kalemler())],
|
||||
);
|
||||
// 30000 + 18000 + 4800 = 52800
|
||||
assert_eq!(result, Value::Number(dec!(52800)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_filter_by_boolean_field() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
{
|
||||
let mut m = IndexMap::new();
|
||||
m.insert(SmolStr::new("name"), Value::String("A".into()));
|
||||
m.insert(SmolStr::new("active"), Value::Boolean(true));
|
||||
Value::Object(Rc::new(m))
|
||||
},
|
||||
{
|
||||
let mut m = IndexMap::new();
|
||||
m.insert(SmolStr::new("name"), Value::String("B".into()));
|
||||
m.insert(SmolStr::new("active"), Value::Boolean(false));
|
||||
Value::Object(Rc::new(m))
|
||||
},
|
||||
{
|
||||
let mut m = IndexMap::new();
|
||||
m.insert(SmolStr::new("name"), Value::String("C".into()));
|
||||
m.insert(SmolStr::new("active"), Value::Boolean(true));
|
||||
Value::Object(Rc::new(m))
|
||||
},
|
||||
]));
|
||||
let result = run_expr_with_globals(r#"items.filter("active").length()"#, vec![("items", items)]);
|
||||
assert_eq!(result, Value::Number(dec!(2)));
|
||||
}
|
||||
|
||||
// --- List: find ---
|
||||
|
||||
#[test]
|
||||
fn test_list_find_by_value() {
|
||||
let result = run_expr_with_globals(
|
||||
r#"kalemler.find("siraNo", 5).adi"#,
|
||||
vec![("kalemler", kalemler())],
|
||||
);
|
||||
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_find_not_found() {
|
||||
let result = run_expr_with_globals(
|
||||
r#"kalemler.find("siraNo", 99)"#,
|
||||
vec![("kalemler", kalemler())],
|
||||
);
|
||||
assert_eq!(result, Value::Null);
|
||||
}
|
||||
|
||||
// --- List: sort ---
|
||||
|
||||
#[test]
|
||||
fn test_list_sort_by_field() {
|
||||
let result = run_expr_with_globals(
|
||||
r#"kalemler.sort("tutar").first().adi"#,
|
||||
vec![("kalemler", kalemler())],
|
||||
);
|
||||
// Smallest tutar is 1500 (SSL Sertifikasi)
|
||||
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_sort_by_field_last() {
|
||||
let result = run_expr_with_globals(
|
||||
r#"kalemler.sort("tutar").last().adi"#,
|
||||
vec![("kalemler", kalemler())],
|
||||
);
|
||||
// Largest tutar is 45000 (Web Uygulama Gelistirme)
|
||||
assert_eq!(result, Value::String("Web Uygulama Gelistirme".into()));
|
||||
}
|
||||
|
||||
// --- List: in operator ---
|
||||
|
||||
#[test]
|
||||
fn test_list_in_operator() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
Value::String("a".into()),
|
||||
Value::String("b".into()),
|
||||
Value::String("c".into()),
|
||||
]));
|
||||
let result = run_expr_with_globals(r#""b" in items"#, vec![("items", items)]);
|
||||
assert_eq!(result, Value::Boolean(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_in_operator_false() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
Value::Number(dec!(1)),
|
||||
Value::Number(dec!(2)),
|
||||
]));
|
||||
let result = run_expr_with_globals("5 in items", vec![("items", items)]);
|
||||
assert_eq!(result, Value::Boolean(false));
|
||||
}
|
||||
|
||||
// --- List: len() builtin function ---
|
||||
|
||||
#[test]
|
||||
fn test_list_builtin_len() {
|
||||
let result = run_expr_with_globals("len(kalemler)", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(15)));
|
||||
}
|
||||
|
||||
// --- List: from_json ---
|
||||
|
||||
#[test]
|
||||
fn test_list_from_json() {
|
||||
let json = r#"[{"a": 1}, {"a": 2}]"#;
|
||||
let val = Value::from_json(json).unwrap();
|
||||
match &val {
|
||||
Value::List(list) => {
|
||||
assert_eq!(list.len(), 2);
|
||||
match &list[0] {
|
||||
Value::Object(map) => assert_eq!(map.get("a"), Some(&Value::Number(dec!(1)))),
|
||||
other => panic!("Expected Object, got {:?}", other),
|
||||
}
|
||||
}
|
||||
other => panic!("Expected List, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_from_json_mixed() {
|
||||
// Mixed types in array → List
|
||||
let json = r#"[1, "hello", true]"#;
|
||||
let val = Value::from_json(json).unwrap();
|
||||
match &val {
|
||||
Value::List(list) => {
|
||||
assert_eq!(list.len(), 3);
|
||||
assert_eq!(list[0], Value::Number(dec!(1)));
|
||||
assert_eq!(list[1], Value::String("hello".into()));
|
||||
assert_eq!(list[2], Value::Boolean(true));
|
||||
}
|
||||
other => panic!("Expected List, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
// --- List: serialization round-trip ---
|
||||
|
||||
#[test]
|
||||
fn test_list_serialize_deserialize() {
|
||||
let original = Value::List(Rc::new(vec![
|
||||
Value::Number(dec!(42)),
|
||||
Value::String("hello".into()),
|
||||
Value::Boolean(true),
|
||||
]));
|
||||
let bytes = original.serialize();
|
||||
let (deserialized, _) = Value::deserialize(&bytes).unwrap();
|
||||
assert_eq!(original, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_serialize_deserialize_objects() {
|
||||
let original = kalemler();
|
||||
let bytes = original.serialize();
|
||||
let (deserialized, _) = Value::deserialize(&bytes).unwrap();
|
||||
assert_eq!(original, deserialized);
|
||||
}
|
||||
|
||||
// --- List: chained operations ---
|
||||
|
||||
#[test]
|
||||
fn test_list_chain_filter_map_sum() {
|
||||
// Filter only "Adet" items, then sum their tutars
|
||||
let code = r#"kalemler.filter("birim", "Adet").map("tutar").sum()"#;
|
||||
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
|
||||
// Adet items: 45000+35000+12000+1500+18000+8000+3500+5000+7500+6000+9000 = 150500
|
||||
assert_eq!(result, Value::Number(dec!(150500)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_chain_filter_length() {
|
||||
let code = r#"kalemler.filter("birim", "Ay").length()"#;
|
||||
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
|
||||
// Only item 6 (Veritabani Yonetimi) has birim="Ay"
|
||||
assert_eq!(result, Value::Number(dec!(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_chain_sort_slice_map() {
|
||||
// Sort by tutar, take top 3, get names
|
||||
let code = r#"kalemler.sort("tutar").reverse().slice(0, 3).map("adi").join(", ")"#;
|
||||
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::String("Web Uygulama Gelistirme, Mobil Uygulama Gelistirme, UI/UX Tasarim Hizmeti".into()));
|
||||
}
|
||||
|
||||
// --- Object values() now returns List for mixed types ---
|
||||
|
||||
#[test]
|
||||
fn test_object_values_mixed_returns_list() {
|
||||
let mut map = IndexMap::new();
|
||||
map.insert(SmolStr::new("name"), Value::String("Alice".into()));
|
||||
map.insert(SmolStr::new("age"), Value::Number(dec!(30)));
|
||||
let obj = Value::Object(Rc::new(map));
|
||||
let result = run_expr_with_globals("item.values().length()", vec![("item", obj)]);
|
||||
assert_eq!(result, Value::Number(dec!(2)));
|
||||
}
|
||||
|
||||
// --- List: Display ---
|
||||
|
||||
#[test]
|
||||
fn test_list_display() {
|
||||
let list = Value::List(Rc::new(vec![
|
||||
Value::Number(dec!(1)),
|
||||
Value::String("hello".into()),
|
||||
Value::Boolean(true),
|
||||
]));
|
||||
assert_eq!(format!("{}", list), r#"[1, "hello", true]"#);
|
||||
}
|
||||
|
||||
// --- List: complex invoice scenario ---
|
||||
|
||||
#[test]
|
||||
fn test_invoice_total_with_kdv() {
|
||||
let code = r#"
|
||||
araToplam = kalemler.map("tutar").sum()
|
||||
kdv = araToplam * kdvOrani
|
||||
result = araToplam + kdv
|
||||
"#;
|
||||
let result = run_and_get_result_with_globals(code, vec![
|
||||
("kalemler", kalemler()),
|
||||
("kdvOrani", Value::Number(dec!(0.20))),
|
||||
]);
|
||||
// 227300 + 45460 = 272760
|
||||
assert_eq!(result, Value::Number(dec!(272760.00)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invoice_item_count_by_birim() {
|
||||
let code = r#"
|
||||
adetSayisi = kalemler.filter("birim", "Adet").length()
|
||||
saatSayisi = kalemler.filter("birim", "Saat").length()
|
||||
result = adetSayisi + saatSayisi
|
||||
"#;
|
||||
let result = run_and_get_result_with_globals(code, vec![("kalemler", kalemler())]);
|
||||
// 11 Adet + 3 Saat = 14
|
||||
assert_eq!(result, Value::Number(dec!(14)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invoice_find_and_access_property() {
|
||||
let code = r#"kalemler.find("adi", "SSL Sertifikasi").birimFiyat"#;
|
||||
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(500)));
|
||||
}
|
||||
|
||||
// ==================== LIST PROPERTY PROJECTION TESTS ====================
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_number() {
|
||||
// kalemler.tutar → NumberList
|
||||
let result = run_expr_with_globals("kalemler.tutar.sum()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(227300)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_string() {
|
||||
// kalemler.adi → StringList
|
||||
let result = run_expr_with_globals(r#"kalemler.birim.contains("Saat")"#, vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Boolean(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_sum_multiply() {
|
||||
// The main use case: kalemler.tutar.sum() * kdvOrani
|
||||
let code = "kalemler.tutar.sum() * kdvOrani";
|
||||
let result = run_expr_with_globals(code, vec![
|
||||
("kalemler", kalemler()),
|
||||
("kdvOrani", Value::Number(dec!(0.20))),
|
||||
]);
|
||||
assert_eq!(result, Value::Number(dec!(45460.00)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_min_max() {
|
||||
let result = run_expr_with_globals("kalemler.tutar.min()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(1500)));
|
||||
|
||||
let result = run_expr_with_globals("kalemler.tutar.max()", vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::Number(dec!(45000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_join() {
|
||||
let items = Value::List(Rc::new(vec![
|
||||
make_kalem(1, "A", 1, "X", dec!(10), dec!(10)),
|
||||
make_kalem(2, "B", 1, "Y", dec!(20), dec!(20)),
|
||||
]));
|
||||
let result = run_expr_with_globals(r#"items.adi.join(", ")"#, vec![("items", items)]);
|
||||
assert_eq!(result, Value::String("A, B".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_chained_with_filter() {
|
||||
// filter → projection: kalemler.filter("birim", "Saat").tutar.sum()
|
||||
let code = r#"kalemler.filter("birim", "Saat").tutar.sum()"#;
|
||||
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
|
||||
// Saat items: 30000 + 18000 + 4800 = 52800
|
||||
assert_eq!(result, Value::Number(dec!(52800)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_empty() {
|
||||
let empty = Value::List(Rc::new(vec![]));
|
||||
let result = run_expr_with_globals("items.tutar", vec![("items", empty)]);
|
||||
match result {
|
||||
Value::List(l) => assert!(l.is_empty()),
|
||||
_ => panic!("Expected empty List, got {:?}", result),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_property_projection_with_sort() {
|
||||
// sort → projection
|
||||
let code = r#"kalemler.sort("tutar").adi.first()"#;
|
||||
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
|
||||
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invoice_full_scenario() {
|
||||
let code = r#"
|
||||
araToplam = kalemler.tutar.sum()
|
||||
kdv = araToplam * kdvOrani
|
||||
genelToplam = araToplam + kdv
|
||||
saatlikHizmetler = kalemler.filter("birim", "Saat").tutar.sum()
|
||||
result = genelToplam
|
||||
"#;
|
||||
let result = run_and_get_result_with_globals(code, vec![
|
||||
("kalemler", kalemler()),
|
||||
("kdvOrani", Value::Number(dec!(0.20))),
|
||||
]);
|
||||
assert_eq!(result, Value::Number(dec!(272760.00)));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dexpr-wasm"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -37,6 +37,9 @@ fn value_to_json(val: &Value) -> serde_json::Value {
|
||||
.collect();
|
||||
serde_json::Value::Object(obj)
|
||||
}
|
||||
Value::List(list) => {
|
||||
serde_json::Value::Array(list.iter().map(|v| value_to_json(v)).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user