sozsoft-platform/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs
2026-10-11 22:57:23 +03:00

498 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Sozsoft.Platform.Editors;
/// <summary>
/// Tek bir Script Builder kurali. <see cref="EditorScript"/> uzerindeki tarif
/// metotlariyla olusturulur, akici metotlarla kosul ve tetikleyici eklenir.
/// </summary>
public sealed class EditorScriptRule
{
/// <summary>Zorunlu parametreler. Dialogdaki isRuleComplete kontrolunun karsiligi.</summary>
private static readonly Dictionary<string, string[]> RequiredParams = new Dictionary<string, string[]>
{
["multiply"] = new[] { "left", "right", "target" },
["subtract"] = new[] { "left", "right", "target" },
["percent"] = new[] { "amount", "rate", "target" },
["sum"] = new[] { "target" },
["formula"] = new[] { "target", "expression" },
["today"] = new[] { "target" },
["days"] = new[] { "start", "end", "target" },
["hours"] = new[] { "start", "end", "target" },
["copy"] = new[] { "source", "target" },
["setValue"] = new[] { "target", "text" },
["clear"] = new string[0],
["readOnly"] = new[] { "target" },
["notify"] = new[] { "message" },
["ask"] = new[] { "message" },
["openUrl"] = new[] { "url" },
["apiToField"] = new[] { "target", "url" },
["location"] = new[] { "lat", "lng" },
["custom"] = new[] { "code" }
};
/// <summary>Alan listesi isteyen tarifler.</summary>
private static readonly HashSet<string> FieldListRecipes = new HashSet<string> { "sum", "clear" };
/// <summary>
/// Grup bazli varsayilan tetikleyici. Deger ureten kurallar form acilirken de
/// calisir; kullaniciya bir sey gosteren ya da dis cagri yapan kurallar
/// yalnizca deger degistiginde calisir.
/// </summary>
private static readonly Dictionary<string, EditorScriptTrigger> GroupDefaultTrigger =
new Dictionary<string, EditorScriptTrigger>
{
["multiply"] = EditorScriptTrigger.Both,
["subtract"] = EditorScriptTrigger.Both,
["percent"] = EditorScriptTrigger.Both,
["sum"] = EditorScriptTrigger.Both,
["formula"] = EditorScriptTrigger.Both,
["today"] = EditorScriptTrigger.Both,
["days"] = EditorScriptTrigger.Both,
["hours"] = EditorScriptTrigger.Both,
["copy"] = EditorScriptTrigger.Both,
["setValue"] = EditorScriptTrigger.Both,
["clear"] = EditorScriptTrigger.Both,
["readOnly"] = EditorScriptTrigger.Both,
["notify"] = EditorScriptTrigger.Change,
["ask"] = EditorScriptTrigger.Change,
["openUrl"] = EditorScriptTrigger.Change,
["apiToField"] = EditorScriptTrigger.Change,
["location"] = EditorScriptTrigger.Change,
["custom"] = EditorScriptTrigger.Change
};
private readonly string _recipe;
private readonly List<KeyValuePair<string, string>> _params = new List<KeyValuePair<string, string>>();
private readonly List<string> _fields = new List<string>();
private readonly List<EditorScriptCondition> _conditions = new List<EditorScriptCondition>();
private string _id;
private bool _anyCondition;
private EditorScriptTrigger _trigger = EditorScriptTrigger.Default;
internal EditorScriptRule(string recipe)
{
_recipe = recipe;
}
/* ---------------------------------------------------------------- *
* Akici yapilandirma
* ---------------------------------------------------------------- */
/// <summary>Kural yalnizca verilen kosullarin <b>hepsi</b> saglandiginda calisir.</summary>
public EditorScriptRule When(params EditorScriptCondition[] conditions) => AddConditions(conditions, false);
/// <summary>Kural verilen kosullardan <b>herhangi biri</b> saglandiginda calisir.</summary>
public EditorScriptRule WhenAny(params EditorScriptCondition[] conditions) => AddConditions(conditions, true);
/// <summary>Yalnizca alanin degeri degistiginde calissin.</summary>
public EditorScriptRule OnChange() => WithTrigger(EditorScriptTrigger.Change);
/// <summary>Yalnizca form acilirken calissin (varsayilan deger uretmek icin).</summary>
public EditorScriptRule OnOpen() => WithTrigger(EditorScriptTrigger.Open);
/// <summary>Hem acilista hem deger degistiginde calissin.</summary>
public EditorScriptRule OnOpenAndChange() => WithTrigger(EditorScriptTrigger.Both);
/// <summary>Tetikleyiciyi acikca belirler. Default ise tarifin grup varsayilani kullanilir.</summary>
public EditorScriptRule WithTrigger(EditorScriptTrigger trigger)
{
_trigger = trigger;
return this;
}
/// <summary>
/// Kural kimligini sabitler. Verilmezse Build sirasinda tarif adi ve sira
/// numarasindan uretilir (ornek: "multiply_1").
/// </summary>
public EditorScriptRule WithId(string id)
{
_id = id;
return this;
}
/// <summary>
/// <see cref="ToJson"/> ciktisini (dialogun <c>@builder</c> basligindaki kural) geri okur; bilinmeyen
/// tarif <see cref="IsComplete"/>'ten gecmez ve uretimde atlanir.
/// </summary>
internal static EditorScriptRule FromJson(System.Text.Json.JsonElement json)
{
static string Text(System.Text.Json.JsonElement element, string name) =>
element.ValueKind == System.Text.Json.JsonValueKind.Object && element.TryGetProperty(name, out var value)
? value.ValueKind == System.Text.Json.JsonValueKind.String ? value.GetString() : value.ToString()
: null;
static EditorScriptCondition Condition(System.Text.Json.JsonElement element) =>
EditorScriptCondition.Of(Text(element, "operator") ?? "always", Text(element, "source"), Text(element, "value"));
var rule = new EditorScriptRule(Text(json, "recipe") ?? string.Empty).WithId(Text(json, "id"));
if (json.TryGetProperty("params", out var parameters) && parameters.ValueKind == System.Text.Json.JsonValueKind.Object)
{
foreach (var parameter in parameters.EnumerateObject())
rule.Set(parameter.Name, Text(parameters, parameter.Name));
}
if (json.TryGetProperty("fields", out var fields) && fields.ValueKind == System.Text.Json.JsonValueKind.Array)
rule.SetFields(fields.EnumerateArray().Select(field => field.ToString()));
var conditions = new List<EditorScriptCondition>();
if (json.TryGetProperty("condition", out var condition))
conditions.Add(Condition(condition));
if (json.TryGetProperty("conditions", out var more) && more.ValueKind == System.Text.Json.JsonValueKind.Array)
conditions.AddRange(more.EnumerateArray().Select(Condition));
// Ek "always" kosullari dialogun basliginda durur; atilirsa ToJson farkli baslik uretir ve
// script dialogda "elle duzenlenmis" sayilir. Tek "always" ise EffectiveConditions ile aynidir.
rule.AddConditions([.. conditions], Text(json, "join") == "or");
return rule.WithTrigger(Text(json, "trigger") switch
{
"change" => EditorScriptTrigger.Change,
"open" => EditorScriptTrigger.Open,
"both" => EditorScriptTrigger.Both,
_ => EditorScriptTrigger.Default
});
}
/// <summary>
/// Mantiksal alana yazan <c>setValue</c> kuralinin degerini <c>true</c>/<c>false</c>'a cevirir; dialog bu
/// alanlarda yalnizca Evet/Hayir sectirir. <c>tpl("0")</c> metni onay kutusunu dolu sayardi.
/// </summary>
internal EditorScriptRule WithBooleanValue(System.Func<string, bool> isBooleanField)
{
if (_recipe != "setValue" || !isBooleanField(Param("target")))
{
return this;
}
var flag = Param("text").Trim().ToLowerInvariant() switch
{
"true" or "1" or "yes" or "evet" or "on" => "true",
"false" or "0" or "no" or "hayir" or "hayır" or "off" => "false",
_ => null
};
var index = _params.FindIndex(pair => pair.Key == "text");
if (flag != null && index >= 0)
{
_params[index] = new KeyValuePair<string, string>("text", flag);
}
return this;
}
/// <summary>Tek kurallik script metni. Birden fazla kural icin <see cref="EditorScript.Build(EditorScriptRule[])"/> kullanin.</summary>
public string ToScript() => EditorScript.Build(this);
/// <summary>Kurali dogrudan EditorScript alanina atanabilir hale getirir.</summary>
public static implicit operator string(EditorScriptRule rule) => rule?.ToScript() ?? string.Empty;
/* ---------------------------------------------------------------- *
* Ic kullanim
* ---------------------------------------------------------------- */
internal EditorScriptRule Set(string key, string value)
{
// Bos parametreler dialogda da yazilmaz; JSON'un birebir esit kalmasi icin atlanir.
if (!string.IsNullOrEmpty(value))
{
_params.Add(new KeyValuePair<string, string>(key, value));
}
return this;
}
internal EditorScriptRule SetNumber(string key, int? value)
=> value.HasValue ? Set(key, value.Value.ToString(CultureInfo.InvariantCulture)) : this;
internal EditorScriptRule SetFields(IEnumerable<string> fields)
{
if (fields != null)
{
_fields.AddRange(fields.Where(field => !string.IsNullOrEmpty(field)));
}
return this;
}
internal void EnsureId(int order)
{
if (string.IsNullOrEmpty(_id))
{
_id = _recipe + "_" + order.ToString(CultureInfo.InvariantCulture);
}
}
internal EditorScriptTrigger EffectiveTrigger()
{
if (_trigger != EditorScriptTrigger.Default)
{
return _trigger;
}
return GroupDefaultTrigger.TryGetValue(_recipe, out var trigger) ? trigger : EditorScriptTrigger.Change;
}
internal bool IsComplete()
{
if (!RequiredParams.TryGetValue(_recipe, out var required))
{
return false;
}
if (_conditions.Any(condition => !condition.IsComplete()))
{
return false;
}
if (FieldListRecipes.Contains(_recipe) && _fields.Count == 0)
{
return false;
}
return required.All(key => _params.Any(pair => pair.Key == key && pair.Value.Trim().Length > 0));
}
internal string ToJson()
{
var conditions = EffectiveConditions();
var json = "{\"id\":" + EditorScript.Quote(_id) +
",\"recipe\":" + EditorScript.Quote(_recipe) +
",\"condition\":" + conditions[0].ToJson();
if (conditions.Count > 1)
{
json += ",\"conditions\":[" + string.Join(",", conditions.Skip(1).Select(c => c.ToJson())) + "]" +
",\"join\":" + EditorScript.Quote(_anyCondition ? "or" : "and");
}
json += ",\"params\":{" +
string.Join(",", _params.Select(pair => EditorScript.Quote(pair.Key) + ":" + EditorScript.Quote(pair.Value))) +
"}";
json += ",\"fields\":[" + string.Join(",", _fields.Select(EditorScript.Quote)) + "]";
if (_trigger != EditorScriptTrigger.Default)
{
json += ",\"trigger\":" + EditorScript.Quote(TriggerName(_trigger));
}
return json + "}";
}
/// <summary>
/// Kuralin tek satirlik kodu. runsOnOpen yalnizca script'in tamami acilista da
/// calisiyorsa true gelir; o durumda her kuralin ne zaman calisacagi isReady
/// ile ayristirilir.
/// </summary>
internal string ToCode(bool runsOnOpen)
{
var body = BuildBody();
if (string.IsNullOrEmpty(body))
{
return string.Empty;
}
var guards = new List<string>();
if (runsOnOpen)
{
var trigger = EffectiveTrigger();
if (trigger == EditorScriptTrigger.Change)
{
guards.Add("!isReady");
}
else if (trigger == EditorScriptTrigger.Open)
{
guards.Add("isReady");
}
}
// readOnly'de kosul dogrudan aksiyonun argumanidir, if ile sarmalanmaz.
if (_recipe != "readOnly")
{
var condition = ConditionExpression();
if (!string.IsNullOrEmpty(condition))
{
guards.Add(condition);
}
}
return guards.Count > 0 ? "if (" + string.Join(" && ", guards) + ") " + body : body;
}
private List<EditorScriptCondition> EffectiveConditions()
=> _conditions.Count > 0 ? _conditions : new List<EditorScriptCondition> { EditorScriptCondition.Always() };
private string ConditionExpression()
{
var parts = EffectiveConditions()
.Select(condition => condition.ToExpression())
.Where(expression => !string.IsNullOrEmpty(expression))
.ToList();
if (parts.Count == 0)
{
return string.Empty;
}
if (parts.Count == 1)
{
return parts[0];
}
return string.Join(_anyCondition ? " || " : " && ", parts.Select(part => "(" + part + ")"));
}
private EditorScriptRule AddConditions(EditorScriptCondition[] conditions, bool any)
{
if (conditions != null)
{
_conditions.AddRange(conditions.Where(condition => condition != null));
}
if (_conditions.Count > 1)
{
_anyCondition = any;
}
return this;
}
private string Param(string key)
{
var match = _params.FirstOrDefault(pair => pair.Key == key);
return match.Key == null ? string.Empty : match.Value;
}
private string Digits() => Param("digits").Length > 0 ? Param("digits") : "2";
private string BuildBody()
{
var q = new System.Func<string, string>(EditorScript.Quote);
var n = new System.Func<string, string>(EditorScript.NumberOrField);
switch (_recipe)
{
case "multiply":
return "set(" + q(Param("target")) + ", round(" + n(Param("left")) + " * " + n(Param("right")) + ", " + Digits() + "))";
case "subtract":
return "set(" + q(Param("target")) + ", round(" + n(Param("left")) + " - " + n(Param("right")) + ", " + Digits() + "))";
case "percent":
{
var amount = n(Param("amount"));
var portion = amount + " * " + n(Param("rate")) + " / 100";
var expression = Param("mode") == "add"
? amount + " + " + portion
: Param("mode") == "subtract"
? amount + " - " + portion
: portion;
return "set(" + q(Param("target")) + ", round(" + expression + ", " + Digits() + "))";
}
case "sum":
return "set(" + q(Param("target")) + ", round(sum(" + string.Join(", ", _fields.Select(q)) + "), " + Digits() + "))";
case "formula":
return "set(" + q(Param("target")) + ", " + (Param("expression").Length > 0 ? Param("expression") : "null") + ")";
case "today":
{
// TS ile ayni: gun sayisi tam sayiya kirpilir (Math.trunc(Number(offset))).
var offset = Param("offset");
var parsed = decimal.TryParse(offset, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
? (int)decimal.Truncate(value)
: 0;
var start = parsed != 0
? "new Date(Date.now() + " + parsed.ToString(CultureInfo.InvariantCulture) + " * 86400000)"
: "new Date()";
return "set(" + q(Param("target")) + ", " + start + ".toISOString().slice(0, 10))";
}
case "days":
return "set(" + q(Param("target")) + ", days(" + q(Param("start")) + ", " + q(Param("end")) + "))";
case "hours":
return "set(" + q(Param("target")) + ", hours(" + q(Param("start")) + ", " + q(Param("end")) + "))";
case "copy":
return "copy(" + q(Param("source")) + ", " + q(Param("target")) + ")";
case "setValue":
return "set(" + q(Param("target")) + ", " + ValueLiteral(Param("text")) + ")";
case "clear":
return "clear(" + string.Join(", ", _fields.Select(q)) + ")";
case "readOnly":
{
var expression = ConditionExpression();
if (expression.Length == 0)
{
expression = "true";
}
var flag = Param("invert") == "true" ? "!(" + expression + ")" : expression;
return "readOnly(" + q(Param("target")) + ", " + flag + ")";
}
case "notify":
return "notify(" + q(Param("message")) + ")";
case "ask":
return "if (!ask(" + q(Param("message")) + ")) return";
case "openUrl":
return Param("target").Length > 0 && Param("target") != "_blank"
? "openUrl(" + q(Param("url")) + ", " + q(Param("target")) + ")"
: "openUrl(" + q(Param("url")) + ")";
case "apiToField":
return "set(" + q(Param("target")) + ", await api(" + q(Param("url")) +
(Param("path").Length > 0 ? ", " + q(Param("path")) : string.Empty) + "))";
case "location":
return "await geoTo(" + q(Param("lat")) + ", " + q(Param("lng")) +
(Param("accuracy").Length > 0 ? ", " + q(Param("accuracy")) : string.Empty) + ")";
case "custom":
{
// TS ile ayni: yalnizca sondaki tek ';' silinir.
var code = Param("code").Trim();
return code.EndsWith(';') ? code[..^1] : code;
}
default:
return string.Empty;
}
}
/// <summary>
/// <c>true</c>/<c>false</c> mantiksal sabit olarak yazilir; metin olarak (<c>tpl('false')</c>) yazilsa
/// onay kutusu dolu sayardi. Diger degerler sablondur. scriptRecipes.ts <c>valueLiteral</c> ile ayni.
/// </summary>
private static string ValueLiteral(string text)
{
var trimmed = (text ?? string.Empty).Trim();
return trimmed.Equals("true", System.StringComparison.OrdinalIgnoreCase) || trimmed.Equals("false", System.StringComparison.OrdinalIgnoreCase)
? trimmed.ToLowerInvariant()
: "tpl(" + EditorScript.Quote(text) + ")";
}
private static string TriggerName(EditorScriptTrigger trigger)
{
switch (trigger)
{
case EditorScriptTrigger.Change: return "change";
case EditorScriptTrigger.Open: return "open";
default: return "both";
}
}
}