Packages
A blazingly fast Elixir library for repairing malformed JSON using binary pattern matching. Handles LLM outputs, legacy data, and broken JSON with intelligent context-aware fixes.
Current section
14 Versions
Jump to
Current section
14 Versions
Compare versions
5
files changed
+116
additions
-27
deletions
| @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |
| 7 7 | |
| 8 8 | ## [Unreleased] |
| 9 9 | |
| 10 | + ## [0.1.9] - 2025-10-28 |
| 11 | + |
| 12 | + ### Fixed |
| 13 | + - **Multi-item arrays**: Layer 2 now closes missing braces and brackets between array elements, avoiding mismatched delimiter swaps during comma handling and restoring proper validation (#8). |
| 14 | + |
| 15 | + ### Added |
| 16 | + - **Regression coverage**: Added LF and CRLF variants of the reporter's samples to ensure multi-element arrays with missing terminators stay repaired. |
| 17 | + |
| 10 18 | ## [0.1.8] - 2025-10-27 |
| 11 19 | |
| 12 20 | ### Added |
| @@ -340,7 +348,8 @@ This is a **100% rewrite** - all previous code has been replaced with the new la | |
| 340 348 | - Minimal memory overhead (< 8KB for repairs) |
| 341 349 | - All operations pass performance thresholds |
| 342 350 | |
| 343 | - [Unreleased]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.8...HEAD |
| 351 | + [Unreleased]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.9...HEAD |
| 352 | + [0.1.9]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.8...v0.1.9 |
| 344 353 | [0.1.8]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.7...v0.1.8 |
| 345 354 | [0.1.7]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.6...v0.1.7 |
| 346 355 | [0.1.6]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.5...v0.1.6 |
| @@ -74,6 +74,7 @@ Runs **before** the main layer pipeline to handle complex patterns that would ot | |
| 74 74 | |
| 75 75 | ### 🏗️ **Structural Repairs (Layer 2)** |
| 76 76 | - **Missing closing delimiters**: `{"name": "Alice"` → `{"name": "Alice"}` |
| 77 | + - **Array element terminators**: Multi-item arrays recover missing braces/brackets between elements *(v0.1.9)* |
| 77 78 | - **Extra delimiters**: `{"name": "Alice"}}}` → `{"name": "Alice"}` |
| 78 79 | - **Mismatched delimiters**: `[{"name": "Alice"}]` → proper structure |
| 79 80 | - **Missing opening braces**: `["key": "value"]` → `[{"key": "value"}]` |
| @@ -159,7 +160,7 @@ Add JsonRemedy to your `mix.exs`: | |
| 159 160 | ```elixir |
| 160 161 | def deps do |
| 161 162 | [ |
| 162 | - {:json_remedy, "~> 0.1.8"} |
| 163 | + {:json_remedy, "~> 0.1.9"} |
| 163 164 | ] |
| 164 165 | end |
| 165 166 | ``` |
| @@ -2,7 +2,7 @@ | |
| 2 2 | [{<<"Documentation">>,<<"https://hexdocs.pm/json_remedy">>}, |
| 3 3 | {<<"GitHub">>,<<"https://github.com/nshkrdotcom/json_remedy">>}]}. |
| 4 4 | {<<"name">>,<<"json_remedy">>}. |
| 5 | - {<<"version">>,<<"0.1.8">>}. |
| 5 | + {<<"version">>,<<"0.1.9">>}. |
| 6 6 | {<<"description">>, |
| 7 7 | <<"A blazingly fast Elixir library for repairing malformed JSON using binary pattern matching. Handles LLM outputs, legacy data, and broken JSON with intelligent context-aware fixes.">>}. |
| 8 8 | {<<"elixir">>,<<"~> 1.14">>}. |
| @@ -137,6 +137,9 @@ defmodule JsonRemedy.Layer2.StructuralRepair do | |
| 137 137 | "]" -> |
| 138 138 | handle_closing_bracket(state, char) |
| 139 139 | |
| 140 | + "," -> |
| 141 | + handle_comma(state, char) |
| 142 | + |
| 140 143 | _ -> |
| 141 144 | # Regular character, just add to result |
| 142 145 | %{state | result_chars: [char | state.result_chars]} |
| @@ -288,6 +291,12 @@ defmodule JsonRemedy.Layer2.StructuralRepair do | |
| 288 291 | end |
| 289 292 | end |
| 290 293 | |
| 294 | + @spec handle_comma(state :: state_map(), char :: <<_::8>>) :: state_map() |
| 295 | + defp handle_comma(state, char) do |
| 296 | + adjusted_state = maybe_close_contexts_before_separator(state) |
| 297 | + %{adjusted_state | result_chars: [char | adjusted_state.result_chars]} |
| 298 | + end |
| 299 | + |
| 291 300 | @spec determine_state_from_stack(stack :: [context_frame()]) :: parser_state() |
| 292 301 | defp determine_state_from_stack([]), do: :root |
| 293 302 | defp determine_state_from_stack([%{type: :brace} | _]), do: :object |
| @@ -344,29 +353,7 @@ defmodule JsonRemedy.Layer2.StructuralRepair do | |
| 344 353 | state.context_stack |
| 345 354 | |> Enum.reduce({[], []}, fn context_frame, {chars_acc, repairs_acc} -> |
| 346 355 | {close_char, repair} = |
| 347 | - case context_frame.type do |
| 348 | - :brace -> |
| 349 | - repair = %{ |
| 350 | - layer: :structural_repair, |
| 351 | - action: "added missing closing brace", |
| 352 | - position: state.position + length(chars_acc), |
| 353 | - original: nil, |
| 354 | - replacement: "}" |
| 355 | - } |
| 356 | - |
| 357 | - {"}", repair} |
| 358 | - |
| 359 | - :bracket -> |
| 360 | - repair = %{ |
| 361 | - layer: :structural_repair, |
| 362 | - action: "added missing closing bracket", |
| 363 | - position: state.position + length(chars_acc), |
| 364 | - original: nil, |
| 365 | - replacement: "]" |
| 366 | - } |
| 367 | - |
| 368 | - {"]", repair} |
| 369 | - end |
| 356 | + closing_info(context_frame.type, state.position + length(chars_acc)) |
| 370 357 | |
| 371 358 | {[close_char | chars_acc], [repair | repairs_acc]} |
| 372 359 | end) |
| @@ -380,6 +367,98 @@ defmodule JsonRemedy.Layer2.StructuralRepair do | |
| 380 367 | } |
| 381 368 | end |
| 382 369 | |
| 370 | + @spec maybe_close_contexts_before_separator(state_map()) :: state_map() |
| 371 | + defp maybe_close_contexts_before_separator(state) do |
| 372 | + next_char = next_significant_char(state) |
| 373 | + |
| 374 | + cond do |
| 375 | + next_char in [nil, "\""] -> |
| 376 | + state |
| 377 | + |
| 378 | + requires_array_boundary_closure?(state.context_stack) -> |
| 379 | + close_contexts_until_array(state) |
| 380 | + |
| 381 | + true -> |
| 382 | + state |
| 383 | + end |
| 384 | + end |
| 385 | + |
| 386 | + @spec next_significant_char(state_map()) :: String.t() | nil |
| 387 | + defp next_significant_char(state) do |
| 388 | + remaining_length = String.length(state.input) - state.position - 1 |
| 389 | + |
| 390 | + if remaining_length <= 0 do |
| 391 | + nil |
| 392 | + else |
| 393 | + state.input |
| 394 | + |> String.slice(state.position + 1, remaining_length) |
| 395 | + |> String.graphemes() |
| 396 | + |> Enum.find(fn char -> |
| 397 | + char not in [" ", "\t", "\n", "\r"] |
| 398 | + end) |
| 399 | + end |
| 400 | + end |
| 401 | + |
| 402 | + @spec requires_array_boundary_closure?([context_frame()]) :: boolean() |
| 403 | + defp requires_array_boundary_closure?(stack) do |
| 404 | + case Enum.find_index(stack, &match?(%{type: :bracket}, &1)) do |
| 405 | + nil -> false |
| 406 | + 0 -> false |
| 407 | + _ -> true |
| 408 | + end |
| 409 | + end |
| 410 | + |
| 411 | + @spec close_contexts_until_array(state_map()) :: state_map() |
| 412 | + defp close_contexts_until_array(state) do |
| 413 | + {to_close, remaining} = |
| 414 | + Enum.split_while(state.context_stack, fn %{type: type} -> type != :bracket end) |
| 415 | + |
| 416 | + if Enum.empty?(to_close) do |
| 417 | + state |
| 418 | + else |
| 419 | + {result_chars, repairs} = |
| 420 | + Enum.with_index(to_close) |
| 421 | + |> Enum.reduce({state.result_chars, state.repairs}, fn {%{type: type}, idx}, |
| 422 | + {chars_acc, repairs_acc} -> |
| 423 | + {close_char, repair} = closing_info(type, state.position + idx) |
| 424 | + {[close_char | chars_acc], [repair | repairs_acc]} |
| 425 | + end) |
| 426 | + |
| 427 | + %{ |
| 428 | + state |
| 429 | + | context_stack: remaining, |
| 430 | + current_state: determine_state_from_stack(remaining), |
| 431 | + result_chars: result_chars, |
| 432 | + repairs: repairs |
| 433 | + } |
| 434 | + end |
| 435 | + end |
| 436 | + |
| 437 | + @spec closing_info(delimiter_type(), non_neg_integer()) :: {String.t(), repair_action()} |
| 438 | + defp closing_info(:brace, position) do |
| 439 | + repair = %{ |
| 440 | + layer: :structural_repair, |
| 441 | + action: "added missing closing brace", |
| 442 | + position: position, |
| 443 | + original: nil, |
| 444 | + replacement: "}" |
| 445 | + } |
| 446 | + |
| 447 | + {"}", repair} |
| 448 | + end |
| 449 | + |
| 450 | + defp closing_info(:bracket, position) do |
| 451 | + repair = %{ |
| 452 | + layer: :structural_repair, |
| 453 | + action: "added missing closing bracket", |
| 454 | + position: position, |
| 455 | + original: nil, |
| 456 | + replacement: "]" |
| 457 | + } |
| 458 | + |
| 459 | + {"]", repair} |
| 460 | + end |
| 461 | + |
| 383 462 | # LayerBehaviour callback implementations |
| 384 463 | |
| 385 464 | @doc """ |
| @@ -1,7 +1,7 @@ | |
| 1 1 | defmodule JsonRemedy.MixProject do |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 | - @version "0.1.8" |
| 4 | + @version "0.1.9" |
| 5 5 | @source_url "https://github.com/nshkrdotcom/json_remedy" |
| 6 6 | |
| 7 7 | def project do |