sozsoft-platform/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs

499 lines
19 KiB
C#
Raw Normal View History

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" },
2026-09-21 15:01:40 +00:00
["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,
2026-09-21 15:01:40 +00:00
["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;
}
2026-10-07 22:07:28 +00:00
/// <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));
2026-10-11 19:57:23 +00:00
// 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");
2026-10-07 22:07:28 +00:00
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":
{
2026-10-11 19:57:23 +00:00
// TS ile ayni: gun sayisi tam sayiya kirpilir (Math.trunc(Number(offset))).
var offset = Param("offset");
2026-10-11 19:57:23 +00:00
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":
2026-10-07 22:07:28 +00:00
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) + "))";
2026-09-21 15:01:40 +00:00
case "location":
return "await geoTo(" + q(Param("lat")) + ", " + q(Param("lng")) +
(Param("accuracy").Length > 0 ? ", " + q(Param("accuracy")) : string.Empty) + ")";
case "custom":
2026-10-11 19:57:23 +00:00
{
// TS ile ayni: yalnizca sondaki tek ';' silinir.
var code = Param("code").Trim();
return code.EndsWith(';') ? code[..^1] : code;
}
default:
return string.Empty;
}
}
2026-10-07 22:07:28 +00:00
/// <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";
}
}
}