Метапрограммирование на Go


Большинство современных сайтов реализуют некий MV*-фреймворк как формально, так и неформально. Если вы пишете много кода, скорее всего, вы пишете много моделей снова и снова. Они в основном похожи по структуре и отличаются только деталями схемы. Вы определяете SQL-схему, создаете структуры и соединяете некоторые базовые CRUD API. Затем вы подправляете всё это по мере развития логики приложения. Разве не здорово было бы автоматизировать что-то из этого, чтобы сократить и время, и количество ошибок? В этой статье мы сделаем именно это. Мы рассмотрим метапрограммирование в Go для автоматического создания простого CRUD API, основанного на определениях таблиц SQL.

Введение

Метапрограммирование —  это, в сущности, написание программы для написания программы. Таким образом, вы на один уровень абстракции выше конкретной программы и решаете задачу для класса программ. Известный пример этого  —  генераторы Rails (Ruby on Rails). Запустив new test rails, вы создадите полнофункциональный проект под названием test. Метапрограммирование обычно занимает немного больше времени, но в конце концов у вас появляется инструмент, который вы можете повторно использовать для решения схожих проблем.

Обычно я использую Postgres, поэтому хотел бы автоматически преобразовать набор определений таблиц SQL Create Table в код на Go. Синтаксис Postgres для CREATE TABLE невероятно богат, хотя это всего лишь одно выражение. К счастью, нам не нужно разбирать весь синтаксис, чтобы достичь цели. Чтобы построить CRUD API, нам нужно распознавать:

  • Существующее выражение.
  • Имя таблицы.
  • Имена колонок.
  • Типы данных.
  • Кажется, много? На самом деле это позволит игнорировать большинство необязательных частей синтаксиса CREATE TABLE. Мы можем искать, что хотим, игнорируя всё остальное.

    Создадим простой компилятор для просеивания определений таблиц SQL, сохраняя необходимые части, чтобы генерировать наш новый код из найденных выражений. Компилятор будет состоять из трех основных частей: лексера, парсера и генератора. Грамматика основана на документации Postgres и выглядит так:

    CREATE [some_stuff]* TABLE [IF NOT EXISTS] table_name ( column_name data_type [some_stuff]* [, ...] ) [some_stuff]*;

    Наш код должен пропускать некоторый код, но игнорировать остальной синтаксис SQL: например, мы должны разобрать опцию [IF NOT EXISTS]. Если мы будем рассматривать ее как произвольный текст, то можем пропустить table_name. Кроме того, для простоты мы не будем поддерживать table_constraints и подобные операторы в списке столбцов: это вынудит нас к гораздо более сложному синтаксическому анализу. Выражение должно заканчиваться на ;. Мы будем поддерживать все основные типы данных и переводить их в Go следующим образом:

    SQL GO ----------------|------- BOOLEAN bool BOOL bool CHAR(n) string VARCHAR(n) string TEXT string SMALLINT int16 INT int32 INTEGER int32 BIGINT int64 SMALLSERIAL int16 SERIAL int32 BIGSERIAL int64 FLOAT(n) float64 REAL float32 FLOAT8 float32 DECIMAL float64 NUMERIC float64 NUMERIC(p,s) float64 DOUBLE PRECISION float64 DATE time.Time TIME time.Time TIMESTAMPTZ time.Time TIMESTAMP time.Time INTERVAL time.Time JSON []byte JSONB []byte UUID string
    • func CreateTableFoo(db *sql.DB) (err error){}
    • type Foo struct{}
    • func (foo *Foo) CreateFoo(db *sql.DB) (result Foo, err error){}
    • func (foo *Foo) RetrieveFoo(db *sql.DB) (result Foo, err error){}
    • func (foo *Foo) RetrieveAllFoo(db *sql.DB) (foo []Foo, err error){}
    • func (foo *Foo) UpdateFoo(db *sql.DB) (result Foo, err error){}
    • func (foo *Foo) DeleteFoo(db *sql.DB) (err error){}
    • func DeleteAllFoo(db *sql.DB) (err error){}

    Лексический анализатор

    Лексический анализ  —  это процесс обнаружения лексем в потоке символов. Это так же просто, как поиск слов, разделенных пробелами, или более распространенное распознавание определенных ключевых слов и идентификаторов. В Go есть пакеты для сканирования и лексического анализа. Я выбрал хорошо документированный пакет Lexmachine Тима Хендерсона.

    Используя отличный пример Тима Dot lexer в качестве шаблона, мы можем построить лексер для нашего упрощенного подмножества SQL. Ключевые слова и литералы довольно просты: CREATE, TABLE, IF, NOT, EXISTS и наш список статических типов данных. 

    Опять же, мы должны обнаружить опцию IF NOT EXISTS, чтобы устранить двусмысленность table_name. Литералы — это просто (),;.

    Идентификаторы немного сложнее. Они включают в себя типы CHAR(n), VARCHAR(n), FLOAT(n), NUMERIC(p,s), ID(ID), table_name, column_name. Lexmachine использует регулярные выражения для идентификаторов. Вот наш код:

    VARCHAR(n): [vV][aA][rR][cC][hH][aA][rR]\([0-9]+\) CHAR(n): [cC][hH][aA][rR]\([0-9]+\) FLOAT(n): [fF][lL][oO][aA][tT]\([0-9]+\) NUMERIC(p,s): [nN][uU][mM][eE][rR][iI][cC]\([0-9]+,[0-9]+\) ID(ID): ([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|\$)* \(([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|\$)*\) ID: ([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|\$)*

    Обратите внимание, что Lexmachine пока не поддерживает такие режимы, как (?i), что объясняет приведенное выше регулярное выражение. ID требуется, чтобы разрешить ссылки, которые в противном случае были бы ошибками из-за скобок. ID может использоваться как для table_name, так и для column_name. Лексер пропустит любой пробел. Вот и всё, он готов.

    metaapi/metasql/lexer.go: // Основано на https://hackthology.com/writing-a-lexer-in-go-with-lexmachine.html package metasqlimport ( "strings"lex "github.com/timtadh/lexmachine" "github.com/timtadh/lexmachine/machines" )var Literals []string var Keywords []string var Tokens []string var TokenIds map[string]int var Lexer *lex.Lexer // Вызывается при инициализации. Создаёт лексер и списки токенов. func init() { initTokens() var err error Lexer, err = initLexer() if err != nil { panic(err) } } func initTokens() { Tokens = []string{ "VARCHARID", "CHARID", "FLOATID", "NUMERICID", "REFID", "ID", } Keywords = []string{ "CREATE", "TABLE", "IF", "NOT", "EXISTS", "BOOLEAN", "BOOL", "TEXT", "SMALLINT", "INTEGER", "BIGINT", "INT", "SMALLSERIAL", "BIGSERIAL", "SERIAL", "REAL", "FLOAT8", "DECIMAL", "NUMERIC", "DOUBLE", "PRECISION", "DATE", "TIMESTAMPTZ", "TIMESTAMP", "TIME", "INTERVAL", "JSONB", "JSON", "UUID", } Literals = []string{ "(", ")", ",", ";", } Tokens = append(Tokens, Keywords...) Tokens = append(Tokens, Literals...) TokenIds = make(map[string]int) for i, tok := range Tokens { TokenIds[tok] = i } } // Создаёт объект лексера и компилирует недетерминизированный конечный автомат. func initLexer() (*lex.Lexer, error) { lexer := lex.NewLexer()for _, lit := range Literals { r := "\\" + strings.Join(strings.Split(lit, ""), "\\") lexer.Add([]byte(r), token(lit)) } for _, name := range Keywords { lexer.Add([]byte(strings.ToLower(name)), token(name)) } lexer.Add([]byte(`[vV][aA][rR][cC][hH][aA][rR]\([0-9]+\)`), token("VARCHARID")) lexer.Add([]byte(`[cC][hH][aA][rR]\([0-9]+\)`), token("CHARID")) lexer.Add([]byte(`[fF][lL][oO][aA][tT]\([0-9]+\)`), token("FLOATID")) lexer.Add([]byte(`[nN][uU][mM][eE][rR][iI][cC]\([0-9]+,[0-9]+\)`), token("NUMERICID")) lexer.Add([]byte(`([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|\$)*\(([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|\$)*\)`), token("REFID")) lexer.Add([]byte(`([a-z]|[A-Z]|_|#|@)([a-z]|[A-Z]|[0-9]|_|#|@|\$)*`), token("ID")) lexer.Add([]byte("( |\t|\n|\r)+"), skip) err := lexer.Compile() if err != nil { return nil, err } return lexer, nil } // lex.Action - функция, которая пропускает совпадения. func skip(*lex.Scanner, *machines.Match) (interface{}, error) { return nil, nil } // lex.Action конструирует токен данного типа по имени типа. func token(name string) lex.Action { return func(s *lex.Scanner, m *machines.Match) (interface{}, error) { return s.Token(TokenIds[name], string(m.Bytes), m), nil } }

    Парсер

    Синтаксический анализ  —  это процесс обнаружения частей определенной грамматики в потоке лексем, созданных лексером.Создадим простой синтаксический анализатор для нашей ограниченной грамматики SQL. Для сложной грамматики мы склонялись бы к реальному анализатору, основанному на рекурсивном спуске или сгенерированному генератором синтаксических анализаторов. Наша задача достаточно проста, чтобы конечная машина состояний справилась с ней. 

    Простая машина состояний следует шаблону: если я нахожусь в состоянии CurState и вижу какой-то вход, то перемещаюсь в следующее состояние и выполняю какое-то действие. И так до достижения некоторого терминального состояния, обычно EOF. В нашем случае вводом будет тип токена, а действием — некоторая функция, которую мы вызываем для сохранения табличных данных или их игнорирования. Ниже таблица состояний. Вы заметите, что большая ее часть связана с обнаружением различных типов данных:

    Cur Next State Input State Action --------------------------------------- 0 Error 0 error_state 1 CREATE 2 create_table 2 TABLE 3 nop 2 ID 2 some_stuff 3 IF 4 nop 4 NOT 5 nop 5 EXISTS 3 nop 3 ID 6 table_name 6 ( 7 nop 7 ID 8 column_name 7 UUID 8 column_name 8 BOOLEAN 9 data_type 8 BOOL 9 data_type 8 CHARID 9 data_type 8 VARCHARID 9 data_type 8 TEXT 9 data_type 8 SMALLINT 9 data_type 8 INT 9 data_type 8 INTEGER 9 data_type 8 BIGINT 9 data_type 8 SMALLSERIAL 9 data_type 8 SERIAL 9 data_type 8 BIGSERIAL 9 data_type 8 FLOATID 9 data_type 8 REAL 9 data_type 8 FLOAT8 9 data_type 8 DECIMAL 9 data_type 8 NUMERIC 9 data_type 8 NUMERICID 9 data_type 8 DOUBLE 10 nop 10 PRECISION 9 data_type 8 DATE 9 data_type 8 TIME 9 data_type 8 TIMESTAMPTZ 9 data_type 8 TIMESTAMP 9 data_type 8 INTERVAL 9 data_type 8 JSON 9 data_type 8 JSONB 9 data_type 8 UUID 9 data_type 9 , 7 nop 9 ) 11 nop 9 REFID 9 some_stuff 9 NOT 9 some_stuff 9 ID 9 some_stuff 11 ; 1 end_table 11 ID 11 some_stuff

    В дополнение к пониманию и применению нашей грамматики синтаксический анализатор должен также захватывать все данные, необходимые для генерации кода. В реальном компиляторе для этого можно построить абстрактное синтаксическое дерево, которое затем анализируется генератором кода. В нашем случае мы просто построим список таблиц и их имена столбцов и типы, используемые для генератора. Наша машина состояний сама по себе является просто картой Go, принимающей строку в качестве входных данных и возвращающей структуру следующего состояния и действия (метода) для запуска. Строка карты содержит как текущее состояние, так и входной токен, соединенные вместе, чтобы сформировать одну строку для карты. "CurState, InToken": {NextState, FunctionToCall}.

    metaapi/metasql/parse.go: package metasqlimport ( "errors" "fmt" "log" lex "github.com/timtadh/lexmachine" ) type Column struct { Name string Type string } type Table struct { Name string Query string Columns []Column } type StateMachine struct { FName string CurState int Tables []Table } type NextAction struct { State int Fn func(*StateMachine, *lex.Token) } func getColumn(sm *StateMachine) *Column { if len(sm.Tables) > 0 { table := &(sm.Tables[len(sm.Tables)-1]) if len(table.Columns) > 0 { return &(table.Columns[len(table.Columns)-1]) } else { return nil } } else { return nil }} func InitState(fname string) *StateMachine { sm := new(StateMachine) sm.FName = fname sm.CurState = 1 return sm } func error_state(sm *StateMachine, token *lex.Token) { //состояние не найдено log.Fatal("Error in SQL Syntax!") } func nop(sm *StateMachine, token *lex.Token) { //nop } func create_table(sm *StateMachine, token *lex.Token) { sm.Tables = append(sm.Tables, Table{}) } func table_name(sm *StateMachine, token *lex.Token) { if len(sm.Tables) > 0 { sm.Tables[len(sm.Tables)-1].Name = string(token.Lexeme) } } func column_name(sm *StateMachine, token *lex.Token) { if len(sm.Tables) > 0 { table := &(sm.Tables[len(sm.Tables)-1]) table.Columns = append(table.Columns, Column{}) table.Columns[len(table.Columns)-1].Name = string(token.Lexeme) } } func data_type(sm *StateMachine, token *lex.Token) { column := getColumn(sm) column.Type = Tokens[token.Type] } func some_stuff(sm *StateMachine, token *lex.Token) { //nop } func end_table(sm *StateMachine, token *lex.Token) { //nop } func appendQuery(sm *StateMachine, st string) { if len(sm.Tables) > 0 { (&(sm.Tables[len(sm.Tables)-1])).Query += st + " " } } func printQuery(sm *StateMachine) { if len(sm.Tables) > 0 { fmt.Println("query: ", (&(sm.Tables[len(sm.Tables)-1])).Query, " <<") } } func ProcessState(sm *StateMachine, token *lex.Token) (err error) { //Машина состояний, формат: //"CurState, InToken": {NextState, FunctionToCall} stateMap := map[string]NextAction{ "Error": {0, error_state}, "1,CREATE": {2, create_table}, "2,TABLE": {3, nop}, "2,ID": {2, some_stuff}, "3,IF": {4, nop}, "4,NOT": {5, nop}, "5,EXISTS": {3, nop}, "3,ID": {6, table_name}, "6,(": {7, nop}, "7,ID": {8, column_name}, "7,UUID": {8, column_name}, "8,BOOLEAN": {9, data_type}, "8,BOOL": {9, data_type}, "8,CHARID": {9, data_type}, "8,VARCHARID": {9, data_type}, "8,TEXT": {9, data_type}, "8,SMALLINT": {9, data_type}, "8,INT": {9, data_type}, "8,INTEGER": {9, data_type}, "8,BIGINT": {9, data_type}, "8,SMALLSERIAL": {9, data_type}, "8,SERIAL": {9, data_type}, "8,BIGSERIAL": {9, data_type}, "8,FLOATID": {9, data_type}, "8,REAL": {9, data_type}, "8,FLOAT8": {9, data_type}, "8,DECIMAL": {9, data_type}, "8,NUMERIC": {9, data_type}, "8,NUMERICID": {9, data_type}, "8,DOUBLE": {10, nop}, "10,PRECISION": {9, data_type}, "8,DATE": {9, data_type}, "8,TIME": {9, data_type}, "8,TIMESTAMPTZ": {9, data_type}, "8,TIMESTAMP": {9, data_type}, "8,INTERVAL": {9, data_type}, "8,JSON": {9, data_type}, "8,JSONB": {9, data_type}, "8,UUID": {9, data_type}, "9,,": {7, nop}, "9,)": {11, nop}, "9,REFID": {9, some_stuff}, "9,NOT": {9, some_stuff}, "9,ID": {9, some_stuff}, "11,;": {1, end_table}, "11,ID": {11, some_stuff}, } mapStr := fmt.Sprintf("%d,%s", sm.CurState, Tokens[token.Type]) nextState := stateMap[mapStr] //отображение нулей во все поля структуры, если они не найдены if nextState.State == 0 { nextState = stateMap["Error"] printQuery(sm) err = errors.New("Syntax Error: " + Tokens[token.Type]) return } sm.CurState = nextState.State nextState.Fn(sm, token) appendQuery(sm, string(token.Lexeme)) return nil }

    Генератор

    Генератор отвечает за генерацию кода нашего “компилятора”. Он принимает внутреннее представление таблиц SQL, созданное на предыдущих стадиях, и генерирует CRUD API. Если все пойдет хорошо, полученный код должен быть готов к компиляции и запуску в другом проекте.

    Подход к созданию генератора такой: мы начинаем с некоторого известного и работающего кода, представляющего нашу цель  —  API. Мы переименуем этот код в файл txt, подаваемый на вход генератору в качестве шаблона, и медленно преобразуем его до полного соответствия шаблону. Мы напишем соответствующие методы приемника в generate.go и пройдём по шаблону. При запуске генератора необходимо воссоздать исходную цель. Этот процесс позволяет действительно легко сравнивать генерируемый код рядом с исходной целью и исправлять любые ошибки.

    В моём случае целевым API будет todo CRUD API из проекта govueintro. todo.go, которые я буду конвертировать, чтобы использовать автоматически сгенерированный todo_generated.go из метаапи. crud.txt ниже начинали свою жизнь как todo_generated.go, и я итеративно преобразовал его в crud.txt. Я заменил разделы методами приемника в generate.go. crud.txt сейчас выглядит уродливо, но поверьте мне, итеративный процесс преобразования прост. generate.go просто использует шаблоны go, чтобы сложить определенные табличные данные в универсальный crud.txt

    metaapi/metasql/generate.go: package metasqlimport ( "errors" "io/ioutil" "os" "strconv" "strings" "text/template" ) // Generate предполагает, что первичный идентификатор находится в первом столбце (индекс 0) func Generate(sm *StateMachine, txtFile string) error { if sm.FName == "" { return (errors.New("No file name")) } dot := strings.Index(sm.FName, ".") var prefix string if dot > 0 { prefix = sm.FName[:dot] } else { prefix = sm.FName } dat, err := ioutil.ReadFile("./" + txtFile) if err != nil { return err } tt := template.Must(template.New(prefix).Parse(string(dat))) dest := prefix + "_generated.go" file, err := os.Create(dest) if err != nil { return err } tt.Execute(file, sm) file.Close() return nil } //======== строковые помощники // должны использовать: https://github.com/blakeembrey/pluralize func singularize(s string) string { if strings.HasSuffix(strings.ToLower(s), "s") { return strings.TrimSuffix(strings.ToLower(s), "s") } else { return strings.ToLower(s) } } func capitalize(s string) string { return strings.Title(s) } func lowerize(s string) string { return strings.ToLower(s) } //преобразует submitted_at в SubmittedAt и не только func camelize(s string) string { return strings.ReplaceAll(strings.Title(strings.ReplaceAll(strings.ToLower(s), "_", " ")), " ", "") } func comma(i int, length int) string { if i < (length - 1) { return "," } else { return "" } } //======== template methodsfunc (sm *StateMachine) Package() string { return os.Getenv("GOPACKAGE") } // Написание для расширения func (sm *StateMachine) Import() string {var s string var includeTime boolincludeTime = false for _, table := range sm.Tables { for _, column := range table.Columns { switch column.Type { case "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL": includeTime = true default: } } } s += "import (\n\t\"database/sql\"\n" if includeTime { s += "\t\"time\"\n" } s += ")" return s } func (table Table) SingName() string { return singularize(table.Name) } func (table Table) CapName() string { return capitalize(lowerize(table.Name)) } func (table Table) CapSingName() string { return capitalize(singularize(table.Name)) } func (table Table) DropTableStatement() string { var s string s += "(\"DROP TABLE IF EXISTS " + table.Name + "\")" return s } func (table Table) CreateTableStatement() string { var s string s += "(`" + table.Query + "`)" return s } func (table Table) StructFields() string {var typeMap = map[string]string{ "BOOLEAN": "bool", "BOOL": "bool", "CHARID": "string", "VARCHARID": "string", "TEXT": "string", "SMALLINT": "int16", "INT": "int32", "INTEGER": "int32", "BIGINT": "int64", "SMALLSERIAL": "int16", "SERIAL": "int32", "BIGSERIAL": "int64", "FLOATID": "float64", "REAL": "float32", "FLOAT8": "float32", "DECIMAL": "float64", "NUMERIC": "float64", "NUMERICID": "float64", "PRECISION": "float64", "DATE": "time.Time", "TIME": "time.Time", "TIMESTAMPTZ": "time.Time", "TIMESTAMP": "time.Time", "INTERVAL": "time.Time", "JSON": "[]byte", "JSONB": "[]byte", "UUID": "string", } var s string for _, column := range table.Columns { s += "\t" + camelize(column.Name) s += " " + typeMap[column.Type] s += "`xml:\"" + camelize(column.Name) + "\" json:\"" + lowerize(camelize(column.Name)) + "\"`" s += "\n" } return s } func (table Table) Star() string { var s string for i, column := range table.Columns { s += " " + column.Name s += comma(i, len(table.Columns)) } return s } func (table Table) ScanAll() string {var s string s += ".Scan(" for i, column := range table.Columns { s += " &result." + camelize(column.Name) s += comma(i, len(table.Columns)) } s += ")" return s } func (table Table) CreateStatement() string { var s string s += "(\"INSERT INTO " + table.Name + " ("for i, column := range table.Columns { if i == 0 { continue } s += " " + column.Name s += comma(i, len(table.Columns)) } s += ") VALUES (" index := 1 for i, _ := range table.Columns { if i == 0 { continue } s += "$" s += strconv.Itoa(index) s += comma(i, len(table.Columns)) index++ } s += ") RETURNING" for i, column := range table.Columns { s += " " + column.Name s += comma(i, len(table.Columns)) } s += "\")" return s } func (table Table) CreateQuery() string { var s string s += "(" for i, column := range table.Columns { if i == 0 { continue } s += " " + table.SingName() + "." + camelize(column.Name) s += comma(i, len(table.Columns)) } s += ")" s += table.ScanAll() return s } func (table Table) RetrieveStatement() string { var s string s += "(\"SELECT" + table.Star() + " FROM " + table.Name + " WHERE (" index := 1 for i, column := range table.Columns { if i == 0 { s += column.Name + " = $" + strconv.Itoa(index) s += ")\", " + table.SingName() + "." + camelize(column.Name) + ")" } break } s += table.ScanAll() return s } func (table Table) RetrieveAllStatement() string { var s string s += "(\"SELECT" + table.Star() + " FROM " + table.Name + " ORDER BY " for i, column := range table.Columns { if i == 0 { s += column.Name } break } s += " DESC\")" return s } func (table Table) UpdateStatement() string { var s string s += "(\"UPDATE " + table.Name + " SET" index := 2 for i, column := range table.Columns { if i == 0 { continue } s += " " + column.Name + " = $" + strconv.Itoa(index) index++ s += comma(i, len(table.Columns)) } s += " WHERE (" index = 1 for i, column := range table.Columns { if i == 0 { s += column.Name + " = $" + strconv.Itoa(index) s += ") RETURNING" } break } for i, column := range table.Columns { s += " " + column.Name s += comma(i, len(table.Columns)) } s += "\")" return s } func (table Table) UpdateQuery() string { var s string s += "(" for i, column := range table.Columns { s += " " + table.SingName() + "." + camelize(column.Name) s += comma(i, len(table.Columns)) } s += ")" s += table.ScanAll() return s } func (table Table) DeleteStatement() string { var s string s += "(\"DELETE FROM " + table.Name + " WHERE (" index := 1 for i, column := range table.Columns { if i == 0 { s += column.Name + " = $" + strconv.Itoa(index) } break } s += ")\")" return s } func (table Table) DeleteQuery() string { var s string for i, column := range table.Columns { if i == 0 { s += "(" + table.SingName() + "." + camelize(column.Name) + ")" } break } return s } func (table Table) DeleteAllStatement() string { var s string s += "(\"DELETE FROM " + table.Name + "\")" return s } metaapi/metasql/crud.txt //Сгенерировано с помощью MetaApi https://github.com/exyzzy/metaapi package {{ .Package }}{{ .Import }}{{ range $index, $table := .Tables }} // CREATE TABLE func CreateTable{{ $table.CapName }}(db *sql.DB) (err error) { _, err = db.Exec{{ $table.DropTableStatement }} if err != nil { return } _, err = db.Exec{{ $table.CreateTableStatement }} return } // Структура type {{ $table.CapSingName }} struct { {{ $table.StructFields }} } // Create func ({{ $table.SingName }} *{{ $table.CapSingName }}) Create{{ $table.CapSingName }}(db *sql.DB) (result {{ $table.CapSingName }}, err error) { stmt, err := db.Prepare{{ $table.CreateStatement }} if err != nil { return } defer stmt.Close() err = stmt.QueryRow{{ $table.CreateQuery }} return } // Извлечение func ({{ $table.SingName }} *{{ $table.CapSingName }}) Retrieve{{ $table.CapSingName }}(db *sql.DB) (result {{ $table.CapSingName }}, err error) { result = {{ $table.CapSingName }}{} err = db.QueryRow{{ $table.RetrieveStatement }} return }// Извлечение всего func ({{ $table.SingName }} *{{ $table.CapSingName }}) RetrieveAll{{ $table.CapName }}(db *sql.DB) ({{ $table.Name }} []{{ $table.CapSingName }}, err error) { rows, err := db.Query{{ $table.RetrieveAllStatement }} if err != nil { return } for rows.Next() { result := {{ $table.CapSingName }}{} if err = rows{{ $table.ScanAll }}; err != nil { return } {{ $table.Name }} = append({{ $table.Name }}, result) } rows.Close() return }//Update func ({{ $table.SingName }} *{{ $table.CapSingName }}) Update{{ $table.CapSingName }}(db *sql.DB) (result {{ $table.CapSingName }}, err error) { stmt, err := db.Prepare{{ $table.UpdateStatement }} if err != nil { return } defer stmt.Close()err = stmt.QueryRow{{ $table.UpdateQuery }} return }//Delete func ({{ $table.SingName }} *{{ $table.CapSingName }}) Delete{{ $table.CapSingName }}(db *sql.DB) (err error) { stmt, err := db.Prepare{{ $table.DeleteStatement }} if err != nil { return } defer stmt.Close()_, err = stmt.Exec{{ $table.DeleteQuery }} return } //DeleteAll func DeleteAll{{ $table.CapSingName }}s(db *sql.DB) (err error) { stmt, err := db.Prepare{{ $table.DeleteAllStatement}} if err != nil { return } defer stmt.Close()_, err = stmt.Exec() return } {{ end }}

    Короткий main держит все вместе и использует флаги go для передачи имен файлов.

    metaapi/main.go: package mainimport ( "flag" "fmt" "io/ioutil" "log" "strings" "github.com/exyzzy/metaapi/metasql" lex "github.com/timtadh/lexmachine" ) // Включаем печать отладки var DEBUG = falsefunc main() { sqlPtr := flag.String("sql", "", ".sql input file to parse") txtPtr := flag.String("txt", "crud.txt", "go template as .txt file") flag.Parse() sqlFile := strings.ToLower(*sqlPtr) txtFile := strings.ToLower(*txtPtr) if (sqlFile == "") || (!strings.HasSuffix(sqlFile, ".sql")) { log.Fatal("No .sql File") } if (txtFile == "") || (!strings.HasSuffix(txtFile, ".txt")) { log.Fatal("No .txt File") } dat, err := ioutil.ReadFile("./" + sqlFile) if err != nil { log.Fatal(err) } s, err := metasql.Lexer.Scanner([]byte(dat)) if err != nil { log.Fatal(err) } sm := metasql.InitState(sqlFile) for tok, err, eof := s.Next(); !eof; tok, err, eof = s.Next() { if err != nil { log.Fatal(err) } token := tok.(*lex.Token) if DEBUG { fmt.Printf("%-10v | %-12v | %v:%v-%v:%v\n", metasql.Tokens[token.Type], string(token.Lexeme), token.StartLine, token.StartColumn, token.EndLine, token.EndColumn) } err = metasql.ProcessState(sm, token) if err != nil { log.Fatal(err) } } err = metasql.Generate(sm, txtFile) if err != nil { log.Fatal(err) } if DEBUG { fmt.Printf("Table Capture:\n%+v\n", sm) } }

    Компилятор работает?

    Чтобы использовать его, пройдите шаги ниже:

    • Клонируйте и установите проект metaapi.
    • Создайте каталог для нового проекта.
    • Скопируйте crud.txt, ваш sql.todo и опционально файл todo.go в новый проект.
    • Выполните go.generate или вручную запустите metaapi.

    Для своего проекта я использовал такой код:

    metasql/todo.go: //go:generate metaapi -sql=todo.sql -txt=crud.txt package metasql //Посмотрите: todo_generated.go metasql/todo.sql: create table todos ( id integer generated always as identity primary key, updated_at timestamptz, done boolean, title text );

    Давайте запустим его и посмотрим, что получится:

    go get github.com/exyzzy/metaapi go install $GOPATH/src/github.com/exyzzy/metaapi mkdir myproj cd myproj cp $GOPATH/src/github.com/exyzzy/metaapi/metasql/crud.txt . cp $GOPATH/src/github.com/exyzzy/metaapi/metasql/todo.sql . cp $GOPATH/src/github.com/exyzzy/metaapi/metasql/todo.go . go generate

    todo_generated.go создан автоматически:

    //Auto generated with MetaApi https://github.com/exyzzy/metaapi package metasqlimport ( "database/sql" "time" ) //Create Table func CreateTableTodos(db *sql.DB) (err error) { _, err = db.Exec("DROP TABLE IF EXISTS todos") if err != nil { return } _, err = db.Exec(`create table todos ( id integer generated always as identity primary key , updated_at timestamptz , done boolean , title text ) ; `) return }//Структура type Todo struct { Id int32`xml:"Id" json:"id"` UpdatedAt time.Time`xml:"UpdatedAt" json:"updatedat"` Done bool`xml:"Done" json:"done"` Title string`xml:"Title" json:"title"`}//Create func (todo *Todo) CreateTodo(db *sql.DB) (result Todo, err error) { stmt, err := db.Prepare("INSERT INTO todos ( updated_at, done, title) VALUES ($1,$2,$3) RETURNING id, updated_at, done, title") if err != nil { return } defer stmt.Close() err = stmt.QueryRow( todo.UpdatedAt, todo.Done, todo.Title).Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title) return } //Извлечение func (todo *Todo) RetrieveTodo(db *sql.DB) (result Todo, err error) { result = Todo{} err = db.QueryRow("SELECT id, updated_at, done, title FROM todos WHERE (id = $1)", todo.Id).Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title) return } //Извлечение всего func (todo *Todo) RetrieveAllTodos(db *sql.DB) (todos []Todo, err error) { rows, err := db.Query("SELECT id, updated_at, done, title FROM todos ORDER BY id DESC") if err != nil { return } for rows.Next() { result := Todo{} if err = rows.Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title); err != nil { return } todos = append(todos, result) } rows.Close() return } //Update func (todo *Todo) UpdateTodo(db *sql.DB) (result Todo, err error) { stmt, err := db.Prepare("UPDATE todos SET updated_at = $2, done = $3, title = $4 WHERE (id = $1) RETURNING id, updated_at, done, title") if err != nil { return } defer stmt.Close() err = stmt.QueryRow( todo.Id, todo.UpdatedAt, todo.Done, todo.Title).Scan( &result.Id, &result.UpdatedAt, &result.Done, &result.Title) return } //Delete func (todo *Todo) DeleteTodo(db *sql.DB) (err error) { stmt, err := db.Prepare("DELETE FROM todos WHERE (id = $1)") if err != nil { return } defer stmt.Close() _, err = stmt.Exec(todo.Id) return } //DeleteAll func DeleteAllTodos(db *sql.DB) (err error) { stmt, err := db.Prepare("DELETE FROM todos") if err != nil { return } defer stmt.Close() _, err = stmt.Exec() return }

    Миссия выполнена. Теперь у нас есть инструмент, который может генерировать базовый CRUD API для произвольной таблицы SQL. Мы также можем расширить или настроить лексер, парсер, генератор и шаблон для создания новых файлов. Не нравится мой API? Клонируйте репозиторий, измените шаблон и сделайте свой собственный CRUD!


    Перевод статьи Eric Lang: Metaprogram in Go


    Поделиться статьей:


    Вернуться к статьям

    Комментарии

      Ничего не найдено.