Skip to content

Commit

Permalink
Shrink and simplify the route table structures.
Browse files Browse the repository at this point in the history
- Use generics (introduced in Go 1.18) to provide compile-time type
  checking and eliminate type introspection at runtime.
- Drop the unused `prefix` flag in the trie node objects; just point
  directly to the http.Handler (interface). Improves readability and
  saves an unnecessary indirection on every lookup.
- Make the clever table-driven unit tests a bit more readable by using
  more common terminology.
- Unexport some symbols which shouldn't have been exported.
- Minor editorial improvements to documentation.
- Update the copyright assertions in the LICENCE files to keep them
  accurate.
  • Loading branch information
sengi committed Oct 17, 2023
1 parent c5768e3 commit 8395e95
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 188 deletions.
5 changes: 3 additions & 2 deletions LICENCE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013 Government Digital Service
MIT Licence

Copyright © 2013-2023 Crown Copyright (Government Digital Service)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
4 changes: 3 additions & 1 deletion trie/LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Copyright (c) 2013 Government Digital Service
MIT Licence

Copyright © 2013, 2023 Crown Copyright (Government Digital Service)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
6 changes: 3 additions & 3 deletions trie/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ rather than just strings.
This makes it suitable for efficiently storing information about hierarchical
systems in general, rather than being specifically geared towards string lookup.

Read the documentation on [godoc.org][docs] for details of how to use `trie`.
Read the [documentation] for details of how to use `trie`.

[docs]: http://godoc.org/github.com/alphagov/router/trie
[go]: http://golang.org
[documentation]: https://pkg.go.dev/github.com/alphagov/router/trie
[go]: https://go.dev/
[trie]: https://en.wikipedia.org/wiki/Trie
141 changes: 63 additions & 78 deletions trie/trie.go
Original file line number Diff line number Diff line change
@@ -1,136 +1,121 @@
// Package trie implements a simple trie data structure that maps "paths" (which
// are slices of strings) to arbitrary data values (type interface{}).
// are slices of strings) to values of some type T.
package trie

type trieChildren map[string]*Trie
type trieChildren[T interface{}] map[string]*Trie[T]

type Trie struct {
Leaf bool
Entry interface{}
Children trieChildren
type Trie[T interface{}] struct {
leaf bool
entry T
children trieChildren[T]
}

// NewTrie makes a new empty Trie
func NewTrie() *Trie {
return &Trie{
Children: make(trieChildren),
}
// NewTrie makes a new, empty Trie.
func NewTrie[T interface{}]() *Trie[T] {
return &Trie[T]{children: make(trieChildren[T])}
}

// Get retrieves an element from the Trie
// Get retrieves an entry from the Trie. If there is no fully-matching entry,
// Get returns `(nil, false)`. `path` can be empty, to denote the root node.
//
// Takes a path (which can be empty, to denote the root element of the Trie),
// and returns the object if the path exists in the Trie, or nil and a status of
// false. Example:
// Example:
//
// if res, ok := trie.Get([]string{"foo", "bar"}); ok {
// fmt.Println("Value at /foo/bar was", res)
// }
func (t *Trie) Get(path []string) (entry interface{}, ok bool) {
// if res, ok := trie.Get([]string{"foo", "bar"}); ok {
// fmt.Println("Value at /foo/bar was", res)
// }
func (t *Trie[T]) Get(path []string) (entry T, ok bool) {
if len(path) == 0 {
return t.getentry()
return t.getEntry()
}

key := path[0]
newpath := path[1:]
key, newPath := path[0], path[1:]

res, ok := t.Children[key]
res, ok := t.children[key]
if !ok {
// Path doesn't exist: shortcut return value
return nil, false
return
}

return res.Get(newpath)
return res.Get(newPath)
}

// GetLongestPrefix retrieves an element from the Trie
// GetLongestPrefix retrieves the longest matching entry from the Trie.
//
// GetLongestPrefix returns a full match if there is one, or the entry with the
// longest matching prefix. If there is no match at all, GetLongestPrefix
// returns `(nil, false)`. `path` can be empty, to denote the root node.
//
// Takes a path (which can be empty, to denote the root element of the Trie).
// If a matching object exists, it is returned. Otherwise the object with the
// longest matching prefix is returned. If nothing matches at all, nil and a
// status of false is returned. Example:
// Example:
//
// if res, ok := trie.GetLongestPrefix([]string{"foo", "bar"}); ok {
// fmt.Println("Value at /foo/bar was", res)
// }
func (t *Trie) GetLongestPrefix(path []string) (entry interface{}, ok bool) {
// if res, ok := trie.GetLongestPrefix([]string{"foo", "bar"}); ok {
// fmt.Println("Value at /foo/bar was", res)
// }
func (t *Trie[T]) GetLongestPrefix(path []string) (entry T, ok bool) {
if len(path) == 0 {
return t.getentry()
return t.getEntry()
}

key := path[0]
newpath := path[1:]
key, newPath := path[0], path[1:]

res, ok := t.Children[key]
res, ok := t.children[key]
if !ok {
// Path doesn't exist: return this node as possible best match
return t.getentry()
return t.getEntry() // Full path not found, but this is the longest match.
}

entry, ok = res.GetLongestPrefix(newpath)
entry, ok = res.GetLongestPrefix(newPath)
if ok {
return entry, ok
}
// We haven't found a match yet, return this node
return t.getentry()
return t.getEntry() // No match yet, so return this node.
}

// Set creates an element in the Trie
//
// Takes a path (which can be empty, to denote the root element of the Trie),
// and an arbitrary value (interface{}) to use as the leaf data.
func (t *Trie) Set(path []string, value interface{}) {
// Set adds an entry to the Trie. `path` can be empty, to denote the root node.
func (t *Trie[T]) Set(path []string, value T) {
if len(path) == 0 {
t.setentry(value)
t.setEntry(value)
return
}

key := path[0]
newpath := path[1:]
key, newPath := path[0], path[1:]

res, ok := t.Children[key]
res, ok := t.children[key]
if !ok {
// Trie node that should hold entry doesn't already exist, so let's create it
res = NewTrie()
t.Children[key] = res
res = NewTrie[T]()
t.children[key] = res
}

res.Set(newpath, value)
res.Set(newPath, value)
}

// Del removes an element from the Trie. Returns a boolean indicating whether an
// element was actually deleted.
func (t *Trie) Del(path []string) bool {
// Del removes an entry from the Trie, returning true if it deleted an entry.
func (t *Trie[T]) Del(path []string) bool {
if len(path) == 0 {
return t.delentry()
return t.delEntry()
}

key := path[0]
newpath := path[1:]
key, newPath := path[0], path[1:]

res, ok := t.Children[key]
res, ok := t.children[key]
if !ok {
return false
}

return res.Del(newpath)
return res.Del(newPath)
}

func (t *Trie) setentry(value interface{}) {
t.Leaf = true
t.Entry = value
func (t *Trie[T]) setEntry(value T) {
t.leaf = true
t.entry = value
}

func (t *Trie) getentry() (entry interface{}, ok bool) {
if t.Leaf {
return t.Entry, true
func (t *Trie[T]) getEntry() (entry T, ok bool) {
if t.leaf {
return t.entry, true
}
return nil, false
return
}

func (t *Trie) delentry() (ok bool) {
ok = t.Leaf
t.Leaf = false
t.Entry = nil
func (t *Trie[T]) delEntry() (ok bool) {
ok = t.leaf
t.leaf = false
var zero T
t.entry = zero
return
}
Loading

0 comments on commit 8395e95

Please sign in to comment.