Initial code commit
proof of concept
This commit is contained in:
parent
11dfed7bc9
commit
e88d9cf3f4
20
FunctionalCuid.Tests/FunctionalCuid.Tests.fsproj
Normal file
20
FunctionalCuid.Tests/FunctionalCuid.Tests.fsproj
Normal file
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Expecto" Version="5.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FunctionalCuid\FunctionalCuid.fsproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
17
FunctionalCuid.Tests/Program.fs
Normal file
17
FunctionalCuid.Tests/Program.fs
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
open Expecto
|
||||
|
||||
// these tests are bad, and I should feel bad...
|
||||
let tests =
|
||||
testList "Smoke Tests" [
|
||||
test "Generate a CUID" {
|
||||
Cuid.cuid () |> ignore
|
||||
}
|
||||
test "Generate a slug" {
|
||||
Cuid.slug () |> ignore
|
||||
}
|
||||
]
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
runTestsWithArgs defaultConfig argv tests
|
11
FunctionalCuid/FunctionalCuid.fsproj
Normal file
11
FunctionalCuid/FunctionalCuid.fsproj
Normal file
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Library.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
106
FunctionalCuid/Library.fs
Normal file
106
FunctionalCuid/Library.fs
Normal file
|
@ -0,0 +1,106 @@
|
|||
module Cuid
|
||||
|
||||
open System
|
||||
|
||||
(*
|
||||
Functional CUID
|
||||
Collision-resistant Unique IDentifiers, written in F#
|
||||
|
||||
adapted from https://github.com/ericelliott/cuid
|
||||
MIT License
|
||||
*)
|
||||
|
||||
[<AutoOpen>]
|
||||
module private Support =
|
||||
let blockSize = 4
|
||||
let baseSize = 36UL
|
||||
let discreteValues = pown baseSize blockSize
|
||||
|
||||
let pad size num =
|
||||
let s = sprintf "000000000%s" num
|
||||
s.Substring(s.Length - size)
|
||||
|
||||
let padToSize = pad blockSize
|
||||
|
||||
let base36Chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
let toBase36 nbr =
|
||||
let rec convert nbr current =
|
||||
match nbr with
|
||||
| 0UL -> sprintf "%s0" current
|
||||
| _ when nbr < baseSize -> sprintf "%c%s" base36Chars.[int nbr] current
|
||||
| _ -> convert (nbr / baseSize) (sprintf "%c%s" base36Chars.[int (nbr % baseSize)] current)
|
||||
convert nbr ""
|
||||
|
||||
let padBase36 = toBase36 >> padToSize
|
||||
|
||||
let rnd = Random ()
|
||||
|
||||
let randomBlock () =
|
||||
(uint64 >> padBase36) (rnd.NextDouble () * float discreteValues)
|
||||
|
||||
let mutable c = 0UL
|
||||
|
||||
let safeCounter () =
|
||||
c <- if c < discreteValues then c else 0UL
|
||||
c <- c + 1UL
|
||||
c - 1UL
|
||||
|
||||
let epoch = DateTime (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
|
||||
let timestampNow () =
|
||||
(uint64 >> toBase36) (DateTime.Now - epoch).TotalMilliseconds
|
||||
|
||||
let hostname =
|
||||
try Environment.MachineName
|
||||
with _ -> string (Random().Next ())
|
||||
|
||||
let fingerprint () =
|
||||
let padTo2 = uint64 >> toBase36 >> pad 2
|
||||
[ Diagnostics.Process.GetCurrentProcess().Id |> padTo2
|
||||
hostname |> Seq.fold (fun acc chr -> acc + int chr) (hostname.Length + 36) |> padTo2
|
||||
]
|
||||
|> List.reduce (+)
|
||||
|
||||
let rightChars chars (str : string) =
|
||||
match chars with
|
||||
| _ when chars > str.Length -> str
|
||||
| _ -> str.[str.Length - chars..]
|
||||
|
||||
/// Generate a CUID
|
||||
///
|
||||
/// The CUID is made up of 5 parts:
|
||||
/// - The letter "c" (is for both cookies and CUIDs; lowercase letter makes it HTML element ID friendly)
|
||||
/// - A timestamp (in milliseconds send the Unix epoch)
|
||||
/// - A sequential counter, used to prevent same-machine collisions
|
||||
/// - A fingerprint, generated from the hostname and process ID
|
||||
/// - 8 characters of random gibberish
|
||||
///
|
||||
/// The timestamp, fingerprint, and randomness are all encoded in base 36, using 0-9 and a-z.
|
||||
let cuid () =
|
||||
[ "c"
|
||||
timestampNow ()
|
||||
(safeCounter >> padBase36) ()
|
||||
fingerprint ()
|
||||
randomBlock ()
|
||||
randomBlock ()
|
||||
]
|
||||
|> List.reduce (+)
|
||||
|
||||
/// Generate a slug
|
||||
///
|
||||
/// The slug is not as collision-resistant as the CUID, and is also not monotonically increasing, which is desirable
|
||||
/// for indexed database IDs; full CUIDs should be used in this case. A slug is made up of 4 parts:
|
||||
/// - The two right-most characters of the timestamp
|
||||
/// - The non-padded counter value (may be 1 to 4 characters)
|
||||
/// - The first and last characters of the fingerprint
|
||||
/// - 2 characters of random gibberish
|
||||
let slug () =
|
||||
let print = fingerprint ()
|
||||
[ (timestampNow >> rightChars 2) ()
|
||||
(safeCounter >> string >> rightChars 4) ()
|
||||
print.[0..0]
|
||||
rightChars 1 print
|
||||
(randomBlock >> rightChars 2) ()
|
||||
]
|
||||
|> List.reduce (+)
|
Loading…
Reference in New Issue
Block a user