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