package main import ( "fmt" "os" "strconv" "strings" "time" ) type Config struct { IncludeBNodes bool CorsOrigins string DefaultNodeLimit int DefaultEdgeLimit int MaxNodeLimit int MaxEdgeLimit int EdgeBatchSize int FreeOSMemoryAfterSnapshot bool LogSnapshotTimings bool SparqlHost string SparqlEndpoint string SparqlUser string SparqlPass string SparqlInsecureTLS bool SparqlDataFile string SparqlGraphIRI string SparqlLoadOnStart bool SparqlClearOnStart bool SparqlTimeout time.Duration SparqlReadyRetries int SparqlReadyDelay time.Duration SparqlReadyTimeout time.Duration HierarchyLayoutEngine string HierarchyLayoutBridgeBin string HierarchyLayoutBridgeWorkdir string HierarchyLayoutTimeout time.Duration HierarchyLayoutRootIRI string ListenAddr string } func LoadConfig() (Config, error) { cfg := Config{ IncludeBNodes: envBool("INCLUDE_BNODES", false), CorsOrigins: envString("CORS_ORIGINS", "*"), DefaultNodeLimit: envInt("DEFAULT_NODE_LIMIT", 800_000), DefaultEdgeLimit: envInt("DEFAULT_EDGE_LIMIT", 2_000_000), MaxNodeLimit: envInt("MAX_NODE_LIMIT", 10_000_000), MaxEdgeLimit: envInt("MAX_EDGE_LIMIT", 20_000_000), EdgeBatchSize: envInt("EDGE_BATCH_SIZE", 100_000), FreeOSMemoryAfterSnapshot: envBool("FREE_OS_MEMORY_AFTER_SNAPSHOT", false), LogSnapshotTimings: envBool("LOG_SNAPSHOT_TIMINGS", false), SparqlHost: envString("SPARQL_HOST", "http://anzograph:8080"), SparqlEndpoint: envString("SPARQL_ENDPOINT", ""), SparqlUser: envString("SPARQL_USER", ""), SparqlPass: envString("SPARQL_PASS", ""), SparqlInsecureTLS: envBool("SPARQL_INSECURE_TLS", false), SparqlDataFile: envString("SPARQL_DATA_FILE", ""), SparqlGraphIRI: envString("SPARQL_GRAPH_IRI", ""), SparqlLoadOnStart: envBool("SPARQL_LOAD_ON_START", false), SparqlClearOnStart: envBool("SPARQL_CLEAR_ON_START", false), HierarchyLayoutEngine: envString("HIERARCHY_LAYOUT_ENGINE", "go"), HierarchyLayoutBridgeBin: envString("HIERARCHY_LAYOUT_BRIDGE_BIN", "/app/radial_sugiyama_go_bridge"), HierarchyLayoutBridgeWorkdir: envString("HIERARCHY_LAYOUT_BRIDGE_WORKDIR", "/workspace/radial_sugiyama"), HierarchyLayoutRootIRI: envString("HIERARCHY_LAYOUT_ROOT_IRI", "http://purl.obolibrary.org/obo/BFO_0000001"), SparqlReadyRetries: envInt("SPARQL_READY_RETRIES", 30), ListenAddr: envString("LISTEN_ADDR", ":8000"), } var err error cfg.SparqlTimeout, err = envSeconds("SPARQL_TIMEOUT_S", 300) if err != nil { return Config{}, err } cfg.SparqlReadyDelay, err = envSeconds("SPARQL_READY_DELAY_S", 4) if err != nil { return Config{}, err } cfg.SparqlReadyTimeout, err = envSeconds("SPARQL_READY_TIMEOUT_S", 10) if err != nil { return Config{}, err } cfg.HierarchyLayoutTimeout, err = envSeconds("HIERARCHY_LAYOUT_TIMEOUT_S", 60) if err != nil { return Config{}, err } if cfg.SparqlLoadOnStart && strings.TrimSpace(cfg.SparqlDataFile) == "" { return Config{}, fmt.Errorf("SPARQL_LOAD_ON_START=true but SPARQL_DATA_FILE is not set") } if cfg.DefaultNodeLimit < 1 { return Config{}, fmt.Errorf("DEFAULT_NODE_LIMIT must be >= 1") } if cfg.DefaultEdgeLimit < 1 { return Config{}, fmt.Errorf("DEFAULT_EDGE_LIMIT must be >= 1") } if cfg.MaxNodeLimit < 1 { return Config{}, fmt.Errorf("MAX_NODE_LIMIT must be >= 1") } if cfg.MaxEdgeLimit < 1 { return Config{}, fmt.Errorf("MAX_EDGE_LIMIT must be >= 1") } if cfg.DefaultNodeLimit > cfg.MaxNodeLimit { return Config{}, fmt.Errorf("DEFAULT_NODE_LIMIT must be <= MAX_NODE_LIMIT") } if cfg.DefaultEdgeLimit > cfg.MaxEdgeLimit { return Config{}, fmt.Errorf("DEFAULT_EDGE_LIMIT must be <= MAX_EDGE_LIMIT") } if cfg.EdgeBatchSize < 1 { return Config{}, fmt.Errorf("EDGE_BATCH_SIZE must be >= 1") } if cfg.EdgeBatchSize > cfg.MaxEdgeLimit { return Config{}, fmt.Errorf("EDGE_BATCH_SIZE must be <= MAX_EDGE_LIMIT") } switch strings.ToLower(strings.TrimSpace(cfg.HierarchyLayoutEngine)) { case "go", "rust": cfg.HierarchyLayoutEngine = strings.ToLower(strings.TrimSpace(cfg.HierarchyLayoutEngine)) default: return Config{}, fmt.Errorf("HIERARCHY_LAYOUT_ENGINE must be 'go' or 'rust'") } if strings.TrimSpace(cfg.HierarchyLayoutBridgeBin) == "" { return Config{}, fmt.Errorf("HIERARCHY_LAYOUT_BRIDGE_BIN must not be empty") } if strings.TrimSpace(cfg.HierarchyLayoutBridgeWorkdir) == "" { return Config{}, fmt.Errorf("HIERARCHY_LAYOUT_BRIDGE_WORKDIR must not be empty") } if strings.TrimSpace(cfg.HierarchyLayoutRootIRI) == "" { return Config{}, fmt.Errorf("HIERARCHY_LAYOUT_ROOT_IRI must not be empty") } if cfg.HierarchyLayoutTimeout <= 0 { return Config{}, fmt.Errorf("HIERARCHY_LAYOUT_TIMEOUT_S must be > 0") } return cfg, nil } func (c Config) EffectiveSparqlEndpoint() string { if strings.TrimSpace(c.SparqlEndpoint) != "" { return strings.TrimSpace(c.SparqlEndpoint) } return strings.TrimRight(c.SparqlHost, "/") + "/sparql" } func (c Config) corsOriginList() []string { raw := strings.TrimSpace(c.CorsOrigins) if raw == "" || raw == "*" { return []string{"*"} } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p == "" { continue } out = append(out, p) } if len(out) == 0 { return []string{"*"} } return out } func envString(name, def string) string { v := os.Getenv(name) if strings.TrimSpace(v) == "" { return def } return v } func envBool(name string, def bool) bool { v := strings.TrimSpace(os.Getenv(name)) if v == "" { return def } switch strings.ToLower(v) { case "1", "true", "yes", "y", "on": return true case "0", "false", "no", "n", "off": return false default: return def } } func envInt(name string, def int) int { v := strings.TrimSpace(os.Getenv(name)) if v == "" { return def } v = strings.ReplaceAll(v, "_", "") n, err := strconv.Atoi(v) if err != nil { return def } return n } func envSeconds(name string, def float64) (time.Duration, error) { v := strings.TrimSpace(os.Getenv(name)) if v == "" { return time.Duration(def * float64(time.Second)), nil } f, err := strconv.ParseFloat(v, 64) if err != nil { return 0, fmt.Errorf("%s must be a number (seconds): %w", name, err) } return time.Duration(f * float64(time.Second)), nil }