Skip to content
This repository has been archived by the owner on Sep 24, 2024. It is now read-only.

Commit

Permalink
Add ulid to uuid conversion (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahelmy authored Feb 2, 2024
1 parent 4df6906 commit 7853cc7
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 5 deletions.
Binary file modified .DS_Store
Binary file not shown.
20 changes: 20 additions & 0 deletions api/ulid.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,24 @@ func ulidAPI(app *fiber.App) {
ulidResponse := Response{Success: true, Data: map[string]interface{}{"ulid": internal.GenerateULID()}}
return c.JSON(ulidResponse)
})

app.Get(APIPrefix+ULIDPath+"/convert", func(c fiber.Ctx) error {
ulid := c.Query("ulid", "")
if ulid == "" {
return c.JSON(Response{Success: false, Message: "ulid is required"})
}

convertTo := c.Query("to", "uuid")
var convertedString string
if convertTo == "uuid" {
uuid, err := internal.ULIDtoUUID(ulid)
if err != nil {
return c.JSON(Response{Success: false, Message: err.Error()})
}
convertedString = uuid
} else {
return c.JSON(Response{Success: false, Message: "invalid conversion type"})
}
return c.JSON(Response{Success: true, Data: map[string]interface{}{"result": convertedString}})
})
}
81 changes: 81 additions & 0 deletions api/ulid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package api

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -25,4 +27,83 @@ func TestULIDAPI(t *testing.T) {
assert.True(t, ulidResponse.Success)
assert.NotNil(t, ulidResponse.Data.(map[string]interface{})["ulid"])
})
t.Run("Convert ULID to UUID", func(t *testing.T) {
ulid := "461MR407H385QT3ZFMSK3Q70SX"
expectedUUID := "860d3040-1e23-416f-a1fd-f4ccc773833d"

req, _ := http.NewRequest("GET", fmt.Sprintf("/api/ulid/convert?ulid=%s&to=uuid", ulid), nil)
resp, _ := app.Test(req)

if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, resp.StatusCode)
}

body, _ := io.ReadAll(resp.Body)
var result Response
json.Unmarshal(body, &result)

if !result.Success {
t.Errorf("Expected success to be true, but got false")
}

if _, ok := result.Data.(map[string]interface{})["result"]; !ok {
t.Errorf("Expected 'result' field in response data, but not found")
}

ulid, ok := result.Data.(map[string]interface{})["result"].(string)
if !ok {
t.Errorf("Expected 'result' field to be a string, but got %T", result.Data.(map[string]interface{})["result"])
}

if ulid != expectedUUID {
t.Errorf("Expected ULID to be %s, but got %s", expectedUUID, ulid)
}
})

t.Run("Invalid ULID", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/ulid/convert?ulid=ddddddc773833", nil)
resp, _ := app.Test(req)

if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, resp.StatusCode)
}

body, _ := io.ReadAll(resp.Body)
var result Response
json.Unmarshal(body, &result)

if result.Success {
t.Errorf("Expected success to be false, but got true")
}
})

t.Run("Missing ULID", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/ulid/convert?to=uuid", nil)
resp, _ := app.Test(req)
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, resp.StatusCode)
}

})

t.Run("Invalid conversion type", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/ulid/convert?ulid=461MR407H385QT3ZFMSK3Q70SX&to=invalid", nil)
resp, _ := app.Test(req)

if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, resp.StatusCode)
}

body, _ := io.ReadAll(resp.Body)
var result Response
json.Unmarshal(body, &result)

if result.Success {
t.Errorf("Expected success to be false, but got true")
}

if result.Message != "invalid conversion type" {
t.Errorf("Expected message to be 'invalid conversion type', but got '%s'", result.Message)
}
})
}
3 changes: 1 addition & 2 deletions api/uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -40,7 +39,7 @@ func TestUUIDAPI(t *testing.T) {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, resp.StatusCode)
}

body, _ := ioutil.ReadAll(resp.Body)
body, _ := io.ReadAll(resp.Body)
var result Response
json.Unmarshal(body, &result)

Expand Down
19 changes: 17 additions & 2 deletions cmd/ulid.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,28 @@ var ulidCmd = &cobra.Command{
Short: "Generate a ulid string",
Long: `Generate a ulid string. For example:`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(internal.GenerateULID())
if len(args) == 0 {
fmt.Println(internal.GenerateULID())
} else {
to := cmd.Flag("to").Value.String()
switch to {
case "uuid":
ulid, err := internal.ULIDtoUUID(args[0])
if err != nil {
fmt.Println(err)
} else {
fmt.Println(ulid)
}
default:
fmt.Println("Unknown conversion")
}
}
},
}

func init() {
rootCmd.AddCommand(ulidCmd)

ulidCmd.Flags().StringP("to", "t", "ulid", "Convert from ulid to (uuid)")
// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
Expand Down
17 changes: 16 additions & 1 deletion internal/ulid.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
package internal

import "github.com/oklog/ulid/v2"
import (
"encoding/hex"

"github.com/oklog/ulid/v2"
)

func GenerateULID() string {
return ulid.Make().String()
}

func ULIDtoUUID(ulidStr string) (string, error) {
_, err := ulid.Parse(ulidStr)
if err != nil {
return "", err
}
decoded, _ := crockfordDecode(ulidStr)
uuid := hex.EncodeToString(decoded)
uuid = uuid[0:8] + "-" + uuid[8:12] + "-" + uuid[12:16] + "-" + uuid[16:20] + "-" + uuid[20:]
return uuid, nil
}
21 changes: 21 additions & 0 deletions internal/ulid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,24 @@ func TestGenerateULID(t *testing.T) {
t.Errorf("Expected ULID length to be 26, but got %d", len(ulid))
}
}

func TestULIDtoUUID(t *testing.T) {
t.Run("Valid ULID", func(t *testing.T) {
ulid := "461MR407H385QT3ZFMSK3Q70SX"
expectedUUID := "860d3040-1e23-416f-a1fd-f4ccc773833d"
uuid, err := ULIDtoUUID(ulid)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if uuid != expectedUUID {
t.Errorf("Expected UUID to be %s, but got %s", expectedUUID, uuid)
}
})
t.Run("Invalid ULID", func(t *testing.T) {
ulid := "01F7Z6T2T0Z2VXY6J8X1Z5Q3"
_, err := ULIDtoUUID(ulid)
if err == nil {
t.Errorf("Expected error, but got nil")
}
})
}
51 changes: 51 additions & 0 deletions internal/uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package internal
import (
"bytes"
"encoding/hex"
"errors"
"strings"

"github.com/google/uuid"
Expand Down Expand Up @@ -65,3 +66,53 @@ func reverseBuffer(input []byte) []byte {
}
return reversed
}

func reverseString(s string) string {
var result []rune
for i := len(s) - 1; i >= 0; i-- {
result = append(result, rune(s[i]))
}
return string(result)
}

func crockfordDecode(input string) ([]byte, error) {
sanitizedInput := toUpperCase(input)

// Work from the end
sanitizedInput = reverseString(sanitizedInput)

var output []byte
var bitsRead, buffer uint
for _, char := range sanitizedInput {
byteVal := byte(strings.Index(B32_CHARACTERS, string(char)))
if byteVal == 255 {
return nil, errors.New("Invalid base 32 character found in string: " + string(char))
}

buffer |= uint(byteVal) << bitsRead
bitsRead += 5

for bitsRead >= 8 {
output = append([]byte{byte(buffer & 0xff)}, output...)
buffer >>= 8
bitsRead -= 8
}
}

if bitsRead >= 5 || buffer > 0 {
output = append([]byte{byte(buffer & 0xff)}, output...)
}

return output, nil
}

func toUpperCase(s string) string {
var result []rune
for _, char := range s {
if char >= 'a' && char <= 'z' {
char -= 'a' - 'A'
}
result = append(result, char)
}
return string(result)
}
54 changes: 54 additions & 0 deletions internal/uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,57 @@ func TestUUIDtoULID(t *testing.T) {
}
})
}

func TestToUpperCase(t *testing.T) {
t.Run("Lowercase string", func(t *testing.T) {
input := "hello world"
expected := "HELLO WORLD"
result := toUpperCase(input)
if result != expected {
t.Errorf("Expected result to be %s, but got %s", expected, result)
}
})

t.Run("Uppercase string", func(t *testing.T) {
input := "HELLO WORLD"
expected := "HELLO WORLD"
result := toUpperCase(input)
if result != expected {
t.Errorf("Expected result to be %s, but got %s", expected, result)
}
})

t.Run("Mixed case string", func(t *testing.T) {
input := "HeLlO WoRlD"
expected := "HELLO WORLD"
result := toUpperCase(input)
if result != expected {
t.Errorf("Expected result to be %s, but got %s", expected, result)
}
})
}

func TestCrockfordDecode(t *testing.T) {
t.Run("Valid input", func(t *testing.T) {
input := "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T"
expected := []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}
result, err := crockfordDecode(input)
if err == nil {
t.Errorf("Expected result length to be %d, but got %d", len(expected), len(result))
}
for i := 0; i < len(result); i++ {
if result[i] != expected[i] {
t.Errorf("Expected result[%d] to be %d, but got %d", i, expected[i], result[i])
}
}
})

t.Run("Invalid input", func(t *testing.T) {
input := "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T!"
_, err := crockfordDecode(input)
if err == nil {
t.Errorf("Expected error, but got nil")
}
})

}
Loading

0 comments on commit 7853cc7

Please sign in to comment.