idfm-api/types/container.go

281 lines
7.2 KiB
Go

package types
import (
"encoding/json"
"fmt"
"sync"
"github.com/pkg/errors"
)
// these are the types that can be embedded.
const (
EmbeddedStopArea = "stop_area" // This is a Place & a PT Object
EmbeddedPOI = "poi" // This is a place
EmbeddedAddress = "address" // This is a place
EmbeddedStopPoint = "stop_point" // This is a place
EmbeddedAdmin = "administrative_region" // This is a place
EmbeddedLine = "line" // This is a PT Object
EmbeddedRoute = "route" // This is a PT Object
EmbeddedNetwork = "network" // This is a PT Object
EmbeddedCommercialMode = "commercial_mode" // This is a PT Object
EmbeddedTrip = "trip" // This is a PT Object
)
// EmbeddedTypes lists all the possible embedded types you can find in a Container
var EmbeddedTypes = [...]string{
EmbeddedStopArea,
EmbeddedPOI,
EmbeddedAddress,
EmbeddedStopPoint,
EmbeddedAdmin,
EmbeddedLine,
EmbeddedRoute,
EmbeddedNetwork,
EmbeddedCommercialMode,
EmbeddedTrip,
}
// embeddedTypesPlace stores a list of embedded types you can find in a container containing a Place.
var embeddedTypesPlace = [...]string{
EmbeddedStopArea,
EmbeddedPOI,
EmbeddedAddress,
EmbeddedStopPoint,
EmbeddedAdmin,
}
// embeddedTypesPTObject stores a list of embedded types you can find in a container containing a PTObject.
var embeddedTypesPTObject = [...]string{
EmbeddedStopArea,
EmbeddedLine,
EmbeddedRoute,
EmbeddedNetwork,
EmbeddedCommercialMode,
EmbeddedTrip,
}
// An Object is what is contained by a Container
type Object interface{}
// A Container holds an Object, which can be a Place or a PT Object
type Container struct {
ID ID `json:"id"`
Name string `json:"name"`
EmbeddedType string `json:"embedded_type"`
Quality int `json:"quality"`
embeddedJSON json.RawMessage
// embeddedObject acts as a cache, it is the only element guarded by the RWMutex
embeddedObject Object
// If multiple goroutines try to access embeddedObject while one is writing it, results are undefined.
// So we guard against it through a mutex.
mu *sync.RWMutex
}
// IsPlace returns true if the container's content is a Place
func (c *Container) IsPlace() bool {
t := c.EmbeddedType
return t == EmbeddedStopArea ||
t == EmbeddedPOI ||
t == EmbeddedAddress ||
t == EmbeddedStopPoint ||
t == EmbeddedAdmin
}
// IsPTObject returns true if the container's content is a PTObject
func (c *Container) IsPTObject() bool {
t := c.EmbeddedType
return t == EmbeddedStopArea ||
t == EmbeddedLine ||
t == EmbeddedRoute ||
t == EmbeddedNetwork ||
t == EmbeddedCommercialMode ||
t == EmbeddedTrip
}
// ErrInvalidContainer is returned after a check on a Container
type ErrInvalidContainer struct {
// If the Container has a zero ID.
NoID bool
// If the PlaceContainer has an EmbeddedType yet non-empty embedded content.
NoEmbeddedType bool
// If the PlaceContainer has an unknown EmbeddedType
UnknownEmbeddedType bool
}
// Error satisfies the error interface
func (err ErrInvalidContainer) Error() string {
// Count the number of anomalies
var anomalies uint
msg := "Error: Invalid non-empty PlaceContainer (%d anomalies):"
if err.NoID {
msg += "\n\tNo ID specified"
anomalies++
}
if err.NoEmbeddedType {
msg += "\n\tEmpty EmbeddedType yet non-empty embedded content"
anomalies++
}
if err.UnknownEmbeddedType {
msg += "\n\tUnknown EmbeddedType"
anomalies++
}
return fmt.Sprintf(msg, anomalies)
}
// Empty returns true if the container is empty (zero value)
func (c *Container) Empty() bool {
return c.ID == "" && c.Name == "" && c.EmbeddedType == "" && c.Quality == 0 && len(c.embeddedJSON) == 0 && c.embeddedObject == nil
}
// Check checks the validity of the Container. Returns an ErrInvalidContainer.
//
// An empty Container is valid. But those cases aren't:
// - If the Container has an empty ID.
// - If the Container has an empty EmbeddedType & a non-empty embedded types inside.
// - If the Container has an unknown EmbeddedType.
func (c *Container) Check() error {
// Check if the container is empty
if c.Empty() {
return nil
}
// Create the error to be populated
err := ErrInvalidContainer{}
// Check for zero ID
err.NoID = c.ID == ""
// Check if the embedded type is empty & there is a non-empty embedded content inside, that's an error
if c.EmbeddedType == "" && (len(c.embeddedJSON) != 0 || c.embeddedObject != nil) {
err.NoEmbeddedType = true
return err
} else if c.EmbeddedType == "" { // Else, if the embedded type indicator is empty, the rest is useless
return nil
}
// Else, check if the declared EmbeddedType is known.
var known bool
for _, ket := range EmbeddedTypes {
if c.EmbeddedType == ket {
known = true
break
}
}
err.UnknownEmbeddedType = !known
// Check if there's any change
emptyErr := ErrInvalidContainer{}
if err != emptyErr {
return err
}
return nil
}
// Object returns the Object contained in a Container.
// If the Container is empty, Object returns an error.
// Check() is run on the Container.
func (c *Container) Object() (Object, error) {
if c.EmbeddedType == "" {
return nil, nil
}
// If we already have an embedded object, return it
c.mu.RLock()
o := c.embeddedObject
c.mu.RUnlock()
if o != nil {
return o, nil
}
// Create the receiver
var obj Object
// Switch through
switch c.EmbeddedType {
case EmbeddedStopArea:
obj = &StopArea{}
case EmbeddedPOI:
obj = &POI{}
case EmbeddedAddress:
obj = &Address{}
case EmbeddedStopPoint:
obj = &StopPoint{}
case EmbeddedAdmin:
obj = &Admin{}
case EmbeddedLine:
obj = &Line{}
case EmbeddedRoute:
obj = &Route{}
case EmbeddedNetwork:
obj = &Network{}
case EmbeddedCommercialMode:
obj = &CommercialMode{}
case EmbeddedTrip:
obj = &Trip{}
default:
return nil, errors.Errorf("no known embedded type indicated (we have \"%s\"), can't return a place !", c.EmbeddedType)
}
// Unmarshal into the receiver
err := json.Unmarshal(c.embeddedJSON, obj)
if err != nil {
return nil, errors.Wrapf(err, "couldn't unmarshal the embedded type (%s)", c.EmbeddedType)
}
// Let's add it to the container
c.mu.Lock()
c.embeddedObject = obj
c.mu.Unlock()
// Let's return it
return obj, nil
}
// Place returns the Place contained in the container if that is what's inside
//
// If the Object isn't a Place or the Container is empty or invalid, Place returns an error
func (c *Container) Place() (Place, error) {
// Check if its a Place
if !c.IsPlace() {
return nil, errors.Errorf("container's content isn't a Place")
}
// Return it then
obj, err := c.Object()
if err != nil {
return nil, err
}
// Type assert
return obj.(Place), nil
}
// PTObject returns the PTObject contained in the container if that is what's inside
//
// If the Object isn't a PTObject or the Container is empty or invalid, Place returns an error
func (c *Container) PTObject() (PTObject, error) {
// Check if its a Place
if !c.IsPTObject() {
return nil, errors.Errorf("container's content isn't a PTObject")
}
// Return it then
obj, err := c.Object()
if err != nil {
return nil, err
}
// Type assert
return obj.(PTObject), nil
}