- Gong API
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).
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
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 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()
anCheckout()
are not trivial but they are part of the knowledge of all programmers who git, the de facto standard tool of configuration management.
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.
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
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() {
....
}
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 orm
package.
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.
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"
)
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:")
Gong uses gin, a web framework written in Go.
// setup controlers
r := gin.Default()
r.Use(cors.Default())
controllers.RegisterControllers(r)
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),
}
}
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.