Geração de código no Go pelo exemplo da criação de um cliente para o banco de dados

Neste artigo, eu gostaria de considerar os problemas de geração de código em Golang. Notei que, frequentemente, nos comentários dos artigos sobre Go mencionam a geração e a reflexão de códigos, o que causa um debate acalorado. Ao mesmo tempo, existem poucos artigos sobre geração de código no hub, embora sejam usados ​​bastante em projetos no Go. No artigo, tentarei dizer o que é geração de código, para descrever o escopo da aplicação com exemplos de código. Além disso, não vou ignorar o reflexo.

Quando a geração de código é usada


Em Habré já existem bons artigos sobre o assunto aqui e aqui , não vou repetir.

A geração de código deve ser usada nos casos:

  • Aumentar a velocidade do código, ou seja, para substituir a reflexão;
  • Reduzir a rotina de um programador (e erros associados a ele);
  • Implementação de wrappers de acordo com as regras fornecidas.

A partir dos exemplos, podemos considerar a biblioteca Stringer, que está incluída no fornecimento de idiomas padrão e permite gerar automaticamente métodos String () para conjuntos de constantes numéricas. Usando-o, você pode implementar a saída de nomes de variáveis. Exemplos da biblioteca foram descritos em detalhes nos artigos acima. O exemplo mais interessante foi a derivação do nome da cor da paleta. A aplicação da geração de código evita a alteração do código em vários locais ao alterar a paleta.

De um exemplo mais prático, podemos mencionar a biblioteca easyjson do Mail.ru. Essa biblioteca permite acelerar a execução do masrshall / unmarshall JSON da / para a estrutura. Sua implementação em benchmarks contornou todas as alternativas. Para usar a biblioteca, você precisa chamar easyjson, ele irá gerar código para todas as estruturas que encontrar no arquivo transferido ou apenas para aquelas para as quais o comentário // easyjson: json é indicado. Tome a estrutura do usuário como um exemplo:

type User struct{
    ID int
    Login string
    Email string
    Level int
}

Para o arquivo em que está contido, execute a geração do código:

easyjson -all main.go

Como resultado, obtemos métodos para o usuário:

  • MarshalEasyJSON (w * jwriter.Writer) - para converter a estrutura em uma matriz de bytes JSON;
  • UnmarshalEasyJSON (l * jlexer.Lexer) - para converter de uma matriz de bytes em uma estrutura.

As funções de erro MarshalJSON () ([] byte, erro) e UnmarshalJSON (dados [] byte) são necessárias para compatibilidade com a interface json padrão.

Código Easyjson

func TestEasyJSON() {
	testJSON := `{"ID":123, "Login":"TestUser", "Email":"user@gmail.com", "Level":12}`
	JSONb := []byte(testJSON)
	fmt.Println(testJSON)
	recvUser := &User{}
	recvUser.UnmarshalJSON(JSONb)
	fmt.Println(recvUser)
	recvUser.Level += 1
	outJSON, _ := recvUser.MarshalJSON()
	fmt.Println(string(outJSON))
}


Nesta função, primeiro convertemos o JSON em uma estrutura, adicionamos um nível e imprimimos o JSON resultante. A geração de código pelo easyjson significa livrar-se da reflexão em tempo de execução e aumentar o desempenho do código.

A geração de código é usada ativamente para criar microsserviços que se comunicam via gRPC. Ele usa o formato protobuf para descrever os métodos de serviços - usando a linguagem intermediária EDL. Após a descrição do serviço, o compilador protoc é iniciado, o que gera código para a linguagem de programação desejada. No código gerado, obtemos as interfaces que precisam ser implementadas no servidor e os métodos usados ​​no cliente para organizar a comunicação. Acontece que, convenientemente, podemos descrever nossos serviços em um único formato e gerar código para a linguagem de programação na qual cada um dos elementos de interação será descrito.

Além disso, a geração de código pode ser usada no desenvolvimento de estruturas. Por exemplo, para implementar código que não precisa ser gravado pelo desenvolvedor do aplicativo, mas é necessário para a operação correta. Por exemplo, para criar validadores de campo de formulário, gere automaticamente o Middleware, gere dinamicamente clientes para DBMS.

Implementação do Go Code Generator


Vamos examinar na prática como o mecanismo de geração de código no Go funciona. Antes de tudo, é necessário mencionar a AST - Abstract Syntax Tree ou Abstract Syntax Tree. Para detalhes, você pode ir à Wikipedia . Para nossos propósitos, é necessário entender que todo o programa é construído na forma de um gráfico, onde os vértices são mapeados (marcados) com os operadores da linguagem de programação e as folhas com os operandos correspondentes.

Portanto, para iniciantes, precisamos de pacotes:

go / ast
go / parser /
go / token / A

análise do arquivo com o código e a compilação da árvore são executadas pelos seguintes comandos


fset := token.NewFileSet()
node, err := parser.ParseFile(fset, os.Args[1], nil, parser.ParseComments)

Indicamos que o nome do arquivo deve ser retirado do primeiro argumento da linha de comando; também solicitamos que comentários sejam adicionados à árvore.

Em geral, para controlar a geração do código, o usuário (o desenvolvedor do código com base no qual outro código é gerado) pode usar comentários ou tags (enquanto escrevemos `json:" "` próximo ao campo da estrutura).

Por exemplo, escreveremos um gerador de código para trabalhar com um banco de dados. O gerador de código examinará o arquivo transferido para ele, procurará estruturas que tenham um comentário correspondente e criará um wrapper sobre a estrutura (métodos CRUD) para interação com o banco de dados. Vamos usar os parâmetros:

  • dbe comment: {"table": "users"}, no qual você pode definir a tabela na qual os registros da estrutura estarão;
  • tag dbe para os campos da estrutura, na qual é possível especificar o nome da coluna na qual colocar o valor e os atributos do campo para o banco de dados: primary_key e not_null. Eles serão usados ​​ao criar a tabela. E para o nome do campo, você pode usar "-" para não criar uma coluna para ele.

Farei uma reserva antecipada de que o projeto ainda não está em combate, não conterá parte das verificações e proteções necessárias. Se houver interesse, continuarei seu desenvolvimento.

Então, decidimos sobre a tarefa e os parâmetros para controlar a geração do código, podemos começar a escrever o código.

Os links para todo o código estarão no final do artigo.

Começamos a ignorar a árvore resultante e analisaremos cada elemento do primeiro nível. O Go possui tipos predefinidos para análise: BadDecl, GenDecl e FuncDecl.

Tipo Descrição
// A BadDecl node is a placeholder for declarations containing
// syntax errors for which no correct declaration nodes can be
// created.
//
BadDecl struct {
    From, To token.Pos // position range of bad declaration
}
// A GenDecl node (generic declaration node) represents an import,
// constant, type or variable declaration. A valid Lparen position
// (Lparen.IsValid()) indicates a parenthesized declaration.
//
// Relationship between Tok value and Specs element type:
//
// token.IMPORT *ImportSpec
// token.CONST *ValueSpec
// token.TYPE *TypeSpec
// token.VAR *ValueSpec
//
GenDecl struct {
    Doc *CommentGroup // associated documentation; or nil
    TokPos token.Pos // position of Tok
    Tok token.Token // IMPORT, CONST, TYPE, VAR
    Lparen token.Pos // position of '(', if any
    Specs []Spec
    Rparen token.Pos // position of ')', if any
}
// A FuncDecl node represents a function declaration.
FuncDecl struct {
    Doc *CommentGroup // associated documentation; or nil
    Recv *FieldList // receiver (methods); or nil (functions)
    Name *Ident // function/method name
    Type *FuncType // function signature: parameters, results, and position of "func" keyword
    Body *BlockStmt // function body; or nil for external (non-Go) function
}


Como estamos interessados ​​em estruturas, usamos o GenDecl. Nesse estágio, FuncDecl pode ser útil, no qual as definições de funções se encontram e você as agrupa, mas agora não precisamos delas. A seguir, examinamos a matriz Specs em cada nó e procuramos trabalhar com um campo de definição de tipo (* ast.TypeSpec) e essa é uma estrutura (* ast.StructType). Depois de determinarmos que temos uma estrutura, verificamos que ela possui um comentário // dbe. O código de passagem da árvore completa e a definição de qual estrutura trabalhar estão abaixo.

Atravessar árvores e obter estruturas

for _, f := range node.Decls {
	genD, ok := f.(*ast.GenDecl)
	if !ok {
		fmt.Printf("SKIP %T is not *ast.GenDecl\n", f)
		continue
	}
	targetStruct := &StructInfo{}
	var thisIsStruct bool
	for _, spec := range genD.Specs {
		currType, ok := spec.(*ast.TypeSpec)
		if !ok {
			fmt.Printf("SKIP %T is not ast.TypeSpec\n", spec)
			continue
		}

		currStruct, ok := currType.Type.(*ast.StructType)
		if !ok {
			fmt.Printf("SKIP %T is not ast.StructType\n", currStruct)
			continue
		}
		targetStruct.Name = currType.Name.Name
		thisIsStruct = true
	}
	//Getting comments
	var needCodegen bool
	var dbeParams string
	if thisIsStruct {
		for _, comment := range genD.Doc.List {
			needCodegen = needCodegen || strings.HasPrefix(comment.Text, "// dbe")
			if len(comment.Text) < 7 {
				dbeParams = ""
			} else {
				dbeParams = strings.Replace(comment.Text, "// dbe:", "", 1)
			}
		}
	}
	if needCodegen {
		targetStruct.Target = genD
		genParams := &DbeParam{}
		if len(dbeParams) != 0 {
			err := json.Unmarshal([]byte(dbeParams), genParams)
			if err != nil {
				fmt.Printf("Error encoding DBE params for structure %s\n", targetStruct.Name)
				continue
			}
		} else {
			genParams.TableName = targetStruct.Name
		}

		targetStruct.GenParam = genParams
		generateMethods(targetStruct, out)
	}
}

, :

type DbeParam struct {
	TableName string `json:"table"`
}

type StructInfo struct {
	Name     string
	GenParam *DbeParam
	Target   *ast.GenDecl
}


Agora prepararemos informações sobre os campos da estrutura, para que, com base nas informações recebidas, geremos funções de criação de tabela (createTable) e métodos CRUD.

Código para obter campos de uma estrutura

func generateMethods(reqStruct *StructInfo, out *os.File) {
	for _, spec := range reqStruct.Target.Specs {
		fmt.Fprintln(out, "")
		currType, ok := spec.(*ast.TypeSpec)
		if !ok {
			continue
		}
		currStruct, ok := currType.Type.(*ast.StructType)
		if !ok {
			continue
		}

		fmt.Printf("\tgenerating createTable methods for %s\n", currType.Name.Name)

		curTable := &TableInfo{
			TableName: reqStruct.GenParam.TableName,
			Columns:   make([]*ColInfo, 0, len(currStruct.Fields.List)),
		}

		for _, field := range currStruct.Fields.List {
			if len(field.Names) == 0 {
				continue
			}
			tableCol := &ColInfo{FieldName: field.Names[0].Name}
			var fieldIsPrimKey bool
			var preventThisField bool
			if field.Tag != nil {
				tag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1])
				tagVal := tag.Get("dbe")
				fmt.Println("dbe:", tagVal)
				tagParams := strings.Split(tagVal, ",")
			PARAMSLOOP:
				for _, param := range tagParams {
					switch param {
					case "primary_key":
						if curTable.PrimaryKey == nil {
							fieldIsPrimKey = true
							tableCol.NotNull = true
						} else {
							log.Panicf("Table %s cannot have more then 1 primary key!", currType.Name.Name)
						}
					case "not_null":
						tableCol.NotNull = true
					case "-":
						preventThisField = true
						break PARAMSLOOP
					default:
						tableCol.ColName = param
					}

				}
				if preventThisField {
					continue
				}
			}
			if tableCol.ColName == "" {
				tableCol.ColName = tableCol.FieldName
			}
			if fieldIsPrimKey {
				curTable.PrimaryKey = tableCol
			}
			//Determine field type
			var fieldType string
			switch field.Type.(type) {
			case *ast.Ident:
				fieldType = field.Type.(*ast.Ident).Name
			case *ast.SelectorExpr:
				fieldType = field.Type.(*ast.SelectorExpr).Sel.Name
			}
			//fieldType := field.Type.(*ast.Ident).Name
			fmt.Printf("%s- %s\n", tableCol.FieldName, fieldType)
			//Check for integers
			if strings.Contains(fieldType, "int") {
				tableCol.ColType = "integer"
			} else {
				//Check for other types
				switch fieldType {
				case "string":
					tableCol.ColType = "text"
				case "bool":
					tableCol.ColType = "boolean"
				case "Time":
					tableCol.ColType = "TIMESTAMP"
				default:
					log.Panicf("Field type %s not supported", fieldType)
				}
			}
			tableCol.FieldType = fieldType
			curTable.Columns = append(curTable.Columns, tableCol)
			curTable.StructName = currType.Name.Name

		}
		curTable.generateCreateTable(out)

		fmt.Printf("\tgenerating CRUD methods for %s\n", currType.Name.Name)
		curTable.generateCreate(out)
		curTable.generateQuery(out)
		curTable.generateUpdate(out)
		curTable.generateDelete(out)
	}
}


Analisamos todos os campos da estrutura desejada e começamos a analisar as tags de cada campo. Usando a reflexão, obtemos a tag na qual estamos interessados ​​(afinal, pode haver outras tags no campo, por exemplo, para json). Analisamos o conteúdo da tag e determinamos se o campo é uma chave primária (se mais de uma chave primária for especificada, xingar e interromper a execução), existe um requisito para que o campo seja diferente de zero, precisamos trabalhar com o banco de dados para esse campo e definir nome da coluna se ele foi substituído na tag. Também precisamos determinar o tipo da coluna da tabela com base no tipo do campo da estrutura. Há um conjunto finito de tipos de campos, geraremos apenas para tipos básicos, reduziremos todas as linhas ao tipo de campo TEXT, embora, em geral, você possa adicionar a definição do tipo de coluna às tags para poder configurar com mais precisão. Por outro lado,ninguém se preocupa em criar a tabela desejada no banco de dados antecipadamente ou em corrigir a criada automaticamente.

Após analisar a estrutura, iniciamos o método para criar o código para a função de criação de tabela e os métodos para criar as funções Criar, Consultar, Atualizar e Excluir. Preparamos uma expressão SQL para cada função e uma ligação a ser executada. Não me incomodei com o tratamento de erros, apenas forneci o erro do driver do banco de dados. Para geração de código, é conveniente usar modelos da biblioteca de texto / modelo. Com a ajuda deles, você pode obter um código muito mais suportável e previsível (o código é visível imediatamente, mas não manchado pelo código do gerador).

Criação de tabela

func (tableD *TableInfo) generateCreateTable(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") createTable(db *sql.DB) (error) {\n")
	var resSQLq = fmt.Sprintf("\tsqlQ := `CREATE TABLE %s (\n", tableD.TableName)
	for _, col := range tableD.Columns {
		colSQL := col.ColName + " " + col.ColType
		if col.NotNull {
			colSQL += " NOT NULL"
		}
		if col == tableD.PrimaryKey {
			colSQL += " AUTO_INCREMENT"
		}
		colSQL += ",\n"
		resSQLq += colSQL
	}
	if tableD.PrimaryKey != nil {
		resSQLq += fmt.Sprintf("PRIMARY KEY (%s)\n", tableD.PrimaryKey.ColName)
	}
	resSQLq += ")`\n"
	fmt.Fprint(out, resSQLq)
	fmt.Fprint(out, "\t_, err := db.Exec(sqlQ)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n")
	fmt.Fprint(out, "\t return nil\n}\n\n")
	return nil
}


Adicionar registro


	fmt.Fprint(out, "func (in *"+tableD.StructName+") Create(db *sql.DB) (error) {\n")
	var columns, valuePlaces, valuesListParams string
	for _, col := range tableD.Columns {
		if col == tableD.PrimaryKey {
			continue
		}
		columns += "`" + col.ColName + "`,"
		valuePlaces += "?,"
		valuesListParams += "in." + col.FieldName + ","
	}
	columns = columns[:len(columns)-1]
	valuePlaces = valuePlaces[:len(valuePlaces)-1]
	valuesListParams = valuesListParams[:len(valuesListParams)-1]

	resSQLq := fmt.Sprintf("\tsqlQ := \"INSERT INTO %s (%s) VALUES (%s);\"\n",
		tableD.TableName,
		columns,
		valuePlaces)
	fmt.Fprintln(out, resSQLq)
	fmt.Fprintf(out, "result, err := db.Exec(sqlQ, %s)\n", valuesListParams)
	fmt.Fprintln(out, `if err != nil {
		return err
	}`)
	//Setting id if we have primary key
	if tableD.PrimaryKey != nil {
		fmt.Fprintf(out, `lastId, err := result.LastInsertId()
		if err != nil {
			return nil
		}`)
		fmt.Fprintf(out, "\nin.%s = %s(lastId)\n", tableD.PrimaryKey.FieldName, tableD.PrimaryKey.FieldType)
	}
	fmt.Fprintln(out, "return nil\n}\n\n")
	//in., _ := result.LastInsertId()`)
	return nil
}


Recuperando Registros de uma Tabela

func (tableD *TableInfo) generateQuery(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") Query(db *sql.DB) ([]*"+tableD.StructName+", error) {\n")

	fmt.Fprintf(out, "\tsqlQ := \"SELECT * FROM %s;\"\n", tableD.TableName)
	fmt.Fprintf(out, "rows, err := db.Query(sqlQ)\n")
	fmt.Fprintf(out, "results := make([]*%s, 0)\n", tableD.StructName)
	fmt.Fprintf(out, `for rows.Next() {`)
	fmt.Fprintf(out, "\t tempR := &%s{}\n", tableD.StructName)
	var valuesListParams string
	for _, col := range tableD.Columns {
		valuesListParams += "&tempR." + col.FieldName + ","
	}
	valuesListParams = valuesListParams[:len(valuesListParams)-1]

	fmt.Fprintf(out, "\terr = rows.Scan(%s)\n", valuesListParams)
	fmt.Fprintf(out, `if err != nil {
		return nil, err
		}`)
	fmt.Fprintf(out, "\n\tresults = append(results, tempR)")
	fmt.Fprintf(out, `}
		return results, nil
	}`)
	fmt.Fprintln(out, "")
	fmt.Fprintln(out, "")
	return nil
}


Atualização de registro (funciona por chave primária)

func (tableD *TableInfo) generateUpdate(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") Update(db *sql.DB) (error) {\n")
	var updVals, valuesListParams string
	for _, col := range tableD.Columns {
		if col == tableD.PrimaryKey {
			continue
		}
		updVals += "`" + col.ColName + "`=?,"
		valuesListParams += "in." + col.FieldName + ","
	}
	updVals = updVals[:len(updVals)-1]
	valuesListParams += "in." + tableD.PrimaryKey.FieldName

	resSQLq := fmt.Sprintf("\tsqlQ := \"UPDATE %s SET %s WHERE %s = ?;\"\n",
		tableD.TableName,
		updVals,
		tableD.PrimaryKey.ColName)
	fmt.Fprintln(out, resSQLq)
	fmt.Fprintf(out, "_, err := db.Exec(sqlQ, %s)\n", valuesListParams)
	fmt.Fprintln(out, `if err != nil {
		return err
	}`)

	fmt.Fprintln(out, "return nil\n}\n\n")
	//in., _ := result.LastInsertId()`)
	return nil
}


Excluir um registro (funciona por chave primária)

func (tableD *TableInfo) generateDelete(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") Delete(db *sql.DB) (error) {\n")
	fmt.Fprintf(out, "sqlQ := \"DELETE FROM %s WHERE id = ?\"\n", tableD.TableName)

	fmt.Fprintf(out, "_, err := db.Exec(sqlQ, in.%s)\n", tableD.PrimaryKey.FieldName)

	fmt.Fprintln(out, `if err != nil {
		return err
	}
	return nil
}`)
	fmt.Fprintln(out)
	return nil
}


O início do gerador de código resultante é realizado pela execução usual, passamos o caminho para o arquivo para o qual você deseja gerar o código na flag -name. Como resultado, obtemos o arquivo com o sufixo _dbe, no qual está o código gerado. Para testes, crie métodos para a seguinte estrutura:


// dbe:{"table": "users"}
type User struct {
	ID       int    `dbe:"id,primary_key"`
	Login    string `dbe:"login,not_null"`
	Email    string
	Level    uint8
	IsActive bool
	UError   error `dbe:"-"`
}

O código resultante

package main

import "database/sql"

func (in *User) createTable(db *sql.DB) error {
	sqlQ := `CREATE TABLE users (
	id integer NOT NULL AUTO_INCREMENT,
	login text NOT NULL,
	Email text,
	Level integer,
	IsActive boolean,
	PRIMARY KEY (id)
	)`
	_, err := db.Exec(sqlQ)
	if err != nil {
		return err
	}
	return nil
}

func (in *User) Create(db *sql.DB) error {
	sqlQ := "INSERT INTO users (`login`,`Email`,`Level`,`IsActive`) VALUES (?,?,?,?);"

	result, err := db.Exec(sqlQ, in.Login, in.Email, in.Level, in.IsActive)
	if err != nil {
		return err
	}
	lastId, err := result.LastInsertId()
	if err != nil {
		return nil
	}
	in.ID = int(lastId)
	return nil
}

func (in *User) Query(db *sql.DB) ([]*User, error) {
	sqlQ := "SELECT * FROM users;"
	rows, err := db.Query(sqlQ)
	results := make([]*User, 0)
	for rows.Next() {
		tempR := &User{}
		err = rows.Scan(&tempR.ID, &tempR.Login, &tempR.Email, &tempR.Level, &tempR.IsActive)
		if err != nil {
			return nil, err
		}
		results = append(results, tempR)
	}
	return results, nil
}

func (in *User) Update(db *sql.DB) error {
	sqlQ := "UPDATE users SET `login`=?,`Email`=?,`Level`=?,`IsActive`=? WHERE id = ?;"

	_, err := db.Exec(sqlQ, in.Login, in.Email, in.Level, in.IsActive, in.ID)
	if err != nil {
		return err
	}
	return nil
}

func (in *User) Delete(db *sql.DB) error {
	sqlQ := "DELETE FROM users WHERE id = ?"
	_, err := db.Exec(sqlQ, in.ID)
	if err != nil {
		return err
	}
	return nil
}


Para testar a operação do código gerado, crie um objeto com dados arbitrários, crie uma tabela para ele (se a tabela existir no banco de dados, um erro será retornado). Depois de colocar esse objeto na tabela, leia todos os campos da tabela, atualize os valores de nível e exclua o objeto.

Chame os métodos resultantes

var err error
db, err := sql.Open("mysql", DSN)
if err != nil {
	fmt.Println("Unable to connect to DB", err)
	return
}
err = db.Ping()
if err != nil {
	fmt.Println("Unable to ping BD")
	return
}
newUser := &User{
	Login:    "newUser",
	Email:    "new@test.com",
	Level:    0,
	IsActive: false,
	UError:   nil,
}

err = newUser.createTable(db)
if err != nil {
	fmt.Println("Error creating table.", err)

}
err = newUser.Create(db)
if err != nil {
	fmt.Println("Error creating user.", err)
	return
}

nU := &User{}
dbUsers, err := nU.Query(db)
if err != nil {
	fmt.Println("Error selecting users.", err)
	return
}
fmt.Printf("From table users selected %d fields", len(dbUsers))
var DBUser *User
for _, user := range dbUsers {
	fmt.Println(user)
	DBUser = user
}
DBUser.Level = 2
err = DBUser.Update(db)
if err != nil {
	fmt.Println("Error updating users.", err)
	return
}
err = DBUser.Delete(db)
if err != nil {
	fmt.Println("Error deleting users.", err)
	return
}


Na implementação atual, a funcionalidade do cliente no banco de dados é muito limitada:

  • somente MySQL é suportado;
  • Nem todos os tipos de campo são suportados.
  • não há filtragem e limites para SELECT.

No entanto, a correção de bugs já está além do escopo de analisar o código-fonte Go e gerar um novo código com base nele.

Usar um gerador de código nesse cenário permitirá alterar os campos e tipos de estruturas usadas no aplicativo em um único local; não é necessário lembrar de fazer alterações no código para interagir com o banco de dados; você só precisa executar o gerador de código sempre. Essa tarefa poderia ser resolvida com a ajuda da reflexão, mas isso teria afetado o desempenho.

O gerador de código-fonte e um exemplo do código gerado postado no Github .

All Articles