Skip to content

Latest commit

 

History

History
313 lines (219 loc) · 11.2 KB

gong-go-api.md

File metadata and controls

313 lines (219 loc) · 11.2 KB

Gong API

Stack organization

Code is organized with a fixed directory structure. At the top are 2 directories and a file:

  • go for the go code.
  • ng for the angular code.
  • main.go

By default, main.go provides the web server, the business logic and the database in one single binary. main.go is located in the root directory because it embeds the ng directory (thanks to go v1.16 embed feature).

Developping a stack

To develop a stack with gong, the programmer will, by default, edit the following places:

  • the data model and business logic of the application in the go/models directory

  • optional, one or more angular library in ng\src\projects\<angular library>

  • the angular application in ng\src\app, where the the composition of front-end stacks is defined

  • the main.go file at the root level where th the composition of back-end stacks is defined

The gong repository

This repository is the home of gongc (in go/gongc), a compiler that compiles the business logic written in go and generates code in go and ng directories.

This repository is also the home of the gong stack whose data model is the description of the data model that is parsed by gongc. The gong stack an be reused in other stacks. (for instance, in gongdoc, an UML editor).

Gong back-end API and the repository pattern

Rationale

Gong is a programming language that is a subset of the go language. When a go program is compiled by gongc, the gong compiler, it generates additionnal go code. The original go code and the generated go code coexist in the back-end and are arranged in a software architecture.

Program written in gong can invoke functions on gong object (Stage() for instance) that will be generated later by the gong compiler (Stage() functions are generated in the gong.go file). Compilation errors occur if the generated code is not yet present. Fortuntaly, the gong compiler is robust to those errors.

Gong back-end architecture is based on the repository design pattern (a "repository" is the database and it is accessed as a git repository). With the repository, a set of staged objects (the "unit of work" in the repository pattern) can be persisted to a database (the "repository") with a single "check in" operation. Conversely, objects can be retrievedto the Stage from the database with a single "check out" operation.

Persisted objects can be CRUD via a REST API (also generated by gong). The front-end in angular access the back via this REST API.

This architecture presents two advantages:

  • since gongc generates the code implementing the repository pattern, it insulates the programmer from the the complexity of database management and REST API implementation. Database management includes pointer encoding, instances management, null values encoding, data migration and REST API implementation includes GET/POST/UPDATE/DELETE implementation and generation of the open api 2.0 specification of the API.

  • It leverages the "repository pattern", a mental model that is familiar to all programmers. Functions such as Stage(), Unstage(), Checkin() an Checkout() are not trivial but they are part of the knowledge of all programmers who git, the de facto standard tool of configuration management.

Repostory pattern from the programmer perspective

To implement the repository pattern, Gong divides programming objects into three sets (memory, stage and backRepo) that falls in two packages (models and orm). A third package, controllers, provides a REST API to the back repo objects.

Staging gongstruct instances

The first set of go instance is the memory set. It lives in the models package. With gong, the models package is where manual coding takes places. The rest of the backend code is generated by gongc, the gong compiler.

The second set is the stage set, a subset of the memory set (this is where it follows the repository design pattern). As with git, the programmer stages instances (or unstages them) of the memory set.

To be staged, go instances have to be instances of a special kind of go struct called gongstruct. A gongstruct is an a exported go struct with a Name field.

package models

// Astruct is an example of gongstruct
// It has two association to Bstruct, another gong struct
// - a pointer (a 0..1 relationship), 
// - an array of pointers (a 0..N relationship)
type Astruct struct {
    Name string
    Associationtob *Bstruct
    Anarrayofb []*Bstruct
}

type Bstruct struct {
    Name string
}

In the same package, gongc will generate a gong.go file, that includes the generated functions Stage() and Unstage functions for each gongstruct.

// Stage puts astruct to the model stage
func (astruct *Astruct) Stage() *Astruct {
        ....
    return astruct
}

// Unstage removes astruct off the model stage
func (astruct *Astruct) Unstage() *Astruct {
    ....
        return astruct
}

Calling Stage() to an instance is straightforward.

    // the following code stages bstruct1 and astruct1
    // bstruct1 is associated to astruct1
    // therefore, they form an unit of work that have to be commited together
    bstruct1 := (&models.Bstruct{Name: "B1"}).Stage()

    astruct1 := (&models.Astruct{
        Name:                "A1",
        Associationtob:      bstruct1,
        Anarrayofb: []*models.Bstruct{
            bstruct1,
        },
    }).Stage()

Only gongstruct instances can be staged or unstaged. It is interesting to stage a gongstruct instance for different reasons:

  • if the instance need to be persisted in a database

  • if the instance need to be seen in the front end via the REST API

Commiting a unit of work of the stage

To commit staged commit, the commit() function have to be called on the models.Stage object (a generated singloton in the models package).

The models.Stage object can also serves as a in-memory datastore in the models package.

// Stage puts astruct to the model stage
func (astruct *Astruct) Stage() *Astruct {
    ...
    return astruct
}

// Unstage puts astruct off the model stage
func (astruct *Astruct) Unstage() *Astruct {
    ...
    return astruct
}

// StageStruct is the struct of the Stage singloton
type StageStruct struct { 
    Astructs           map[*Astruct]struct{} // set of Astruct instances
    Astructs_mapString map[string]*Astruct // map of Astruct by their Name
    ....
}

// Stage singloton
var Stage StageStruct = StageStruct{ 
    ....
}

// Commit staged objects of stage
func (stage *StageStruct) Commit() {
    ....
}

// Checkout objects to stage
func (stage *StageStruct) Checkout() {
    ....
}

Back Repo objects

The third set is the backRepo, it contains sister instances of instances of the stage set. The sister instances in the backRepo differ from the staged instance in two ways that make them fit for storage in a database:

  • basic field are of type sql

  • pointer fields are encoded into basic fields. Therefore, they are without pointers (acronym WOP).

The sister instances of the backRepo are instances in the ormpackage.

package orm

type AstructDB struct {
    gorm.Model

    Name_Data sql.NullString

    // encoding of pointers
    AstructPointersEnconding
}

type AstructPointersEnconding struct {
    // field Associationtob is a pointer to another Struct (optional or 0..1)
    // This field is generated into another field to enable AS ONE association
    AssociationtobID sql.NullInt64
}

Notice that the stage can be checked-out from the backRepo.

For each commited object, a WOP twin object is created and it is persisted in the database.

Initializing a gong stack

Imports of the 3 go packages

By default, all tree generated packages have to be imported.

import (

    "github.com/fullstack-lang/gong/test/go/models"
    "github.com/fullstack-lang/gong/test/go/orm"
    "github.com/fullstack-lang/gong/test/go/controllers"
)

Init the back repo

In the main() program, the first element to init is the back repository. The back repo leverages the gorm framework, to manages persistance into sqlite, the default database.

If you do not need to persist into a file database, the API provides the path to the database. Notice that the database is migrated if the data model has been changed.

    // setup GORM
    db := orm.SetupModels(false, "./test.db")

If you do not need to persist into a file, sqlite provides a in memory database.

    // setup GORM
    db := orm.SetupModels(false, ":memory:")

Registring controllers

Gong uses gin, a web framework written in Go.

    // setup controlers
    r := gin.Default()
    r.Use(cors.Default())
    
    controllers.RegisterControllers(r)

Serving angular file with gin and embedding

go allows embeding of the ng/dist/ng directory generated by the angular compiler command ng build. Then, it is possible to serve this directory with gin.

// the following comment is the embed directive in go (version >= 1.16)
//go:embed ng/dist/ng
var ng embed.FS

...
    r.Use(static.Serve("/", EmbedFolder(ng, "ng/dist/ng")))
    r.NoRoute(func(c *gin.Context) {
        fmt.Println(c.Request.URL.Path, "doesn't exists, redirect on /")
        c.Redirect(http.StatusMovedPermanently, "/")
        c.Abort()
    })

    r.Run()
}


type embedFileSystem struct {
    http.FileSystem
}

func (e embedFileSystem) Exists(prefix string, path string) bool {
    _, err := e.Open(path)
    return err == nil
}

func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
    fsys, err := fs.Sub(fsEmbed, targetPath)
    if err != nil {
        panic(err)
    }
    return embedFileSystem{
        FileSystem: http.FS(fsys),
    }
}

OnAfterUpdate

Code like this

func (command *Command) OnAfterUpdate(stage *StageStruct, stagedCommand, frontCommand *Command) {

	log.Println(time.Now().Format("2006-01-02 15:04:05.000000"), "received command update",
		frontCommand.Command.ToString())
}

will have the compiler to orchestrate calls to this function when the update from the front.