Skip to content

Commit

Permalink
Merge pull request #5 from AntoineAugusti/useless-pointers-and-docume…
Browse files Browse the repository at this point in the history
…ntation

Do not pass pointers where not needed + documentation
  • Loading branch information
AntoineAugusti committed Dec 13, 2015
2 parents 637a13c + 14baeeb commit a8f7221
Show file tree
Hide file tree
Showing 11 changed files with 89 additions and 69 deletions.
2 changes: 2 additions & 0 deletions db/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"github.com/boltdb/bolt"
)

// Get the name of the bucket
func GetBucketName() string {
return "features"
}

// Generate the default bucket if it does not exist yet
func GenerateDefaultBucket(name string, db *bolt.DB) {
_ = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(name))
Expand Down
3 changes: 3 additions & 0 deletions helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"strconv"
)

// Transform a uint32 to a byte slice
func Uint32ToBytes(u uint32) []byte {
return []byte(strconv.FormatUint(uint64(u), 10))
}

// Check if an int is in a slice
func IntInSlice(a uint32, list []uint32) bool {
for _, b := range list {
if b == a {
Expand All @@ -17,6 +19,7 @@ func IntInSlice(a uint32, list []uint32) bool {
return false
}

// Check if a string is in a slice
func StringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
Expand Down
37 changes: 19 additions & 18 deletions http/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,35 @@ package http

import (
"encoding/json"
"fmt"
"net/http"

m "github.com/antoineaugusti/feature-flags/models"
services "github.com/antoineaugusti/feature-flags/services"
"github.com/gorilla/mux"
)

// Handles incoming requests
type APIHandler struct {
FeatureService services.FeatureService
}

// A simple structure to respond with error messages
type APIMessage struct {
code int
Status string `json:"status"`
// The HTTP status code
code int
// A status message
Status string `json:"status"`
// A human readable message
Message string `json:"message"`
}

// Describes the request when checking the access to a feature
type AccessRequest struct {
Groups []string `json:"groups"`
User uint32 `json:"user"`
}

func (handler *APIHandler) Welcome(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!\n")
}

func (handler *APIHandler) FeatureIndex(w http.ResponseWriter, r *http.Request) {
func (handler APIHandler) FeatureIndex(w http.ResponseWriter, r *http.Request) {
features, err := handler.FeatureService.GetFeatures()
if err != nil {
panic(err)
Expand All @@ -42,11 +43,11 @@ func (handler *APIHandler) FeatureIndex(w http.ResponseWriter, r *http.Request)
}
}

func (handler *APIHandler) FeatureShow(w http.ResponseWriter, r *http.Request) {
func (handler APIHandler) FeatureShow(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

// Check if the feature exists
if !handler.FeatureExists(vars["featureKey"]) {
if !handler.featureExists(vars["featureKey"]) {
writeNotFound(w)
return
}
Expand All @@ -64,12 +65,12 @@ func (handler *APIHandler) FeatureShow(w http.ResponseWriter, r *http.Request) {
}
}

func (handler *APIHandler) FeatureAccess(w http.ResponseWriter, r *http.Request) {
func (handler APIHandler) FeatureAccess(w http.ResponseWriter, r *http.Request) {
var ar AccessRequest
vars := mux.Vars(r)

// Check if the feature exists
if !handler.FeatureExists(vars["featureKey"]) {
if !handler.featureExists(vars["featureKey"]) {
writeNotFound(w)
return
}
Expand Down Expand Up @@ -109,11 +110,11 @@ func (handler *APIHandler) FeatureAccess(w http.ResponseWriter, r *http.Request)
}
}

func (handler *APIHandler) FeatureRemove(w http.ResponseWriter, r *http.Request) {
func (handler APIHandler) FeatureRemove(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

// Check if the feature exists
if !handler.FeatureExists(vars["featureKey"]) {
if !handler.featureExists(vars["featureKey"]) {
writeNotFound(w)
return
}
Expand All @@ -127,7 +128,7 @@ func (handler *APIHandler) FeatureRemove(w http.ResponseWriter, r *http.Request)
writeMessage(http.StatusOK, "feature_deleted", "The feature was successfully deleted", w)
}

func (handler *APIHandler) FeatureCreate(w http.ResponseWriter, r *http.Request) {
func (handler APIHandler) FeatureCreate(w http.ResponseWriter, r *http.Request) {
var feature m.FeatureFlag

if err := json.NewDecoder(r.Body).Decode(&feature); err != nil {
Expand All @@ -153,11 +154,11 @@ func (handler *APIHandler) FeatureCreate(w http.ResponseWriter, r *http.Request)
}
}

func (handler *APIHandler) FeatureEdit(w http.ResponseWriter, r *http.Request) {
func (handler APIHandler) FeatureEdit(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

// Check if the feature exists
if !handler.FeatureExists(vars["featureKey"]) {
if !handler.featureExists(vars["featureKey"]) {
writeNotFound(w)
return
}
Expand Down Expand Up @@ -192,7 +193,7 @@ func (handler *APIHandler) FeatureEdit(w http.ResponseWriter, r *http.Request) {
}
}

func (handler *APIHandler) FeatureExists(featureKey string) bool {
func (handler APIHandler) featureExists(featureKey string) bool {
return handler.FeatureService.FeatureExists(featureKey)
}

Expand Down
2 changes: 1 addition & 1 deletion http/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var (

func onStart() {
database = getTestDB()
server = httptest.NewServer(NewRouter(&APIHandler{FeatureService: s.FeatureService{DB: database}}))
server = httptest.NewServer(NewRouter(APIHandler{FeatureService: s.FeatureService{DB: database}}))
base = fmt.Sprintf("%s/features", server.URL)
}

Expand Down
2 changes: 2 additions & 0 deletions http/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"
)

// Serve and log an incoming request
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
Expand All @@ -24,6 +25,7 @@ func Logger(inner http.Handler, name string) http.Handler {
})
}

// Extract the IP address from a request
func getIPAddress(r *http.Request) string {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)

Expand Down
5 changes: 3 additions & 2 deletions http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import (
"github.com/gorilla/mux"
)

func NewRouter(api *APIHandler) *mux.Router {

// Bind routes to handlers and create a router
func NewRouter(api APIHandler) *mux.Router {
router := mux.NewRouter().StrictSlash(true)

for _, route := range getRoutes(api) {
var handler http.Handler

Expand Down
18 changes: 8 additions & 10 deletions http/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@ package http
import "net/http"

type Route struct {
Name string
Method string
Pattern string
// A human readable name for the route
Name string
// The HTTP method
Method string
// The URL
Pattern string
// The handler for this endpoint
HandlerFunc http.HandlerFunc
}

type Routes []Route

func getRoutes(api *APIHandler) Routes {
func getRoutes(api APIHandler) Routes {
return Routes{
Route{
"Welcome",
"GET",
"/",
api.Welcome,
},
Route{
"FeatureIndex",
"GET",
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ func main() {
api := h.APIHandler{FeatureService: s.FeatureService{DB: database}}

// Create and listen for the HTTP server
router := h.NewRouter(&api)
router := h.NewRouter(api)
log.Fatal(http.ListenAndServe(*address, router))
}
51 changes: 35 additions & 16 deletions models/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,26 @@ import (
helpers "github.com/antoineaugusti/feature-flags/helpers"
)

// Represents a feature flag
type FeatureFlag struct {
Key string `json:"key"`
Enabled bool `json:"enabled"`
Users []uint32 `json:"users"`
Groups []string `json:"groups"`
Percentage uint32 `json:"percentage"`
// The key of a feature flag
Key string `json:"key"`
// Tell if a feature flag is enabled. If set to false,
// the feature flag can still be partially enabled thanks to
// the Users, Groups and Percentage properties
Enabled bool `json:"enabled"`
// Gives access to a feature to specific user IDs
Users []uint32 `json:"users"`
// Gives access to a feature to specific groups
Groups []string `json:"groups"`
// Gives access to a feature to a percentage of users
Percentage uint32 `json:"percentage"`
}

type FeatureFlags []FeatureFlag

func (f *FeatureFlag) Validate() error {
// Self validate the properties of a feature flag
func (f FeatureFlag) Validate() error {
// Validate percentage
if f.Percentage < 0 || f.Percentage > 100 {
return fmt.Errorf("Percentage must be between 0 and 100")
Expand All @@ -35,46 +44,56 @@ func (f *FeatureFlag) Validate() error {
return nil
}

func (f *FeatureFlag) IsEnabled() bool {
// Check if a feature flag is enabled
func (f FeatureFlag) IsEnabled() bool {
return f.Enabled || f.Percentage == 100
}

func (f *FeatureFlag) IsPartiallyEnabled() bool {
// Check if a feature flag is partially enabled
func (f FeatureFlag) IsPartiallyEnabled() bool {
return !f.IsEnabled() && (f.hasUsers() || f.hasGroups() || f.hasPercentage())
}

func (f *FeatureFlag) GroupHasAccess(group string) bool {
// Check if a group has access to a feature
func (f FeatureFlag) GroupHasAccess(group string) bool {
return f.IsEnabled() || (f.IsPartiallyEnabled() && f.groupInGroups(group))
}

func (f *FeatureFlag) UserHasAccess(user uint32) bool {
// Check if a user has access to a feature
func (f FeatureFlag) UserHasAccess(user uint32) bool {
// A user has access:
// - if the feature is enabled
// - if the feature is partially enabled and he has been given access explicity
// - if the feature is partially enabled and he is in the allowed percentage
return f.IsEnabled() || (f.IsPartiallyEnabled() && (f.userInUsers(user) || f.userIsAllowedByPercentage(user)))
}

func (f *FeatureFlag) hasUsers() bool {
// Tell if specific users have access to the feature
func (f FeatureFlag) hasUsers() bool {
return len(f.Users) > 0
}

func (f *FeatureFlag) hasGroups() bool {
// Tell if specific groups have access to the feature
func (f FeatureFlag) hasGroups() bool {
return len(f.Groups) > 0
}

func (f *FeatureFlag) hasPercentage() bool {
// Tell if a specific percentage of users has access to the feature
func (f FeatureFlag) hasPercentage() bool {
return f.Percentage > 0
}

func (f *FeatureFlag) userIsAllowedByPercentage(user uint32) bool {
// Check if a user has access to the feature thanks to the percentage value
func (f FeatureFlag) userIsAllowedByPercentage(user uint32) bool {
return crc32.ChecksumIEEE(helpers.Uint32ToBytes(user))%100 < f.Percentage
}

func (f *FeatureFlag) userInUsers(user uint32) bool {
// Check if a user is in the list of allowed users
func (f FeatureFlag) userInUsers(user uint32) bool {
return helpers.IntInSlice(user, f.Users)
}

func (f *FeatureFlag) groupInGroups(group string) bool {
// Check if a group is in the list of allowed groups
func (f FeatureFlag) groupInGroups(group string) bool {
return helpers.StringInSlice(group, f.Groups)
}
5 changes: 5 additions & 0 deletions repos/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/boltdb/bolt"
)

// Update a feature flag
func PutFeature(tx *bolt.Tx, feature m.FeatureFlag) error {
features := tx.Bucket([]byte(db.GetBucketName()))

Expand All @@ -25,6 +26,7 @@ func PutFeature(tx *bolt.Tx, feature m.FeatureFlag) error {
return nil
}

// Get a list of feature flags
func GetFeatures(tx *bolt.Tx) (m.FeatureFlags, error) {
featuresBucket := tx.Bucket([]byte(db.GetBucketName()))
cursor := featuresBucket.Cursor()
Expand All @@ -44,12 +46,14 @@ func GetFeatures(tx *bolt.Tx) (m.FeatureFlags, error) {
return features, nil
}

// Tell if a feature exists
func FeatureExists(tx *bolt.Tx, featureKey string) bool {
features := tx.Bucket([]byte(db.GetBucketName()))
bytes := features.Get([]byte(featureKey))
return bytes != nil
}

// Get a feature flag thanks to its key
func GetFeature(tx *bolt.Tx, featureKey string) (m.FeatureFlag, error) {
features := tx.Bucket([]byte(db.GetBucketName()))

Expand All @@ -68,6 +72,7 @@ func GetFeature(tx *bolt.Tx, featureKey string) (m.FeatureFlag, error) {
return feature, nil
}

// Delete a feature flag thanks to its key
func RemoveFeature(tx *bolt.Tx, featureKey string) error {
features := tx.Bucket([]byte(db.GetBucketName()))
return features.Delete([]byte(featureKey))
Expand Down
Loading

0 comments on commit a8f7221

Please sign in to comment.