Repository: knuckleswtf/scribe (php)

Issue:
parse exists rules
### Scribe version

4.37.1

### Your question

Hey,

I noticed that the **exists** rule isn't currently handled in the parseRule switch case within the ParsesValidationRules trait. This rule seems like it **could be added easily**.

Is there a particular reason why it hasn’t been included?

Thanks!


### Docs

- [X] I've checked [the docs](https://scribe.knuckles.wtf/laravel), the [troubleshooting guide](https://scribe.knuckles.wtf/laravel/troubleshooting), and [existing issues](https://github.com/knuckleswtf/scribe/issues?q=is%3Aissue+), but I didn't find a solution

Pull request:
feat: Add parsing support for exists rule in parseRule switch case
Adds support for parsing the `exists` validation rule in the `parseRule` method of the `ParsesValidationRules` trait. 

This will ensure that the generated documentation accurately reflects the use of the `exists` rule, preventing incomplete or misleading validation descriptions.

Fixes #882


Patch:
diff --git a/resources/css/theme-default.style.css b/resources/css/theme-default.style.css
index b46a0745..9a4741c1 100644
--- a/resources/css/theme-default.style.css
+++ b/resources/css/theme-default.style.css
@@ -894,6 +894,11 @@ html {
     text-shadow: 0 1px 2px rgba(0, 0, 0, .4)
 }
 
+.content blockquote pre.sf-dump,
+.content pre pre.sf-dump {
+    width: 100%;
+}
+
 .content .annotation {
     background-color: #292929;
     color: #fff;
diff --git a/resources/js/tryitout.js b/resources/js/tryitout.js
index 01c584f2..2a1d2b8a 100644
--- a/resources/js/tryitout.js
+++ b/resources/js/tryitout.js
@@ -139,6 +139,17 @@ function handleResponse(endpointId, response, status, headers) {
 
     const responseContentEl = document.querySelector('#execution-response-content-' + endpointId);
 
+    // Check if the response contains Laravel's  dd() default dump output
+    const isLaravelDump = response.includes('Sfdump');
+
+    // If it's a Laravel dd() dump, use innerHTML to render it safely
+    if (isLaravelDump) {
+        responseContentEl.innerHTML = response === '' ? responseContentEl.dataset.emptyResponseText : response;
+    } else {
+        // Otherwise, stick to textContent for regular responses
+        responseContentEl.textContent = response === '' ? responseContentEl.dataset.emptyResponseText : response;
+    }
+
     // Prettify it if it's JSON
     let isJson = false;
     try {
@@ -146,11 +157,12 @@ function handleResponse(endpointId, response, status, headers) {
         if (jsonParsed !== null) {
             isJson = true;
             response = JSON.stringify(jsonParsed, null, 4);
+            responseContentEl.textContent = response;
         }
     } catch (e) {
 
     }
-    responseContentEl.textContent = response === '' ? responseContentEl.dataset.emptyResponseText : response;
+
     isJson && window.hljs.highlightElement(responseContentEl);
     const statusEl = document.querySelector('#execution-response-status-' + endpointId);
     statusEl.textContent = ` (${status})`;
diff --git a/resources/views/themes/elements/components/field-details.blade.php b/resources/views/themes/elements/components/field-details.blade.php
index b2682fc0..d8c57cf4 100644
--- a/resources/views/themes/elements/components/field-details.blade.php
+++ b/resources/views/themes/elements/components/field-details.blade.php
@@ -49,12 +49,12 @@
                 @endif
             </div>
         @endif
-        @if(!$hasChildren && !is_null($example) && $example != '')
+        @if(!$hasChildren && !is_null($example) && $example !== '')
             <div class="sl-stack sl-stack--horizontal sl-stack--2 sl-flex sl-flex-row sl-items-baseline sl-text-muted">
                 <span>Example:</span> <!-- <span> important for spacing -->
                 <div class="sl-flex sl-flex-1 sl-flex-wrap" style="gap: 4px;">
                     <div class="sl-max-w-full sl-break-all sl-px-1 sl-bg-canvas-tint sl-text-muted sl-rounded sl-border">
-                        {{ is_array($example) ? json_encode($example) : $example }}
+                        {{ is_array($example) || is_bool($example) ? json_encode($example) : $example }}
                     </div>
                 </div>
             </div>
diff --git a/resources/views/themes/elements/index.blade.php b/resources/views/themes/elements/index.blade.php
index dfa760b6..b921e15c 100644
--- a/resources/views/themes/elements/index.blade.php
+++ b/resources/views/themes/elements/index.blade.php
@@ -137,6 +137,9 @@
                             }
                         } catch (e) {}
 
+                        // Replace HTML entities
+                        responseContent = responseContent.replace(/[<>&]/g, (i) => '&#' + i.charCodeAt(0) + ';');
+
                         contentEl.innerHTML = responseContent;
                         isJson && window.hljs.highlightElement(contentEl);
                     })
diff --git a/src/Extracting/ParsesValidationRules.php b/src/Extracting/ParsesValidationRules.php
index b78b113e..0dd9038c 100644
--- a/src/Extracting/ParsesValidationRules.php
+++ b/src/Extracting/ParsesValidationRules.php
@@ -531,7 +531,9 @@ trait ParsesValidationRules
                 case 'different':
                     $parameterData['description'] .= " The value and <code>{$arguments[0]}</code> must be different.";
                     break;
-
+                case 'exists':
+                    $parameterData['description'] .= " The <code>{$arguments[1]}</code> of an existing record in the {$arguments[0]} table.";
+                    break;
                 default:
                     // Other rules not supported
                     break;
diff --git a/src/Writing/PostmanCollectionWriter.php b/src/Writing/PostmanCollectionWriter.php
index a115f465..29be7765 100644
--- a/src/Writing/PostmanCollectionWriter.php
+++ b/src/Writing/PostmanCollectionWriter.php
@@ -293,7 +293,7 @@ class PostmanCollectionWriter
                     // See https://www.php.net/manual/en/function.parse-str.php
                     $query[] = [
                         'key' => "{$name}[$index]",
-                        'value' => $value,
+                        'value' => is_string($value) ? $value : strval($value),
                         'description' => strip_tags($parameterData->description),
                         // Default query params to disabled if they aren't required and have empty values
                         'disabled' => !$parameterData->required && empty($parameterData->example),


Test patch:
diff --git a/tests/Unit/ValidationRuleParsingTest.php b/tests/Unit/ValidationRuleParsingTest.php
index 03aa4e9a..5a877ac9 100644
--- a/tests/Unit/ValidationRuleParsingTest.php
+++ b/tests/Unit/ValidationRuleParsingTest.php
@@ -3,6 +3,7 @@
 namespace Knuckles\Scribe\Tests\Unit;
 
 use Illuminate\Foundation\Application;
+use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Translation\Translator;
 use Illuminate\Validation\Rule;
@@ -40,6 +41,11 @@ class ValidationRuleParsingTest extends BaseLaravelTest
      */
     public function can_parse_supported_rules(array $ruleset, array $customInfo, array $expected)
     {
+        // Needed for `exists` rule
+        Schema::create('users', function ($table) {
+            $table->id();
+        });
+        
         $results = $this->strategy->parse($ruleset, $customInfo);
 
         $parameterName = array_keys($ruleset)[0];
@@ -49,7 +55,9 @@ class ValidationRuleParsingTest extends BaseLaravelTest
             $this->assertEquals($expected['type'], $results[$parameterName]['type']);
         }
 
-        // Validate that the generated values actually pass validation
+        // Validate that the generated values actually pass validation (for rules where we can generate some data)
+        if (is_string($ruleset[$parameterName]) && str_contains($ruleset[$parameterName], "exists")) return;
+        
         $exampleData = [$parameterName => $results[$parameterName]['example']];
         $validator = Validator::make($exampleData, $ruleset);
         try {
@@ -439,6 +447,13 @@ class ValidationRuleParsingTest extends BaseLaravelTest
                 'description' => 'Must be accepted.',
             ],
         ];
+        yield 'exists' => [
+            ['exists_param' => 'exists:users,id'],
+            [],
+            [
+                'description' => 'The <code>id</code> of an existing record in the users table.',
+            ],
+        ];
         yield 'unsupported' => [
             ['unsupported_param' => [new DummyValidationRule, 'bail']],
             ['unsupported_param' => ['description' => $description]],