spec-patch.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import type { Spec, JsonPatch } from "@json-render/core";
  2. import { setByPath, getByPath, removeByPath } from "@json-render/core";
  3. export function setSpecValue(
  4. newSpec: Spec,
  5. path: string,
  6. value: unknown,
  7. ): void {
  8. if (path === "/root") {
  9. newSpec.root = value as string;
  10. return;
  11. }
  12. if (path === "/state") {
  13. newSpec.state = value as Record<string, unknown>;
  14. return;
  15. }
  16. if (path.startsWith("/state/")) {
  17. if (!newSpec.state) newSpec.state = {};
  18. setByPath(
  19. newSpec.state as Record<string, unknown>,
  20. path.slice("/state".length),
  21. value,
  22. );
  23. return;
  24. }
  25. if (path.startsWith("/elements/")) {
  26. const pathParts = path.slice("/elements/".length).split("/");
  27. const elementKey = pathParts[0];
  28. if (!elementKey) return;
  29. if (pathParts.length === 1) {
  30. if (value == null || typeof value !== "object") return;
  31. const el = value as Record<string, unknown>;
  32. newSpec.elements[elementKey] = {
  33. ...el,
  34. type: typeof el.type === "string" ? el.type : "",
  35. props: el.props != null && typeof el.props === "object" ? el.props : {},
  36. children: Array.isArray(el.children) ? el.children : [],
  37. } as Spec["elements"][string];
  38. } else {
  39. const element = newSpec.elements[elementKey];
  40. if (element) {
  41. const newElement = { ...element };
  42. setByPath(
  43. newElement as unknown as Record<string, unknown>,
  44. "/" + pathParts.slice(1).join("/"),
  45. value,
  46. );
  47. newSpec.elements[elementKey] = newElement;
  48. }
  49. }
  50. }
  51. }
  52. export function removeSpecValue(newSpec: Spec, path: string): void {
  53. if (path === "/state") {
  54. delete newSpec.state;
  55. return;
  56. }
  57. if (path.startsWith("/state/") && newSpec.state) {
  58. removeByPath(
  59. newSpec.state as Record<string, unknown>,
  60. path.slice("/state".length),
  61. );
  62. return;
  63. }
  64. if (path.startsWith("/elements/")) {
  65. const pathParts = path.slice("/elements/".length).split("/");
  66. const elementKey = pathParts[0];
  67. if (!elementKey) return;
  68. if (pathParts.length === 1) {
  69. delete newSpec.elements[elementKey];
  70. } else {
  71. const element = newSpec.elements[elementKey];
  72. if (element) {
  73. const newElement = { ...element };
  74. removeByPath(
  75. newElement as unknown as Record<string, unknown>,
  76. "/" + pathParts.slice(1).join("/"),
  77. );
  78. newSpec.elements[elementKey] = newElement;
  79. }
  80. }
  81. }
  82. }
  83. export function getSpecValue(spec: Spec, path: string): unknown {
  84. if (path === "/root") return spec.root;
  85. if (path === "/state") return spec.state;
  86. if (path.startsWith("/state/") && spec.state) {
  87. return getByPath(
  88. spec.state as Record<string, unknown>,
  89. path.slice("/state".length),
  90. );
  91. }
  92. return getByPath(spec as unknown as Record<string, unknown>, path);
  93. }
  94. export function normalizeSpec(spec: Spec): void {
  95. if (
  96. spec.state === null ||
  97. (spec.state !== undefined && typeof spec.state !== "object")
  98. ) {
  99. spec.state = undefined;
  100. }
  101. for (const key of Object.keys(spec.elements)) {
  102. const el = spec.elements[key];
  103. if (!el || typeof el !== "object") {
  104. delete spec.elements[key];
  105. continue;
  106. }
  107. if (el.props == null || typeof el.props !== "object") {
  108. spec.elements[key] = { ...el, props: {} } as Spec["elements"][string];
  109. }
  110. if (!Array.isArray(spec.elements[key]!.children)) {
  111. spec.elements[key] = {
  112. ...spec.elements[key]!,
  113. children: [],
  114. } as Spec["elements"][string];
  115. }
  116. }
  117. }
  118. export function applySpecPatch(spec: Spec, patch: JsonPatch): Spec {
  119. const newSpec = {
  120. ...spec,
  121. elements: { ...spec.elements },
  122. ...(spec.state ? { state: { ...spec.state } } : {}),
  123. };
  124. switch (patch.op) {
  125. case "add":
  126. case "replace":
  127. setSpecValue(newSpec, patch.path, patch.value);
  128. break;
  129. case "remove":
  130. removeSpecValue(newSpec, patch.path);
  131. break;
  132. case "move":
  133. if (patch.from) {
  134. const moveValue = getSpecValue(newSpec, patch.from);
  135. removeSpecValue(newSpec, patch.from);
  136. setSpecValue(newSpec, patch.path, moveValue);
  137. }
  138. break;
  139. case "copy":
  140. if (patch.from) {
  141. setSpecValue(newSpec, patch.path, getSpecValue(newSpec, patch.from));
  142. }
  143. break;
  144. case "test":
  145. break;
  146. }
  147. normalizeSpec(newSpec);
  148. return newSpec;
  149. }