Skip to content

Commit 160361b

Browse files
committed
Add app/Support/JsonFixer.php
1 parent d7c3a9c commit 160361b

2 files changed

Lines changed: 378 additions & 1 deletion

File tree

app/Generators/OpenAIGenerator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
namespace App\Generators;
1414

1515
use App\Contracts\GeneratorContract;
16+
use App\Support\JsonFixer;
1617
use App\Support\OpenAI;
1718
use Illuminate\Support\Arr;
1819
use Symfony\Component\Console\Output\OutputInterface;
@@ -72,6 +73,9 @@ public function generate(string $prompt): string
7273
$output->write($text);
7374
});
7475

75-
return (string) $commitMessages;
76+
return (new JsonFixer())
77+
->missingValue('')
78+
->silent()
79+
->fix(substr((string) $commitMessages, strpos((string) $commitMessages, '[')));
7680
}
7781
}

app/Support/JsonFixer.php

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the guanguans/ai-commit.
7+
*
8+
* (c) guanguans <ityaozm@gmail.com>
9+
*
10+
* This source file is subject to the MIT license that is bundled.
11+
*/
12+
13+
namespace App\Support;
14+
15+
/**
16+
* This file is modified from https://github.com/adhocore/php-json-fixer.
17+
*/
18+
class JsonFixer
19+
{
20+
/**
21+
* @var array Current token stack indexed by position
22+
*/
23+
protected $stack = [];
24+
25+
/**
26+
* @var bool If current char is within a string
27+
*/
28+
protected $inStr = false;
29+
30+
/**
31+
* @var bool Whether to throw Exception on failure
32+
*/
33+
protected $silent = false;
34+
35+
/**
36+
* @var array The complementary pairs
37+
*/
38+
protected $pairs = [
39+
'{' => '}',
40+
'[' => ']',
41+
'"' => '"',
42+
];
43+
44+
/**
45+
* @var int The last seen object `{` type position
46+
*/
47+
protected $objectPos = -1;
48+
49+
/** @var int The last seen array `[` type position */
50+
protected $arrayPos = -1;
51+
52+
/**
53+
* @var string Missing value. (Options: true, false, null)
54+
*/
55+
protected $missingValue = 'null';
56+
57+
/**
58+
* Set/unset silent mode.
59+
*
60+
* @return $this
61+
*/
62+
public function silent(bool $silent = true)
63+
{
64+
$this->silent = $silent;
65+
66+
return $this;
67+
}
68+
69+
/**
70+
* Set missing value.
71+
*
72+
* @return $this
73+
*/
74+
public function missingValue(string $value)
75+
{
76+
// if (null === $value) {
77+
// $value = 'null';
78+
// } elseif (is_bool($value)) {
79+
// $value = $value ? 'true' : 'false';
80+
// }
81+
82+
$this->missingValue = $value;
83+
84+
return $this;
85+
}
86+
87+
/**
88+
* Fix the truncated JSON.
89+
*
90+
* @param string $json the JSON string to fix
91+
*
92+
* @return string Fixed JSON. If failed with silent then original JSON.
93+
*
94+
* @throws \RuntimeException when fixing fails
95+
*/
96+
public function fix(string $json)
97+
{
98+
[$head, $json, $tail] = $this->trim($json);
99+
100+
if (empty($json) || $this->isValid($json)) {
101+
return $json;
102+
}
103+
104+
if (null !== $tmpJson = $this->quickFix($json)) {
105+
return $tmpJson;
106+
}
107+
108+
$this->reset();
109+
110+
return $head.$this->doFix($json).$tail;
111+
}
112+
113+
protected function trim($json)
114+
{
115+
\preg_match('/^(\s*)([^\s]+)(\s*)$/', $json, $match);
116+
117+
$match += ['', '', '', ''];
118+
$match[2] = \trim($json);
119+
120+
\array_shift($match);
121+
122+
return $match;
123+
}
124+
125+
protected function isValid($json): bool
126+
{
127+
/** @psalm-suppress UnusedFunctionCall */
128+
\json_decode($json);
129+
130+
return \JSON_ERROR_NONE === \json_last_error();
131+
}
132+
133+
protected function quickFix($json)
134+
{
135+
if (1 === \strlen($json) && isset($this->pairs[$json])) {
136+
return $json.$this->pairs[$json];
137+
}
138+
139+
if ('"' !== $json[0]) {
140+
return $this->maybeLiteral($json);
141+
}
142+
143+
return $this->padString($json);
144+
}
145+
146+
protected function reset()
147+
{
148+
$this->stack = [];
149+
$this->inStr = false;
150+
$this->objectPos = -1;
151+
$this->arrayPos = -1;
152+
}
153+
154+
protected function maybeLiteral($json)
155+
{
156+
if (! \in_array($json[0], ['t', 'f', 'n'])) {
157+
return null;
158+
}
159+
160+
foreach (['true', 'false', 'null'] as $literal) {
161+
if (0 === \strpos($literal, $json)) {
162+
return $literal;
163+
}
164+
}
165+
166+
// @codeCoverageIgnoreStart
167+
return null;
168+
// @codeCoverageIgnoreEnd
169+
}
170+
171+
protected function doFix($json)
172+
{
173+
[$index, $char] = [-1, ''];
174+
175+
while (isset($json[++$index])) {
176+
[$prev, $char] = [$char, $json[$index]];
177+
178+
$next = $json[$index + 1] ?? '';
179+
180+
if (! \in_array($char, [' ', "\n", "\r"])) {
181+
$this->stack($prev, $char, $index, $next);
182+
}
183+
}
184+
185+
return $this->fixOrFail($json);
186+
}
187+
188+
protected function stack($prev, $char, $index, $next)
189+
{
190+
if ($this->maybeStr($prev, $char, $index)) {
191+
return;
192+
}
193+
194+
$last = $this->lastToken();
195+
196+
if (\in_array($last, [',', ':', '"']) && \preg_match('/\"|\d|\{|\[|t|f|n/', $char)) {
197+
$this->popToken();
198+
}
199+
200+
if (\in_array($char, [',', ':', '[', '{'])) {
201+
$this->stack[$index] = $char;
202+
}
203+
204+
$this->updatePos($char, $index);
205+
}
206+
207+
protected function lastToken()
208+
{
209+
return \end($this->stack);
210+
}
211+
212+
/**
213+
* @noinspection OffsetOperationsInspection
214+
*/
215+
protected function popToken($token = null)
216+
{
217+
// Last one
218+
if (null === $token) {
219+
return \array_pop($this->stack);
220+
}
221+
222+
$keys = \array_reverse(\array_keys($this->stack));
223+
foreach ($keys as $key) {
224+
if ($this->stack[$key] === $token) {
225+
unset($this->stack[$key]);
226+
227+
break;
228+
}
229+
}
230+
}
231+
232+
protected function maybeStr($prev, $char, $index)
233+
{
234+
if ('\\' !== $prev && '"' === $char) {
235+
$this->inStr = ! $this->inStr;
236+
}
237+
238+
if ($this->inStr && '"' !== $this->lastToken()) {
239+
$this->stack[$index] = '"';
240+
}
241+
242+
return $this->inStr;
243+
}
244+
245+
protected function updatePos($char, int $index)
246+
{
247+
if ('{' === $char) {
248+
$this->objectPos = $index;
249+
} elseif ('}' === $char) {
250+
$this->popToken('{');
251+
$this->objectPos = -1;
252+
} elseif ('[' === $char) {
253+
$this->arrayPos = $index;
254+
} elseif (']' === $char) {
255+
$this->popToken('[');
256+
$this->arrayPos = -1;
257+
}
258+
}
259+
260+
protected function fixOrFail($json)
261+
{
262+
$length = \strlen($json);
263+
$tmpJson = $this->pad($json);
264+
265+
if ($this->isValid($tmpJson)) {
266+
return $tmpJson;
267+
}
268+
269+
if ($this->silent) {
270+
return $json;
271+
}
272+
273+
throw new \RuntimeException(\sprintf('Could not fix JSON (tried padding `%s`)', \substr($tmpJson, $length)));
274+
}
275+
276+
/* trait PadsJson */
277+
public function pad($tmpJson)
278+
{
279+
if (! $this->inStr) {
280+
$tmpJson = \rtrim($tmpJson, ',');
281+
while (',' === $this->lastToken()) {
282+
$this->popToken();
283+
}
284+
}
285+
286+
$tmpJson = $this->padLiteral($tmpJson);
287+
$tmpJson = $this->padObject($tmpJson);
288+
289+
return $this->padStack($tmpJson);
290+
}
291+
292+
protected function padLiteral($tmpJson)
293+
{
294+
if ($this->inStr) {
295+
return $tmpJson;
296+
}
297+
298+
$match = \preg_match('/(tr?u?e?|fa?l?s?e?|nu?l?l?)$/', $tmpJson, $matches);
299+
300+
if (! $match || null === $literal = $this->maybeLiteral($matches[1])) {
301+
return $tmpJson;
302+
}
303+
304+
return \substr($tmpJson, 0, -\strlen($matches[1])).$literal;
305+
}
306+
307+
protected function padStack($tmpJson)
308+
{
309+
foreach (\array_reverse($this->stack, true) as $token) {
310+
if (isset($this->pairs[$token])) {
311+
$tmpJson .= $this->pairs[$token];
312+
}
313+
}
314+
315+
return $tmpJson;
316+
}
317+
318+
protected function padObject($tmpJson)
319+
{
320+
if (! $this->objectNeedsPadding($tmpJson)) {
321+
return $tmpJson;
322+
}
323+
324+
$part = \substr($tmpJson, $this->objectPos + 1);
325+
if (\preg_match('/(\s*\"[^"]+\"\s*:\s*[^,]+,?)+$/', $part, $matches)) {
326+
return $tmpJson;
327+
}
328+
329+
if ($this->inStr) {
330+
$tmpJson .= '"';
331+
}
332+
333+
$tmpJson = $this->padIf($tmpJson, ':');
334+
$tmpJson .= $this->missingValue;
335+
336+
if ('"' === $this->lastToken()) {
337+
$this->popToken();
338+
}
339+
340+
return $tmpJson;
341+
}
342+
343+
protected function objectNeedsPadding($tmpJson): bool
344+
{
345+
$last = \substr($tmpJson, -1);
346+
$empty = '{' === $last && ! $this->inStr;
347+
348+
return ! $empty && $this->arrayPos < $this->objectPos;
349+
}
350+
351+
protected function padString($string)
352+
{
353+
$last = \substr($string, -1);
354+
$last2 = \substr($string, -2);
355+
356+
if ('\"' === $last2 || '"' !== $last) {
357+
return $string.'"';
358+
}
359+
360+
// @codeCoverageIgnoreStart
361+
return null;
362+
// @codeCoverageIgnoreEnd
363+
}
364+
365+
protected function padIf($string, $substr)
366+
{
367+
if (\substr($string, -\strlen($substr)) !== $substr) {
368+
return $string.$substr;
369+
}
370+
371+
return $string;
372+
}
373+
}

0 commit comments

Comments
 (0)