generated from UCLALibrary/service-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add profiles/profile structs
- Loading branch information
Showing
2 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
// This code provides utilities for creating and working with validation profiles. | ||
// | ||
// Example usage: | ||
// | ||
// profiles := NewProfiles() | ||
// if profile, err := NewProfile("example", []string{"Validation1", "Validation2"}); err != nil { | ||
// require.NoError(t, err) | ||
// } else { | ||
// err = profiles.SetProfile(profile) | ||
// require.NoError(t, err) | ||
// | ||
// fmt.Println(profiles.GetProfile("example").GetName()) | ||
// } | ||
// | ||
// snapshot := profiles.Snapshot() | ||
// if jsonData, err := json.Marshal(snapshot); err != nil { | ||
// require.NoError(t, err) | ||
// } else { | ||
// fmt.Println(string(jsonData)) | ||
// } | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"sort" | ||
"sync" | ||
"time" | ||
) | ||
|
||
// Profile is a single thread-safe validation profile. | ||
type Profile struct { | ||
mutex sync.RWMutex | ||
name string | ||
lastUpdate time.Time | ||
validations []string | ||
} | ||
|
||
// ProfileSnapshot is a temporary struct used for marshaling to JSON. | ||
type ProfileSnapshot struct { | ||
Name string `json:"name"` | ||
LastUpdate time.Time `json:"lastUpdate"` | ||
Validations []string `json:"validations"` | ||
} | ||
|
||
// Profiles contains a thread-safe mapping of validation Profile(s). | ||
// | ||
// We don't marshal this to JSON, but use a ProfilesSnapshot for that. | ||
type Profiles struct { | ||
mutex sync.RWMutex | ||
profile map[string]*Profile | ||
lastUpdate time.Time | ||
} | ||
|
||
// ProfilesSnapshot is a temporary struct used for marshaling to JSON. | ||
type ProfilesSnapshot struct { | ||
Profile map[string]ProfileSnapshot `json:"profiles"` | ||
LastUpdate time.Time `json:"lastUpdate"` | ||
} | ||
|
||
// NewProfile is a constructor function to initialize a new Profile. | ||
func NewProfile(name string, validations []string) (*Profile, error) { | ||
if name == "" { | ||
return nil, fmt.Errorf("profile name cannot be empty") | ||
} | ||
|
||
return &Profile{ | ||
name: name, | ||
lastUpdate: time.Now(), | ||
validations: append([]string(nil), validations...), | ||
}, nil | ||
} | ||
|
||
// NewProfiles is a constructor function to initialize a new Profiles. | ||
func NewProfiles() *Profiles { | ||
return &Profiles{ | ||
profile: make(map[string]*Profile), | ||
} | ||
} | ||
|
||
// Count the number of Profile(s) in this Profiles instance. | ||
func (profiles *Profiles) Count() int { | ||
profiles.mutex.RLock() | ||
defer profiles.mutex.RUnlock() | ||
return len(profiles.profile) | ||
} | ||
|
||
// GetName gets the name of the current Profile. | ||
func (profile *Profile) GetName() string { | ||
profile.mutex.RLock() | ||
defer profile.mutex.RUnlock() | ||
return profile.name | ||
} | ||
|
||
// GetLastUpdate gets the last update of the current Profile. | ||
func (profile *Profile) GetLastUpdate() time.Time { | ||
profile.mutex.RLock() | ||
defer profile.mutex.RUnlock() | ||
return profile.lastUpdate | ||
} | ||
|
||
// GetValidations gets the validation names of the current Profile. | ||
func (profile *Profile) GetValidations() []string { | ||
profile.mutex.RLock() | ||
defer profile.mutex.RUnlock() | ||
return append([]string(nil), profile.validations...) | ||
} | ||
|
||
// AddValidation adds a new validation name to the current Profile. | ||
func (profile *Profile) AddValidation(validation string) { | ||
profile.mutex.Lock() | ||
defer profile.mutex.Unlock() | ||
|
||
for _, v := range profile.validations { | ||
if v == validation { | ||
return // Skip duplicates | ||
} | ||
} | ||
|
||
profile.validations = append(profile.validations, validation) | ||
} | ||
|
||
// GetProfile gets the Profile with the supplied name. | ||
func (profiles *Profiles) GetProfile(name string) *Profile { | ||
profiles.mutex.RLock() | ||
defer profiles.mutex.RUnlock() | ||
|
||
profile, exists := profiles.profile[name] | ||
if !exists { | ||
return nil | ||
} | ||
|
||
return profile | ||
} | ||
|
||
// SetName a new Profile name. | ||
func (profile *Profile) SetName(name string) { | ||
profile.mutex.Lock() | ||
defer profile.mutex.Unlock() | ||
|
||
profile.lastUpdate = time.Now() | ||
profile.name = name | ||
} | ||
|
||
// SetValidations a Profile validations. | ||
func (profile *Profile) SetValidations(validations []string) { | ||
profile.mutex.Lock() | ||
defer profile.mutex.Unlock() | ||
|
||
profile.lastUpdate = time.Now() | ||
uniqueValidations := make(map[string]struct{}) | ||
for _, v := range validations { | ||
uniqueValidations[v] = struct{}{} | ||
} | ||
|
||
profile.validations = make([]string, 0, len(uniqueValidations)) | ||
for v := range uniqueValidations { | ||
profile.validations = append(profile.validations, v) | ||
} | ||
|
||
// Sort for consistency | ||
sort.Strings(profile.validations) | ||
} | ||
|
||
// SetProfile sets a new Profile in Profiles. | ||
func (profiles *Profiles) SetProfile(profile *Profile) error { | ||
if profile == nil { | ||
return fmt.Errorf("cannot set a nil profile") | ||
} | ||
|
||
if profile.GetName() == "" { | ||
return fmt.Errorf("cannot set a profile with an empty name") | ||
} | ||
|
||
profiles.mutex.Lock() | ||
defer profiles.mutex.Unlock() | ||
|
||
if profiles.profile == nil { | ||
profiles.profile = make(map[string]*Profile) | ||
} | ||
|
||
profiles.lastUpdate = time.Now() | ||
profiles.profile[profile.GetName()] = profile | ||
|
||
return nil | ||
} | ||
|
||
// Snapshot provides a copy of Profile to marshal to JSON. | ||
func (profile *Profile) Snapshot() ProfileSnapshot { | ||
profile.mutex.RLock() | ||
defer profile.mutex.RUnlock() | ||
|
||
// Populate the temporary struct with current values | ||
return ProfileSnapshot{ | ||
Name: profile.name, | ||
LastUpdate: profile.lastUpdate, | ||
Validations: append([]string(nil), profile.validations...), | ||
} | ||
} | ||
|
||
// Snapshot provides a copy of Profiles to marshal to JSON. | ||
func (profiles *Profiles) Snapshot() ProfilesSnapshot { | ||
profiles.mutex.RLock() | ||
defer profiles.mutex.RUnlock() | ||
|
||
if profiles.profile == nil { | ||
return ProfilesSnapshot{ | ||
Profile: make(map[string]ProfileSnapshot), | ||
LastUpdate: time.Now(), | ||
} | ||
} | ||
|
||
// Populate the temporary struct with current values | ||
snapshot := ProfilesSnapshot{ | ||
Profile: make(map[string]ProfileSnapshot), | ||
LastUpdate: profiles.lastUpdate, | ||
} | ||
|
||
for name, profile := range profiles.profile { | ||
snapshot.Profile[name] = profile.Snapshot() | ||
} | ||
|
||
return snapshot | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
//go:build unit | ||
|
||
package main | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"log" | ||
"os" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
// TestProfiles tests basic Get/Set functionality. | ||
func TestProfiles(t *testing.T) { | ||
profiles := NewProfiles() | ||
defaultProfile, err := NewProfile("default", []string{"SpaceCheck", "ARKFormat", "EOLCheck"}) | ||
require.NoError(t, err) | ||
otherProfile, err := NewProfile("other", []string{"SpaceCheck", "ARKFormat"}) | ||
require.NoError(t, err) | ||
|
||
err = profiles.SetProfile(defaultProfile) | ||
require.NoError(t, err) | ||
err = profiles.SetProfile(otherProfile) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, 2, profiles.Count()) | ||
assert.Equal(t, profiles.GetProfile("default").GetName(), "default") | ||
assert.Equal(t, profiles.GetProfile("other").GetName(), "other") | ||
assert.Equal(t, len(profiles.GetProfile("default").GetValidations()), 3) | ||
assert.Equal(t, len(profiles.GetProfile("other").GetValidations()), 2) | ||
assert.False(t, profiles.GetProfile("default").GetLastUpdate().IsZero()) | ||
assert.False(t, profiles.GetProfile("other").GetLastUpdate().IsZero()) | ||
} | ||
|
||
// TestSnapshot tests creating a bare-bones Snapshot through marshaling it to JSON. | ||
func TestSnapshot(t *testing.T) { | ||
profiles := NewProfiles() | ||
snapshot := profiles.Snapshot() | ||
|
||
jsonData, err := json.Marshal(snapshot) | ||
if err != nil { | ||
log.Fatalf("Error marshaling to JSON: %v", err) | ||
} | ||
|
||
// Confirm Snapshot is okay with no profiles set (i.e., we've created the slice) | ||
assert.Equal(t, "{\"profiles\":{},\"lastUpdate\":\"0001-01-01T00:00:00Z\"}", string(jsonData)) | ||
} | ||
|
||
// TestExampleCode tests the code that's used in profiles.go's inline docs. | ||
func TestExampleCode(t *testing.T) { | ||
// The function that's captured contains the example code used in the docs | ||
output := captureOutput(t, func() { | ||
profiles := NewProfiles() | ||
if profile, err := NewProfile("example", []string{"Validation1", "Validation2"}); err == nil { | ||
err = profiles.SetProfile(profile) | ||
require.NoError(t, err) | ||
|
||
fmt.Println(profiles.GetProfile("example").GetName()) | ||
} else { | ||
require.NoError(t, err) | ||
} | ||
|
||
snapshot := profiles.Snapshot() | ||
if jsonData, err := json.Marshal(snapshot); err == nil { | ||
fmt.Println(string(jsonData)) | ||
} else { | ||
require.NoError(t, err) | ||
} | ||
}) | ||
|
||
// A simple test that gets around lastUpdate being different | ||
assert.True(t, strings.HasPrefix(output, "example")) | ||
} | ||
|
||
// captureOutput captures StdOut and allows running assertions on it | ||
func captureOutput(t *testing.T, f func()) string { | ||
var buf bytes.Buffer | ||
|
||
originalStdout := os.Stdout | ||
r, w, _ := os.Pipe() | ||
os.Stdout = w | ||
|
||
// Call the function we've passed in | ||
f() | ||
|
||
if err := w.Close(); err != nil { | ||
t.Fatalf("Failed to close pipe writer: %v", err) | ||
} | ||
|
||
if _, err := buf.ReadFrom(r); err != nil { | ||
t.Fatalf("Failed to read from pipe reader: %v", err) | ||
} | ||
|
||
os.Stdout = originalStdout | ||
return buf.String() | ||
} |