rays

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 2 Imported by: 0

README ¶

BeamFoundry Rays 🚀

Rays is a powerful, enterprise-grade Dependency Injection (DI) and Inversion of Control (IoC) framework for Go, built by BeamFoundry.

Inspired by mature ecosystems like Spring Boot, Rays brings structured lifecycles, annotation-driven configuration, and robust dependency wiring to Go, while respecting Go's idiomatic use of composition and interfaces. It eliminates boilerplate wiring, enforces clean boundaries, and makes your Go applications highly testable.

Introduction & Philosophy

Go is famous for its simplicity and explicit dependency passing. However, as Go applications scale to hundreds of services, repositories, and cross-cutting concerns, manual wiring (often done in massive init() functions or main.go) becomes fragile and difficult to maintain.

BeamFoundry Rays solves this by introducing a robust Inversion of Control (IoC) container.

Core Principles
  1. Annotation-Driven, but Type-Safe: We use struct tags (@:"Inject") for declaring dependencies, but rely on Go's strict typing to ensure the container wires the correct pointers.
  2. Encapsulation Matters: Unlike manual DI which forces you to expose dependencies via public fields or constructor arguments, Rays natively injects into private fields. Your internal state remains protected.
  3. Two-Phase Bootstrapping: To safely handle circular dependencies—a common pain point in large systems—Rays separates instantiation/injection from initialization.
  4. Context-Aware Scoping: Singletons are great for services, but short-lived state requires a different approach. Rays introduces Foundry[T] to bridge the gap between global services and request-scoped prototypes.

Quickstart

📦 Installation
go get github.com/BeamFoundry/rays
🚀 The Minimal Application

Here is a complete example of a Rays application. It's not really functional but demonstrate the following :

  • Beam are defined by marker Stereotype interfaces like Component, Service and Application.
  • Dependency injection is done via struct tag.
  • A PostConstruct method can be defined on any Beam. They will be called once an instance is injected.
  • A custom Error type.
  • A Run method can be defined on any Beam. It does not need to be on an Application.

Notice how UserService does not need a constructor taking *Database as an argument.

package main

import (
    "github.com/BeamFoundry/rays/pkg/core"
    "github.com/BeamFoundry/rays/pkg/lang"
    "github.com/BeamFoundry/rays"
)

// 1. Define a Repository
type Database struct {
    core.Component // Stereotype
    dsn string
}

func (db *Database) New() *Database {
    db.dsn = "postgres://localhost/mydb"
    return db
}

// 2. Define a Service
type UserService struct {
    core.Service //
    db     *Database    `@:"Inject"` // Private field injection!
    logger core.Logging `@:"Inject"`
}

func (s *UserService) PostConstruct() lang.Error {
    s.logger.Info("UserService ready!","connection", s.db.dsn)
    return nil
}

// 3. Define an Application, hosting a Run method.
type BatchUserApplication struct {
    core.Application
    userService *UserService `@:"Inject"`
    logger      core.Logging `@:"Inject"`
}

// 4. Define a Run method
func (b *BatchUserApplication) Run(wg *sync.WaitGroup) {
    wg.Add(1) // 1. Register this worker with the global WaitGroup

    b.logger.Info("BatchUserApplication Run started...")

    go func() {
        defer wg.Done() // 2. Ensure we decrement the WaitGroup when finished

        b.logger.Info("BatchUserApplication is running...")

        // Background loop logic goes here...
        for {
            time.Sleep(10 * time.Second)
        }
    }()
}

// 5. Boot the Container
func main() {
    ctx := rays.ApplicationContext()

    if err := ctx.Initialize(); err != nil {
        err.LogErrorWithStackTrace()
        return
    }

    ctx.Run() // Runs application lifecycle
}

Stereotypes & Beams

In Rays, a "Beam" is any object managed by the IoC container. You declare a struct as a Beam by embedding a Stereotype.

🧹 Simplifying with Dot Imports

Before we dive in, a quick note on style. To keep your domain code clean and free of repetitive package prefixes, it is highly recommended to use Go's dot import feature for the core and lang packages.

This allows you to use framework types like Service, Component, and Error directly as if they were native keywords. We will use this convention in the following examples.

The Marker Interface System

Under the hood, Rays relies on a Marker Interface pattern.

Every specific stereotype (Component, Service, etc.) implements a base interface called Stereotype.

When you call ctx.Initialize(), the framework uses Go's reflection to scan your module for any struct that implements Stereotype.

By simply embedding a stereotype, your struct automatically satisfies this interface and becomes visible to the IoC container.

NOTE: In testing packages a call to ctx.EnableScanning() is required in each test to enable scanning for all non-testing Stereotype. Testing Stereotype beams must be added manually. See Testing for more details.

Core Stereotypes
  • Configuration: A factory struct that defines other Beams programmatically.
  • ConfigurationProperties: Provides a way to map configuration files to struct.
  • Component: The base stereotype for generic, stateless singleton objects.
  • Repository: Indicates a data access object.
  • Controller: Indicates a web controller.
  • Service: Indicates a class holding business logic. Semantically identical to Component, but useful for architectural clarity.
  • Application: Indicates an application, holding a Run method.

NOTE: Rays will not provide its own web framework, but will provide existing web framework connectors to use the Controller Stereotype. Similarly, there will be connectors for the Repository Stereotype.

Ensuring Runtime Detection (Compiler Stripping)

Because Rays relies on runtime reflection to instantiate Beams, Go's aggressive compiler and linker might not see any direct usage of your struct in your source code.

If the compiler thinks a struct is "dead code", it will strip it from the final binary, making it invisible to the framework's scanner!

To ensure your Beam is compiled in and discoverable at runtime, you must add a static cast at the package level. This creates an explicit reference to the struct and verifies interface compliance.

package config

import (
    "database/sql"
    . "github.com/BeamFoundry/rays/pkg/core"
)

type DatabaseConfig struct {
    Configuration
}

// Forces the compiler to retain DatabaseConfig in the binary
// and verifies it implements the Stereotype interface.
var _, _ = any(&DatabaseConfig{}).(Stereotype)

// The container calls this method and registers the returned pointer as a Beam.
func (c *DatabaseConfig) ProvidePostgres() *sql.DB {
    db, err := sql.Open("postgres", "...")
    if err != nil {
        return nil // Will not provide a Beam, injecting will fail.
    }
    return db
}

Alternatively you can use a provided go tool to generate a file named zz_stereotype.go in each package in your project containing a Stereotype beam.

Add the tool to your go.mod file :

go get -tool github.com/BeamFoundry/rays/cmd/stereotyper

Update you project before running a test or compiling it :

go tool stereotyper

Dependency Injection

Rays relies on struct tags to identify injection points.

Basic Injection

Add @:"Inject" to any field that is a pointer to a struct or an interface. The container will automatically find and wire the matching Beam by type.

Note: You cannot inject into a concrete value type (like db Database instead of db *Database), as this would break the singleton pattern by copying the struct by value.

import . "github.com/BeamFoundry/rays/pkg/core"

type OrderService struct {
    Service
    inventory *InventoryService `@:"Inject"`
    payment   PaymentGateway    `@:"Inject"` // Interfaces do not need a '*'
}
Private Field Superiority

Idiomatic Go encourages hiding internal state. Rays uses reflection to inject into private fields, meaning your dependencies aren't exposed to other packages.

Qualifier & Primary (Resolving Ambiguity)

If you have multiple Beams of the same type, the container will panic with an ambiguity error. Use Qualifier to differentiate them, and Primary to set a default fallback.

A default Qualifier is always defined for all beams. When using Configuration, the qualifier it is taken from the method's name, uncapitialized. On it can be overwitten using the Qualifier interface in an anonymous struct (a non-named type struct) as the first argument to the method.

import . "github.com/BeamFoundry/rays/pkg/core"

type CloudConfig struct {
    Configuration
}

// Define identical types, marking one as Primary
func (c *CloudConfig) PrimaryStorage(
    _ struct {
        Primary
        Qualifier `Name:"aws"`
    },
) *Storage {
    return &Storage{provider: "AWS"}
}

func (c *CloudConfig) BackupStorage(
    _ struct {
        Qualifier `Name:"gcp"`
    },
) *Storage {
    return &Storage{provider: "GCP"}
}

// 1. Inject a specific one using the Qualifier tag syntax
type BackupService struct {
    Service
    store *Storage `@:"Inject,Qualifier{gcp}"`
}

// 2. Inject the primary one implicitly (no Qualifier needed)
type ImageService struct {
    Service
    store *Storage `@:"Inject"` // Automatically resolves to the PrimaryStorage (AWS)
}
BeamAttributes

Other attributes can be defined. Those are easily extendable.

For example the Server BeamAttribute, defined in the core package, with a corresponding BeamAttributeEvaluator implementation (hidden in an internal package), lets you another attribute in the injection tag.

type Server interface {
	  BeamAttribute
	  _Server()
}

type ServerEvaluator struct {
	  Server
}

// Ensute type is compiled in.
var _, _ = any(&ServerEvaluator{}).(BeamAttributeEvaluator)

// Ensure interface
var _ BeamAttributeEvaluator = &ServerEvaluator{}

// EvaluateBeamAttribute implements [BeamAttributeEvaluator].
func (q *ServerEvaluator) EvaluateBeamAttribute(beam Beam) string {
    // FindFieldForInterface() and GetFieldFirstTag() are defined in the [core] package.
    field := FindFieldForInterface[Server](beam.AnnotatedType())
    return GetFieldFirstTag(field, []string{"_", "Name", "Server"})
}

So example your could wire on the Server attribute :

import . "github.com/BeamFoundry/rays/pkg/core"

type CloudConfig struct {
    Configuration
}

func (c *CloudConfig) AwsStorage(
    _ struct {
        Primary
    },
) *Storage {
    return &Storage{provider: "AWS"}
}

func (c *CloudConfig) GcpStorage(
    _ struct {
        Server `Name:"gcp"`
    },
) *Storage {
    return &Storage{provider: "GCP"}
}

type BackupService struct {
    Service
    store *Storage `@:"Inject,Server{gcp}"`
}

type ImageService struct {
    Service
    store *Storage `@:"Inject"`
}
Optional Injection (Optional[T])

By default, if you inject a dependency that does not exist, the container panics and stops booting. If a dependency is truly optional, wrap it in Optional[T] (in the lang package).

import (
    . "github.com/BeamFoundry/rays/pkg/core"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

type MetricsService struct {
    Service
    tracer Optional[*DatadogTracer] `@:"Inject"`
}

func (m *MetricsService) Report() {
    if m.tracer.IsPresent() {
        m.tracer.Get().Send(...)
    }
}
Multi-Binding (Slices & Maps)

Advanced architectural patterns (like the Strategy Pattern) rely on injecting multiple implementations of an interface or struct. Rays natively supports multi-binding into slices and maps for both interface types and struct pointers.

Slice Injection ([]T)

Injecting into a slice will populate it with all registered Beams that match the slice's underlying type.

Map Injection (map[string]T)

Injecting into a map requires the key to be of type string. Rays will map the injected Beams by their Qualifier name. The key will be the injected Beam's Qualifier value.

import . "github.com/BeamFoundry/rays/pkg/core"

type CommandHandler interface {
    HandleCommand()
}

type ServerNotification struct {
    Configuration
    Server `Name:"primary"`
}

func (s *ServerNotification) Email() CommandHandler {
    ...
}

func (s *ServerNotification) Sms() CommandHandler {
    ...
}

type NotificationDispatcher struct {
    Service

    // Injects all available handlers as a slice
    allHandlers []CommandHandler `@:"Inject"`

    // Injects handlers mapped by their qualifier name ("email", "sms")
    handlerMap map[string]CommandHandler `@:"Inject"`
}

func (d *NotificationDispatcher) Dispatch(handlerName string) {
    if handler, exists := d.handlerMap[handlerName]; exists {
        handler.HandleCommand()
    }
}
Map Injection with custom grouping

Of course you can group on different attributes, an a default value can be specified from beams not having the attribute defined.

import . "github.com/BeamFoundry/rays/pkg/core"

type PrimaryServerNotification struct {
    Configuration
    Server `Name:"primary"`
}

func (p *PrimaryServerNotification) Email() CommandHandler {
    ...
}

func (p *PrimaryServerNotification) Sms() CommandHandler {
    ...
}

type SecondaryServerNotification struct {
    Configuration
}

func (s *SecondaryServerNotification) Log() CommandHandler {
    ...
}

type NotificationDispatcher struct {
    Service

    // Inject all handlers by Server attribute, defaulting to secondary
    handlers map[string][]CommandHandler `@:"Inject,GroupBy=Server,Default=secondary"`
}

func (d *NotificationDispatcher) Dispatch (serverName string) {
  if handlers, exists := d.handlers[serverName]; exists {
    for _, handler := range handlers {
      handler.HandleCommand()
    }
  }
}

Configuration & Profiles

In enterprise applications, configuration and environment management go hand-in-hand.

Rays provides a unified system to bind YAML configurations directly to your structs, override them based on active profiles (e.g., dev, prod), and conditionally load specific components depending on the environment.

1. Binding Data

Embed ConfigurationProperties and define the YAML path via the Name tag.

NOTE: Rays strictly enforces a 1-to-1 parity between your YAML files and your Go code. The yaml tag is not honored. Only properties with the exact same case name as the Go struct field will be injected.

Valid types

All numeric types, bool and string are honored.

Rays includes native support for time.Duration. Text values like "5000ms" in the YAML file are automatically parsed using Go's standard time.ParseDuration function.

Struct and struct pointers (sub-object mapping) are supported.

Struct implementing the encoding.TextUnmarshaler interface can also be used (string to struct mapping). The struct type (not a pointer to that) can be used directly.

Slices of all the above types are supported.

Finally map of string and of encoding.TextUnmarshaler to all the above types and to other map of those two types are supported.

Validation

Validation is done using the Validation project, given you provides the corresponding tags.

NOTE: To validate private fields on a struct, reflection structs will be constructed internally with public fields instead. If using Cross-Field validation, fields should be indicated in their public form. Errors will be formatted using the original private field instead.

Example
// myrepo/myapp/services/kafka.go
package services

import (
    "time"
    . "github.com/BeamFoundry/rays/pkg/core"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

// 1. Define the Configuration
type KafkaConfig struct {
    ConfigurationProperties `Name:"app.kafka"`

    // Exact same-case matching is required.
    //
    // Ensure it's not empty and has valid hostname/port string values.
    brokers []string `validate:"gt=0,dive,hostname_port"`

    // Automatically parses a duration string
    // Ensure a minimum value
    timeout time.Duration `validate:"gt=100ms"`
    retry   bool
}

// 2. Inject it into a Service
type KafkaServer struct {
    Service
    config *KafkaConfig `@:"Inject"`
    logger Logging      `@:"Inject"`
}

// 3. Initialize the service using the injected configuration
func (s *KafkaServer) PostConstruct() Error {
    s.logger.Info("Connecting to kafka", "brokers", len(s.config.brokers), "timeout", s.config.timeout)
    return nil
}
2. Profile-Based Loading

Rays natively supports Spring-like environment profiles. You define a base configuration file, and then create environment-specific overrides using a suffix convention (-<profile>.yaml).

When the application boots, the base file is always loaded. If a specific profile is activated, its corresponding file is loaded on top, overriding the base values.

The YAML Files

Place your configuration files in a directory (e.g., config/).

## config/application.yaml (Loaded for everyone)
app:
  kafka:
    brokers: ["localhost:9092"]
    timeout: "5000ms"
    retry: true
## config/application-prod.yaml (Loaded ONLY if the "prod" profile is active)
app:
  kafka:
    brokers: ["prod-kafka-1:9092", "prod-kafka-2:9092"]
    timeout: "2000ms"
3. Conditional Registration

Beyond just overriding values, profiles can completely alter the architecture of your application.

You can use anonymous struct parameters in your Configuration factories to conditionally load different Beams based on active profiles or YAML properties.

// myrepo/myapp/services/cache.go
package services

import (
    . "github.com/BeamFoundry/rays/pkg/core"

    // Cache implementation
    "repo/cache"
)

// Example of a Cache configuration defined by Conditional
// It is meant to be injected into a not documented Beam.
type CacheConfig struct {
    Configuration
}

// Only loads if the "local" profile is explicitly active
// and Redis is not enabled
func (c *CacheConfig) LocalMemoryCache(
    _ struct {
        ConditionalOnProfile  `Name:"local"`
        ConditionalOnProperty `Name:"redis.enabled" HavingValue:"false" MatchIfMissing:"true"`
    },
) *cache.Cache {
    return &cache.Cache{type: "in-memory"}
}

// Only loads if the YAML file contains "redis.enabled: true"
func (c *CacheConfig) RedisCache(
    _ struct {
        ConditionalOnProperty `Name:"redis.enabled" MatchIfMissing:"false"`
    },
) *cache.Cache {
    return &cache.Cache{type: "redis"}
}

// Only loads if NO OTHER *Cache beam has been registered yet
func (c *CacheConfig) FallbackCache(
    _ struct {
        ConditionalOnMissingBeam
    },
) *cache.Cache {
    return &cache.Cache{type: "noop"}
}
4. The Application Entrypoint

In your main.go, you bring it all together: set the active profiles, load the configuration using a filesystem object (fs.FS), and use an underscore import to ensure your isolated packages are compiled into the binary.

// main.go
package main

import (
    "os"

    // 3rd-Party packages for main
    flag "github.com/spf13/pflag"

    // Rays
    "github.com/BeamFoundry/rays"

    // Application package imports
    // Blank import ensures the package is compiled and visible to the scanner
    _ "myrepo/myapp/services"
)

var profiles []string = []string{}

func main() {
    // Parsing flags
    flag.StringSliceVarP(&profiles, "profile", "p", []string{}, "Profile(s) to use, default : none")
    flag.Parse()

    // Getting rays ApplicationContext
    ctx := rays.ApplicationContext()

    // Tell the container we are running in production
    ctx.SetActiveProfiles(profiles)

    // Load configuration from the "config" directory.
    // This will load both "application.yaml" AND "application-prod.yaml"
    if stat, err := os.Stat("config"); os.IsExist(err) && stat.IsDir() {
        ctx.AddConfigFS(os.DirFS(config))
    }
  }

    // Initialize Container
    if err := ctx.Initialize(); err != nil {
        err.LogErrorWithStackTrace()
        os.Exit(1)
    }

    // Finally Call run
    ctx.Run()
}

Logging Framework

Enterprise applications require structured, leveled, and contextual logging.

Rays provides a dedicated configurable, logging framework.

The Logging Interface

Rays logging is powered by Go's standard slog library under the hood, providing high-performance, structured logging. However, standard slog natively lacks a "Trace" level for highly granular debugging.

Rays extends the standard implementation by adding full support for the Trace level, including the corresponding Trace() and TraceContext() functions on the Logging interface.

Configuration

You can configure the logging framework globally or on a per-package basis using your application.yaml file under the rays.logging property.

rays:
  logging:
    # The default minimum log level
    #
    # Valid values are "TRACE", "DEBUG", "INFO", "WARN", "ERROR" in that order.
    # A value of "INFO" for example will hide all logging from "TRACE" and "DEBUG"
    #
    # Type: text
    # Default: INFO
    level: DEBUG

    # The slog handler to use.
    #
    # Valid values are "DEFAULT", "TEXT", or "JSON".
    #
    # Type: text
    # Default: DEFAULT
    handler: DEFAULT

    # Add source location to each log entry.
    #
    # Type: boolean
    # Default: true
    source: true

    # Override the log level for specific packages.
    #
    # Before injecting a [Logging], a new one will be created and its default level will be changed
    # if the struct requesting the [Logging] has its package name referenced here.
    #
    # Type: Dictionary of "source: level"
    #
    # "source" is either a package name (the one of the struct injecting the [Logging]) or
    # the package name followed by a slash and the struct name.
    #
    # It should be noted that "level" will only apply to that "source"
    # and will not apply to its sub packages UNLESS it ends with a slash.
    #
    loggers:
      github.com/BeamFoundry/rays-sample/: INFO
      github.com/BeamFoundry/rays-sample/pkg/config: DEBUG
      github.com/BeamFoundry/rays-sample/pkg/config/DatabaseConfig: INFO
Basic Injection

To use the logger, inject the Logging interface into your Beams.

import (
    . "github.com/BeamFoundry/rays/pkg/core"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

type PaymentService struct {
    Service

    // Note: No pointer '*' because Logging is an interface
    logger Logging `@:"Inject"`
}

func (s *PaymentService) Process(amount float64) Error {
    s.logger.Trace("Entering Process method", "amount", amount) // Extremely granular logging

    // Logging supports structured key-value pairs natively
    s.logger.Info("Processing payment", "amount", amount)

    return nil
}

Error Handling

If a container fails to boot, you need to know exactly where the wiring failed. Rays replaces standard Go error handling during the lifecycle with a richer Error interface that captures stack traces and key-value attributes automatically.

import . "github.com/BeamFoundry/rays/pkg/lang"

func (s *UserService) PostConstruct() Error {
    if s.db == nil {
        err := NewError("WiringError", "Database pointer is nil")

        // AddAttribute does not return the Error object, so it cannot be chained.
        err.AddAttribute("component", "UserService")
        err.AddAttribute("action", "PostConstruct")

        return err
    }
    return nil
}
Integration with Logging

Because Rays integrates its lang package directly with the logging framework (covered in Chapter 4), an Error can report itself seamlessly.

Instead of manually unpacking the error to log it, simply call err.LogError(). This function automatically extracts the error message, the stack trace, and all attached attributes, serializing them into your configured logging backend.

import . "github.com/BeamFoundry/rays/pkg/lang"

func (s *PaymentService) Process(amount float64) Error {
    if amount <= 0 {
        err := NewError("ValidationError", "Payment amount must be greater than zero")
        err.AddAttribute("component", "PaymentService")
        err.AddAttribute("amount", amount)

        // Automatically logs the error with all context using the framework logger!
        err.LogError()

        return err
    }
    return nil
}
The Try/Catch Pattern

While standard Go relies heavily on if err != nil checks, complex procedural logic can sometimes benefit from a more traditional exception-handling flow. The lang package provides a robust Try/Catch mechanism designed specifically around the framework's Error interface.

Core Functions
  • Try(try func(), catches ...ErrorHandler) Error: Executes the try function. If a panic occurs involving an Error, it evaluates the provided catches and returns the resulting Error. If no catch matches, the original error is returned automatically. Additionally, if standard Go code or a sub-function triggers a regular panic(), the Try block will safely catch it and wrap it into a new framework Error with the name ""PanicError"", ensuring your application does not crash unexpectedly.
  • Catch[E Error](action func(E) Error) ErrorHandler: Catches an error based on its specific Go type.
  • CatchName(name string, action func(Error) Error) ErrorHandler: Catches an error based on its internal string name.
  • Finally(action func()) ErrorHandler: Executes an action after the try block finishes, regardless of success or failure.
Raise Helpers

To easily trigger panics within a Try block when dealing with standard Go returns, use the Raise helpers:

  • RaiseErr(err error) bool: Panics if err is not nil.
  • RaiseVal[T any](t T, err error) T: Panics if err is not nil; otherwise, returns the object T. This is perfect for wrapping standard library calls like RaiseVal(os.ReadFile("config.yaml")).
Example Usage

Here is a complete example defining a custom exception and handling it cleanly using the Try/Catch flow.

import (
    "fmt"
    . "github.com/BeamFoundry/rays/pkg/core"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

// 1. Define a Custom Exception
type TestException struct {
    *DefaultError
}

func (e TestException) New() *TestException {
    e.DefaultError = NewError("TestException", "A test exception has occurred")
    return &e
}

type ProcessingService struct {
    Service
}

// 2. Use Try/Catch in Business Logic
func (s *ProcessingService) Process() Error {
    return Try(func() {

        // Simulate an action that throws an error
        RaiseErr(TestException{}.New())

        fmt.Println("This line will never be reached")

    }, CatchName("DatabaseError", func(e Error) Error {
        // Will not match "TestException"
        return e
    }), Catch(func(e *TestException) Error {
        // This WILL catch the specific TestException type
        fmt.Println("Caught specifically:", e.Message())
        return e
    }), Finally(func() {
        fmt.Println("Cleanup resources here...")
    }))
}

Type-Safe Enums

Go does not have native, strict enums. This often leads to configuration bugs where an invalid string from a YAML file is happily parsed into a string field, only to fail deep within the application logic.

Rays solves this with Enum[T]. It strictly binds string values from YAML into custom Go types and validates them during the framework's Instantiation phase.

1. Define the Enum Type

You can define your enums using a struct to hold the variants as fields.

import . "github.com/BeamFoundry/rays/pkg/lang"

type LogLevels struct {
    DEBUG, INFO, ERROR Enum[LogLevels]
}

type LogLevel = Enum[LogLevels]

Alternatively, this can be simplified by embedding the generic enum type directly into the struct:

import . "github.com/BeamFoundry/rays/pkg/lang"

type LogLevel struct {
    Enum[LogLevel]
    DEBUG, INFO, ERROR Enum[LogLevel]
}
2. Use it in Configuration

Simply declare the field using your custom type. Note that it works seamlessly without wrapping the struct field in generic notation.

import . "github.com/BeamFoundry/rays/pkg/core"

type AppConfig struct {
    ConfigurationProperties `Name:"app"`
    level LogLevel
}
3. Safe Access in Business Logic

Because of the generic structuring, you must initialize the enum type by calling .New() to ensure its internal state is properly configured before accessing its variants.

You can then compare it directly to your configuration fields without needing a getter function.

import (
    "fmt"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

func (s *MyService) PostConstruct() Error {
    // Initialize the Enum structure and compare the property directly
    if s.config.level == (LogLevel{}.New().DEBUG) {
        fmt.Println("Debug mode is enabled!")
    }
    return nil
}

Documentation ¶

Overview ¶

Rays ¶

This package let you configure and initialize the IoC container.

It is primarily used from a main() file.

Sample main file ¶

This is a sample default main() function for rays.

It defines three flags to:

  • Set active profiles (-p|--profile profile1,profile2)
  • Add local directory to load configuration files from (-c|--config ./config)
  • Override a configuration parameter (--override rays.logging.level=DEBUG)

All flags can be used multiple times and/or values can be comma-separated :

./app --profile testing,sqlite --profile admin

The override flag let you define key=value pairs to replace configuration values. It use the same dot separated format for the key hierarchie as ConfigurationProperties

For example, for a yaml configuration :

app:
  module:
    value: foo

To replace the inner "value" key, you should define an override like :

--override app.module.config.value=bar

The "overrides" system is designed to work with string values even if the target value is a bool or numeric value.

Additionally it will add overrides from environment variables whose name starts by "RAYS" or "APP", lowering the case and replacing '_' with '.'.

package main

import (
    // Stdlib packages
    "os"

    // 3rd-Party packages for main
    flag "github.com/spf13/pflag"

    // Rays
    "github.com/BeamFoundry/rays"

    // Importing rays/lang
    . "github.com/BeamFoundry/rays/pkg/lang"

    // Initializing rays external modules
    _ "github.com/BeamFoundry/rays-fiber"
    _ "github.com/BeamFoundry/rays-orm"
    _ "github.com/BeamFoundry/rays-web-security"

    // Importing embedded resources
    "myrepo/myapp/resources"

    // Initializing local packages
    _ "myrepo/myapp/pkg/configurations"
    _ "myrepo/myapp/pkg/controllers"
    _ "myrepo/myapp/pkg/domains"
    _ "myrepo/myapp/pkg/repositories"
    _ "myrepo/myapp/pkg/services"
)

var (
    config   []string = []string{}
    profiles []string = []string{}
    overrides map[string]string = map[string]string{}
)

func main() {
    // Adding flags
    flag.StringSliceVarP(&config, "config", "c", []string{}, "Additional configuration paths")
    flag.StringSliceVarP(&profiles, "profile", "p", []string{}, "Profile to use, default : none")
    flag.StringToStringVar(&overrides, "override", map[string]string{}, "Overrides configuration key inline")
    flag.Parse()

    // Getting rays ApplicationContext
    ctx := rays.ApplicationContext()

    // Loading embedded configuration
    ctx.AddConfigFS(resources.Config())

    // Set profiles
    if len(profiles) > 0 {
        ctx.SetActiveProfiles(profiles)
    }

    // Add additional local config directory
    for _, dir := range config {
        if stat, err := os.Stat(dir); os.IsExist(err) && stat.IsDir() {
            ctx.AddConfigFS(os.DirFS(dir))
        }
    }

    // Load any overrides defined
    for key, value := range overrides {
        ctx.AddOverride(key, value)
    }

    // Load overrides with environment variables
    //
    // Only adding environment variables keys starting with 'RAYS_' or 'APP_',
    // lowering the case and replacing '_' with '.' .
    for _, e := range os.Environ() {
        env := Strings(e).SplitN("=", 2)
        key := String(env[0]).ToUpperCase()
        if key.StartsWith("RAYS_") || key.StartsWith("APP_") {
            key = key.Replace("_", ".").ToLowerCase()
            ctx.AddOverride(key.String(), env[1])
        }
    }

    // Initialize the container
    if err := ctx.Initialize(); err != nil {
        panic(err)
    }

    // Find and run "Runnables"
    ctx.Run()
}

Index ¶

Constants ¶

This section is empty.

Variables ¶

This section is empty.

Functions ¶

func ApplicationContext ¶

func ApplicationContext() core.ContainerConfiguration

ApplicationContext() returns a core.ContainerConfiguration, letting you control Rays container initialization.

Types ¶

This section is empty.

Directories ¶

Path Synopsis
cmd
stereotyper command
internal
pkg
web

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL