[add] initial commit

This commit is contained in:
2022-09-30 14:59:51 -05:00
commit 3da5d6a560
15 changed files with 825 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
package application
import (
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type Mode uint
const (
Debug Mode = iota
Test
Production
)
type Version struct {
Major uint8
Minor uint8
Patch uint8
}
type AppError struct {
Error string `json:"error"`
Debug string `json:"debug"`
}
type Application struct {
version Version
urlRoot string
database *Database
engine *gin.Engine
mode Mode
}
func (a *Application) SqliteDB(path string) {
a.database = &Database{
databaseType: SQLite3,
path: path,
mode: a.mode,
}
}
func (a *Application) OpenDB() error {
return a.database.open()
}
func (a *Application) CloseDB() error {
return a.database.close()
}
func (a Application) Db() *gorm.DB {
return a.database.db
}
func (a Application) Version() string {
return fmt.Sprintf("%d.%d.%d", a.version.Major, a.version.Minor, a.version.Patch)
}
func (a Application) VersionNamespace() string {
return fmt.Sprintf("%d", a.version.Major)
}
func (a Application) UrlRoot() string {
return a.urlRoot
}
func (a Application) Engine() *gin.Engine {
return a.engine
}
func (a *Application) Run(address string) error {
return a.engine.Run(address)
}
func (a Application) Database() *Database {
return a.database
}
func (a Application) Mode() Mode {
return a.mode
}
func (a Application) NewError(error string, debug string) interface{} {
if a.mode == Debug {
return AppError{
Error: error,
Debug: debug,
}
} else {
return struct {
Error string `json:"error"`
Debug string `json:"-"`
}{error, debug}
}
}
func NewApplication(version Version, urlRoot string, mode Mode) *Application {
switch mode {
case Production:
gin.SetMode(gin.ReleaseMode)
case Test:
gin.SetMode(gin.TestMode)
}
app := Application{
version: Version{
Major: version.Major,
Minor: version.Minor,
Patch: version.Patch,
},
urlRoot: urlRoot,
engine: gin.Default(),
mode: mode,
}
return &app
}

View File

@@ -0,0 +1,52 @@
package application
import (
"errors"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type DatabaseType uint8
const (
SQL DatabaseType = iota
SQLite3
)
type Database struct {
databaseType DatabaseType
path string
db *gorm.DB
mode Mode
}
func (d *Database) open() error {
var err error
switch d.databaseType {
case SQL:
err = errors.New("not yet implemented")
case SQLite3:
var config gorm.Config
if d.mode == Debug {
config = gorm.Config{}
} else {
config = gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
}
db, err := gorm.Open(sqlite.Open(d.path), &config)
if err == nil {
d.db = db
}
default:
err = errors.New("unknown database type")
}
return err
}
func (d *Database) close() error {
db, err := d.db.DB()
if err == nil {
err = db.Close()
}
return err
}

View File

@@ -0,0 +1,22 @@
package controllers
import (
"gelvin/app/application"
"gelvin/app/models"
)
type Controller struct {
app *application.Application
model *models.Model
}
func (c Controller) Model() *models.Model {
return c.model
}
func NewController(app *application.Application) *Controller {
return &Controller{
app: app,
model: models.NewModel(app),
}
}

134
app/controllers/sensor.go Normal file
View File

@@ -0,0 +1,134 @@
package controllers
import (
"errors"
"gelvin/app/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"net/http"
"strconv"
)
func (c Controller) GetSensors(ctx *gin.Context) {
var sensors []models.Sensor
result := c.app.Db().Find(&sensors)
if result.Error != nil {
ctx.IndentedJSON(http.StatusNotFound, c.app.NewError("sensor not found", result.Error.Error()))
} else {
ctx.IndentedJSON(http.StatusOK, sensors)
}
}
func (c Controller) GetSensor(ctx *gin.Context) {
var sensor models.Sensor
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
ctx.IndentedJSON(http.StatusBadRequest, c.app.NewError("invalid input", err.Error()))
return
}
result := c.app.Db().First(&sensor, id)
if result.Error != nil {
ctx.IndentedJSON(http.StatusNotFound, c.app.NewError("sensor not found", result.Error.Error()))
} else {
ctx.IndentedJSON(http.StatusOK, sensor)
}
}
func (c Controller) AddSensor(ctx *gin.Context) {
var sensor models.Sensor
var err = ctx.BindJSON(&sensor)
if err == nil && !sensor.IsValid() {
err = errors.New("invalid sensor")
}
var result uint
httpCode := http.StatusCreated
if err == nil {
var txSensor models.Sensor
tx := c.app.Db().Where(&models.Sensor{Name: sensor.Name}).First(&txSensor)
if tx.Error == nil {
result, err = c.model.UpdateSensor(sensor, txSensor, tx)
httpCode = http.StatusOK
} else {
result, err = c.model.AddSensor(sensor)
}
}
if err != nil {
ctx.IndentedJSON(http.StatusBadRequest, c.app.NewError("invalid input", err.Error()))
} else {
id := struct {
Id uint `json:"id"`
}{result}
ctx.IndentedJSON(httpCode, id)
}
}
func (c Controller) GetMeasurements(ctx *gin.Context) {
var measurements []models.Measurement
//result := c.app.Db().Find(&measurements)
// MAX(created_at) converts time.Time to string, `as created_at` or datetime() is not sufficient
result := c.app.Db().Select("MAX(created_at)",
"id", "created_at", "deleted_at", "sensor_id", "value").Group("sensor_id").Find(&measurements)
if result.Error != nil {
ctx.IndentedJSON(http.StatusOK, struct{}{})
} else {
ctx.IndentedJSON(http.StatusOK, measurements)
} //result := c.app.Db().Select("id sensor_id MAX(created_at) value").Group("sensor_id").Find(&measurements)
}
func (c Controller) GetMeasurement(ctx *gin.Context) {
var measurements []models.Measurement
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
ctx.IndentedJSON(http.StatusBadRequest, c.app.NewError("invalid input", err.Error()))
return
}
result := c.app.Db().Where(&models.Measurement{SensorID: uint(id)}).Find(&measurements)
if result.Error != nil {
ctx.IndentedJSON(http.StatusOK, struct{}{})
} else {
ctx.IndentedJSON(http.StatusOK, measurements)
}
}
func (c Controller) AddMeasurement(ctx *gin.Context) {
var measurement models.Measurement
err := ctx.BindJSON(&measurement)
if err != nil {
ctx.IndentedJSON(http.StatusBadRequest, c.app.NewError("invalid input", err.Error()))
return
}
c.addMeasurement(ctx, measurement)
}
func (c Controller) AddMeasurementID(ctx *gin.Context) {
var measurement models.Measurement
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
ctx.IndentedJSON(http.StatusBadRequest, c.app.NewError("invalid input", err.Error()))
return
}
err = ctx.BindJSON(&measurement)
if err != nil {
ctx.IndentedJSON(http.StatusBadRequest, c.app.NewError("invalid input", err.Error()))
return
}
measurement.SensorID = uint(id)
c.addMeasurement(ctx, measurement)
}
func (c Controller) addMeasurement(ctx *gin.Context, measurement models.Measurement) {
result, err := c.model.AddMeasurement(measurement)
if err != nil {
if err == gorm.ErrRecordNotFound {
ctx.IndentedJSON(http.StatusNotFound, c.app.NewError("sensor not found", err.Error()))
} else {
ctx.IndentedJSON(http.StatusBadRequest, c.app.NewError("invalid input", err.Error()))
}
} else {
id := struct {
Id uint `json:"id"`
}{result}
ctx.IndentedJSON(http.StatusOK, id)
}
}

19
app/models/model.go Normal file
View File

@@ -0,0 +1,19 @@
package models
import (
"gelvin/app/application"
)
type Model struct {
app *application.Application
}
func NewModel(app *application.Application) *Model {
return &Model{
app: app,
}
}
func Migrate(app *application.Application) error {
return app.Db().AutoMigrate(&Sensor{}, &Measurement{})
}

124
app/models/sensor.go Normal file
View File

@@ -0,0 +1,124 @@
package models
import (
"database/sql/driver"
"errors"
"gorm.io/gorm"
"strconv"
"strings"
"time"
)
type SensorType uint
const (
Temperature SensorType = iota + 1 // to allow gorm: not null
Humidity
Pressure
Gas
CO2
)
func (s SensorType) IsValid() bool {
switch s {
case Temperature, Humidity, Pressure, Gas, CO2:
return true
default:
return false
}
}
type Position struct {
X int `json:"x"`
Y int `json:"y"`
NotNull bool `json:"-"` // to allow gorm: not null with x/y = 0
}
type Sensor struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
Name string `json:"name" gorm:"uniqueIndex;not null;default:null"`
SensorType SensorType `json:"sensor_type" gorm:"not null;default:null"`
Position Position `json:"position" gorm:"not null;default:null"`
}
type Measurement struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"time"` // TODO: change to datetime2
UpdatedAt time.Time `json:"-"` // TODO: remove
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // TODO: remove
SensorID uint `json:"sensor_id" gorm:"<-:create;not null;default:null"`
Sensor Sensor `json:"-"`
Value float64 `json:"value" gorm:"<-:create;default:null"` // TODO on migrate set column NOT NULL
}
func (s Sensor) IsValid() bool {
return s.SensorType.IsValid()
}
func (p Position) Value() (driver.Value, error) {
return strconv.FormatInt(int64(p.X), 10) + "." + strconv.FormatInt(int64(p.Y), 10), nil
}
func (p *Position) Scan(value interface{}) error {
p.X = 0
p.Y = 0
p.NotNull = true
positionString, isString := value.(string)
if !isString {
return errors.New("invalid scan value")
}
positionArray := strings.Split(positionString, ".")
if len(positionArray) != 2 {
return errors.New("invalid string value")
}
var err error
var position int64
position, err = strconv.ParseInt(positionArray[0], 10, 32)
if err != nil {
return errors.New("invalid x value")
}
p.X = int(position)
position, err = strconv.ParseInt(positionArray[1], 10, 32)
if err != nil {
return errors.New("invalid y value")
}
p.Y = int(position)
return nil
}
// http -v POST localhost:8080/sensors name="sensor_$RANDOM" sensor_type:=1 position:='{"x":1, "y":2}'
// http -v POST localhost:8080/sensors name="sensor_$RANDOM" sensor_type:=2
func (m Model) AddSensor(sensor Sensor) (uint, error) {
sensor.ID = 0
sensor.Position.NotNull = true
if !sensor.SensorType.IsValid() {
return 0, errors.New("invalid sensor type")
}
result := m.app.Db().Create(&sensor)
return sensor.ID, result.Error
}
func (m Model) UpdateSensor(sensor Sensor, txSensor Sensor, tx *gorm.DB) (uint, error) {
sensor.ID = txSensor.ID
sensor.Position.NotNull = true
if !sensor.SensorType.IsValid() {
return 0, errors.New("invalid sensor type")
}
result := tx.Updates(&sensor)
return sensor.ID, result.Error
}
// http -v POST localhost:8080/measurements sensor_id:=3 value:=32
// http -v GET localhost:8080/measurements/3
func (m Model) AddMeasurement(measurement Measurement) (uint, error) {
measurement.ID = 0
err := m.app.Db().First(&Sensor{}, measurement.SensorID).Error
if err != nil {
return 0, err
}
result := m.app.Db().Create(&measurement)
return measurement.ID, result.Error
}