github.com/sttk/sabi@v0.5.0/doc.go (about) 1 // Copyright (C) 2022-2023 Takayuki Sato. All Rights Reserved. 2 // This program is free software under MIT License. 3 // See the file LICENSE in this distribution for more details. 4 5 /* 6 Package github.com/sttk/sabi is a small framework to separate logic parts and data accesses parts for Golang applications. 7 8 # Logic 9 10 A logic is implemented as a function. 11 This function takes only an argument, dax which is an interface and collects data access methods used in this function. 12 Also, this function returns only a sabi.Err value which indicates that this function succeeds or not. 13 Since the dax hides details of data access procedures, only logical procedure appears in this function. 14 In this logic part, it's no concern where a data comes from or goes to. 15 16 For example, in the following code, GreetLogic is a logic function and GreetDax is a dax interface. 17 18 import "github.com/sttk/sabi" 19 20 type GreetDax interface { 21 UserName() (string, sabi.Err) 22 Say(greeting string) sabi.Err 23 } 24 25 type ( // possible error reasons 26 NoName struct {} 27 FailToOutput struct {Text string} 28 ) 29 30 func GreetLogic(dax GreetDax) sabi.Err { 31 name, err := dax.UserName() 32 if !err.IsOk() { 33 return err 34 } 35 return dax.Say("Hello, " + name) 36 } 37 38 In GreetLogic function, there are no codes for getting a user name and output a greeting text. 39 In this logic function, it's only concern to create a greeting text from a user name. 40 41 # Dax for unit tests 42 43 To test a logic function, the simplest dax implementation is what using a map. 44 The following code is an example of a dax implementation using a map and having two methods: UserName and Say which are same to GreetDax interface above. 45 46 type mapGreetDax struct { 47 m map[string]any 48 } 49 50 func (dax mapGreetDax) UserName() (string, sabi.Err) { 51 username, exists := dax.m["username"] 52 if !exists { 53 return "", sabi.NewErr(NoName{}) 54 } 55 return username.(string), sabi.Ok() 56 } 57 58 func (dax mapGreetDax) Say(greeting string) sabi.Err { 59 dax.m["greeting"] = greeting 60 return sabi.Ok() 61 } 62 63 func NewMapGreetDaxBase(m map[string]any) sabi.DaxBase { 64 base := sabi.NewDaxBase() 65 return struct { 66 sabi.DaxBase 67 mapGreetDax 68 } { 69 DaxBase: base, 70 mapGreetDax: mapGreetDax{m: m}, 71 } 72 } 73 74 And the following code is an example of a test case. 75 76 import ( 77 "github.com/stretchr/testify/assert" 78 "testing" 79 ) 80 81 func TestGreetLogic_normal(t *testing.T) { 82 m := make(map[string]any) 83 base := NewMapGreetDaxBase(m) 84 85 m["username"] = "World" 86 err := sabi.RunTxn(base, GreetLogic) 87 assert.Equal(t, m["greeting"], "Hello, World") 88 } 89 90 # Dax for real data accesses 91 92 In actual case, multiple data sources are often used. 93 In this example, an user name is input as command line argument, and greeting is output to standard output (console output). 94 Therefore, two dax implementations are attached to the single GreetDax interface. 95 96 The following code is an example of a dax implementation which inputs an user name from command line argument. 97 98 import "os" 99 100 type CliArgsUserDax struct { 101 } 102 103 func (dax CliArgsUserDax) UserName() (string, sabi.Err) { 104 if len(os.Args) <= 1 { 105 return "", sabi.NewErr(NoName{}) 106 } 107 return os.Args[1], sabi.Ok() 108 } 109 110 In addition, the following code is an example of a dax implementation which outputs a greeting test to console. 111 112 import "fmt" 113 114 type ConsoleOutputDax struct { 115 } 116 117 func (dax ConsoleOutputDax) Say(text string) sabi.Err { 118 _, e := fmt.Println(text) 119 if e != nil { 120 return sabi.NewErr(FailToOutput{Text: text}, e) 121 } 122 return sabi.Ok() 123 } 124 125 And these dax implementations are combined to a DaxBase as follows: 126 127 func NewGreetDaxBase() sabi.DaxBase { 128 base := sabi.NewDaxBase() 129 return struct { 130 sabi.DaxBase 131 CliArgsUserDax 132 ConsoleOutputDax 133 } { 134 DaxBase: base, 135 CliArgsUserDax: CliArgsUserDax{}, 136 ConsoleOutputDax: ConsoleOutputDax{}, 137 } 138 } 139 140 # Executing logic 141 142 The following code implements a main function which execute a GreetLogic. 143 sabi.RunTxn executes the GreetLogic function in a transaction process. 144 145 import "log" 146 147 func main() { 148 base := NewGreetDaxBase() 149 err := sabi.RunTxn(base, GreetLogic) 150 if !err.IsOk() { 151 log.Fatalln(err.Reason()) 152 } 153 } 154 155 # Moving outputs to another transaction process 156 157 sabi.RunTxn executes logic functions in a transaction. If a logic function updates database and causes an error in the transaction, its update is rollbacked. 158 If console output is executed in the same transaction with database update, the rollbacked result is possible to be output to console. 159 Therefore, console output is wanted to execute after the transaction of database update is successfully completed. 160 161 What should be done to achieve it are to add a dax interface for next transaction, to change ConsoleOutputDax to hold a greeting text in Say method, to add a new method to output it in next transaction, and to execute the next transaction in the main function. 162 163 type PrintDax interface { // Added. 164 Print() sabi.Err 165 } 166 167 type ConsoleOutputDax struct { 168 text string // Added 169 } 170 func (dax *ConsoleOutputDax) Say(text string) sabi.Err { // Changed to pointer 171 dax.text = text // Changed 172 return sabi.Ok() 173 } 174 func (dax *ConsoleOutputDax) Print() sabi.Err { // Added 175 _, e := fmt.Println(dax.text) 176 if e != nil { 177 return sabi.NewErr(FailToOutput{Text: dax.text}, e) 178 } 179 return sabi.Ok() 180 } 181 182 func NewGreetDaxBase() sabi.DaxBase { 183 base := sabi.NewDaxBase() 184 return struct { 185 sabi.DaxBase 186 CliArgsUserDax 187 *ConsoleOutputDax // Changed 188 }{ 189 DaxBase: base, 190 CliArgsUserDax: CliArgsUserDax{}, 191 ConsoleOutputDax2: &ConsoleOutputDax2{}, // Changed 192 } 193 } 194 195 And the main function is modified as follows: 196 197 func main() { 198 err := sabi.RunTxn(base, GreetLogic) 199 if !err.IsOk() { 200 log.Fatalln(err.Reason()) 201 } 202 err = sabi.RunTxn(base, func(dax PrintDax) sabi.Err { 203 return dax.Print() 204 }) 205 if !err.IsOk() { 206 log.Fatalln(err.Reason()) 207 } 208 } 209 210 Or, the main function is able to rewrite as follows: 211 212 func main() { 213 txn0 := sabi.Txn(base, GreetLogic) 214 txn1 := sabi.Txn(base, func(dax PrintDax) sabi.Err { 215 return dax.Print() 216 }) 217 err := sabi.RunSeq(txn0, txn1) 218 if !err.IsOk() { 219 log.Fatalln(err.Reason()) 220 } 221 } 222 223 The important point is that the GreetLogic function is not changed. 224 Since this change is not related to the application logic, it is confined to the data access part only. 225 */ 226 package sabi