Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Allow arbitrary character length in nanoid #4004

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

oimo23
Copy link

@oimo23 oimo23 commented Feb 22, 2025

I hope you don't mind, but I've gone ahead and implemented it.
#3954.

Changes Introduced:

・Allow arbitrary character length in nanoid.
・Since the existing implementation assumed the first argument to nanoid would be a custom error message, I made sure not to break that behavior.
・I added an interface for arguments such as min, max, and length.

// Both approaches work

// Added in this PR
nanoid(64, { message: "Must be 64 characters long" });

// Current Version
nanoid("Custom Error");

Issue Reference:

Closes #3954.

Summary by CodeRabbit

  • New Features

    • Enhanced identifier validation to allow users to specify a custom length for generated IDs.
    • Improved error feedback by providing tailored messages when an identifier does not meet the expected length.
  • Tests

    • Added a new test scenario to ensure the custom-length identifier functionality and error handling work as intended.

Copy link

coderabbitai bot commented Feb 22, 2025

Walkthrough

This pull request updates the nanoid validation functionality. A new test case for a 64-character nanoid and custom error messages was added in two test suites. The type definitions and method for nanoid validation were modified: the ZodStringCheck type now includes a numeric value property for length, and the nanoid method accepts multiple parameter types (number, string, or object) for flexible configuration. Additionally, dynamic regex generation based on the provided length was introduced.

Changes

File(s) Change Summary
deno/lib/__tests__/string.test.ts
src/__tests__/string.test.ts
Added a new test case custom character length nanoid to verify a 64-character nanoid string, checking for proper parsing and the custom error message on invalid inputs.
deno/lib/types.ts
src/types.ts
Updated ZodStringCheck to include a numeric value property and modified the nanoid method signature to accept either a number, string, or object for specifying length and a custom error message. Introduced nanoidRegex for dynamic regex generation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Validator as ZodString/nanoid
    participant RegexGen as nanoidRegex()
    participant CheckHandler as _addCheck()

    User->>Validator: Provide parameters (length/message/object)
    Validator->>RegexGen: Generate regex based on specified length
    RegexGen-->>Validator: Return dynamic regex
    Validator->>CheckHandler: Add nanoid check with length and error message
    CheckHandler-->>Validator: Return validation outcome
    Validator-->>User: Return parsed result or validation error
Loading

Poem

I'm a bouncy rabbit coding under the moon,
My nanoid hops in tune with the glowing rune.
With tests and types all set in a clever spin,
I nibble on errors till the code's a win.
Carrots and code, together we croon!

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

netlify bot commented Feb 22, 2025

Deploy Preview for guileless-rolypoly-866f8a ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 3968e4e
🔍 Latest deploy log https://app.netlify.com/sites/guileless-rolypoly-866f8a/deploys/67ba0071a4aaf800088b4152
😎 Deploy Preview https://deploy-preview-4004--guileless-rolypoly-866f8a.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@oimo23 oimo23 marked this pull request as ready for review February 22, 2025 16:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
deno/lib/types.ts (1)

884-885: Add length boundary checks when constructing the nanoid regex.
Although the regex works when check.value is valid, there’s no safeguard against non-integer or negative values. Consider rejecting length values below 1 or above a reasonable upper bound to prevent unexpected behavior.

src/__tests__/string.test.ts (1)

383-400: LGTM! Well-structured test implementation.

The test case effectively validates the new functionality for arbitrary character length nanoid validation, including custom error message handling.

Consider adding more test cases for comprehensive coverage.

Consider adding tests for:

  1. Maximum length validation
  2. Invalid character validation
  3. Edge cases (empty string, null, undefined)

Apply this diff to add more test cases:

 test("custom character length nanoid", () => {
   const nanoid64 = z.string().nanoid(64);
   const valid64 =
     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
   nanoid64.parse(valid64);

+  // Test maximum length validation
+  const nanoid128 = z.string().nanoid(128);
+  const valid128 = valid64 + valid64;
+  nanoid128.parse(valid128);
+
+  // Test invalid characters
+  const invalidChars = valid64.replace("a", "@");
+  expect(() => nanoid64.parse(invalidChars)).toThrow();
+
+  // Test edge cases
+  expect(() => nanoid64.parse("")).toThrow();
+  expect(() => nanoid64.parse(null as any)).toThrow();
+  expect(() => nanoid64.parse(undefined as any)).toThrow();

   const nanoid64WithCustomError = z
     .string()
     .nanoid(64, { message: "custom error" });
   nanoid64WithCustomError.parse(valid64);

   const shortId = "abc123";
   const result = nanoid64WithCustomError.safeParse(shortId);
   expect(result.success).toEqual(false);
   if (!result.success) {
     expect(result.error.issues[0].message).toEqual("custom error");
   }
 });
deno/lib/__tests__/string.test.ts (1)

384-401: LGTM! Consider adding more test cases for comprehensive coverage.

The test implementation is well-structured and covers the basic validation paths. However, consider adding test cases for:

  • Invalid characters in a 64-character string
  • Edge cases like empty string, null, undefined
  • Boundary values (63 and 65 characters)

Here's a suggested expansion of the test cases:

 test("custom character length nanoid", () => {
   const nanoid64 = z.string().nanoid(64);
   const valid64 =
     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
   nanoid64.parse(valid64);

   const nanoid64WithCustomError = z
     .string()
     .nanoid(64, { message: "custom error" });
   nanoid64WithCustomError.parse(valid64);

   const shortId = "abc123";
   const result = nanoid64WithCustomError.safeParse(shortId);
   expect(result.success).toEqual(false);
   if (!result.success) {
     expect(result.error.issues[0].message).toEqual("custom error");
   }
+
+  // Test invalid characters
+  const invalidChars = valid64.replace("a", "@");
+  expect(nanoid64.safeParse(invalidChars).success).toEqual(false);
+
+  // Test boundary values
+  const shortBy1 = valid64.slice(0, -1);
+  expect(nanoid64.safeParse(shortBy1).success).toEqual(false);
+
+  const longBy1 = valid64 + "A";
+  expect(nanoid64.safeParse(longBy1).success).toEqual(false);
+
+  // Test edge cases
+  expect(nanoid64.safeParse("").success).toEqual(false);
+  expect(nanoid64.safeParse(undefined).success).toEqual(false);
+  expect(nanoid64.safeParse(null).success).toEqual(false);
 });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eea05ae and 3968e4e.

📒 Files selected for processing (4)
  • deno/lib/__tests__/string.test.ts (1 hunks)
  • deno/lib/types.ts (4 hunks)
  • src/__tests__/string.test.ts (1 hunks)
  • src/types.ts (4 hunks)
🔇 Additional comments (4)
deno/lib/types.ts (1)

595-595: Recommend validating the value field for non-negative lengths.
Currently, there's no explicit check to ensure that value (representing the nanoid length) is positive. Consider validating that it meets a minimum requirement, e.g., >= 1, to avoid unintended behaviors.

src/types.ts (3)

595-595: LGTM! Type definition updated to support arbitrary character length.

The addition of the value property to the nanoid check type aligns with the PR objective and follows the pattern used by other string checks.


644-646: LGTM! Well-implemented dynamic regex generator.

The regex pattern correctly validates:

  • Alphanumeric characters plus underscore and hyphen
  • Case-insensitive matching
  • Exact length enforcement

1122-1146: LGTM! Excellent implementation with backward compatibility.

The implementation successfully:

  • Maintains backward compatibility with existing code using string parameter
  • Adds support for arbitrary length specification
  • Provides clear parameter overloads
  • Uses a sensible default length of 21

Comment on lines +644 to +646
function nanoidRegex(length?: number): RegExp {
return new RegExp(`^[a-z0-9_-]{${length}}$`, "i");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle undefined or invalid length in regex generation.
If length is undefined or negative, the template string produces an invalid quantifier (e.g., ^[a-z0-9_-]{undefined}$). This will lead to a runtime SyntaxError. Consider defaulting to a safe value (e.g., 21) or throwing a descriptive error when length is missing or invalid.

Comment on lines +1122 to +1145
nanoid(
arg1?: number | string | { length?: number; message?: string },
arg2?: { message?: string }
) {
let length = 21;
let message: string | undefined;

// This workaround ensures backward compatibility for custom error message (existing calls still work)
// while enabling a new way to specify a custom length and message.
// old: nanoid("custom error") & new: nanoid(64, { message: "custom error" })
if (typeof arg1 === "string") {
message = arg1;
} else if (typeof arg1 === "number") {
length = arg1;
if (arg2?.message) {
message = arg2.message;
}
}

return this._addCheck({
kind: "nanoid",
value: length,
...errorUtil.errToObj(message),
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extend handling for object-based arguments in the nanoid(...) method.
The new signature allows passing an object { length?: number; message?: string }, but the code only updates length/message when arg1 is a string or a number. To ensure consistency with the documented usage, consider adding an else if (typeof arg1 === "object") block, for example:

+    } else if (typeof arg1 === "object" && arg1 !== null) {
+      length = arg1.length ?? 21;
+      message = arg1.message;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nanoid(
arg1?: number | string | { length?: number; message?: string },
arg2?: { message?: string }
) {
let length = 21;
let message: string | undefined;
// This workaround ensures backward compatibility for custom error message (existing calls still work)
// while enabling a new way to specify a custom length and message.
// old: nanoid("custom error") & new: nanoid(64, { message: "custom error" })
if (typeof arg1 === "string") {
message = arg1;
} else if (typeof arg1 === "number") {
length = arg1;
if (arg2?.message) {
message = arg2.message;
}
}
return this._addCheck({
kind: "nanoid",
value: length,
...errorUtil.errToObj(message),
});
nanoid(
arg1?: number | string | { length?: number; message?: string },
arg2?: { message?: string }
) {
let length = 21;
let message: string | undefined;
// This workaround ensures backward compatibility for custom error message (existing calls still work)
// while enabling a new way to specify a custom length and message.
// old: nanoid("custom error") & new: nanoid(64, { message: "custom error" })
if (typeof arg1 === "string") {
message = arg1;
} else if (typeof arg1 === "number") {
length = arg1;
if (arg2?.message) {
message = arg2.message;
}
} else if (typeof arg1 === "object" && arg1 !== null) {
length = arg1.length ?? 21;
message = arg1.message;
}
return this._addCheck({
kind: "nanoid",
value: length,
...errorUtil.errToObj(message),
});
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant