Skip to content

Latest commit

 

History

History
411 lines (311 loc) · 11.1 KB

README.md

File metadata and controls

411 lines (311 loc) · 11.1 KB

Logo

Sqids (pronounced "squids") is a small library that lets you generate YouTube-looking IDs from numbers. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker database lookups.

Table of contents

🚀 Getting started

Installation

Sqids is available on Hackage (hackage.haskell.org/package/sqids). To install it, run:

cabal install sqids

Or using Stack:

stack install sqids

Usage

The library exposes two versions of the API;

  • Web.Sqids relies on Haskell's Int data type, whereas
  • Web.Sqids.Integer uses Integers, which support arbitrarily large integers.

If you need to work with (i.e. encode and decode to) large numbers, it is recommended to choose the latter option, in which case you would import the library as:

import Web.Sqids.Integer

The Haskell standard (see here) guarantees the range supported by Int to have an upper bound of at least 229 - 1 (= 536,870,911). If this does not present a problem for your use case, instead use:

import Web.Sqids

Use encode to translate a list of non-negative integers into an ID, and decode to retrieve back the list of numbers encoded by an ID.

encode :: (Integral a) => [a] -> Sqids Text
decode :: (Integral a) => Text -> Sqids [a]

These functions return (monadic) values of type Sqids Int a or Sqids Integer a. Calling sqids, which uses the default configuration, or runSqids (see below) is the most straightforward way to extract the something from a Sqids s something value.

sqids :: Sqids s a -> Either SqidsError a

This gives you a value of type Either SqidsError a, where a is the ID in the case of encode. If encoding fails for some reason, then the Left constructor contains the error. For some use cases, directly calling sqids or runSqids in this way is sufficient. Doing so in multiple locations in your code, however, doesn't scale very well, especially when IO or other effects are involved. In this case, the SqidsT monad transformer is a better choice.

Encoding

module Main where

import Web.Sqids

main :: IO ()
main =
  case sqids (encode [1, 2, 3]) of
    Left  {}   -> print "Something went wrong."
    Right sqid -> print sqid

The output of this program is:

"86Rf07"

Decoding

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Web.Sqids

main :: IO ()
main =
  case sqids (decode "86Rf07") of
    Left  {}   -> print "Something went wrong."
    Right nums -> print nums

The output of this program is:

[1,2,3]
A note about the OverloadedStrings language extension

decode takes a Text value as input. If you are not compiling with OverloadedStrings enabled, the "86Rf07" string literal in the previous example would need to be explicitly converted, using the pack function from Data.Text.

import Data.Text (pack)
decode (pack "86Rf07")

Setting options

To pass custom options to encode and decode, use runSqids which takes an additional SqidsOptions argument.

runSqids :: SqidsOptions -> Sqids s a -> Either SqidsError a

See here for available options. You can override the default values using defaultSqidsOptions, and the following idiom:

main =
  case runSqids defaultSqidsOptions{ minLength = 24 } (encode [1, 2, 3]) of
    Left  {}   -> print "Something went wrong."
    Right sqid -> print sqid

The output of this program is:

"86Rf07xd4zBmiJXQG6otHEbe"

To set a custom alphabet:

main =
  case runSqids defaultSqidsOptions{ alphabet = "mTHivO7hx3RAbr1f586SwjNnK2lgpcUVuG09BCtekZdJ4DYFPaWoMLQEsXIqyz" } (encode [1, 2, 3]) of
    Left  {}   -> print "Something went wrong."
    Right sqid -> print sqid

The output of this program is:

"oz6E9F"

Or, you can set all options at once:

main = do
  let options = SqidsOptions
        { alphabet  = "1234567890"
        , minLength = 8
        , blocklist = []
        }
  case runSqids options (encode [1, 2, 3]) of
    Left  {}   -> print "Something went wrong."
    Right sqid -> print sqid

The output of this program is:

"38494176"

Monad transformer

In a more realistically sized application, calling runSqids every time you need to access the value returned by encode or decode isn't ideal. Instead, you probably want to create your SqidsOptions once, and then do things with the IDs across the code without having to pass the options object along every time. Assuming your application relies on a transformer stack to combine effects from different monads, then this implies adding the SqidsT transformer at some suitable layer of the stack. Instead of sqids and runSqids, there are two corresponding functions to fish out 🎣 the value from inside of SqidsT:

sqidsT :: Monad m => SqidsT s m a -> m (Either SqidsError a)
runSqidsT :: Monad m => SqidsOptions -> SqidsT s m a -> m (Either SqidsError a)

Below is an example where SqidsT is used in combination with the Writer and IO monads.

module Main where

import Control.Monad (forM_)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Writer (WriterT, execWriterT, tell)
import Data.Text (Text)
import Web.Sqids

main :: IO ()
main = do
  w <- sqidsT (execWriterT makeIds)
  case w of
    Left  err -> print ("Error: " <> show err)
    Right ids -> print ids

makeIds :: WriterT [Text] (SqidsT Int IO) ()
makeIds = do
  liftIO $ print "Generating IDs"
  forM_ [1 .. 50] $ \n -> do
    sqid <- encode [n, n, n, n]
    tell [sqid]

The output of this program is:

"Generating IDs"
["Q8Ac4uf3","fU9zWydl","aStUNEra","KR5zQbHB","n7qefHP0","pRkWlenI","ylr03cjE","H0V1tEjl","0rTYteaW","jQw6pcuZ","P9NfMbEk","IYvhBx6l","0vTGthaI","UXLhWExs","u52hY2FK","IjvHBv6e","pqk3lJnQ","PKNDMnEj","RJepNxTd","K15yQcHf","1c72LltW","dY4YwC0z","127FLStT","F0GBXRKm","ZDMTUa09","aFtHNir0","U4LiWBxu","oRltrlxW","1w7ULqtK","nYq5fnPa","HNVMtQjF","IRv4B26F","3wWEpjeF","oXlIrpxD","RNeTNnTN","OQJXLTbo","OAJwLube","onlgrbxt","u42vYoFH","FmGvXwKx","d84vwS0K","QuAl41fQ","H9VRtOjU","sh80jrCd","sB8CjqC3","ZKMzUJ0a","XkbEbTzD","OZJnL3bj","RGevNZTU","36WapueZ"]

Error handling

Encoding and decoding can fail for various reasons.

  case runSqids options (encode numbers) of
    Left SqidsNegativeNumberInInput ->
      print "Negative numbers are not allowed as input."
    _ ->
      -- etc...

See here for possible errors.

The following is an example of how to handle errors with the help of MonadErrors exception-handling mechanism:

module Main where

import Control.Monad (when)
import Control.Monad.Except (catchError)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Either (fromRight)
import Data.Text (unpack)
import Text.Read (readMaybe)
import Web.Sqids

putStrLn_ :: String -> SqidsT Int IO ()
putStrLn_ = liftIO . putStrLn

repl :: SqidsT Int IO ()
repl = do
  input <- liftIO $ do
    putStrLn "Enter a comma-separated list of non-negative integers, or \"exit\"."
    putStr "> "
    getLine
  when (input /= "exit") $ do
    case readMaybe ("[" <> input <> "]") of
      Nothing ->
        putStrLn_ "Invalid input: Please try again."
      Just numbers ->
        catchError (encode numbers >>= putStrLn_ . unpack) $ \err ->
          case err of
            SqidsNegativeNumberInInput ->
              putStrLn_ "Only non-negative integers are accepted as input."
            _ ->
              putStrLn_ "Unexpected error"
    repl

runRepl :: IO (Either SqidsError ())
runRepl = runSqidsT defaultSqidsOptions repl

main :: IO ()
main = fromRight () <$> runRepl

Program example output:

Example

Options

alphabet :: Text

The alphabet used by the algorithm for encoding and decoding.

  • Default value: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789

minLength :: Int

The minimum allowed length of IDs.

  • Default value: 0

blocklist :: [Text]

A list of words that must never appear in IDs.

Errors

SqidsAlphabetTooShort

The alphabet must be at least 3 characters long.

SqidsAlphabetRepeatedCharacters

The provided alphabet contains duplicate characters. E.g., "abcdefgg" is not a valid alphabet.

SqidsInvalidMinLength

The given minLength value is not within the valid range.

SqidsNegativeNumberInInput

One or more numbers in the list passed to encode are negative. Only non-negative integers can be used as input.

SqidsMaxEncodingAttempts

Encoding failed after too many recursive iterations. This happens if the blocklist is too restrictive. Consider the following example:

  let options = defaultSqidsOptions
        { alphabet  = "abc"
        , blocklist = [ "cab", "abc", "bca" ]
        , minLength = 3
        }
   in
     runSqids options (encode [0])

SqidsAlphabetContainsMultibyteCharacters

The alphabet must consist of only characters in the standard single-byte character set.

Notes

  • Do not encode sensitive data. These IDs can be easily decoded.
  • Default blocklist is auto-enabled. It's configured for the most common profanity words. Create your own custom list by using the blocklist parameter, or pass an empty list to allow all words.
  • Read more at https://sqids.org/haskell

API documentation

See https://hackage.haskell.org/package/sqids.

License

MIT