[add] initial commit
This commit is contained in:
119
app/application/application.go
Normal file
119
app/application/application.go
Normal 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
|
||||
}
|
||||
52
app/application/database.go
Normal file
52
app/application/database.go
Normal 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
|
||||
}
|
||||
22
app/controllers/controller.go
Normal file
22
app/controllers/controller.go
Normal 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
134
app/controllers/sensor.go
Normal 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
19
app/models/model.go
Normal 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
124
app/models/sensor.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user