├── go.mod ├── .gitignore ├── .travis.yml ├── doc.go ├── parsetags_test.go ├── parsetag.go ├── README.md ├── LICENSE ├── inject_test.go └── inject.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/logrange/linker 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .classpath 4 | .metadata 5 | .project 6 | .settings 7 | .idea/ 8 | 9 | target/ 10 | bin/ 11 | binaries/ 12 | 13 | *.iml 14 | *.ipr 15 | *.iws 16 | 17 | *.py[co] 18 | *.egg 19 | *.egg-info 20 | 21 | *.o 22 | *.obj 23 | *.so 24 | *.dylib 25 | *.out 26 | *.test 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.11.x 4 | 5 | before_script: 6 | - go get -t -v -u ./... 7 | 8 | script: 9 | - go test -race -coverprofile=coverage.txt -covermode=atomic ./... 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) -t "0d098d50-872f-4694-b14b-6a3a49cba5b1" -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The logrange Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package linker provides Dependency Injection and Inversion of Control functionality. 17 | The core component is Injector, which allows to register Components. Component 18 | is an object, which can have any type, which requires some initialization, or can be used 19 | for initializing other components. Every component is registered in the Injector 20 | by the component name or anonymously (empty name). Same object can be registered 21 | by different names. This could be useful if the object implements different 22 | interfaces that can be used by different components. 23 | 24 | The package contains several interfaces: PostConstructor, Initializer and 25 | Shutdowner, which could be implemented by components with a purpose to be called 26 | by Injector on different initialization/de-initialization phases. 27 | 28 | Init() function of Injector allows to initialize registered components. The 29 | initialization process supposes that components with 'pointer to struct' type 30 | or interfaces, which contains a 'pointer to struct' will be initialized. The 31 | initialization supposes to inject (assign) the struct fields values using other 32 | registered components. Injector matches them by name or by type. Injector uses 33 | fail-fast strategy so any error is considered like misconfiguraion and a panic 34 | happens. 35 | 36 | When all components are initialized, the components, which implement PostConstructor 37 | interface will be notified via PostConsturct() function call. The order of 38 | PostConstruct() calls is not defined. 39 | 40 | After the construction phase, injector builds dependencies graph with a purpose 41 | to detect dependency loops and to establish components initialization order. 42 | If a dependency loop is found, Injector will panic. Components, which implement 43 | Initializer interface, will be notified in specific order by Init(ctx) function 44 | call. Less dependant components will be initialized before the components that 45 | have dependency on the first ones. 46 | 47 | Injector is supposed to be called from one go-routine and doesn't support calls 48 | from multiple go-routines. 49 | 50 | Initialization process could take significant time, so context is provided. If 51 | the context is cancelled or closed it will be detected either by appropriate 52 | component or by the Injector what will cause of de-intializing already initialized 53 | components using Shutdown() function call (if provided) in reverse of the 54 | initialization order. Panic will happen then. 55 | */ 56 | package linker 57 | -------------------------------------------------------------------------------- /parsetags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The logrange Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package linker 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestParseParams(t *testing.T) { 22 | var pr parseRes 23 | err := pr.parseParams(nil) 24 | if err != nil { 25 | t.Fatal("Expecting no error, but err=", err) 26 | } 27 | 28 | err = pr.parseParams([]string{"aaaa"}) 29 | if err != nil || pr.val != "aaaa" { 30 | t.Fatal("Expecting no error, but err=", err) 31 | } 32 | 33 | err = pr.parseParams([]string{"aaaa", "bbb"}) 34 | if err == nil { 35 | t.Fatal("Expecting an error, but err=", err) 36 | } 37 | 38 | err = pr.parseParams([]string{"", "optional:123 23"}) 39 | if err != nil || pr.defVal != "123 23" { 40 | t.Fatal("Expecting no error, but err=", err) 41 | } 42 | 43 | err = pr.parseParams([]string{"", "optional :\"asd\""}) 44 | if err != nil || pr.defVal != "\"asd\"" || !pr.optional { 45 | t.Fatal("Expecting no error, but err=", err) 46 | } 47 | 48 | pr.optional = false 49 | err = pr.parseParams([]string{"", " optional"}) 50 | if err != nil || !pr.optional { 51 | t.Fatal("Expecting no error, but err=", err) 52 | } 53 | 54 | pr.optional = false 55 | err = pr.parseParams([]string{"optional"}) 56 | if err != nil || pr.optional || pr.val != "optional" { 57 | t.Fatal("Expecting no error, but err=", err) 58 | } 59 | } 60 | 61 | func TestParseTags(t *testing.T) { 62 | testParseTags(t, "inject:\"test\", json:\"asdf\"", parseRes{"test", false, ""}) 63 | testParseTags(t, "inject:\"\"", parseRes{"", false, ""}) 64 | testParseTags(t, "json:\"aaa\",inject:\"abc,optional\"", parseRes{"abc", true, ""}) 65 | testParseTags(t, "inject:\"abc,optional :asdf\"", parseRes{"abc", true, "asdf"}) 66 | testParseTags(t, "inject:\"a\\\"bc, optional : asdf\"", parseRes{"a\\\"bc", true, " asdf"}) 67 | 68 | testParseTagsError(t, "inject-", nil) 69 | testParseTagsError(t, "json:\"\"", errTagNotFound) 70 | testParseTagsError(t, "INJECT:\"\"", errTagNotFound) 71 | testParseTagsError(t, "inject:\"asdf", nil) 72 | testParseTagsError(t, "inject:asdf", nil) 73 | testParseTagsError(t, "inject:\"asdf", nil) 74 | } 75 | 76 | func testParseTags(t *testing.T, tags string, pr parseRes) { 77 | tpr, err := parseTag("inject", tags) 78 | if err != nil { 79 | t.Fatal("Unexpected error when parsing tags=", tags, " err=", err) 80 | } 81 | 82 | if tpr != pr { 83 | t.Fatal("Expected ", pr, ", but got ", tpr, " for tags=", tags) 84 | } 85 | } 86 | 87 | func testParseTagsError(t *testing.T, tags string, expErr error) { 88 | _, err := parseTag("inject", tags) 89 | if err == nil { 90 | t.Fatal("Expecting an error, but got it nil for tags=", tags, " err=", err) 91 | } 92 | 93 | if expErr != nil && expErr != err { 94 | t.Fatal("Expecting ", expErr, ", but got ", err) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /parsetag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The logrange Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package linker 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | type parseRes struct { 23 | val string 24 | optional bool 25 | defVal string 26 | } 27 | 28 | var errTagNotFound = fmt.Errorf("Tag not found") 29 | 30 | // parseTag receives a tag name and the tags strings. The tag in the tags is expected 31 | // to be for example: 32 | // 33 | // `inject: "componentName, optional: \"abc\"", anotherTag:...` 34 | // 35 | // the tag name separated by collon with its value. Value must be quoted. 36 | // tag value consists of its fields, which are comma separated. The result contains 37 | // the parseRes or an error if any 38 | func parseTag(name, tags string) (parseRes, error) { 39 | if len(name) >= len(tags) { 40 | return parseRes{}, errTagNotFound 41 | } 42 | 43 | i := strings.Index(tags, name) 44 | if i < 0 { 45 | return parseRes{}, errTagNotFound 46 | } 47 | 48 | tags = tags[i+len(name):] 49 | tags = strings.TrimLeft(tags, " ") 50 | if len(tags) == 0 || tags[0] != ':' { 51 | return parseRes{}, fmt.Errorf("Found the tag name=%s, but could not found semicolon after the tag name", name) 52 | } 53 | tags = strings.TrimLeft(tags[1:], " ") 54 | 55 | if len(tags) == 0 || tags[0] != '"' { 56 | return parseRes{}, fmt.Errorf("tag value expected to be in qoutes") 57 | } 58 | tags = tags[1:] 59 | 60 | for i = 0; i < len(tags) && tags[i] != '"'; i++ { 61 | if tags[i] == '\\' { 62 | i++ 63 | } 64 | } 65 | if i >= len(tags) { 66 | return parseRes{}, fmt.Errorf("tag value expected to be in qoutes, but closing quote is not found.") 67 | } 68 | 69 | tags = tags[:i] 70 | var res parseRes 71 | err := res.parseParams(strings.Split(tags, ",")) 72 | return res, err 73 | } 74 | 75 | // parseParams receives slice of string params and turns them into parseParams fields 76 | // supported params: 77 | // optional - if found, then optional field is set true. It can contain 78 | // default value after a collon if provided. 79 | func (pr *parseRes) parseParams(params []string) error { 80 | if len(params) == 0 { 81 | // empty params, does nothing 82 | return nil 83 | } 84 | 85 | // name always comes first 86 | pr.val = params[0] 87 | for i := 1; i < len(params); i++ { 88 | v := strings.Trim(params[i], " ") 89 | if strings.HasPrefix(v, "optional") { 90 | pr.optional = true 91 | v = v[len("optional"):] 92 | v = strings.TrimLeft(v, " ") 93 | if len(v) > 0 { 94 | // seems we have default value here 95 | if v[0] != ':' { 96 | return fmt.Errorf("optional value should be set via tag value in optional[:] form, but it is not: %s", params[i]) 97 | } 98 | pr.defVal = v[1:] 99 | } 100 | continue 101 | } 102 | return fmt.Errorf("Unknown parameter in the tag value: %s. \"optional\" supported so far.", params[i]) 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linker 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/logrange/linker)](https://goreportcard.com/report/github.com/logrange/linker) [![codecov](https://codecov.io/gh/logrange/linker/branch/master/graph/badge.svg)](https://codecov.io/gh/logrange/linker) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/logrange/linker/blob/master/LICENSE) [![GoDoc](https://godoc.org/github.com/logrange/linker?status.png)](https://godoc.org/github.com/logrange/linker) 4 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go) 5 | 6 | Linker is Dependency Injection and Inversion of Control package. It supports the following features: 7 | 8 | - Components registry 9 | - Automatic dependency injection of the registered components 10 | - Components lifecycle support via `PostConstructor`, `Initializer` and `Shutdowner` interfaces implementations 11 | - Post-injection notification 12 | - Automatic ordering of components initialization 13 | - Circular dependency detection 14 | - Components shutdowning 15 | 16 | Please refer to [this blogpost](https://www.logrange.io/blog/linker.html) for some details. 17 | 18 | Linker is used by [Logrange](https://github.com/logrange/logrange), please take a look how it is used [there](https://github.com/logrange/logrange/blob/be1cc8dc0ae8fa9154eec91bea33cd2105509e11/server/server.go#L53). 19 | 20 | ```golang 21 | 22 | import ( 23 | "github.com/logrange/linker" 24 | ) 25 | 26 | type DatabaseAccessService interface { 27 | RunQuery(query string) DbResult 28 | } 29 | 30 | // MySQLAccessService implements DatabaseAccessService 31 | type MySQLAccessService struct { 32 | // Conns uses field's tag to specify injection param name(mySqlConns) 33 | // or sets-up the default value(32), if the param is not provided 34 | Conns int `inject:"mySqlConns, optional:32"` 35 | } 36 | 37 | type BigDataService struct { 38 | // DBa has DatabaseAccessService type which value will be injected by the injector 39 | // in its Init() function, or it fails if there is no appropriate component with the name(dba) 40 | // was registered... 41 | DBa DatabaseAccessService `inject:"dba"` 42 | } 43 | ... 44 | 45 | func main() { 46 | // 1st step is to create the injector 47 | inj := linker.New() 48 | 49 | // 2nd step is to register components 50 | inj.Register( 51 | linker.Component{Name: "dba", Value: &MySQLAccessService{}}, 52 | linker.Component{Name: "", Value: &BigDataService{}}, 53 | linker.Component{Name: "mySqlConns", Value: int(msconns)}, 54 | ... 55 | ) 56 | 57 | // 3rd step is to inject dependecies and initialize the registered components 58 | inj.Init(ctx) 59 | 60 | // the injector fails-fast, so if no panic everything is good so far. 61 | 62 | ... 63 | // 4th de-initialize all compoments properly 64 | inj.Shutdown() 65 | } 66 | 67 | ``` 68 | ### Annotate fields using fields tags 69 | The `inject` tag field has the following format: 70 | ``` 71 | inject: "[,optional[:= 0; idx-- { 247 | c := i.iComps[idx] 248 | err := c.shutdown(i.log) 249 | if err != nil { 250 | i.log.Info("An error while shutdown. err=", err) 251 | } 252 | } 253 | i.iComps = nil 254 | i.comps = nil 255 | i.named = nil 256 | i.log.Info("Shutdown(): done.") 257 | } 258 | 259 | // assignOrPanic assigns value for the field with index fi according to the tagInfo provided. 260 | // The function panics on any error related to the assignment. 261 | func (i *Injector) assignOrPanic(c *component, fi int, tagInfo parseRes) { 262 | f := c.val.Elem().Field(fi) 263 | fType := f.Type() 264 | fName := c.tp.Elem().Field(fi).Name 265 | 266 | compName := tagInfo.val 267 | if compName != "" { 268 | c1, ok := i.named[compName] 269 | if !ok { 270 | if tagInfo.optional { 271 | err := setFieldValueByString(f, tagInfo.defVal) 272 | if err != nil { 273 | i.panic(fmt.Sprintf("Could not assign the default value=\"%s\" to the field %s (with type %s) in the type %s.", 274 | tagInfo.defVal, fName, fType, c.tp)) 275 | } 276 | 277 | i.log.Info("Field #", fi, "(", fName, "): the component name ", compName, ", is not found, but the field population is optional. Skipping the value. ") 278 | return 279 | } 280 | i.panic(fmt.Sprintf("Could not set field %s in type %s, cause no component with such name(%s) was found.", fName, c.tp, compName)) 281 | } 282 | 283 | if !c1.tp.AssignableTo(fType) { 284 | i.panic(fmt.Sprintf("Component named %s of type %s is not assignable to field %s (with type %s) in %s.", 285 | compName, c1.tp, fName, fType, c.tp)) 286 | } 287 | 288 | f.Set(c1.val) 289 | c.addDep(c1) 290 | i.log.Debug("Field #", fi, "(", fName, "): Populating the field in type ", c.tp, " by the component ", c1.tp) 291 | return 292 | } 293 | 294 | // search for a assignable type to the field 295 | var found *component 296 | for _, uc := range i.comps { 297 | if uc.tp.AssignableTo(fType) { 298 | if found != nil { 299 | i.panic(fmt.Sprintf("Ambiguous component assignment for the field %s with type %s in the type %s. Both unnamed components %s and %s, matched to the field.", 300 | fName, fType, c.tp, found.tp, uc.tp)) 301 | } 302 | found = uc 303 | } 304 | } 305 | 306 | if found != nil { 307 | f.Set(found.val) 308 | c.addDep(found) 309 | i.log.Debug("Field #", fi, "(", fName, "): Populating the field in type ", c.tp, " by component ", found.tp) 310 | return 311 | } 312 | 313 | // If nothing is found, apply default value if it's possible. 314 | if tagInfo.optional { 315 | err := setFieldValueByString(f, tagInfo.defVal) 316 | if err != nil { 317 | i.panic(fmt.Sprintf("Could not assign the default value=\"%s\" to the field %s (with type %s) in the type %s.", 318 | tagInfo.defVal, fName, fType, c.tp)) 319 | } 320 | 321 | i.log.Info("Field #", fi, "(", fName, "): the component name ", compName, ", is not found, but the field population is optional. Skipping the value. ") 322 | return 323 | } 324 | 325 | i.panic(fmt.Sprintf("Could not find a component to initialize field %s (with type %s) in the type %s", fName, fType, c.tp)) 326 | } 327 | 328 | func (i *Injector) initStructPtr(c *component) { 329 | if !isStructPtr(c.tp) { 330 | i.log.Debug("Skipping component with type ", c.tp, " cause it is not a pointer to a struct") 331 | return 332 | } 333 | 334 | i.log.Debug("Init component with type ", c.tp, " (", c.val.Elem().NumField(), " fields)") 335 | 336 | for fi := 0; fi < c.val.Elem().NumField(); fi++ { 337 | f := c.val.Elem().Field(fi) 338 | fType := f.Type() 339 | fTag := string(c.tp.Elem().Field(fi).Tag) 340 | fName := c.tp.Elem().Field(fi).Name 341 | 342 | tagInfo, err := parseTag(i.tagName, fTag) 343 | if err == errTagNotFound { 344 | i.log.Debug("Field #", fi, "(", fName, "): no tags for the field ", fType) 345 | continue 346 | } 347 | 348 | if err != nil { 349 | i.panic(fmt.Sprintf("Could not parse tag for field %s of %s, err=%s.", fName, c.tp, err)) 350 | } 351 | 352 | if !f.CanSet() { 353 | i.panic(fmt.Sprintf("Could not set field %s valued of %s, cause it is unexported.", fName, c.tp)) 354 | } 355 | 356 | i.assignOrPanic(c, fi, tagInfo) 357 | } 358 | } 359 | 360 | func (i *Injector) panic(err string) { 361 | i.log.Info(err, " Panicing.") 362 | panic(err) 363 | } 364 | 365 | func (nl nullLogger) Info(args ...interface{}) { 366 | } 367 | 368 | func (nl nullLogger) Debug(args ...interface{}) { 369 | } 370 | 371 | func (sl stdLogger) Info(args ...interface{}) { 372 | fmt.Printf("%s INFO: %s\n", time.Now().Format("03:04:05.000"), fmt.Sprint(args...)) 373 | } 374 | 375 | func (sl stdLogger) Debug(args ...interface{}) { 376 | fmt.Printf("%s DEBUG: %s\n", time.Now().Format("03:04:05.000"), fmt.Sprint(args...)) 377 | } 378 | 379 | func isStructPtr(t reflect.Type) bool { 380 | return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct 381 | } 382 | 383 | func (c *component) postConstruct(log Logger) { 384 | if pc, ok := c.value.(PostConstructor); ok { 385 | log.Info("PostConstruct() for ", c.tp, " priority=", c.initOrder) 386 | pc.PostConstruct() 387 | } 388 | } 389 | 390 | func (c *component) init(ctx context.Context, log Logger) (err error) { 391 | defer func() { 392 | r := recover() 393 | if r != nil { 394 | err = fmt.Errorf("Panic in Init() of %s recover=%v", c.tp, r) 395 | } 396 | }() 397 | 398 | if i, ok := c.value.(Initializer); ok { 399 | log.Info("Init() for ", c.tp, " priority=", c.initOrder) 400 | err = i.Init(ctx) 401 | } 402 | 403 | return 404 | } 405 | 406 | func (c *component) shutdown(log Logger) (err error) { 407 | defer func() { 408 | r := recover() 409 | if r != nil { 410 | err = fmt.Errorf("Panic in Shutdown() of %s", c.tp) 411 | } 412 | }() 413 | 414 | if s, ok := c.value.(Shutdowner); ok { 415 | log.Info("Shutdown() for ", c.tp, " priority=", c.initOrder) 416 | s.Shutdown() 417 | } 418 | 419 | return 420 | } 421 | 422 | func (c *component) addDep(c1 *component) { 423 | if c.deps == nil { 424 | c.deps = make(map[*component]bool) 425 | } 426 | c.deps[c1] = true 427 | } 428 | 429 | func (c *component) getInitOrder() int { 430 | if c.initOrder > 0 { 431 | return c.initOrder 432 | } 433 | 434 | blkList := make(map[*component]bool) 435 | return c.setInitOrder(blkList) 436 | } 437 | 438 | func (c *component) setInitOrder(blkList map[*component]bool) int { 439 | if c.initOrder > 0 { 440 | return c.initOrder 441 | } 442 | 443 | o := 1 444 | blkList[c] = true 445 | for c1 := range c.deps { 446 | if _, ok := blkList[c1]; ok { 447 | panic(fmt.Sprintf("Found a loop in the object graph dependencies. Component %s has a reference to %s, which alrady refers to the first one directly or indirectly", 448 | c, c1)) 449 | } 450 | i := c1.setInitOrder(blkList) 451 | if i >= o { 452 | o = i + 1 453 | } 454 | } 455 | delete(blkList, c) 456 | c.initOrder = o 457 | return c.initOrder 458 | } 459 | 460 | func (c *component) String() string { 461 | return fmt.Sprintf("{tp=%s, }", c.tp) 462 | } 463 | 464 | // setFieldValueByString receives a field value and a string which should be assignde to 465 | // it. Numberical and string values are supported only. Returns an error if 466 | // it could not assign the string value to the field 467 | func setFieldValueByString(field reflect.Value, s string) error { 468 | if len(s) == 0 { 469 | return nil 470 | } 471 | 472 | obj := reflect.New(field.Type()).Interface() 473 | if t := reflect.TypeOf(obj); t.Kind() == reflect.Ptr && 474 | t.Elem().Kind() == reflect.String { 475 | s = strconv.Quote(s) 476 | } 477 | 478 | err := json.Unmarshal([]byte(s), obj) 479 | if err != nil { 480 | return err 481 | } 482 | 483 | field.Set(reflect.ValueOf(obj).Elem()) 484 | return nil 485 | } 486 | --------------------------------------------------------------------------------