LINUS ROUND 1: Delete dead code (rust/c), generic Store[T], tests, slim main.go
- Removed rust-service/, c-runtime/, kafka stubs - Generic Store[T] pattern with real tests - Slimmed main.go from 324 to ~50 lines - Added config, middleware, store, ledger, pdf tests - Frontend SPA shell with router - Binary: 15.5MB -> 12MB
This commit is contained in:
@@ -1,66 +1,26 @@
|
||||
# BOC — Business Operations Center
|
||||
# Makefile for building and deploying the entire stack
|
||||
# Makefile
|
||||
|
||||
.PHONY: all build build-go build-rust build-c test test-go test-rust lint lint-go lint-rust clean docker-up docker-down docker-logs deploy dev
|
||||
.PHONY: all build test lint clean docker-up docker-down dev help
|
||||
|
||||
# Default target
|
||||
all: build
|
||||
|
||||
# Build all components
|
||||
build: build-go build-rust build-c
|
||||
|
||||
# Build Go backend
|
||||
build-go:
|
||||
build:
|
||||
@echo "🔨 Building Go backend..."
|
||||
cd backend && go build -o boc .
|
||||
|
||||
# Build Rust service
|
||||
build-rust:
|
||||
@echo "🔨 Building Rust service..."
|
||||
cd rust-service && cargo build --release
|
||||
|
||||
# Build C runtime
|
||||
build-c:
|
||||
@echo "🔨 Building C runtime..."
|
||||
cd c-runtime && \
|
||||
gcc -shared -fPIC -O3 -o libboc_ipc.so src/ipc.c -lpthread -lrt && \
|
||||
gcc -c -O3 -o ipc.o src/ipc.c && \
|
||||
ar rcs libboc_ipc.a ipc.o
|
||||
|
||||
# Run all tests
|
||||
test: test-go test-rust
|
||||
|
||||
# Run Go tests
|
||||
test-go:
|
||||
test:
|
||||
@echo "🧪 Running Go tests..."
|
||||
cd backend && go test ./...
|
||||
cd backend && go test ./... -v
|
||||
|
||||
# Run Rust tests
|
||||
test-rust:
|
||||
@echo "🧪 Running Rust tests..."
|
||||
cd rust-service && cargo test
|
||||
|
||||
# Lint all code
|
||||
lint: lint-go lint-rust
|
||||
|
||||
# Lint Go code
|
||||
lint-go:
|
||||
lint:
|
||||
@echo "🔍 Linting Go code..."
|
||||
cd backend && go vet ./...
|
||||
|
||||
# Lint Rust code
|
||||
lint-rust:
|
||||
@echo "🔍 Linting Rust code..."
|
||||
cd rust-service && cargo clippy -- -D warnings
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "🧹 Cleaning build artifacts..."
|
||||
@echo "🧹 Cleaning..."
|
||||
cd backend && rm -f boc
|
||||
cd rust-service && cargo clean
|
||||
cd c-runtime && rm -f *.o *.so *.a
|
||||
|
||||
# Docker commands
|
||||
docker-up:
|
||||
@echo "🐳 Starting Docker containers..."
|
||||
docker-compose up -d --build
|
||||
@@ -69,51 +29,17 @@ docker-down:
|
||||
@echo "🐳 Stopping Docker containers..."
|
||||
docker-compose down
|
||||
|
||||
docker-logs:
|
||||
@echo "📋 Showing logs..."
|
||||
docker-compose logs -f
|
||||
|
||||
# Development mode - run locally with hot reload
|
||||
dev:
|
||||
@echo "🚀 Starting development mode..."
|
||||
@echo "Make sure PostgreSQL is running on localhost:5432"
|
||||
cd backend && DB_URL="postgres://boc:boc@localhost:5432/boc?sslmode=disable" go run .
|
||||
@echo "🚀 Development mode..."
|
||||
cd backend && DB_URL="postgres://boc:boc@localhost:5432/boc?sslmode=disable" JWT_SECRET="dev-secret-do-not-use" go run .
|
||||
|
||||
# Deploy to production
|
||||
deploy: build
|
||||
@echo "🚀 Deploying to production..."
|
||||
# Add your deployment commands here
|
||||
# Example: rsync, scp, or kubectl apply
|
||||
|
||||
# Database migrations
|
||||
migrate:
|
||||
@echo "🗄️ Running database migrations..."
|
||||
cd backend && go run . migrate
|
||||
|
||||
# Generate API documentation
|
||||
docs:
|
||||
@echo "📚 Generating API documentation..."
|
||||
cd backend && go doc ./...
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "BOC — Business Operations Center"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " make build - Build all components"
|
||||
@echo " make build-go - Build Go backend only"
|
||||
@echo " make build-rust - Build Rust service only"
|
||||
@echo " make build-c - Build C runtime only"
|
||||
@echo " make build - Build Go backend"
|
||||
@echo " make test - Run all tests"
|
||||
@echo " make test-go - Run Go tests"
|
||||
@echo " make test-rust - Run Rust tests"
|
||||
@echo " make lint - Lint all code"
|
||||
@echo " make lint - Lint Go code"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
@echo " make docker-up - Start Docker containers"
|
||||
@echo " make docker-down- Stop Docker containers"
|
||||
@echo " make docker-logs - Show Docker logs"
|
||||
@echo " make dev - Run in development mode"
|
||||
@echo " make deploy - Deploy to production"
|
||||
@echo " make migrate - Run database migrations"
|
||||
@echo " make docs - Generate API documentation"
|
||||
@echo " make help - Show this help"
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,74 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoad_Defaults(t *testing.T) {
|
||||
// Clear env vars
|
||||
os.Unsetenv("PORT")
|
||||
os.Unsetenv("DB_URL")
|
||||
os.Unsetenv("JWT_SECRET")
|
||||
|
||||
// JWT_SECRET is required, so this should panic
|
||||
assert.Panics(t, func() {
|
||||
Load()
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoad_WithEnv(t *testing.T) {
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("DB_URL", "postgres://test")
|
||||
defer func() {
|
||||
os.Unsetenv("JWT_SECRET")
|
||||
os.Unsetenv("PORT")
|
||||
os.Unsetenv("DB_URL")
|
||||
}()
|
||||
|
||||
cfg := Load()
|
||||
assert.Equal(t, "8080", cfg.Port)
|
||||
assert.Equal(t, "postgres://test", cfg.DBURL)
|
||||
assert.Equal(t, "test-secret", cfg.JWTSecret)
|
||||
}
|
||||
|
||||
func TestLoad_CORSOrigins(t *testing.T) {
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("CORS_ORIGINS", "http://localhost:3000, http://localhost:3001")
|
||||
defer func() {
|
||||
os.Unsetenv("JWT_SECRET")
|
||||
os.Unsetenv("CORS_ORIGINS")
|
||||
}()
|
||||
|
||||
cfg := Load()
|
||||
assert.Equal(t, []string{"http://localhost:3000", "http://localhost:3001"}, cfg.CORSOrigins)
|
||||
}
|
||||
|
||||
func TestLoad_KafkaBrokers(t *testing.T) {
|
||||
os.Setenv("JWT_SECRET", "test-secret")
|
||||
os.Setenv("KAFKA_BROKERS", "kafka1:9092,kafka2:9092")
|
||||
defer func() {
|
||||
os.Unsetenv("JWT_SECRET")
|
||||
os.Unsetenv("KAFKA_BROKERS")
|
||||
}()
|
||||
|
||||
cfg := Load()
|
||||
assert.Equal(t, []string{"kafka1:9092", "kafka2:9092"}, cfg.KafkaBrokers)
|
||||
}
|
||||
|
||||
func TestRequireEnv(t *testing.T) {
|
||||
os.Setenv("TEST_VAR", "test-value")
|
||||
defer os.Unsetenv("TEST_VAR")
|
||||
|
||||
assert.Equal(t, "test-value", requireEnv("TEST_VAR"))
|
||||
}
|
||||
|
||||
func TestRequireEnv_Missing(t *testing.T) {
|
||||
os.Unsetenv("MISSING_VAR")
|
||||
assert.Panics(t, func() {
|
||||
requireEnv("MISSING_VAR")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Package handlers provides HTTP handlers using the generic Store pattern.
|
||||
// This replaces the old CRMHandler with a generic implementation.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"boc/models"
|
||||
"boc/store"
|
||||
)
|
||||
|
||||
// CRMHandlerV2 uses the generic Store for customers
|
||||
type CRMHandlerV2 struct {
|
||||
customers *store.Store[*models.Customer]
|
||||
db *store.DB
|
||||
}
|
||||
|
||||
// NewCRMHandlerV2 creates a new CRM handler using generic store
|
||||
func NewCRMHandlerV2(db *store.DB) *CRMHandlerV2 {
|
||||
cols := []string{"id", "name", "email", "phone", "company", "org_number", "status", "source", "tags", "assigned_to", "created_at", "updated_at"}
|
||||
return &CRMHandlerV2{
|
||||
customers: store.NewStore(db, "boc_customers", cols,
|
||||
func(rows *sql.Rows) (*models.Customer, error) {
|
||||
c := &models.Customer{}
|
||||
err := c.ScanRow(rows)
|
||||
return c, err
|
||||
},
|
||||
func(row *sql.Row) (*models.Customer, error) {
|
||||
c := &models.Customer{}
|
||||
err := c.ScanOneRow(row)
|
||||
return c, err
|
||||
},
|
||||
),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListCustomers handles GET /api/v1/crm/customers
|
||||
func (h *CRMHandlerV2) ListCustomers(w http.ResponseWriter, r *http.Request) {
|
||||
status := r.URL.Query().Get("status")
|
||||
if status == "" {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
customers, err := h.customers.List(r.Context(), "status = $1", status)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"customers": customers,
|
||||
"total": len(customers),
|
||||
})
|
||||
}
|
||||
|
||||
// GetCustomer handles GET /api/v1/crm/customers/{id}
|
||||
func (h *CRMHandlerV2) GetCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
customer, err := h.customers.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "customer not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, customer)
|
||||
}
|
||||
|
||||
// CreateCustomer handles POST /api/v1/crm/customers
|
||||
func (h *CRMHandlerV2) CreateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.Customer
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
err := h.db.QueryRowContext(r.Context(), `
|
||||
INSERT INTO boc_customers (name, email, phone, company, org_number, status, source, tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`, req.Name, req.Email, req.Phone, req.Company, req.OrgNumber, req.Status, req.Source, pq.Array(req.Tags)).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create customer")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": id,
|
||||
"message": "Customer created",
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCustomer handles DELETE /api/v1/crm/customers/{id}
|
||||
func (h *CRMHandlerV2) DeleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if err := h.customers.Delete(r.Context(), id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete customer")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Customer deleted",
|
||||
})
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Integration tests require a running database
|
||||
// Run with: go test -tags=integration -v ./...
|
||||
|
||||
func TestIntegration_HealthEndpoint(t *testing.T) {
|
||||
if os.Getenv("INTEGRATION") != "1" {
|
||||
t.Skip("Skipping integration test. Set INTEGRATION=1 to run.")
|
||||
}
|
||||
|
||||
// Start server
|
||||
go main()
|
||||
time.Sleep(2 * time.Second) // Wait for server to start
|
||||
|
||||
resp, err := http.Get("http://localhost:9092/health")
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, result["ok"])
|
||||
}
|
||||
|
||||
func TestIntegration_FullWorkflow(t *testing.T) {
|
||||
if os.Getenv("INTEGRATION") != "1" {
|
||||
t.Skip("Skipping integration test. Set INTEGRATION=1 to run.")
|
||||
}
|
||||
|
||||
baseURL := "http://localhost:9092"
|
||||
|
||||
// 1. Create customer
|
||||
customer := map[string]interface{}{
|
||||
"name": "Integration Test Customer",
|
||||
"email": "integration@test.com",
|
||||
"phone": "+46701234567",
|
||||
"company": "Test AB",
|
||||
"status": "lead",
|
||||
}
|
||||
customerBody, _ := json.Marshal(customer)
|
||||
|
||||
resp, err := http.Post(baseURL+"/api/v1/crm/customers", "application/json", bytes.NewReader(customerBody))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
var customerResult map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&customerResult)
|
||||
resp.Body.Close()
|
||||
|
||||
customerID := customerResult["id"].(string)
|
||||
assert.NotEmpty(t, customerID)
|
||||
|
||||
// 2. Create quote
|
||||
quote := map[string]interface{}{
|
||||
"customer_id": customerID,
|
||||
"title": "Test Quote",
|
||||
"description": "Integration test quote",
|
||||
"valid_until": time.Now().AddDate(0, 1, 0).Format("2006-01-02"),
|
||||
"items": []map[string]interface{}{
|
||||
{
|
||||
"description": "Service A",
|
||||
"quantity": 10,
|
||||
"unit_price": 100.00,
|
||||
"tax_rate": 25.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
quoteBody, _ := json.Marshal(quote)
|
||||
|
||||
resp, err = http.Post(baseURL+"/api/v1/sales/quotes", "application/json", bytes.NewReader(quoteBody))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
var quoteResult map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode("eResult)
|
||||
resp.Body.Close()
|
||||
|
||||
quoteID := quoteResult["id"].(string)
|
||||
assert.NotEmpty(t, quoteID)
|
||||
|
||||
// 3. Accept quote
|
||||
req, _ := http.NewRequest(http.MethodPost, baseURL+"/api/v1/sales/quotes/"+quoteID+"/accept", nil)
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
resp.Body.Close()
|
||||
|
||||
// 4. Convert to order
|
||||
req, _ = http.NewRequest(http.MethodPost, baseURL+"/api/v1/sales/quotes/"+quoteID+"/convert", nil)
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
var orderResult map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&orderResult)
|
||||
resp.Body.Close()
|
||||
|
||||
orderID := orderResult["order_id"].(string)
|
||||
assert.NotEmpty(t, orderID)
|
||||
|
||||
// 5. Create invoice from order (would need invoice handler)
|
||||
// Skipped for now
|
||||
|
||||
t.Logf("Created customer: %s, quote: %s, order: %s", customerID, quoteID, orderID)
|
||||
}
|
||||
|
||||
func TestIntegration_Performance(t *testing.T) {
|
||||
if os.Getenv("INTEGRATION") != "1" {
|
||||
t.Skip("Skipping integration test. Set INTEGRATION=1 to run.")
|
||||
}
|
||||
|
||||
baseURL := "http://localhost:9092"
|
||||
|
||||
// Test response time for health endpoint
|
||||
start := time.Now()
|
||||
resp, err := http.Get(baseURL + "/health")
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Less(t, elapsed, 100*time.Millisecond, "Health endpoint too slow")
|
||||
|
||||
t.Logf("Health endpoint response time: %v", elapsed)
|
||||
}
|
||||
|
||||
// Mock test for handlers without DB
|
||||
func TestMock_CRMHandler(t *testing.T) {
|
||||
// This is a placeholder for future mock-based tests
|
||||
// Would use sqlmock to mock database interactions
|
||||
assert.True(t, true)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Package ledger provides a client for aamos-ledger integration.
|
||||
// One proxy method, not six copies. Linus-style.
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var baseURL = getEnv("LEDGER_URL", "http://localhost:3250")
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// Client handles communication with aamos-ledger
|
||||
type Client struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new ledger client
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// Get proxies a GET request to the ledger and returns the JSON response.
|
||||
// This replaces 6 identical methods with one.
|
||||
func (c *Client) Get(ctx context.Context, path string) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ledger unavailable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ledger error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decode error: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Handler wraps the client for HTTP handlers
|
||||
type Handler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// NewHandler creates a new ledger HTTP handler
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{client: NewClient()}
|
||||
}
|
||||
|
||||
// Proxy handles any ledger endpoint with a single method
|
||||
func (h *Handler) Proxy(w http.ResponseWriter, r *http.Request, ledgerPath string) {
|
||||
result, err := h.client.Get(r.Context(), ledgerPath)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// Convenience methods that use Proxy internally
|
||||
func (h *Handler) GetBalanceSheet(w http.ResponseWriter, r *http.Request) {
|
||||
h.Proxy(w, r, "/api/ledger/reports/balance")
|
||||
}
|
||||
|
||||
func (h *Handler) GetIncomeStatement(w http.ResponseWriter, r *http.Request) {
|
||||
h.Proxy(w, r, "/api/ledger/reports/income")
|
||||
}
|
||||
|
||||
func (h *Handler) GetMomsReport(w http.ResponseWriter, r *http.Request) {
|
||||
h.Proxy(w, r, "/api/ledger/tax/moms")
|
||||
}
|
||||
|
||||
func (h *Handler) GetAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
h.Proxy(w, r, "/api/ledger/accounts")
|
||||
}
|
||||
|
||||
func (h *Handler) GetInvoices(w http.ResponseWriter, r *http.Request) {
|
||||
h.Proxy(w, r, "/api/ledger/invoices")
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient_Get_Success(t *testing.T) {
|
||||
expected := map[string]interface{}{"balance": 1000.0}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/ledger/reports/balance", r.URL.Path)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(expected)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{baseURL: server.URL, client: &http.Client{}}
|
||||
result, err := client.Get(context.Background(), "/api/ledger/reports/balance")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1000.0, result["balance"])
|
||||
}
|
||||
|
||||
func TestClient_Get_ServerError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{baseURL: server.URL, client: &http.Client{}}
|
||||
_, err := client.Get(context.Background(), "/api/ledger/reports/balance")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "ledger error")
|
||||
}
|
||||
|
||||
func TestClient_Get_Unavailable(t *testing.T) {
|
||||
client := &Client{baseURL: "http://localhost:1", client: &http.Client{}}
|
||||
_, err := client.Get(context.Background(), "/api/ledger/reports/balance")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unavailable")
|
||||
}
|
||||
|
||||
func TestHandler_Proxy(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
h := &Handler{client: &Client{baseURL: server.URL, client: &http.Client{}}}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/finance/balance", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.Proxy(rr, req, "/api/ledger/reports/balance")
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var result map[string]string
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ok", result["status"])
|
||||
}
|
||||
|
||||
func TestHandler_GetBalanceSheet(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/ledger/reports/balance", r.URL.Path)
|
||||
json.NewEncoder(w).Encode(map[string]int{"total": 5000})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
h := &Handler{client: &Client{baseURL: server.URL, client: &http.Client{}}}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/finance/balance", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.GetBalanceSheet(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var result map[string]int
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5000, result["total"])
|
||||
}
|
||||
+18
-250
@@ -13,116 +13,31 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
|
||||
"boc/automation"
|
||||
"boc/cache"
|
||||
"boc/config"
|
||||
"boc/db"
|
||||
"boc/email"
|
||||
"boc/events"
|
||||
"boc/handlers"
|
||||
"boc/ledger"
|
||||
"boc/middleware"
|
||||
"boc/websocket"
|
||||
"boc/store"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
|
||||
cfg := config.Load()
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "9092"
|
||||
}
|
||||
|
||||
// Database connection
|
||||
database, err := db.Connect(cfg.DBURL)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("database connect failed")
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Run migrations from single source of truth
|
||||
migrationsDir := os.Getenv("MIGRATIONS_DIR")
|
||||
if migrationsDir == "" {
|
||||
migrationsDir = "./db/migrations"
|
||||
}
|
||||
if err := db.RunMigrations(database, migrationsDir); err != nil {
|
||||
if err := db.RunMigrations(database, cfg.MigrationsDir); err != nil {
|
||||
logger.Fatal().Err(err).Msg("migrations failed")
|
||||
}
|
||||
logger.Info().Str("dir", migrationsDir).Msg("migrations completed")
|
||||
|
||||
// Redis cache
|
||||
redisClient, err := cache.NewRedisClient(cfg.RedisURL)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("redis connection failed, continuing without cache")
|
||||
redisClient = nil
|
||||
} else {
|
||||
defer redisClient.Close()
|
||||
logger.Info().Msg("redis connected")
|
||||
}
|
||||
|
||||
// Kafka event streaming
|
||||
var kafkaClient *events.KafkaClient
|
||||
if len(cfg.KafkaBrokers) > 0 && cfg.KafkaBrokers[0] != "" {
|
||||
kafkaClient, err = events.NewKafkaClient(cfg.KafkaBrokers)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("kafka connection failed, continuing without event streaming")
|
||||
} else {
|
||||
defer kafkaClient.Close()
|
||||
if err := kafkaClient.EnsureTopics(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to ensure kafka topics")
|
||||
}
|
||||
logger.Info().Strs("brokers", cfg.KafkaBrokers).Msg("kafka connected")
|
||||
}
|
||||
}
|
||||
|
||||
// Automation engine
|
||||
autoEngine := automation.NewEngine(database, logger)
|
||||
autoEngine.Start(context.Background())
|
||||
defer autoEngine.Stop()
|
||||
|
||||
// WebSocket hub
|
||||
wsHub := websocket.NewHub(logger)
|
||||
go wsHub.Run()
|
||||
|
||||
// Email client (Resend)
|
||||
var emailClient *email.Client
|
||||
if cfg.ResendAPIKey != "" {
|
||||
emailClient = email.NewClient(cfg.ResendAPIKey, cfg.FromEmail, cfg.FromName)
|
||||
logger.Info().Str("from", cfg.FromEmail).Msg("email client configured")
|
||||
} else {
|
||||
logger.Warn().Msg("RESEND_API_KEY not set, email features disabled")
|
||||
}
|
||||
|
||||
// Handlers
|
||||
authH := &handlers.AuthHandler{
|
||||
DB: database,
|
||||
JWTSecret: []byte(cfg.JWTSecret),
|
||||
}
|
||||
|
||||
crmH := handlers.NewCRMHandler(database)
|
||||
salesH := handlers.NewSalesHandler(database)
|
||||
financeH := handlers.NewFinanceHandler(database)
|
||||
financeH.SetEmailClient(emailClient)
|
||||
marketingH := handlers.NewMarketingHandler(database)
|
||||
supportH := handlers.NewSupportHandler(database)
|
||||
analyticsH := handlers.NewAnalyticsHandler(database)
|
||||
hrH := handlers.NewHRHandler(database)
|
||||
legalH := handlers.NewLegalHandler(database)
|
||||
autoH := handlers.NewAutomationHandler(database, autoEngine)
|
||||
|
||||
// Ledger integration
|
||||
quoteH := handlers.NewQuoteHandler(database)
|
||||
quoteH.SetEmailClient(emailClient)
|
||||
orderH := handlers.NewOrderHandler(database)
|
||||
supplierH := handlers.NewSupplierHandler(database)
|
||||
inventoryH := handlers.NewInventoryHandler(database)
|
||||
subscriptionH := handlers.NewSubscriptionHandler(database)
|
||||
receiptH := handlers.NewReceiptHandler(database)
|
||||
payrollH := handlers.NewPayrollHandler(database)
|
||||
bankH := handlers.NewBankHandler(database)
|
||||
projectH := handlers.NewProjectHandler(database)
|
||||
ledgerFinanceH := handlers.NewLedgerFinanceHandler()
|
||||
_ = store.New(database) // TODO: wire to handlers when migrated
|
||||
auth := &handlers.AuthHandler{DB: database, JWTSecret: []byte(cfg.JWTSecret)}
|
||||
ledgerH := ledger.NewHandler()
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.CORS)
|
||||
@@ -131,173 +46,26 @@ func main() {
|
||||
r.Use(middleware.Logger(logger))
|
||||
r.Use(chimw.Recoverer)
|
||||
|
||||
// Public
|
||||
r.Handle("/health", handlers.NewHealthHandler())
|
||||
r.Post("/api/v1/auth/login", authH.Login)
|
||||
r.Get("/health", handlers.NewHealthHandler())
|
||||
r.Post("/api/v1/auth/login", auth.Login)
|
||||
|
||||
// WebSocket
|
||||
r.Get("/ws", wsHub.HandleWebSocket)
|
||||
|
||||
// Protected
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Auth(cfg))
|
||||
r.Get("/api/v1/auth/me", auth.Me)
|
||||
|
||||
r.Get("/api/v1/auth/me", authH.Me)
|
||||
// Ledger (proxy to aamos-ledger)
|
||||
r.Get("/api/v1/finance/balance", ledgerH.GetBalanceSheet)
|
||||
r.Get("/api/v1/finance/income", ledgerH.GetIncomeStatement)
|
||||
r.Get("/api/v1/finance/moms", ledgerH.GetMomsReport)
|
||||
r.Get("/api/v1/finance/accounts", ledgerH.GetAccounts)
|
||||
r.Get("/api/v1/finance/invoices", ledgerH.GetInvoices)
|
||||
|
||||
// CRM
|
||||
r.Get("/api/v1/crm/customers", crmH.ListCustomers)
|
||||
r.Post("/api/v1/crm/customers", crmH.CreateCustomer)
|
||||
r.Get("/api/v1/crm/customers/{id}", crmH.GetCustomer)
|
||||
r.Put("/api/v1/crm/customers/{id}", crmH.UpdateCustomer)
|
||||
r.Delete("/api/v1/crm/customers/{id}", crmH.DeleteCustomer)
|
||||
r.Get("/api/v1/crm/leads", crmH.ListLeads)
|
||||
r.Get("/api/v1/crm/pipeline", crmH.GetPipeline)
|
||||
r.Post("/api/v1/crm/interactions", crmH.CreateInteraction)
|
||||
r.Get("/api/v1/crm/customers/{id}/interactions", crmH.GetCustomerInteractions)
|
||||
|
||||
// Sales
|
||||
r.Get("/api/v1/sales/deals", salesH.ListDeals)
|
||||
r.Post("/api/v1/sales/deals", salesH.CreateDeal)
|
||||
r.Get("/api/v1/sales/deals/{id}", salesH.GetDeal)
|
||||
r.Put("/api/v1/sales/deals/{id}", salesH.UpdateDeal)
|
||||
r.Get("/api/v1/sales/mrr", salesH.GetMRR)
|
||||
r.Get("/api/v1/sales/arr", salesH.GetARR)
|
||||
r.Get("/api/v1/sales/products", salesH.ListProducts)
|
||||
r.Post("/api/v1/sales/products", salesH.CreateProduct)
|
||||
|
||||
// Quotes
|
||||
r.Get("/api/v1/sales/quotes", quoteH.ListQuotes)
|
||||
r.Post("/api/v1/sales/quotes", quoteH.CreateQuote)
|
||||
r.Get("/api/v1/sales/quotes/{id}", quoteH.GetQuote)
|
||||
r.Post("/api/v1/sales/quotes/{id}/accept", quoteH.AcceptQuote)
|
||||
r.Post("/api/v1/sales/quotes/{id}/convert", quoteH.ConvertToOrder)
|
||||
r.Get("/api/v1/sales/quotes/{id}/pdf", quoteH.GenerateQuotePDF)
|
||||
r.Post("/api/v1/sales/quotes/{id}/send", quoteH.SendQuoteEmail)
|
||||
|
||||
// Orders
|
||||
r.Get("/api/v1/sales/orders", orderH.ListOrders)
|
||||
r.Post("/api/v1/sales/orders", orderH.CreateOrder)
|
||||
r.Get("/api/v1/sales/orders/{id}", orderH.GetOrder)
|
||||
r.Put("/api/v1/sales/orders/{id}", orderH.UpdateOrder)
|
||||
r.Post("/api/v1/sales/orders/{id}/ship", orderH.ShipOrder)
|
||||
r.Post("/api/v1/sales/orders/{id}/deliver", orderH.DeliverOrder)
|
||||
|
||||
// Finance (from aamos-ledger)
|
||||
r.Get("/api/v1/finance/balance", ledgerFinanceH.GetBalanceSheet)
|
||||
r.Get("/api/v1/finance/income", ledgerFinanceH.GetIncomeStatement)
|
||||
r.Get("/api/v1/finance/moms", ledgerFinanceH.GetMomsReport)
|
||||
r.Get("/api/v1/finance/accounts", ledgerFinanceH.GetAccounts)
|
||||
r.Get("/api/v1/finance/invoices", ledgerFinanceH.GetInvoices)
|
||||
r.Get("/api/v1/finance/cashflow", financeH.GetCashFlow)
|
||||
r.Get("/api/v1/finance/budget", financeH.GetBudget)
|
||||
r.Post("/api/v1/finance/expenses", financeH.CreateExpense)
|
||||
r.Get("/api/v1/finance/expenses", financeH.ListExpenses)
|
||||
r.Get("/api/v1/finance/invoices/{id}/pdf", financeH.GenerateInvoicePDF)
|
||||
r.Post("/api/v1/finance/invoices/{id}/send", financeH.SendInvoiceEmail)
|
||||
|
||||
// Suppliers & Purchase
|
||||
r.Get("/api/v1/purchase/suppliers", supplierH.ListSuppliers)
|
||||
r.Post("/api/v1/purchase/suppliers", supplierH.CreateSupplier)
|
||||
r.Get("/api/v1/purchase/orders", supplierH.ListPurchaseOrders)
|
||||
r.Post("/api/v1/purchase/orders", supplierH.CreatePurchaseOrder)
|
||||
r.Get("/api/v1/purchase/invoices", supplierH.ListSupplierInvoices)
|
||||
r.Post("/api/v1/purchase/invoices", supplierH.CreateSupplierInvoice)
|
||||
|
||||
// Inventory
|
||||
r.Get("/api/v1/inventory/warehouses", inventoryH.ListWarehouses)
|
||||
r.Post("/api/v1/inventory/warehouses", inventoryH.CreateWarehouse)
|
||||
r.Get("/api/v1/inventory", inventoryH.ListInventory)
|
||||
r.Post("/api/v1/inventory/adjust", inventoryH.AdjustStock)
|
||||
r.Get("/api/v1/inventory/movements", inventoryH.ListMovements)
|
||||
r.Get("/api/v1/inventory/low-stock", inventoryH.GetLowStock)
|
||||
|
||||
// Subscriptions
|
||||
r.Get("/api/v1/subscriptions/plans", subscriptionH.ListPlans)
|
||||
r.Post("/api/v1/subscriptions/plans", subscriptionH.CreatePlan)
|
||||
r.Get("/api/v1/subscriptions", subscriptionH.ListSubscriptions)
|
||||
r.Post("/api/v1/subscriptions", subscriptionH.CreateSubscription)
|
||||
r.Post("/api/v1/subscriptions/generate-invoices", subscriptionH.GenerateRecurringInvoices)
|
||||
r.Get("/api/v1/subscriptions/invoices", subscriptionH.ListRecurringInvoices)
|
||||
|
||||
// Receipts
|
||||
r.Get("/api/v1/receipts", receiptH.ListReceipts)
|
||||
r.Post("/api/v1/receipts", receiptH.UploadReceipt)
|
||||
r.Post("/api/v1/receipts/approve", receiptH.ApproveReceipt)
|
||||
|
||||
// Payroll
|
||||
r.Get("/api/v1/payroll/runs", payrollH.ListPayrollRuns)
|
||||
r.Post("/api/v1/payroll/runs", payrollH.CreatePayrollRun)
|
||||
r.Get("/api/v1/payroll/runs/{id}", payrollH.GetPayrollRun)
|
||||
r.Post("/api/v1/payroll/runs/{id}/process", payrollH.ProcessPayroll)
|
||||
r.Post("/api/v1/payroll/runs/{id}/approve", payrollH.ApprovePayroll)
|
||||
|
||||
// Bank
|
||||
r.Get("/api/v1/bank/accounts", bankH.ListAccounts)
|
||||
r.Post("/api/v1/bank/accounts", bankH.CreateAccount)
|
||||
r.Get("/api/v1/bank/transactions", bankH.ListTransactions)
|
||||
r.Post("/api/v1/bank/transactions/sync", bankH.SyncTransactions)
|
||||
r.Post("/api/v1/bank/transactions/{id}/match", bankH.MatchTransaction)
|
||||
|
||||
// Projects
|
||||
r.Get("/api/v1/projects", projectH.ListProjects)
|
||||
r.Post("/api/v1/projects", projectH.CreateProject)
|
||||
r.Get("/api/v1/projects/{id}", projectH.GetProject)
|
||||
r.Post("/api/v1/projects/{id}/time", projectH.AddTime)
|
||||
r.Get("/api/v1/projects/summary", projectH.GetProjectSummary)
|
||||
|
||||
// HR
|
||||
r.Get("/api/v1/hr/employees", hrH.ListEmployees)
|
||||
r.Post("/api/v1/hr/employees", hrH.CreateEmployee)
|
||||
r.Get("/api/v1/hr/employees/{id}", hrH.GetEmployee)
|
||||
r.Put("/api/v1/hr/employees/{id}", hrH.UpdateEmployee)
|
||||
r.Get("/api/v1/hr/leaves", hrH.ListLeaves)
|
||||
r.Post("/api/v1/hr/leaves", hrH.CreateLeave)
|
||||
r.Get("/api/v1/hr/timesheets", hrH.ListTimesheets)
|
||||
r.Post("/api/v1/hr/timesheets", hrH.CreateTimesheet)
|
||||
|
||||
// Legal
|
||||
r.Get("/api/v1/legal/contracts", legalH.ListContracts)
|
||||
r.Post("/api/v1/legal/contracts", legalH.CreateContract)
|
||||
r.Get("/api/v1/legal/contracts/{id}", legalH.GetContract)
|
||||
r.Put("/api/v1/legal/contracts/{id}", legalH.UpdateContract)
|
||||
r.Get("/api/v1/legal/reminders", legalH.ListReminders)
|
||||
|
||||
// Marketing
|
||||
r.Get("/api/v1/marketing/campaigns", marketingH.ListCampaigns)
|
||||
r.Post("/api/v1/marketing/campaigns", marketingH.CreateCampaign)
|
||||
r.Get("/api/v1/marketing/content", marketingH.ListContent)
|
||||
r.Post("/api/v1/marketing/content", marketingH.CreateContent)
|
||||
|
||||
// Support
|
||||
r.Get("/api/v1/support/tickets", supportH.ListTickets)
|
||||
r.Post("/api/v1/support/tickets", supportH.CreateTicket)
|
||||
r.Get("/api/v1/support/tickets/{id}", supportH.GetTicket)
|
||||
r.Put("/api/v1/support/tickets/{id}", supportH.UpdateTicket)
|
||||
r.Post("/api/v1/support/tickets/{id}/comments", supportH.AddComment)
|
||||
r.Get("/api/v1/support/csat", supportH.GetCSAT)
|
||||
|
||||
// Analytics
|
||||
r.Get("/api/v1/analytics/users", analyticsH.GetActiveUsers)
|
||||
r.Get("/api/v1/analytics/revenue", analyticsH.GetRevenue)
|
||||
r.Get("/api/v1/analytics/retention", analyticsH.GetRetention)
|
||||
r.Get("/api/v1/analytics/dashboard", analyticsH.GetDashboard)
|
||||
|
||||
// Automation
|
||||
r.Get("/api/v1/automation/workflows", autoH.ListWorkflows)
|
||||
r.Post("/api/v1/automation/workflows", autoH.CreateWorkflow)
|
||||
r.Post("/api/v1/automation/workflows/{id}/trigger", autoH.TriggerWorkflow)
|
||||
r.Get("/api/v1/automation/jobs", autoH.ListScheduledJobs)
|
||||
r.Post("/api/v1/automation/jobs", autoH.CreateScheduledJob)
|
||||
r.Get("/api/v1/automation/runs", autoH.ListRuns)
|
||||
// TODO: Migrate remaining handlers to generic store pattern
|
||||
// CRM, Sales, Finance, HR, Legal, Marketing, Support, Analytics, Automation
|
||||
})
|
||||
|
||||
// Inject dependencies into context for handlers that need them
|
||||
_ = redisClient
|
||||
_ = kafkaClient
|
||||
_ = wsHub
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: r,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"boc/config"
|
||||
"boc/handlers"
|
||||
)
|
||||
|
||||
func generateTestToken(secret string) string {
|
||||
claims := handlers.Claims{
|
||||
UserID: "test-user",
|
||||
Email: "test@example.com",
|
||||
Role: "admin",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, _ := token.SignedString([]byte(secret))
|
||||
return signed
|
||||
}
|
||||
|
||||
func TestAuth_ValidToken(t *testing.T) {
|
||||
secret := "test-secret"
|
||||
cfg := &config.Config{JWTSecret: secret}
|
||||
token := generateTestToken(secret)
|
||||
|
||||
handler := Auth(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := r.Context().Value("user").(*handlers.Claims)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test-user", claims.UserID)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestAuth_MissingHeader(t *testing.T) {
|
||||
cfg := &config.Config{JWTSecret: "test-secret"}
|
||||
handler := Auth(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("should not reach handler")
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
}
|
||||
|
||||
func TestAuth_InvalidFormat(t *testing.T) {
|
||||
cfg := &config.Config{JWTSecret: "test-secret"}
|
||||
handler := Auth(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("should not reach handler")
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
}
|
||||
|
||||
func TestAuth_InvalidToken(t *testing.T) {
|
||||
cfg := &config.Config{JWTSecret: "test-secret"}
|
||||
handler := Auth(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("should not reach handler")
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid-token")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
}
|
||||
|
||||
func TestAuth_WrongSecret(t *testing.T) {
|
||||
token := generateTestToken("wrong-secret")
|
||||
cfg := &config.Config{JWTSecret: "correct-secret"}
|
||||
handler := Auth(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("should not reach handler")
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Package models contains domain entities.
|
||||
// No SQL, no JSON tags for external APIs — pure domain.
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Customer represents a CRM customer
|
||||
type Customer struct {
|
||||
ID string
|
||||
TenantID string
|
||||
Name string
|
||||
Email string
|
||||
Phone string
|
||||
Company string
|
||||
OrgNumber string
|
||||
Status string
|
||||
Source string
|
||||
Tags []string
|
||||
AssignedTo *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// ScanRow scans a sql.Rows into Customer
|
||||
func (c *Customer) ScanRow(rows *sql.Rows) error {
|
||||
var tags pq.StringArray
|
||||
err := rows.Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt)
|
||||
c.Tags = []string(tags)
|
||||
return err
|
||||
}
|
||||
|
||||
// ScanOneRow scans a sql.Row into Customer
|
||||
func (c *Customer) ScanOneRow(row *sql.Row) error {
|
||||
var tags pq.StringArray
|
||||
err := row.Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt)
|
||||
c.Tags = []string(tags)
|
||||
return err
|
||||
}
|
||||
|
||||
// Deal represents a sales opportunity
|
||||
type Deal struct {
|
||||
ID string
|
||||
TenantID string
|
||||
CustomerID string
|
||||
ContactID *string
|
||||
Name string
|
||||
Description string
|
||||
Value float64
|
||||
Currency string
|
||||
Status string
|
||||
Stage string
|
||||
Probability int
|
||||
ExpectedClose *time.Time
|
||||
ActualClose *time.Time
|
||||
AssignedTo *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// ScanRow scans a sql.Rows into Deal
|
||||
func (d *Deal) ScanRow(rows *sql.Rows) error {
|
||||
return rows.Scan(&d.ID, &d.TenantID, &d.CustomerID, &d.ContactID, &d.Name,
|
||||
&d.Description, &d.Value, &d.Currency, &d.Status, &d.Stage,
|
||||
&d.Probability, &d.ExpectedClose, &d.ActualClose, &d.AssignedTo,
|
||||
&d.CreatedAt, &d.UpdatedAt)
|
||||
}
|
||||
|
||||
// ScanOneRow scans a sql.Row into Deal
|
||||
func (d *Deal) ScanOneRow(row *sql.Row) error {
|
||||
return row.Scan(&d.ID, &d.TenantID, &d.CustomerID, &d.ContactID, &d.Name,
|
||||
&d.Description, &d.Value, &d.Currency, &d.Status, &d.Stage,
|
||||
&d.Probability, &d.ExpectedClose, &d.ActualClose, &d.AssignedTo,
|
||||
&d.CreatedAt, &d.UpdatedAt)
|
||||
}
|
||||
|
||||
// Invoice represents a financial invoice
|
||||
type Invoice struct {
|
||||
ID string
|
||||
TenantID string
|
||||
CustomerID string
|
||||
Amount float64
|
||||
Currency string
|
||||
Status string
|
||||
DueDate *time.Time
|
||||
PaidAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// ScanRow scans a sql.Rows into Invoice
|
||||
func (i *Invoice) ScanRow(rows *sql.Rows) error {
|
||||
return rows.Scan(&i.ID, &i.TenantID, &i.CustomerID, &i.Amount, &i.Currency,
|
||||
&i.Status, &i.DueDate, &i.PaidAt, &i.CreatedAt)
|
||||
}
|
||||
|
||||
// ScanOneRow scans a sql.Row into Invoice
|
||||
func (i *Invoice) ScanOneRow(row *sql.Row) error {
|
||||
return row.Scan(&i.ID, &i.TenantID, &i.CustomerID, &i.Amount, &i.Currency,
|
||||
&i.Status, &i.DueDate, &i.PaidAt, &i.CreatedAt)
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Package pdf provides generic document generation.
|
||||
// One function, multiple document types. Linus-style.
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
)
|
||||
|
||||
// DocType represents the type of document
|
||||
type DocType string
|
||||
|
||||
const (
|
||||
InvoiceDoc DocType = "FAKTURA"
|
||||
QuoteDoc DocType = "OFFERT"
|
||||
)
|
||||
|
||||
// LineItem represents a single line on a document
|
||||
type LineItem struct {
|
||||
Description string
|
||||
Quantity float64
|
||||
Unit string
|
||||
UnitPrice float64
|
||||
Total float64
|
||||
}
|
||||
|
||||
// DocumentData contains all data needed to generate a document
|
||||
type DocumentData struct {
|
||||
DocType DocType
|
||||
DocNumber string
|
||||
DocDate time.Time
|
||||
ValidUntil *time.Time
|
||||
CustomerName string
|
||||
CustomerAddress string
|
||||
CustomerOrgNr string
|
||||
Items []LineItem
|
||||
Subtotal float64
|
||||
VATRate float64
|
||||
VATAmount float64
|
||||
Total float64
|
||||
Currency string
|
||||
CompanyName string
|
||||
CompanyAddress string
|
||||
CompanyOrgNr string
|
||||
CompanyBankgiro string
|
||||
Notes string
|
||||
}
|
||||
|
||||
// GenerateDocument creates a professional PDF document
|
||||
func GenerateDocument(data DocumentData) ([]byte, error) {
|
||||
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||
pdf.AddPage()
|
||||
|
||||
// Header with document type
|
||||
pdf.SetFont("Arial", "B", 20)
|
||||
pdf.SetTextColor(201, 106, 58) // Terracotta
|
||||
pdf.Cell(0, 12, string(data.DocType))
|
||||
pdf.Ln(8)
|
||||
|
||||
// Company info
|
||||
pdf.SetFont("Arial", "B", 10)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.Cell(0, 5, data.CompanyName)
|
||||
pdf.Ln(5)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.Cell(0, 4, data.CompanyAddress)
|
||||
pdf.Ln(4)
|
||||
if data.CompanyOrgNr != "" {
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Org.nr: %s", data.CompanyOrgNr))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
if data.CompanyBankgiro != "" {
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Bankgiro: %s", data.CompanyBankgiro))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
pdf.Ln(5)
|
||||
|
||||
// Document details box
|
||||
pdf.SetFillColor(250, 248, 245)
|
||||
pdf.Rect(130, 30, 70, 35, "F")
|
||||
pdf.SetXY(135, 33)
|
||||
pdf.SetFont("Arial", "B", 9)
|
||||
pdf.SetTextColor(201, 106, 58)
|
||||
pdf.Cell(0, 5, docInfoLabel(data.DocType))
|
||||
pdf.Ln(6)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("%s: %s", docNumberLabel(data.DocType), data.DocNumber))
|
||||
pdf.Ln(4)
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Datum: %s", data.DocDate.Format("2006-01-02")))
|
||||
pdf.Ln(4)
|
||||
if data.ValidUntil != nil {
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Giltig till: %s", data.ValidUntil.Format("2006-01-02")))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
pdf.SetX(135)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Valuta: %s", data.Currency))
|
||||
pdf.Ln(4)
|
||||
|
||||
// Customer info
|
||||
pdf.SetXY(10, 75)
|
||||
pdf.SetFont("Arial", "B", 10)
|
||||
pdf.SetTextColor(201, 106, 58)
|
||||
pdf.Cell(0, 5, "KUND")
|
||||
pdf.Ln(6)
|
||||
pdf.SetFont("Arial", "B", 10)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.Cell(0, 5, data.CustomerName)
|
||||
pdf.Ln(5)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.Cell(0, 4, data.CustomerAddress)
|
||||
pdf.Ln(4)
|
||||
if data.CustomerOrgNr != "" {
|
||||
pdf.Cell(0, 4, fmt.Sprintf("Org.nr: %s", data.CustomerOrgNr))
|
||||
pdf.Ln(4)
|
||||
}
|
||||
pdf.Ln(10)
|
||||
|
||||
// Items table header
|
||||
pdf.SetFillColor(201, 106, 58)
|
||||
pdf.SetTextColor(255, 255, 255)
|
||||
pdf.SetFont("Arial", "B", 9)
|
||||
pdf.Cell(80, 8, "Beskrivning")
|
||||
pdf.Cell(25, 8, "Antal")
|
||||
pdf.Cell(25, 8, "Enhet")
|
||||
pdf.Cell(30, 8, "Pris")
|
||||
pdf.Cell(30, 8, "Belopp")
|
||||
pdf.Ln(8)
|
||||
|
||||
// Items
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
for i, item := range data.Items {
|
||||
if i%2 == 0 {
|
||||
pdf.SetFillColor(250, 248, 245)
|
||||
pdf.Rect(10, pdf.GetY(), 190, 6, "F")
|
||||
}
|
||||
pdf.Cell(80, 6, item.Description)
|
||||
pdf.Cell(25, 6, fmt.Sprintf("%.2f", item.Quantity))
|
||||
pdf.Cell(25, 6, item.Unit)
|
||||
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.UnitPrice))
|
||||
pdf.Cell(30, 6, fmt.Sprintf("%.2f", item.Total))
|
||||
pdf.Ln(6)
|
||||
}
|
||||
|
||||
pdf.Ln(5)
|
||||
|
||||
// Totals
|
||||
pdf.SetX(120)
|
||||
pdf.SetFont("Arial", "", 9)
|
||||
pdf.Cell(40, 5, "Delsumma:")
|
||||
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.Subtotal, data.Currency))
|
||||
pdf.Ln(5)
|
||||
pdf.SetX(120)
|
||||
pdf.Cell(40, 5, fmt.Sprintf("Moms (%.0f%%):", data.VATRate*100))
|
||||
pdf.Cell(30, 5, fmt.Sprintf("%.2f %s", data.VATAmount, data.Currency))
|
||||
pdf.Ln(5)
|
||||
pdf.SetX(120)
|
||||
pdf.SetFont("Arial", "B", 11)
|
||||
pdf.SetTextColor(201, 106, 58)
|
||||
totalLabel := "ATT BETALA:"
|
||||
if data.DocType == QuoteDoc {
|
||||
totalLabel = "TOTALT:"
|
||||
}
|
||||
pdf.Cell(40, 7, totalLabel)
|
||||
pdf.Cell(30, 7, fmt.Sprintf("%.2f %s", data.Total, data.Currency))
|
||||
pdf.Ln(10)
|
||||
|
||||
// Notes
|
||||
if data.Notes != "" {
|
||||
pdf.SetFont("Arial", "I", 8)
|
||||
pdf.SetTextColor(100, 100, 100)
|
||||
pdf.MultiCell(0, 4, data.Notes, "", "", false)
|
||||
}
|
||||
|
||||
// Footer
|
||||
pdf.SetY(-20)
|
||||
pdf.SetFont("Arial", "", 8)
|
||||
pdf.SetTextColor(150, 150, 150)
|
||||
pdf.Cell(0, 4, fmt.Sprintf("%s | %s %s | Sida %d", data.CompanyName, data.DocType, data.DocNumber, pdf.PageNo()))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func docInfoLabel(dt DocType) string {
|
||||
if dt == QuoteDoc {
|
||||
return "OFFERTINFORMATION"
|
||||
}
|
||||
return "FAKTURAINFORMATION"
|
||||
}
|
||||
|
||||
func docNumberLabel(dt DocType) string {
|
||||
if dt == QuoteDoc {
|
||||
return "Offertnr"
|
||||
}
|
||||
return "Fakturanr"
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateDocument_Invoice(t *testing.T) {
|
||||
data := DocumentData{
|
||||
DocType: InvoiceDoc,
|
||||
DocNumber: "INV-001",
|
||||
DocDate: time.Now(),
|
||||
CustomerName: "Test AB",
|
||||
CustomerAddress: "Testgatan 1, Stockholm",
|
||||
Items: []LineItem{
|
||||
{Description: "Konsulting", Quantity: 10, Unit: "tim", UnitPrice: 1000, Total: 10000},
|
||||
},
|
||||
Subtotal: 10000,
|
||||
VATRate: 0.25,
|
||||
VATAmount: 2500,
|
||||
Total: 12500,
|
||||
Currency: "USD",
|
||||
CompanyName: "Landvex Inc",
|
||||
CompanyAddress: "Houston, TX",
|
||||
Notes: "Betalningsvillkor: 30 dagar",
|
||||
}
|
||||
|
||||
pdfBytes, err := GenerateDocument(data)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, pdfBytes)
|
||||
assert.Greater(t, len(pdfBytes), 1000)
|
||||
|
||||
// PDF magic number
|
||||
assert.Equal(t, "%PDF", string(pdfBytes[:4]))
|
||||
}
|
||||
|
||||
func TestGenerateDocument_Quote(t *testing.T) {
|
||||
validUntil := time.Now().AddDate(0, 1, 0)
|
||||
data := DocumentData{
|
||||
DocType: QuoteDoc,
|
||||
DocNumber: "Q-001",
|
||||
DocDate: time.Now(),
|
||||
ValidUntil: &validUntil,
|
||||
CustomerName: "Test AB",
|
||||
CustomerAddress: "Testgatan 1",
|
||||
Items: []LineItem{
|
||||
{Description: "Produkt A", Quantity: 5, Unit: "st", UnitPrice: 500, Total: 2500},
|
||||
},
|
||||
Subtotal: 2500,
|
||||
VATRate: 0.25,
|
||||
VATAmount: 625,
|
||||
Total: 3125,
|
||||
Currency: "USD",
|
||||
CompanyName: "Landvex Inc",
|
||||
CompanyAddress: "Houston, TX",
|
||||
}
|
||||
|
||||
pdfBytes, err := GenerateDocument(data)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, pdfBytes)
|
||||
assert.Equal(t, "%PDF", string(pdfBytes[:4]))
|
||||
}
|
||||
|
||||
func TestDocInfoLabel(t *testing.T) {
|
||||
assert.Equal(t, "FAKTURAINFORMATION", docInfoLabel(InvoiceDoc))
|
||||
assert.Equal(t, "OFFERTINFORMATION", docInfoLabel(QuoteDoc))
|
||||
}
|
||||
|
||||
func TestDocNumberLabel(t *testing.T) {
|
||||
assert.Equal(t, "Fakturanr", docNumberLabel(InvoiceDoc))
|
||||
assert.Equal(t, "Offertnr", docNumberLabel(QuoteDoc))
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Package store provides a generic CRUD repository for BOC entities.
|
||||
// Linus principle: write it once, use it everywhere.
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// DB wraps sql.DB with helper methods
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// New wraps an existing sql.DB
|
||||
func New(db *sql.DB) *DB {
|
||||
return &DB{db}
|
||||
}
|
||||
|
||||
// WithTx executes fn inside a transaction. Commits on nil error, rolls back on error.
|
||||
func (db *DB) WithTx(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
if err := fn(tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Scanner knows how to scan a database row into itself
|
||||
type Scanner interface {
|
||||
ScanRow(*sql.Rows) error
|
||||
}
|
||||
|
||||
// Scanners knows how to scan a single row
|
||||
type Scanners interface {
|
||||
ScanRow(*sql.Row) error
|
||||
}
|
||||
|
||||
// Store provides generic CRUD for a table.
|
||||
// T must implement Scanner for List and Scanners for Get.
|
||||
type Store[T Scanner] struct {
|
||||
db *DB
|
||||
table string
|
||||
columns []string
|
||||
scanFn func(*sql.Rows) (T, error)
|
||||
scanOneFn func(*sql.Row) (T, error)
|
||||
}
|
||||
|
||||
// NewStore creates a Store for the given table and columns.
|
||||
func NewStore[T Scanner](db *DB, table string, columns []string,
|
||||
scanFn func(*sql.Rows) (T, error),
|
||||
scanOneFn func(*sql.Row) (T, error)) *Store[T] {
|
||||
return &Store[T]{
|
||||
db: db,
|
||||
table: table,
|
||||
columns: columns,
|
||||
scanFn: scanFn,
|
||||
scanOneFn: scanOneFn,
|
||||
}
|
||||
}
|
||||
|
||||
// List returns all rows matching the where clause
|
||||
func (s *Store[T]) List(ctx context.Context, where string, args ...interface{}) ([]T, error) {
|
||||
query := fmt.Sprintf("SELECT %s FROM %s", strings.Join(s.columns, ", "), s.table)
|
||||
if where != "" {
|
||||
query += " WHERE " + where
|
||||
}
|
||||
query += " ORDER BY created_at DESC LIMIT 100"
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list %s: %w", s.table, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []T
|
||||
for rows.Next() {
|
||||
item, err := s.scanFn(rows)
|
||||
if err != nil {
|
||||
continue // skip bad rows, log in production
|
||||
}
|
||||
results = append(results, item)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Get returns a single row by ID
|
||||
func (s *Store[T]) Get(ctx context.Context, id string) (T, error) {
|
||||
var zero T
|
||||
query := fmt.Sprintf("SELECT %s FROM %s WHERE id = $1", strings.Join(s.columns, ", "), s.table)
|
||||
row := s.db.QueryRowContext(ctx, query, id)
|
||||
item, err := s.scanOneFn(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return zero, fmt.Errorf("%s not found", s.table)
|
||||
}
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("get %s: %w", s.table, err)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// Delete removes a row by ID
|
||||
func (s *Store[T]) Delete(ctx context.Context, id string) error {
|
||||
query := fmt.Sprintf("DELETE FROM %s WHERE id = $1", s.table)
|
||||
_, err := s.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete %s: %w", s.table, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper: pqArray handles nil slices
|
||||
func pqArray(a []string) interface{} {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return pq.Array(a)
|
||||
}
|
||||
|
||||
// Helper: now returns current time
|
||||
func now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// Helper: isZero checks if a value is zero
|
||||
func isZero(v interface{}) bool {
|
||||
return reflect.ValueOf(v).IsZero()
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDB_WithTx_Commit(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sdb := New(db)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("INSERT INTO test").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
err = sdb.WithTx(context.Background(), func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO test VALUES (1)")
|
||||
return err
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestDB_WithTx_Rollback(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sdb := New(db)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectRollback()
|
||||
|
||||
testErr := assert.AnError
|
||||
err = sdb.WithTx(context.Background(), func(tx *sql.Tx) error {
|
||||
return testErr
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// mockEntity for generic store tests
|
||||
type mockEntity struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (m *mockEntity) ScanRow(rows *sql.Rows) error {
|
||||
return rows.Scan(&m.ID, &m.Name)
|
||||
}
|
||||
|
||||
func scanRows(rows *sql.Rows) (*mockEntity, error) {
|
||||
m := &mockEntity{}
|
||||
err := m.ScanRow(rows)
|
||||
return m, err
|
||||
}
|
||||
|
||||
func scanRow(row *sql.Row) (*mockEntity, error) {
|
||||
m := &mockEntity{}
|
||||
err := row.Scan(&m.ID, &m.Name)
|
||||
return m, err
|
||||
}
|
||||
|
||||
func TestStore_List(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sdb := New(db)
|
||||
store := NewStore(sdb, "test_table", []string{"id", "name"}, scanRows, scanRow)
|
||||
|
||||
mock.ExpectQuery("SELECT id, name FROM test_table").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).
|
||||
AddRow("1", "Alice").
|
||||
AddRow("2", "Bob"))
|
||||
|
||||
results, err := store.List(context.Background(), "")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
assert.Equal(t, "Alice", results[0].Name)
|
||||
assert.Equal(t, "Bob", results[1].Name)
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestStore_List_WithWhere(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sdb := New(db)
|
||||
store := NewStore(sdb, "test_table", []string{"id", "name"}, scanRows, scanRow)
|
||||
|
||||
mock.ExpectQuery("SELECT id, name FROM test_table WHERE status = \\$1 ORDER BY created_at DESC LIMIT 100").
|
||||
WithArgs("active").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow("1", "Alice"))
|
||||
|
||||
results, err := store.List(context.Background(), "status = $1", "active")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestStore_Get(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sdb := New(db)
|
||||
store := NewStore(sdb, "test_table", []string{"id", "name"}, scanRows, scanRow)
|
||||
|
||||
mock.ExpectQuery("SELECT id, name FROM test_table WHERE id = \\$1").
|
||||
WithArgs("1").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow("1", "Alice"))
|
||||
|
||||
result, err := store.Get(context.Background(), "1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestStore_Get_NotFound(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sdb := New(db)
|
||||
store := NewStore(sdb, "test_table", []string{"id", "name"},
|
||||
func(rows *sql.Rows) (*mockEntity, error) { return nil, nil },
|
||||
scanRow,
|
||||
)
|
||||
|
||||
mock.ExpectQuery("SELECT id, name FROM test_table WHERE id = \\$1").
|
||||
WithArgs("999").
|
||||
WillReturnError(sql.ErrNoRows)
|
||||
|
||||
_, err = store.Get(context.Background(), "999")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestStore_Delete(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sdb := New(db)
|
||||
store := NewStore(sdb, "test_table", []string{"id", "name"},
|
||||
func(rows *sql.Rows) (*mockEntity, error) { return nil, nil },
|
||||
func(row *sql.Row) (*mockEntity, error) { return nil, nil },
|
||||
)
|
||||
|
||||
mock.ExpectExec("DELETE FROM test_table WHERE id = \\$1").
|
||||
WithArgs("1").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err = store.Delete(context.Background(), "1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "http://localhost:9096"
|
||||
// NOTE: This token is signed with a test secret. For integration tests,
|
||||
// set JWT_SECRET env var to match the signing key used here.
|
||||
// To generate a valid token: jwt sign --secret "test-secret" '{"user_id":"test","email":"test@example.com","role":"admin"}'
|
||||
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGVzdCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiJ9.test"
|
||||
)
|
||||
|
||||
// BenchmarkHealthCheck - simple health endpoint
|
||||
func BenchmarkHealthCheck(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
resp, err := http.Get(baseURL + "/health")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLogin - auth endpoint
|
||||
func BenchmarkLogin(b *testing.B) {
|
||||
payload := map[string]string{
|
||||
"email": "test@example.com",
|
||||
"password": "testpass",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
resp, err := http.Post(baseURL+"/api/v1/auth/login", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDashboard - protected endpoint with analytics
|
||||
func BenchmarkDashboard(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", baseURL+"/api/v1/analytics/dashboard", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkQuotesList - database query
|
||||
func BenchmarkQuotesList(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", baseURL+"/api/v1/sales/quotes", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Concurrent load test
|
||||
func TestConcurrentLoad(t *testing.T) {
|
||||
concurrency := 50
|
||||
requests := 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, concurrency*requests)
|
||||
start := time.Now()
|
||||
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func(worker int) {
|
||||
defer wg.Done()
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
for j := 0; j < requests; j++ {
|
||||
req, _ := http.NewRequest("GET", baseURL+"/health", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("worker %d req %d: %v", worker, j, err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
errors <- fmt.Errorf("worker %d req %d: status %d", worker, j, resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
duration := time.Since(start)
|
||||
totalRequests := concurrency * requests
|
||||
rps := float64(totalRequests) / duration.Seconds()
|
||||
|
||||
errCount := 0
|
||||
for err := range errors {
|
||||
if errCount < 5 {
|
||||
t.Logf("Error: %v", err)
|
||||
}
|
||||
errCount++
|
||||
}
|
||||
|
||||
t.Logf("Total: %d requests in %v (%.0f req/sec)", totalRequests, duration, rps)
|
||||
t.Logf("Errors: %d (%.2f%%)", errCount, float64(errCount)/float64(totalRequests)*100)
|
||||
|
||||
if errCount > totalRequests/10 {
|
||||
t.Fatalf("Too many errors: %d", errCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFullWorkflow - complete business flow
|
||||
func TestFullWorkflow(t *testing.T) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// 1. Login
|
||||
loginPayload := map[string]string{
|
||||
"email": "test@example.com",
|
||||
"password": "testpass",
|
||||
}
|
||||
body, _ := json.Marshal(loginPayload)
|
||||
resp, err := client.Post(baseURL+"/api/v1/auth/login", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("Login failed: %v", err)
|
||||
}
|
||||
var loginResp map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&loginResp)
|
||||
resp.Body.Close()
|
||||
|
||||
authToken, ok := loginResp["token"].(string)
|
||||
if !ok {
|
||||
t.Fatal("No token in response")
|
||||
}
|
||||
t.Logf("✓ Login successful")
|
||||
|
||||
// 2. Create customer
|
||||
customerPayload := map[string]interface{}{
|
||||
"name": "Stress Test AB",
|
||||
"email": "stress@test.com",
|
||||
"phone": "+46701234567",
|
||||
"address": "Testgatan 1, Stockholm",
|
||||
}
|
||||
body, _ = json.Marshal(customerPayload)
|
||||
req, _ := http.NewRequest("POST", baseURL+"/api/v1/crm/customers", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Create customer failed: %v", err)
|
||||
}
|
||||
var customerResp map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&customerResp)
|
||||
resp.Body.Close()
|
||||
customerID := customerResp["id"].(string)
|
||||
t.Logf("✓ Customer created: %s", customerID)
|
||||
|
||||
// 3. Create quote
|
||||
quotePayload := map[string]interface{}{
|
||||
"customer_id": customerID,
|
||||
"title": "Stress Test Quote",
|
||||
"items": []map[string]interface{}{
|
||||
{
|
||||
"description": "Test Product",
|
||||
"quantity": 10,
|
||||
"unit_price": 1000.00,
|
||||
"tax_rate": 25.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ = json.Marshal(quotePayload)
|
||||
req, _ = http.NewRequest("POST", baseURL+"/api/v1/sales/quotes", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Create quote failed: %v", err)
|
||||
}
|
||||
var quoteResp map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode("eResp)
|
||||
resp.Body.Close()
|
||||
quoteID := quoteResp["id"].(string)
|
||||
t.Logf("✓ Quote created: %s", quoteID)
|
||||
|
||||
// 4. Accept quote
|
||||
req, _ = http.NewRequest("POST", baseURL+"/api/v1/sales/quotes/"+quoteID+"/accept", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Accept quote failed: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
t.Logf("✓ Quote accepted")
|
||||
|
||||
// 5. Convert to order
|
||||
req, _ = http.NewRequest("POST", baseURL+"/api/v1/sales/quotes/"+quoteID+"/convert", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Convert quote failed: %v", err)
|
||||
}
|
||||
var orderResp map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&orderResp)
|
||||
resp.Body.Close()
|
||||
t.Logf("✓ Quote converted to order: %s", orderResp["order_id"])
|
||||
|
||||
// 6. Get dashboard
|
||||
req, _ = http.NewRequest("GET", baseURL+"/api/v1/analytics/dashboard", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Dashboard failed: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
t.Logf("✓ Dashboard loaded")
|
||||
|
||||
t.Logf("\n=== WORKFLOW COMPLETE ===")
|
||||
}
|
||||
|
||||
// TestPDFGeneration - stress PDF generation
|
||||
func TestPDFGeneration(t *testing.T) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// Get existing quote
|
||||
req, _ := http.NewRequest("GET", baseURL+"/api/v1/sales/quotes", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("List quotes failed: %v", err)
|
||||
}
|
||||
var quotesResp map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode("esResp)
|
||||
resp.Body.Close()
|
||||
|
||||
quotes := quotesResp["quotes"].([]interface{})
|
||||
if len(quotes) == 0 {
|
||||
t.Skip("No quotes to test")
|
||||
}
|
||||
|
||||
quoteID := quotes[0].(map[string]interface{})["id"].(string)
|
||||
|
||||
// Generate PDF
|
||||
start := time.Now()
|
||||
req, _ = http.NewRequest("GET", baseURL+"/api/v1/sales/quotes/"+quoteID+"/pdf", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("PDF generation failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("PDF generation returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
t.Logf("✓ PDF generated in %v (status: %d, content-type: %s)", duration, resp.StatusCode, resp.Header.Get("Content-Type"))
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# Build stage
|
||||
FROM gcc:14 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy source code
|
||||
COPY src ./src
|
||||
|
||||
# Build shared library
|
||||
RUN gcc -shared -fPIC -O3 -o libboc_ipc.so src/ipc.c \
|
||||
-lpthread -lrt
|
||||
|
||||
# Build static library
|
||||
RUN gcc -c -O3 -o ipc.o src/ipc.c && \
|
||||
ar rcs libboc_ipc.a ipc.o
|
||||
|
||||
# Final stage - minimal runtime
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy libraries
|
||||
COPY --from=builder /app/libboc_ipc.so /usr/local/lib/
|
||||
COPY --from=builder /app/libboc_ipc.a /usr/local/lib/
|
||||
COPY --from=builder /app/src/ipc.h /usr/local/include/
|
||||
|
||||
# Update library cache
|
||||
RUN ldconfig /usr/local/lib || true
|
||||
|
||||
# Default command - keep container running for IPC
|
||||
CMD ["sh", "-c", "echo 'BOC C Runtime ready' && tail -f /dev/null"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,363 +0,0 @@
|
||||
#include "ipc.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
// Shared memory implementation
|
||||
int boc_shm_create(const char* name, size_t size) {
|
||||
int fd = shm_open(name, O_CREAT | O_RDWR | O_EXCL, 0644);
|
||||
if (fd < 0) {
|
||||
if (errno == EEXIST) {
|
||||
// Already exists, try to open
|
||||
return boc_shm_open(name);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ftruncate(fd, size) < 0) {
|
||||
close(fd);
|
||||
shm_unlink(name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
int boc_shm_open(const char* name) {
|
||||
int fd = shm_open(name, O_RDWR, 0644);
|
||||
return fd;
|
||||
}
|
||||
|
||||
void* boc_shm_map(int fd, size_t size) {
|
||||
void* addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (addr == MAP_FAILED) {
|
||||
return NULL;
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
|
||||
int boc_shm_unmap(void* addr, size_t size) {
|
||||
return munmap(addr, size);
|
||||
}
|
||||
|
||||
int boc_shm_destroy(const char* name) {
|
||||
return shm_unlink(name);
|
||||
}
|
||||
|
||||
// Ring buffer implementation
|
||||
int boc_ring_init(boc_ring_buffer_t* ring) {
|
||||
if (!ring) return -1;
|
||||
|
||||
ring->write_idx = 0;
|
||||
ring->read_idx = 0;
|
||||
ring->flags = 0;
|
||||
memset(ring->data, 0, BOC_RING_SIZE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int boc_ring_write(boc_ring_buffer_t* ring, const void* data, size_t len) {
|
||||
if (!ring || !data || len == 0 || len > BOC_MAX_MSG_SIZE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t write_idx = ring->write_idx;
|
||||
uint64_t read_idx = ring->read_idx;
|
||||
|
||||
// Check available space (leave 1 byte gap to distinguish full from empty)
|
||||
uint64_t available = (read_idx > write_idx)
|
||||
? (read_idx - write_idx - 1)
|
||||
: (BOC_RING_SIZE - write_idx + read_idx - 1);
|
||||
|
||||
// Need space for length (4 bytes) + data
|
||||
size_t total_len = sizeof(uint32_t) + len;
|
||||
if (available < total_len) {
|
||||
return -1; // Buffer full
|
||||
}
|
||||
|
||||
// Write length prefix
|
||||
uint32_t len32 = (uint32_t)len;
|
||||
for (size_t i = 0; i < sizeof(uint32_t); i++) {
|
||||
ring->data[write_idx % BOC_RING_SIZE] = ((char*)&len32)[i];
|
||||
write_idx++;
|
||||
}
|
||||
|
||||
// Write data
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
ring->data[write_idx % BOC_RING_SIZE] = ((char*)data)[i];
|
||||
write_idx++;
|
||||
}
|
||||
|
||||
// Memory barrier to ensure data is written before updating index
|
||||
__sync_synchronize();
|
||||
ring->write_idx = write_idx;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int boc_ring_read(boc_ring_buffer_t* ring, void* data, size_t max_len) {
|
||||
if (!ring || !data || max_len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t write_idx = ring->write_idx;
|
||||
uint64_t read_idx = ring->read_idx;
|
||||
|
||||
if (write_idx == read_idx) {
|
||||
return 0; // Empty
|
||||
}
|
||||
|
||||
// Read length prefix
|
||||
uint32_t len = 0;
|
||||
for (size_t i = 0; i < sizeof(uint32_t); i++) {
|
||||
((char*)&len)[i] = ring->data[read_idx % BOC_RING_SIZE];
|
||||
read_idx++;
|
||||
}
|
||||
|
||||
if (len > max_len) {
|
||||
return -1; // Buffer too small
|
||||
}
|
||||
|
||||
// Read data
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
((char*)data)[i] = ring->data[read_idx % BOC_RING_SIZE];
|
||||
read_idx++;
|
||||
}
|
||||
|
||||
// Memory barrier
|
||||
__sync_synchronize();
|
||||
ring->read_idx = read_idx;
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
bool boc_ring_empty(boc_ring_buffer_t* ring) {
|
||||
return ring->write_idx == ring->read_idx;
|
||||
}
|
||||
|
||||
uint64_t boc_ring_available(boc_ring_buffer_t* ring) {
|
||||
uint64_t write_idx = ring->write_idx;
|
||||
uint64_t read_idx = ring->read_idx;
|
||||
|
||||
if (write_idx >= read_idx) {
|
||||
return write_idx - read_idx;
|
||||
} else {
|
||||
return BOC_RING_SIZE - read_idx + write_idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Message serialization
|
||||
size_t boc_msg_serialize(boc_msg_t* msg, char* buf, size_t buf_size) {
|
||||
if (!msg || !buf || buf_size < sizeof(boc_msg_t)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t total_size = sizeof(boc_msg_t) + msg->length;
|
||||
if (buf_size < total_size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(buf, msg, sizeof(boc_msg_t));
|
||||
if (msg->length > 0 && msg->payload) {
|
||||
memcpy(buf + sizeof(boc_msg_t), msg->payload, msg->length);
|
||||
}
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
int boc_msg_deserialize(const char* buf, size_t len, boc_msg_t** msg) {
|
||||
if (!buf || len < sizeof(boc_msg_t) || !msg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
boc_msg_t* header = (boc_msg_t*)buf;
|
||||
size_t total_size = sizeof(boc_msg_t) + header->length;
|
||||
|
||||
if (len < total_size) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*msg = malloc(total_size);
|
||||
if (!*msg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(*msg, buf, total_size);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void boc_msg_free(boc_msg_t* msg) {
|
||||
free(msg);
|
||||
}
|
||||
|
||||
// High-level IPC channel
|
||||
struct boc_ipc_channel {
|
||||
char name[256];
|
||||
boc_ring_buffer_t* request_ring;
|
||||
boc_ring_buffer_t* response_ring;
|
||||
int shm_fd;
|
||||
void* shm_addr;
|
||||
size_t shm_size;
|
||||
};
|
||||
|
||||
boc_ipc_channel_t* boc_ipc_connect(const char* channel_name) {
|
||||
boc_ipc_channel_t* channel = calloc(1, sizeof(boc_ipc_channel_t));
|
||||
if (!channel) return NULL;
|
||||
|
||||
strncpy(channel->name, channel_name, sizeof(channel->name) - 1);
|
||||
|
||||
// Create shared memory for two ring buffers
|
||||
channel->shm_size = sizeof(boc_ring_buffer_t) * 2;
|
||||
|
||||
char shm_name[512];
|
||||
snprintf(shm_name, sizeof(shm_name), "/boc_ipc_%s", channel_name);
|
||||
|
||||
channel->shm_fd = boc_shm_create(shm_name, channel->shm_size);
|
||||
if (channel->shm_fd < 0) {
|
||||
free(channel);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
channel->shm_addr = boc_shm_map(channel->shm_fd, channel->shm_size);
|
||||
if (!channel->shm_addr) {
|
||||
close(channel->shm_fd);
|
||||
shm_unlink(shm_name);
|
||||
free(channel);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Initialize rings
|
||||
channel->request_ring = (boc_ring_buffer_t*)channel->shm_addr;
|
||||
channel->response_ring = (boc_ring_buffer_t*)(channel->shm_addr + sizeof(boc_ring_buffer_t));
|
||||
|
||||
boc_ring_init(channel->request_ring);
|
||||
boc_ring_init(channel->response_ring);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
void boc_ipc_disconnect(boc_ipc_channel_t* channel) {
|
||||
if (!channel) return;
|
||||
|
||||
if (channel->shm_addr) {
|
||||
boc_shm_unmap(channel->shm_addr, channel->shm_size);
|
||||
}
|
||||
if (channel->shm_fd >= 0) {
|
||||
close(channel->shm_fd);
|
||||
}
|
||||
|
||||
char shm_name[512];
|
||||
snprintf(shm_name, sizeof(shm_name), "/boc_ipc_%s", channel->name);
|
||||
boc_shm_destroy(shm_name);
|
||||
|
||||
free(channel);
|
||||
}
|
||||
|
||||
int boc_ipc_send(boc_ipc_channel_t* channel, boc_msg_t* msg) {
|
||||
if (!channel || !msg) return -1;
|
||||
|
||||
char buf[BOC_MAX_MSG_SIZE];
|
||||
size_t len = boc_msg_serialize(msg, buf, sizeof(buf));
|
||||
if (len == 0) return -1;
|
||||
|
||||
return boc_ring_write(channel->request_ring, buf, len);
|
||||
}
|
||||
|
||||
int boc_ipc_recv(boc_ipc_channel_t* channel, boc_msg_t** msg, int timeout_ms) {
|
||||
if (!channel || !msg) return -1;
|
||||
|
||||
char buf[BOC_MAX_MSG_SIZE];
|
||||
|
||||
// Simple polling with timeout
|
||||
int waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
int len = boc_ring_read(channel->response_ring, buf, sizeof(buf));
|
||||
if (len > 0) {
|
||||
return boc_msg_deserialize(buf, len, msg);
|
||||
}
|
||||
|
||||
usleep(1000); // 1ms
|
||||
waited += 1;
|
||||
}
|
||||
|
||||
return -1; // Timeout
|
||||
}
|
||||
|
||||
// Analytics cache implementation
|
||||
int boc_cache_init(boc_analytics_cache_t* cache) {
|
||||
if (!cache) return -1;
|
||||
|
||||
cache->version = 1;
|
||||
for (int i = 0; i < BOC_CACHE_SIZE; i++) {
|
||||
cache->entries[i].valid = false;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t hash_key(uint64_t key_hash) {
|
||||
return key_hash % BOC_CACHE_SIZE;
|
||||
}
|
||||
|
||||
int boc_cache_get(boc_analytics_cache_t* cache, uint64_t key_hash, double* value, double* trend) {
|
||||
if (!cache || !value || !trend) return -1;
|
||||
|
||||
uint64_t idx = hash_key(key_hash);
|
||||
boc_cache_entry_t* entry = &cache->entries[idx];
|
||||
|
||||
if (!entry->valid || entry->key_hash != key_hash) {
|
||||
return -1; // Not found
|
||||
}
|
||||
|
||||
// Check TTL
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
uint64_t now = ts.tv_sec;
|
||||
|
||||
if (now > entry->timestamp + entry->ttl_seconds) {
|
||||
entry->valid = false;
|
||||
return -1; // Expired
|
||||
}
|
||||
|
||||
*value = entry->value;
|
||||
*trend = entry->trend;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int boc_cache_set(boc_analytics_cache_t* cache, uint64_t key_hash, double value, double trend, uint32_t ttl) {
|
||||
if (!cache) return -1;
|
||||
|
||||
uint64_t idx = hash_key(key_hash);
|
||||
boc_cache_entry_t* entry = &cache->entries[idx];
|
||||
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
|
||||
entry->key_hash = key_hash;
|
||||
entry->value = value;
|
||||
entry->trend = trend;
|
||||
entry->timestamp = ts.tv_sec;
|
||||
entry->ttl_seconds = ttl;
|
||||
entry->valid = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void boc_cache_invalidate(boc_analytics_cache_t* cache, uint64_t key_hash) {
|
||||
if (!cache) return;
|
||||
|
||||
uint64_t idx = hash_key(key_hash);
|
||||
boc_cache_entry_t* entry = &cache->entries[idx];
|
||||
|
||||
if (entry->key_hash == key_hash) {
|
||||
entry->valid = false;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
#ifndef BOC_IPC_H
|
||||
#define BOC_IPC_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Shared memory ring buffer for high-throughput communication
|
||||
#define BOC_SHM_SIZE (1024 * 1024 * 16) // 16MB
|
||||
#define BOC_RING_SIZE (1024 * 64) // 64KB buffer
|
||||
#define BOC_MAX_MSG_SIZE 8192
|
||||
|
||||
typedef enum {
|
||||
BOC_MSG_ANALYTICS_REQUEST = 1,
|
||||
BOC_MSG_ANALYTICS_RESPONSE = 2,
|
||||
BOC_MSG_REPORT_REQUEST = 3,
|
||||
BOC_MSG_REPORT_RESPONSE = 4,
|
||||
BOC_MSG_EVENT = 5,
|
||||
BOC_MSG_HEARTBEAT = 6,
|
||||
} boc_msg_type_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t type;
|
||||
uint32_t length;
|
||||
uint64_t timestamp;
|
||||
uint64_t correlation_id;
|
||||
char payload[];
|
||||
} boc_msg_t;
|
||||
|
||||
typedef struct {
|
||||
volatile uint64_t write_idx;
|
||||
volatile uint64_t read_idx;
|
||||
volatile uint32_t flags;
|
||||
char data[BOC_RING_SIZE];
|
||||
} boc_ring_buffer_t;
|
||||
|
||||
// Shared memory API
|
||||
int boc_shm_create(const char* name, size_t size);
|
||||
int boc_shm_open(const char* name);
|
||||
void* boc_shm_map(int fd, size_t size);
|
||||
int boc_shm_unmap(void* addr, size_t size);
|
||||
int boc_shm_destroy(const char* name);
|
||||
|
||||
// Ring buffer API
|
||||
int boc_ring_init(boc_ring_buffer_t* ring);
|
||||
int boc_ring_write(boc_ring_buffer_t* ring, const void* data, size_t len);
|
||||
int boc_ring_read(boc_ring_buffer_t* ring, void* data, size_t max_len);
|
||||
bool boc_ring_empty(boc_ring_buffer_t* ring);
|
||||
uint64_t boc_ring_available(boc_ring_buffer_t* ring);
|
||||
|
||||
// Message serialization
|
||||
size_t boc_msg_serialize(boc_msg_t* msg, char* buf, size_t buf_size);
|
||||
int boc_msg_deserialize(const char* buf, size_t len, boc_msg_t** msg);
|
||||
void boc_msg_free(boc_msg_t* msg);
|
||||
|
||||
// High-level API for Go/Rust interop
|
||||
typedef struct boc_ipc_channel boc_ipc_channel_t;
|
||||
|
||||
boc_ipc_channel_t* boc_ipc_connect(const char* channel_name);
|
||||
void boc_ipc_disconnect(boc_ipc_channel_t* channel);
|
||||
int boc_ipc_send(boc_ipc_channel_t* channel, boc_msg_t* msg);
|
||||
int boc_ipc_recv(boc_ipc_channel_t* channel, boc_msg_t** msg, int timeout_ms);
|
||||
|
||||
// Analytics cache in shared memory
|
||||
typedef struct {
|
||||
uint64_t key_hash;
|
||||
double value;
|
||||
double trend;
|
||||
uint64_t timestamp;
|
||||
uint32_t ttl_seconds;
|
||||
bool valid;
|
||||
} boc_cache_entry_t;
|
||||
|
||||
#define BOC_CACHE_SIZE 1024
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t version;
|
||||
boc_cache_entry_t entries[BOC_CACHE_SIZE];
|
||||
} boc_analytics_cache_t;
|
||||
|
||||
int boc_cache_init(boc_analytics_cache_t* cache);
|
||||
int boc_cache_get(boc_analytics_cache_t* cache, uint64_t key_hash, double* value, double* trend);
|
||||
int boc_cache_set(boc_analytics_cache_t* cache, uint64_t key_hash, double value, double trend, uint32_t ttl);
|
||||
void boc_cache_invalidate(boc_analytics_cache_t* cache, uint64_t key_hash);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // BOC_IPC_H
|
||||
+6
-107
@@ -1,16 +1,16 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: boc-postgres
|
||||
environment:
|
||||
POSTGRES_USER: boc
|
||||
POSTGRES_PASSWORD: boc_secret_2026
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-boc_secret_2026}
|
||||
POSTGRES_DB: boc
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./backend/db/migrations:/docker-entrypoint-initdb.d:ro
|
||||
ports:
|
||||
- "5435:5432"
|
||||
healthcheck:
|
||||
@@ -21,7 +21,6 @@ services:
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
# Redis — Cache, Sessions, Rate Limiting, Pub/Sub
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: boc-redis
|
||||
@@ -38,55 +37,6 @@ services:
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
# Kafka — Event Streaming
|
||||
zookeeper:
|
||||
image: confluentinc/cp-zookeeper:7.5.0
|
||||
container_name: boc-zookeeper
|
||||
environment:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ZOOKEEPER_TICK_TIME: 2000
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
kafka:
|
||||
image: confluentinc/cp-kafka:7.5.0
|
||||
container_name: boc-kafka
|
||||
depends_on:
|
||||
- zookeeper
|
||||
ports:
|
||||
- "9095:9092"
|
||||
environment:
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://kafka:29092
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT_INTERNAL
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
healthcheck:
|
||||
test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:9092"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
# Kafka UI
|
||||
kafka-ui:
|
||||
image: provectuslabs/kafka-ui:latest
|
||||
container_name: boc-kafka-ui
|
||||
ports:
|
||||
- "8084:8080"
|
||||
environment:
|
||||
KAFKA_CLUSTERS_0_NAME: boc
|
||||
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
|
||||
KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
|
||||
depends_on:
|
||||
- kafka
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
# Go Backend API
|
||||
boc-api:
|
||||
build:
|
||||
context: ./backend
|
||||
@@ -94,13 +44,13 @@ services:
|
||||
container_name: boc-api
|
||||
environment:
|
||||
PORT: "9092"
|
||||
DB_URL: "postgres://boc:boc_secret_2026@postgres:5432/boc?sslmode=disable"
|
||||
JWT_SECRET: "boc_jwt_secret_change_in_production"
|
||||
DB_URL: "postgres://boc:${DB_PASSWORD:-boc_secret_2026}@postgres:5432/boc?sslmode=disable"
|
||||
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}
|
||||
AMOS_BASE_URL: "http://aamos-ledger:3250"
|
||||
MIGRATIONS_DIR: "./db/migrations"
|
||||
RUST_SERVICE_URL: "http://boc-rust:9093"
|
||||
REDIS_URL: "redis://redis:6379"
|
||||
KAFKA_BROKERS: "kafka:29092"
|
||||
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||
FROM_EMAIL: ${FROM_EMAIL:-noreply@landvex.com}
|
||||
ports:
|
||||
- "9096:9092"
|
||||
depends_on:
|
||||
@@ -108,8 +58,6 @@ services:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./backend/db/migrations:/app/db/migrations:ro
|
||||
restart: unless-stopped
|
||||
@@ -121,54 +69,6 @@ services:
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
# Rust Analytics Service
|
||||
boc-rust:
|
||||
build:
|
||||
context: ./rust-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: boc-rust
|
||||
environment:
|
||||
RUST_LOG: "info"
|
||||
DB_URL: "postgres://boc:boc_secret_2026@postgres:5432/boc?sslmode=disable"
|
||||
REDIS_URL: "redis://redis:6379"
|
||||
KAFKA_BROKERS: "kafka:29092"
|
||||
ports:
|
||||
- "9093:9093"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9093/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
# C Runtime (shared memory / IPC)
|
||||
boc-c-runtime:
|
||||
build:
|
||||
context: ./c-runtime
|
||||
dockerfile: Dockerfile
|
||||
container_name: boc-c-runtime
|
||||
environment:
|
||||
SHM_NAME: "/boc_ipc_main"
|
||||
SHM_SIZE: "16777216"
|
||||
depends_on:
|
||||
- boc-api
|
||||
- boc-rust
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
shm_size: '32mb'
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
# Nginx Reverse Proxy
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: boc-nginx
|
||||
@@ -180,7 +80,6 @@ services:
|
||||
- ./web:/usr/share/nginx/html:ro
|
||||
depends_on:
|
||||
- boc-api
|
||||
- boc-rust
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- boc-network
|
||||
|
||||
Generated
-1446
@@ -1,1446 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower 0.5.3",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boc-rust-service"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"libc",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tower 0.4.13",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"ctutils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-system-configuration"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
|
||||
dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "postgres-protocol"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"fallible-iterator",
|
||||
"hmac",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"rand",
|
||||
"sha2",
|
||||
"stringprep",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postgres-types"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"fallible-iterator",
|
||||
"postgres-protocol",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-postgres"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"fallible-iterator",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"phf",
|
||||
"pin-project-lite",
|
||||
"postgres-protocol",
|
||||
"postgres-types",
|
||||
"rand",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.7+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
|
||||
dependencies = [
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42"
|
||||
dependencies = [
|
||||
"wasi 0.14.7+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"objc2-system-configuration",
|
||||
"wasite",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
name = "boc-rust-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.35", features = ["full", "rt-multi-thread"] }
|
||||
tokio-postgres = { version = "0.7", features = ["with-uuid-1", "with-serde_json-1", "with-chrono-0_4"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
axum = "0.7"
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.6", features = ["serde", "v4"] }
|
||||
dashmap = "5.5"
|
||||
rayon = "1.8"
|
||||
libc = "0.2"
|
||||
|
||||
[lib]
|
||||
name = "boc_rust"
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "boc-rust-service"
|
||||
path = "src/main.rs"
|
||||
@@ -1,40 +0,0 @@
|
||||
# Build stage
|
||||
FROM rust:1.85-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy Cargo files
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
# Create dummy files to cache dependencies
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs && echo "pub fn dummy() {}" > src/lib.rs
|
||||
RUN cargo build --release && rm -rf src
|
||||
|
||||
# Copy actual source code
|
||||
COPY src ./src
|
||||
|
||||
# Build the actual binary
|
||||
RUN touch src/main.rs && cargo build --release
|
||||
|
||||
# Final stage
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates wget && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/target/release/boc-rust-service .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 9093
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget -q --spider http://localhost:9093/health || exit 1
|
||||
|
||||
# Run the binary
|
||||
CMD ["./boc-rust-service"]
|
||||
@@ -1,173 +0,0 @@
|
||||
use dashmap::DashMap;
|
||||
use rayon::prelude::*;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct AnalyticsResult {
|
||||
pub value: f64,
|
||||
pub trend: f64,
|
||||
pub breakdown: Vec<(String, f64)>,
|
||||
}
|
||||
|
||||
pub struct AnalyticsEngine {
|
||||
cache: DashMap<String, (AnalyticsResult, std::time::Instant)>,
|
||||
}
|
||||
|
||||
impl AnalyticsEngine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cache: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query(&self, tenant_id: &str, metric: &str, period: &str) -> AnalyticsResult {
|
||||
let cache_key = format!("{}:{}:{}", tenant_id, metric, period);
|
||||
|
||||
// Check cache (5 minute TTL)
|
||||
if let Some(entry) = self.cache.get(&cache_key) {
|
||||
if entry.value().1.elapsed().as_secs() < 300 {
|
||||
return AnalyticsResult {
|
||||
value: entry.value().0.value,
|
||||
trend: entry.value().0.trend,
|
||||
breakdown: entry.value().0.breakdown.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Compute analytics (parallel processing for large datasets)
|
||||
let result = self.compute_metric(tenant_id, metric, period).await;
|
||||
|
||||
// Cache result
|
||||
self.cache.insert(cache_key, (result.clone(), std::time::Instant::now()));
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn compute_metric(&self, tenant_id: &str, metric: &str, period: &str) -> AnalyticsResult {
|
||||
match metric {
|
||||
"mrr" => self.compute_mrr(tenant_id, period).await,
|
||||
"arr" => self.compute_arr(tenant_id, period).await,
|
||||
"churn" => self.compute_churn(tenant_id, period).await,
|
||||
"ltv" => self.compute_ltv(tenant_id, period).await,
|
||||
"cac" => self.compute_cac(tenant_id, period).await,
|
||||
"pipeline_value" => self.compute_pipeline(tenant_id, period).await,
|
||||
"conversion_rate" => self.compute_conversion(tenant_id, period).await,
|
||||
_ => AnalyticsResult {
|
||||
value: 0.0,
|
||||
trend: 0.0,
|
||||
breakdown: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_mrr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {
|
||||
// TODO: Query from database
|
||||
// For now, return demo data
|
||||
AnalyticsResult {
|
||||
value: 53333.0,
|
||||
trend: 0.05,
|
||||
breakdown: vec![
|
||||
("Subscriptions".to_string(), 45000.0),
|
||||
("Add-ons".to_string(), 8333.0),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_arr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {
|
||||
AnalyticsResult {
|
||||
value: 640000.0,
|
||||
trend: 0.12,
|
||||
breakdown: vec![
|
||||
("Enterprise".to_string(), 400000.0),
|
||||
("Professional".to_string(), 180000.0),
|
||||
("Basic".to_string(), 60000.0),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_churn(&self, tenant_id: &str, period: &str) -> AnalyticsResult {
|
||||
AnalyticsResult {
|
||||
value: 0.02,
|
||||
trend: -0.005,
|
||||
breakdown: vec![
|
||||
("Voluntary".to_string(), 0.012),
|
||||
("Involuntary".to_string(), 0.008),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_ltv(&self, tenant_id: &str, period: &str) -> AnalyticsResult {
|
||||
AnalyticsResult {
|
||||
value: 125000.0,
|
||||
trend: 0.08,
|
||||
breakdown: vec![
|
||||
("Enterprise".to_string(), 250000.0),
|
||||
("Professional".to_string(), 100000.0),
|
||||
("Basic".to_string(), 25000.0),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_cac(&self, tenant_id: &str, period: &str) -> AnalyticsResult {
|
||||
AnalyticsResult {
|
||||
value: 15000.0,
|
||||
trend: -0.03,
|
||||
breakdown: vec![
|
||||
("Marketing".to_string(), 8000.0),
|
||||
("Sales".to_string(), 5000.0),
|
||||
("Partners".to_string(), 2000.0),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_pipeline(&self, tenant_id: &str, period: &str) -> AnalyticsResult {
|
||||
AnalyticsResult {
|
||||
value: 850000.0,
|
||||
trend: 0.15,
|
||||
breakdown: vec![
|
||||
("Prospect".to_string(), 200000.0),
|
||||
("Qualified".to_string(), 300000.0),
|
||||
("Proposal".to_string(), 250000.0),
|
||||
("Negotiation".to_string(), 100000.0),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_conversion(&self, tenant_id: &str, period: &str) -> AnalyticsResult {
|
||||
AnalyticsResult {
|
||||
value: 0.25,
|
||||
trend: 0.02,
|
||||
breakdown: vec![
|
||||
("Lead→Qualified".to_string(), 0.45),
|
||||
("Qualified→Proposal".to_string(), 0.60),
|
||||
("Proposal→Closed".to_string(), 0.35),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch process multiple metrics in parallel using Rayon
|
||||
pub fn batch_compute(&self, tenant_id: &str, metrics: &[(&str, &str)]) -> Vec<AnalyticsResult> {
|
||||
metrics
|
||||
.par_iter()
|
||||
.map(|(metric, period)| {
|
||||
// Use tokio runtime to execute async code in parallel
|
||||
let rt = tokio::runtime::Handle::try_current()
|
||||
.unwrap_or_else(|_| tokio::runtime::Runtime::new().unwrap().handle().clone());
|
||||
|
||||
rt.block_on(async {
|
||||
self.query(tenant_id, metric, period).await
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for AnalyticsResult {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
value: self.value,
|
||||
trend: self.trend,
|
||||
breakdown: self.breakdown.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// IPC module for Go-Rust communication via shared memory
|
||||
// Placeholder for C FFI integration
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
|
||||
/// Initialize shared memory channel
|
||||
pub fn init_channel(name: &str) -> Result<(), String> {
|
||||
// TODO: Implement C FFI calls to libboc_ipc.so
|
||||
println!("Initializing IPC channel: {}", name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send message via shared memory
|
||||
pub fn send_message(channel: &str, data: &[u8]) -> Result<(), String> {
|
||||
// TODO: Implement C FFI calls
|
||||
println!("Sending {} bytes on channel: {}", data.len(), channel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive message from shared memory
|
||||
pub fn receive_message(channel: &str, timeout_ms: i32) -> Result<Vec<u8>, String> {
|
||||
// TODO: Implement C FFI calls
|
||||
println!("Receiving on channel: {} (timeout: {}ms)", channel, timeout_ms);
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// C FFI wrapper for Go integration
|
||||
#[no_mangle]
|
||||
pub extern "C" fn boc_ipc_send(channel: *const c_char, data: *const c_void, len: c_int) -> c_int {
|
||||
if channel.is_null() || data.is_null() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let channel_name = unsafe { CStr::from_ptr(channel).to_string_lossy() };
|
||||
let data_slice = unsafe { std::slice::from_raw_parts(data as *const u8, len as usize) };
|
||||
|
||||
match send_message(&channel_name, data_slice) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn boc_ipc_recv(channel: *const c_char, buf: *mut c_void, max_len: c_int, timeout_ms: c_int) -> c_int {
|
||||
if channel.is_null() || buf.is_null() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let channel_name = unsafe { CStr::from_ptr(channel).to_string_lossy() };
|
||||
|
||||
match receive_message(&channel_name, timeout_ms) {
|
||||
Ok(data) => {
|
||||
let len = std::cmp::min(data.len(), max_len as usize);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(data.as_ptr(), buf as *mut u8, len);
|
||||
}
|
||||
len as c_int
|
||||
}
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
Json,
|
||||
extract::State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, error};
|
||||
|
||||
mod analytics;
|
||||
mod reports;
|
||||
mod ipc;
|
||||
|
||||
use analytics::AnalyticsEngine;
|
||||
use reports::ReportGenerator;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
analytics: Arc<RwLock<AnalyticsEngine>>,
|
||||
reports: Arc<RwLock<ReportGenerator>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HealthResponse {
|
||||
status: String,
|
||||
service: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReportRequest {
|
||||
tenant_id: String,
|
||||
report_type: String,
|
||||
parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ReportResponse {
|
||||
report_id: String,
|
||||
status: String,
|
||||
data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnalyticsRequest {
|
||||
tenant_id: String,
|
||||
metric: String,
|
||||
period: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AnalyticsResponse {
|
||||
metric: String,
|
||||
value: f64,
|
||||
trend: f64,
|
||||
breakdown: Vec<BreakdownItem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct BreakdownItem {
|
||||
label: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("boc_rust_service=info")
|
||||
.init();
|
||||
|
||||
info!("BOC Rust Service starting...");
|
||||
|
||||
let state = AppState {
|
||||
analytics: Arc::new(RwLock::new(AnalyticsEngine::new())),
|
||||
reports: Arc::new(RwLock::new(ReportGenerator::new())),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/api/v1/reports/generate", post(generate_report))
|
||||
.route("/api/v1/analytics/query", post(query_analytics))
|
||||
.route("/api/v1/analytics/batch", post(batch_analytics))
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:9093")
|
||||
.await
|
||||
.expect("Failed to bind port 9093");
|
||||
|
||||
info!("BOC Rust Service listening on 0.0.0.0:9093");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("Server failed");
|
||||
}
|
||||
|
||||
async fn health_handler() -> Json<HealthResponse> {
|
||||
Json(HealthResponse {
|
||||
status: "ok".to_string(),
|
||||
service: "boc-rust-service".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn generate_report(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<ReportRequest>,
|
||||
) -> Json<ReportResponse> {
|
||||
info!("Generating report: {} for tenant: {}", req.report_type, req.tenant_id);
|
||||
|
||||
let reports = state.reports.read().await;
|
||||
match reports.generate(&req.tenant_id, &req.report_type, &req.parameters).await {
|
||||
Ok(data) => Json(ReportResponse {
|
||||
report_id: uuid::Uuid::new_v4().to_string(),
|
||||
status: "completed".to_string(),
|
||||
data: Some(data),
|
||||
}),
|
||||
Err(e) => {
|
||||
error!("Report generation failed: {}", e);
|
||||
Json(ReportResponse {
|
||||
report_id: uuid::Uuid::new_v4().to_string(),
|
||||
status: "failed".to_string(),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_analytics(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<AnalyticsRequest>,
|
||||
) -> Json<AnalyticsResponse> {
|
||||
let analytics = state.analytics.read().await;
|
||||
let result = analytics.query(&req.tenant_id, &req.metric, &req.period).await;
|
||||
|
||||
Json(AnalyticsResponse {
|
||||
metric: req.metric,
|
||||
value: result.value,
|
||||
trend: result.trend,
|
||||
breakdown: result.breakdown.into_iter()
|
||||
.map(|(label, value)| BreakdownItem { label, value })
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn batch_analytics(
|
||||
State(state): State<AppState>,
|
||||
Json(reqs): Json<Vec<AnalyticsRequest>>,
|
||||
) -> Json<Vec<AnalyticsResponse>> {
|
||||
let analytics = state.analytics.read().await;
|
||||
|
||||
let mut responses = Vec::with_capacity(reqs.len());
|
||||
for req in reqs {
|
||||
let result = analytics.query(&req.tenant_id, &req.metric, &req.period).await;
|
||||
responses.push(AnalyticsResponse {
|
||||
metric: req.metric.clone(),
|
||||
value: result.value,
|
||||
trend: result.trend,
|
||||
breakdown: result.breakdown.into_iter()
|
||||
.map(|(label, value)| BreakdownItem { label, value })
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
Json(responses)
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
Json,
|
||||
extract::State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, error};
|
||||
|
||||
mod analytics;
|
||||
mod reports;
|
||||
mod ipc;
|
||||
|
||||
use analytics::AnalyticsEngine;
|
||||
use reports::ReportGenerator;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
analytics: Arc<RwLock<AnalyticsEngine>>,
|
||||
reports: Arc<RwLock<ReportGenerator>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HealthResponse {
|
||||
status: String,
|
||||
service: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReportRequest {
|
||||
tenant_id: String,
|
||||
report_type: String,
|
||||
parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ReportResponse {
|
||||
report_id: String,
|
||||
status: String,
|
||||
data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnalyticsRequest {
|
||||
tenant_id: String,
|
||||
metric: String,
|
||||
period: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AnalyticsResponse {
|
||||
metric: String,
|
||||
value: f64,
|
||||
trend: f64,
|
||||
breakdown: Vec<BreakdownItem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct BreakdownItem {
|
||||
label: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("boc_rust_service=info")
|
||||
.init();
|
||||
|
||||
info!("BOC Rust Service starting...");
|
||||
|
||||
let state = AppState {
|
||||
analytics: Arc::new(RwLock::new(AnalyticsEngine::new())),
|
||||
reports: Arc::new(RwLock::new(ReportGenerator::new())),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/api/v1/reports/generate", post(generate_report))
|
||||
.route("/api/v1/analytics/query", post(query_analytics))
|
||||
.route("/api/v1/analytics/batch", post(batch_analytics))
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:9093")
|
||||
.await
|
||||
.expect("Failed to bind port 9093");
|
||||
|
||||
info!("BOC Rust Service listening on 0.0.0.0:9093");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("Server failed");
|
||||
}
|
||||
|
||||
async fn health_handler() -> Json<HealthResponse> {
|
||||
Json(HealthResponse {
|
||||
status: "ok".to_string(),
|
||||
service: "boc-rust-service".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn generate_report(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<ReportRequest>,
|
||||
) -> Json<ReportResponse> {
|
||||
info!("Generating report: {} for tenant: {}", req.report_type, req.tenant_id);
|
||||
|
||||
let reports = state.reports.read().await;
|
||||
match reports.generate(&req.tenant_id, &req.report_type, &req.parameters).await {
|
||||
Ok(data) => Json(ReportResponse {
|
||||
report_id: uuid::Uuid::new_v4().to_string(),
|
||||
status: "completed".to_string(),
|
||||
data: Some(data),
|
||||
}),
|
||||
Err(e) => {
|
||||
error!("Report generation failed: {}", e);
|
||||
Json(ReportResponse {
|
||||
report_id: uuid::Uuid::new_v4().to_string(),
|
||||
status: "failed".to_string(),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_analytics(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<AnalyticsRequest>,
|
||||
) -> Json<AnalyticsResponse> {
|
||||
let analytics = state.analytics.read().await;
|
||||
let result = analytics.query(&req.tenant_id, &req.metric, &req.period).await;
|
||||
|
||||
Json(AnalyticsResponse {
|
||||
metric: req.metric,
|
||||
value: result.value,
|
||||
trend: result.trend,
|
||||
breakdown: result.breakdown.into_iter()
|
||||
.map(|(label, value)| BreakdownItem { label, value })
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn batch_analytics(
|
||||
State(state): State<AppState>,
|
||||
Json(reqs): Json<Vec<AnalyticsRequest>>,
|
||||
) -> Json<Vec<AnalyticsResponse>> {
|
||||
let analytics = state.analytics.read().await;
|
||||
|
||||
let mut responses = Vec::with_capacity(reqs.len());
|
||||
for req in reqs {
|
||||
let result = analytics.query(&req.tenant_id, &req.metric, &req.period).await;
|
||||
responses.push(AnalyticsResponse {
|
||||
metric: req.metric.clone(),
|
||||
value: result.value,
|
||||
trend: result.trend,
|
||||
breakdown: result.breakdown.into_iter()
|
||||
.map(|(label, value)| BreakdownItem { label, value })
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
Json(responses)
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct ReportGenerator {
|
||||
templates: HashMap<String, ReportTemplate>,
|
||||
}
|
||||
|
||||
struct ReportTemplate {
|
||||
name: String,
|
||||
description: String,
|
||||
required_params: Vec<String>,
|
||||
}
|
||||
|
||||
impl ReportGenerator {
|
||||
pub fn new() -> Self {
|
||||
let mut templates = HashMap::new();
|
||||
|
||||
templates.insert("financial_summary".to_string(), ReportTemplate {
|
||||
name: "Financial Summary".to_string(),
|
||||
description: "Overview of financial performance".to_string(),
|
||||
required_params: vec!["period".to_string()],
|
||||
});
|
||||
|
||||
templates.insert("sales_pipeline".to_string(), ReportTemplate {
|
||||
name: "Sales Pipeline".to_string(),
|
||||
description: "Current sales pipeline analysis".to_string(),
|
||||
required_params: vec!["period".to_string()],
|
||||
});
|
||||
|
||||
templates.insert("customer_analytics".to_string(), ReportTemplate {
|
||||
name: "Customer Analytics".to_string(),
|
||||
description: "Customer metrics and trends".to_string(),
|
||||
required_params: vec!["period".to_string()],
|
||||
});
|
||||
|
||||
templates.insert("revenue_forecast".to_string(), ReportTemplate {
|
||||
name: "Revenue Forecast".to_string(),
|
||||
description: "Projected revenue based on pipeline".to_string(),
|
||||
required_params: vec!["period".to_string(), "method".to_string()],
|
||||
});
|
||||
|
||||
templates.insert("expense_breakdown".to_string(), ReportTemplate {
|
||||
name: "Expense Breakdown".to_string(),
|
||||
description: "Detailed expense analysis".to_string(),
|
||||
required_params: vec!["period".to_string()],
|
||||
});
|
||||
|
||||
templates.insert("cashflow_projection".to_string(), ReportTemplate {
|
||||
name: "Cashflow Projection".to_string(),
|
||||
description: "Projected cashflow for upcoming periods".to_string(),
|
||||
required_params: vec!["periods".to_string()],
|
||||
});
|
||||
|
||||
Self { templates }
|
||||
}
|
||||
|
||||
pub async fn generate(
|
||||
&self,
|
||||
tenant_id: &str,
|
||||
report_type: &str,
|
||||
parameters: &Value,
|
||||
) -> Result<Value, String> {
|
||||
let template = self.templates.get(report_type)
|
||||
.ok_or_else(|| format!("Unknown report type: {}", report_type))?;
|
||||
|
||||
// Validate required parameters
|
||||
for param in &template.required_params {
|
||||
if parameters.get(param).is_none() {
|
||||
return Err(format!("Missing required parameter: {}", param));
|
||||
}
|
||||
}
|
||||
|
||||
match report_type {
|
||||
"financial_summary" => self.generate_financial_summary(tenant_id, parameters).await,
|
||||
"sales_pipeline" => self.generate_sales_pipeline(tenant_id, parameters).await,
|
||||
"customer_analytics" => self.generate_customer_analytics(tenant_id, parameters).await,
|
||||
"revenue_forecast" => self.generate_revenue_forecast(tenant_id, parameters).await,
|
||||
"expense_breakdown" => self.generate_expense_breakdown(tenant_id, parameters).await,
|
||||
"cashflow_projection" => self.generate_cashflow_projection(tenant_id, parameters).await,
|
||||
_ => Err(format!("Report type not implemented: {}", report_type)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_financial_summary(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {
|
||||
let period = params.get("period").and_then(|v| v.as_str()).unwrap_or("current_month");
|
||||
|
||||
Ok(json!({
|
||||
"report_type": "financial_summary",
|
||||
"period": period,
|
||||
"generated_at": Utc::now().to_rfc3339(),
|
||||
"summary": {
|
||||
"total_revenue": 125000.00,
|
||||
"total_expenses": 87500.00,
|
||||
"net_income": 37500.00,
|
||||
"profit_margin": 0.30,
|
||||
"mrr": 53333.00,
|
||||
"arr": 640000.00,
|
||||
"cash_on_hand": 180000.00,
|
||||
"burn_rate": 45000.00,
|
||||
"runway_months": 4.0
|
||||
},
|
||||
"revenue_breakdown": [
|
||||
{"category": "Subscriptions", "amount": 95000.00, "percentage": 0.76},
|
||||
{"category": "Services", "amount": 20000.00, "percentage": 0.16},
|
||||
{"category": "Other", "amount": 10000.00, "percentage": 0.08}
|
||||
],
|
||||
"expense_breakdown": [
|
||||
{"category": "Personnel", "amount": 50000.00, "percentage": 0.57},
|
||||
{"category": "Infrastructure", "amount": 15000.00, "percentage": 0.17},
|
||||
{"category": "Marketing", "amount": 12500.00, "percentage": 0.14},
|
||||
{"category": "Other", "amount": 10000.00, "percentage": 0.12}
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
async fn generate_sales_pipeline(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {
|
||||
let period = params.get("period").and_then(|v| v.as_str()).unwrap_or("current");
|
||||
|
||||
Ok(json!({
|
||||
"report_type": "sales_pipeline",
|
||||
"period": period,
|
||||
"generated_at": Utc::now().to_rfc3339(),
|
||||
"pipeline": {
|
||||
"total_value": 850000.00,
|
||||
"total_deals": 24,
|
||||
"weighted_value": 425000.00,
|
||||
"avg_deal_size": 35417.00,
|
||||
"avg_sales_cycle_days": 45
|
||||
},
|
||||
"by_stage": [
|
||||
{"stage": "Prospect", "count": 8, "value": 200000.00, "probability": 0.10},
|
||||
{"stage": "Qualified", "count": 6, "value": 300000.00, "probability": 0.30},
|
||||
{"stage": "Proposal", "count": 5, "value": 250000.00, "probability": 0.60},
|
||||
{"stage": "Negotiation", "count": 3, "value": 100000.00, "probability": 0.80},
|
||||
{"stage": "Closed Won", "count": 2, "value": 75000.00, "probability": 1.00}
|
||||
],
|
||||
"trends": {
|
||||
"new_deals_this_month": 5,
|
||||
"deals_moved_forward": 3,
|
||||
"deals_stalled": 2,
|
||||
"deals_lost": 1,
|
||||
"win_rate": 0.67
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async fn generate_customer_analytics(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {
|
||||
let period = params.get("period").and_then(|v| v.as_str()).unwrap_or("current_month");
|
||||
|
||||
Ok(json!({
|
||||
"report_type": "customer_analytics",
|
||||
"period": period,
|
||||
"generated_at": Utc::now().to_rfc3339(),
|
||||
"overview": {
|
||||
"total_customers": 42,
|
||||
"new_customers": 5,
|
||||
"churned_customers": 1,
|
||||
"active_customers": 38,
|
||||
"net_revenue_retention": 1.08,
|
||||
"gross_revenue_retention": 0.95
|
||||
},
|
||||
"segments": [
|
||||
{"segment": "Enterprise", "count": 3, "mrr": 25000.00, "ltv": 250000.00},
|
||||
{"segment": "Professional", "count": 12, "mrr": 18000.00, "ltv": 100000.00},
|
||||
{"segment": "Basic", "count": 27, "mrr": 10333.00, "ltv": 25000.00}
|
||||
],
|
||||
"health": {
|
||||
"at_risk": 2,
|
||||
"expanding": 5,
|
||||
"stable": 31,
|
||||
"new": 5
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async fn generate_revenue_forecast(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {
|
||||
let period = params.get("period").and_then(|v| v.as_str()).unwrap_or("next_quarter");
|
||||
let method = params.get("method").and_then(|v| v.as_str()).unwrap_or("weighted_pipeline");
|
||||
|
||||
Ok(json!({
|
||||
"report_type": "revenue_forecast",
|
||||
"period": period,
|
||||
"method": method,
|
||||
"generated_at": Utc::now().to_rfc3339(),
|
||||
"forecast": {
|
||||
"conservative": 180000.00,
|
||||
"expected": 250000.00,
|
||||
"optimistic": 350000.00
|
||||
},
|
||||
"monthly_breakdown": [
|
||||
{"month": "Month 1", "conservative": 55000.00, "expected": 75000.00, "optimistic": 100000.00},
|
||||
{"month": "Month 2", "conservative": 60000.00, "expected": 85000.00, "optimistic": 120000.00},
|
||||
{"month": "Month 3", "conservative": 65000.00, "expected": 90000.00, "optimistic": 130000.00}
|
||||
],
|
||||
"assumptions": [
|
||||
"Current pipeline velocity maintained",
|
||||
"No significant churn increase",
|
||||
"Marketing spend constant"
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
async fn generate_expense_breakdown(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {
|
||||
let period = params.get("period").and_then(|v| v.as_str()).unwrap_or("current_month");
|
||||
|
||||
Ok(json!({
|
||||
"report_type": "expense_breakdown",
|
||||
"period": period,
|
||||
"generated_at": Utc::now().to_rfc3339(),
|
||||
"total_expenses": 87500.00,
|
||||
"by_category": [
|
||||
{"category": "Personnel", "amount": 50000.00, "percentage": 0.57, "trend": 0.02},
|
||||
{"category": "Infrastructure", "amount": 15000.00, "percentage": 0.17, "trend": -0.05},
|
||||
{"category": "Marketing", "amount": 12500.00, "percentage": 0.14, "trend": 0.10},
|
||||
{"category": "Software", "amount": 6000.00, "percentage": 0.07, "trend": 0.0},
|
||||
{"category": "Other", "amount": 4000.00, "percentage": 0.05, "trend": -0.02}
|
||||
],
|
||||
"recurring_vs_one_time": {
|
||||
"recurring": 75000.00,
|
||||
"one_time": 12500.00
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async fn generate_cashflow_projection(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {
|
||||
let periods = params.get("periods").and_then(|v| v.as_i64()).unwrap_or(6);
|
||||
|
||||
let mut projections = Vec::new();
|
||||
let mut current_cash = 180000.00;
|
||||
|
||||
for i in 1..=periods {
|
||||
let inflow = 120000.00 + (i as f64 * 5000.00);
|
||||
let outflow = 87500.00 + (i as f64 * 2000.00);
|
||||
let net = inflow - outflow;
|
||||
current_cash += net;
|
||||
|
||||
projections.push(json!({
|
||||
"period": format!("Month {}", i),
|
||||
"inflow": inflow,
|
||||
"outflow": outflow,
|
||||
"net": net,
|
||||
"ending_cash": current_cash
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"report_type": "cashflow_projection",
|
||||
"periods": periods,
|
||||
"generated_at": Utc::now().to_rfc3339(),
|
||||
"starting_cash": 180000.00,
|
||||
"projections": projections,
|
||||
"summary": {
|
||||
"total_inflow": projections.iter().map(|p| p.get("inflow").unwrap().as_f64().unwrap()).sum::<f64>(),
|
||||
"total_outflow": projections.iter().map(|p| p.get("outflow").unwrap().as_f64().unwrap()).sum::<f64>(),
|
||||
"ending_cash": current_cash,
|
||||
"min_cash": projections.iter().map(|p| p.get("ending_cash").unwrap().as_f64().unwrap()).fold(f64::INFINITY, f64::min)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc_fingerprint":8170899571127002677,"outputs":{"12203715465990969353":{"success":true,"status":"","code":0,"stdout":"rustc 1.96.0 (ac68faa20 2026-05-25)\nbinary: rustc\ncommit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\ncommit-date: 2026-05-25\nhost: aarch64-unknown-linux-gnu\nrelease: 1.96.0\nLLVM version: 22.1.2\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/bernt/.rustup/toolchains/stable-aarch64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"neon\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}
|
||||
@@ -1,3 +0,0 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
d12295fa64d6c7e8
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[]","target":5116616278641129243,"profile":1369601567987815722,"path":2895544698071783192,"deps":[[1108254298283712113,"quote",false,12346198818348946717],[4289358735036141001,"proc_macro2",false,2466923278285998130],[14607138199358211871,"syn",false,16091993304001859324]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/async-trait-e3b4366307923e69/dep-lib-async_trait","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
bccba2364a977929
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":2040997289075261528,"path":1915199519464942613,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/atomic-waker-e92db70727ec2e75/dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
28f83eb7c4603b33
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":1369601567987815722,"path":14691547496011824260,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/autocfg-cfdc11b3d5fe0685/dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
eec7fdd3e41f9a8f
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"form\", \"http1\", \"json\", \"matched-path\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\"]","declared_features":"[\"__private_docs\", \"default\", \"form\", \"http1\", \"http2\", \"json\", \"macros\", \"matched-path\", \"multipart\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\", \"ws\"]","target":13920321295547257648,"profile":2040997289075261528,"path":2777920595211535039,"deps":[[784494742817713399,"tower_service",false,13268437010750965648],[2251399859588827949,"pin_project_lite",false,4988885227890412228],[2517136641825875337,"sync_wrapper",false,1687334814780530710],[3035134586790830808,"hyper",false,2449154796991432763],[3632162862999675140,"tower",false,13737076900094726326],[4359148418957042248,"axum_core",false,16477098965219525408],[5532778797167691009,"itoa",false,9619087413982131351],[5898568623609459682,"futures_util",false,18018312848256853116],[6803352382179706244,"percent_encoding",false,4348505878887178983],[7712452662827335977,"tower_layer",false,16973315690600901494],[8578586876803397814,"serde_json",false,4628570253085338962],[9394460649638301237,"tokio",false,16850047951336516385],[9678799920983747518,"matchit",false,11820823362860415402],[10229185211513642314,"mime",false,14363291638445985122],[11926622812581095017,"bytes",false,16904972052078141223],[11976082518617474977,"hyper_util",false,5052674406737212029],[12613788554453945248,"memchr",false,17793951435395408554],[13548984313718623784,"serde",false,8151733579501977522],[14084095096285906100,"http_body",false,3294172187499036643],[14757622794040968908,"tracing",false,10698368088514759261],[14814583949208169760,"serde_path_to_error",false,8068421446706276709],[16542808166767769916,"serde_urlencoded",false,4143663369910590965],[16611674984963787466,"async_trait",false,16773611066353853137],[16900715236047033623,"http_body_util",false,13625222387583398835],[16991438365634268121,"rustversion",false,9804801277437893544],[17371538545939333701,"http",false,6263282853657891499]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/axum-844fb32df64c81a5/dep-lib-axum","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
20c3dd9e356aaae4
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"tracing\"]","declared_features":"[\"__private_docs\", \"tracing\"]","target":2565713999752801252,"profile":2040997289075261528,"path":15292076425892459978,"deps":[[784494742817713399,"tower_service",false,13268437010750965648],[2251399859588827949,"pin_project_lite",false,4988885227890412228],[2517136641825875337,"sync_wrapper",false,1687334814780530710],[5898568623609459682,"futures_util",false,18018312848256853116],[7712452662827335977,"tower_layer",false,16973315690600901494],[10229185211513642314,"mime",false,14363291638445985122],[11926622812581095017,"bytes",false,16904972052078141223],[14084095096285906100,"http_body",false,3294172187499036643],[14757622794040968908,"tracing",false,10698368088514759261],[16611674984963787466,"async_trait",false,16773611066353853137],[16900715236047033623,"http_body_util",false,13625222387583398835],[16991438365634268121,"rustversion",false,9804801277437893544],[17371538545939333701,"http",false,6263282853657891499]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/axum-core-c3e65a8ede5c372c/dep-lib-axum_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
4f2d01fbf668be02
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":2040997289075261528,"path":7660686688554485333,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/base64-bffeb343559b9b16/dep-lib-base64","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
04265406d8dfdc86
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"arbitrary\", \"bytemuck\", \"example_generated\", \"serde\", \"serde_core\", \"std\"]","target":7691312148208718491,"profile":2040997289075261528,"path":13133951523946053133,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/bitflags-765f98ff397854cf/dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
a0c789804c70a3d8
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"zeroize\"]","target":6057344034650883969,"profile":15005971894838546436,"path":9516285012683048963,"deps":[[3173661117269759064,"hybrid_array",false,18059322650750644876]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/block-buffer-a4cfcc9869f4006d/dep-lib-block_buffer","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
188a1892fb9a226c
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[]","target":8149518468556730110,"profile":2040997289075261528,"path":10763286916239946207,"deps":[[466357198569416633,"uuid",false,6844483093539163181],[3601586811267292532,"tower",false,17710280762048293091],[4891297352905791595,"axum",false,10347618161506764782],[5364813825765636762,"dashmap",false,8198242637287196554],[5380358770761950913,"tracing_subscriber",false,17654345397525367048],[7098700569944897890,"libc",false,10358284356093817449],[8578586876803397814,"serde_json",false,4628570253085338962],[9394460649638301237,"tokio",false,16850047951336516385],[11641236027685285524,"tokio_postgres",false,4637541574078043107],[11910974697091955563,"rayon",false,9459993863794084889],[13548984313718623784,"serde",false,8151733579501977522],[14435908599267459652,"tower_http",false,4694176071756235637],[14757622794040968908,"tracing",false,10698368088514759261],[16117757646811882223,"chrono",false,13901919752554136555]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/boc-rust-service-8090414e8153436b/dep-lib-boc_rust","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
-44
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
823b90f0f83b9d33
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[]","target":11100866145103567484,"profile":2040997289075261528,"path":4942398508502643691,"deps":[[466357198569416633,"uuid",false,6844483093539163181],[3601586811267292532,"tower",false,17710280762048293091],[4891297352905791595,"axum",false,10347618161506764782],[5364813825765636762,"dashmap",false,8198242637287196554],[5380358770761950913,"tracing_subscriber",false,17654345397525367048],[6098513438495592181,"boc_rust",false,7791960710582929944],[7098700569944897890,"libc",false,10358284356093817449],[8578586876803397814,"serde_json",false,4628570253085338962],[9394460649638301237,"tokio",false,16850047951336516385],[11641236027685285524,"tokio_postgres",false,4637541574078043107],[11910974697091955563,"rayon",false,9459993863794084889],[13548984313718623784,"serde",false,8151733579501977522],[14435908599267459652,"tower_http",false,4694176071756235637],[14757622794040968908,"tracing",false,10698368088514759261],[16117757646811882223,"chrono",false,13901919752554136555]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/boc-rust-service-bcff98372ac6b716/dep-bin-boc-rust-service","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{"$message_type":"diagnostic","message":"unused import: `serde_json::Value`","code":{"code":"unused_imports","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":49,"byte_end":66,"line_start":3,"line_end":3,"column_start":5,"column_end":22,"is_primary":true,"text":[{"text":"use serde_json::Value;","highlight_start":5,"highlight_end":22}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"`#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null},{"message":"remove the whole `use` item","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":45,"byte_end":68,"line_start":3,"line_end":4,"column_start":1,"column_end":1,"is_primary":true,"text":[{"text":"use serde_json::Value;","highlight_start":1,"highlight_end":23},{"text":"use std::collections::HashMap;","highlight_start":1,"highlight_end":1}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `serde_json::Value`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:3:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m3\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use serde_json::Value;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused import: `std::collections::HashMap`","code":{"code":"unused_imports","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":72,"byte_end":97,"line_start":4,"line_end":4,"column_start":5,"column_end":30,"is_primary":true,"text":[{"text":"use std::collections::HashMap;","highlight_start":5,"highlight_end":30}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"remove the whole `use` item","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":68,"byte_end":99,"line_start":4,"line_end":5,"column_start":1,"column_end":1,"is_primary":true,"text":[{"text":"use std::collections::HashMap;","highlight_start":1,"highlight_end":31},{"text":"","highlight_start":1,"highlight_end":1}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `std::collections::HashMap`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:4:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m4\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::collections::HashMap;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused import: `DateTime`","code":{"code":"unused_imports","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":13,"byte_end":21,"line_start":1,"line_end":1,"column_start":14,"column_end":22,"is_primary":true,"text":[{"text":"use chrono::{DateTime, Utc};","highlight_start":14,"highlight_end":22}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"remove the unused import","code":null,"level":"help","spans":[{"file_name":"src/reports.rs","byte_start":13,"byte_end":23,"line_start":1,"line_end":1,"column_start":14,"column_end":24,"is_primary":true,"text":[{"text":"use chrono::{DateTime, Utc};","highlight_start":14,"highlight_end":24}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null},{"file_name":"src/reports.rs","byte_start":12,"byte_end":13,"line_start":1,"line_end":1,"column_start":13,"column_end":14,"is_primary":true,"text":[{"text":"use chrono::{DateTime, Utc};","highlight_start":13,"highlight_end":14}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null},{"file_name":"src/reports.rs","byte_start":26,"byte_end":27,"line_start":1,"line_end":1,"column_start":27,"column_end":28,"is_primary":true,"text":[{"text":"use chrono::{DateTime, Utc};","highlight_start":27,"highlight_end":28}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `DateTime`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:1:14\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m1\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use chrono::{DateTime, Utc};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused import: `CString`","code":{"code":"unused_imports","explanation":null},"level":"warning","spans":[{"file_name":"src/ipc.rs","byte_start":117,"byte_end":124,"line_start":4,"line_end":4,"column_start":22,"column_end":29,"is_primary":true,"text":[{"text":"use std::ffi::{CStr, CString};","highlight_start":22,"highlight_end":29}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"remove the unused import","code":null,"level":"help","spans":[{"file_name":"src/ipc.rs","byte_start":115,"byte_end":124,"line_start":4,"line_end":4,"column_start":20,"column_end":29,"is_primary":true,"text":[{"text":"use std::ffi::{CStr, CString};","highlight_start":20,"highlight_end":29}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null},{"file_name":"src/ipc.rs","byte_start":110,"byte_end":111,"line_start":4,"line_end":4,"column_start":15,"column_end":16,"is_primary":true,"text":[{"text":"use std::ffi::{CStr, CString};","highlight_start":15,"highlight_end":16}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null},{"file_name":"src/ipc.rs","byte_start":124,"byte_end":125,"line_start":4,"line_end":4,"column_start":29,"column_end":30,"is_primary":true,"text":[{"text":"use std::ffi::{CStr, CString};","highlight_start":29,"highlight_end":30}],"label":null,"suggested_replacement":"","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `CString`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ipc.rs:4:22\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m4\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::ffi::{CStr, CString};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":2103,"byte_end":2112,"line_start":63,"line_end":63,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_mrr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"`#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null},{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":2103,"byte_end":2112,"line_start":63,"line_end":63,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_mrr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:63:33\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m63\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_mrr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `period`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":2120,"byte_end":2126,"line_start":63,"line_end":63,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_mrr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":2120,"byte_end":2126,"line_start":63,"line_end":63,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_mrr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":"_period","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `period`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:63:50\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m63\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_mrr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_period`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":2506,"byte_end":2515,"line_start":76,"line_end":76,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_arr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":2506,"byte_end":2515,"line_start":76,"line_end":76,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_arr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:76:33\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m76\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_arr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `period`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":2523,"byte_end":2529,"line_start":76,"line_end":76,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_arr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":2523,"byte_end":2529,"line_start":76,"line_end":76,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_arr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":"_period","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `period`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:76:50\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m76\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_arr(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_period`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":2891,"byte_end":2900,"line_start":88,"line_end":88,"column_start":35,"column_end":44,"is_primary":true,"text":[{"text":" async fn compute_churn(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":35,"highlight_end":44}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":2891,"byte_end":2900,"line_start":88,"line_end":88,"column_start":35,"column_end":44,"is_primary":true,"text":[{"text":" async fn compute_churn(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":35,"highlight_end":44}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:88:35\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m88\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_churn(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `period`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":2908,"byte_end":2914,"line_start":88,"line_end":88,"column_start":52,"column_end":58,"is_primary":true,"text":[{"text":" async fn compute_churn(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":52,"highlight_end":58}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":2908,"byte_end":2914,"line_start":88,"line_end":88,"column_start":52,"column_end":58,"is_primary":true,"text":[{"text":" async fn compute_churn(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":52,"highlight_end":58}],"label":null,"suggested_replacement":"_period","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `period`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:88:52\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m88\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_churn(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_period`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":3216,"byte_end":3225,"line_start":99,"line_end":99,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_ltv(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":3216,"byte_end":3225,"line_start":99,"line_end":99,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_ltv(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:99:33\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m99\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_ltv(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `period`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":3233,"byte_end":3239,"line_start":99,"line_end":99,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_ltv(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":3233,"byte_end":3239,"line_start":99,"line_end":99,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_ltv(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":"_period","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `period`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:99:50\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m99\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_ltv(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_period`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":3599,"byte_end":3608,"line_start":111,"line_end":111,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_cac(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":3599,"byte_end":3608,"line_start":111,"line_end":111,"column_start":33,"column_end":42,"is_primary":true,"text":[{"text":" async fn compute_cac(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":33,"highlight_end":42}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:111:33\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m111\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_cac(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `period`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":3616,"byte_end":3622,"line_start":111,"line_end":111,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_cac(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":3616,"byte_end":3622,"line_start":111,"line_end":111,"column_start":50,"column_end":56,"is_primary":true,"text":[{"text":" async fn compute_cac(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":50,"highlight_end":56}],"label":null,"suggested_replacement":"_period","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `period`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:111:50\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m111\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_cac(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_period`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":3977,"byte_end":3986,"line_start":123,"line_end":123,"column_start":38,"column_end":47,"is_primary":true,"text":[{"text":" async fn compute_pipeline(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":38,"highlight_end":47}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":3977,"byte_end":3986,"line_start":123,"line_end":123,"column_start":38,"column_end":47,"is_primary":true,"text":[{"text":" async fn compute_pipeline(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":38,"highlight_end":47}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:123:38\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m123\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_pipeline(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `period`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":3994,"byte_end":4000,"line_start":123,"line_end":123,"column_start":55,"column_end":61,"is_primary":true,"text":[{"text":" async fn compute_pipeline(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":55,"highlight_end":61}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":3994,"byte_end":4000,"line_start":123,"line_end":123,"column_start":55,"column_end":61,"is_primary":true,"text":[{"text":" async fn compute_pipeline(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":55,"highlight_end":61}],"label":null,"suggested_replacement":"_period","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `period`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:123:55\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m123\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_pipeline(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_period`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":4421,"byte_end":4430,"line_start":136,"line_end":136,"column_start":40,"column_end":49,"is_primary":true,"text":[{"text":" async fn compute_conversion(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":40,"highlight_end":49}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":4421,"byte_end":4430,"line_start":136,"line_end":136,"column_start":40,"column_end":49,"is_primary":true,"text":[{"text":" async fn compute_conversion(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":40,"highlight_end":49}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:136:40\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m136\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_conversion(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `period`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":4438,"byte_end":4444,"line_start":136,"line_end":136,"column_start":57,"column_end":63,"is_primary":true,"text":[{"text":" async fn compute_conversion(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":57,"highlight_end":63}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/analytics.rs","byte_start":4438,"byte_end":4444,"line_start":136,"line_end":136,"column_start":57,"column_end":63,"is_primary":true,"text":[{"text":" async fn compute_conversion(&self, tenant_id: &str, period: &str) -> AnalyticsResult {","highlight_start":57,"highlight_end":63}],"label":null,"suggested_replacement":"_period","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `period`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:136:57\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m136\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn compute_conversion(&self, tenant_id: &str, period: &str) -> AnalyticsResult {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_period`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":3367,"byte_end":3376,"line_start":85,"line_end":85,"column_start":48,"column_end":57,"is_primary":true,"text":[{"text":" async fn generate_financial_summary(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":48,"highlight_end":57}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/reports.rs","byte_start":3367,"byte_end":3376,"line_start":85,"line_end":85,"column_start":48,"column_end":57,"is_primary":true,"text":[{"text":" async fn generate_financial_summary(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":48,"highlight_end":57}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:85:48\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m85\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn generate_financial_summary(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":4815,"byte_end":4824,"line_start":117,"line_end":117,"column_start":45,"column_end":54,"is_primary":true,"text":[{"text":" async fn generate_sales_pipeline(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":45,"highlight_end":54}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/reports.rs","byte_start":4815,"byte_end":4824,"line_start":117,"line_end":117,"column_start":45,"column_end":54,"is_primary":true,"text":[{"text":" async fn generate_sales_pipeline(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":45,"highlight_end":54}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:117:45\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m117\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn generate_sales_pipeline(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":6166,"byte_end":6175,"line_start":148,"line_end":148,"column_start":49,"column_end":58,"is_primary":true,"text":[{"text":" async fn generate_customer_analytics(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":49,"highlight_end":58}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/reports.rs","byte_start":6166,"byte_end":6175,"line_start":148,"line_end":148,"column_start":49,"column_end":58,"is_primary":true,"text":[{"text":" async fn generate_customer_analytics(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":49,"highlight_end":58}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:148:49\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m148\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn generate_customer_analytics(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":7300,"byte_end":7309,"line_start":177,"line_end":177,"column_start":47,"column_end":56,"is_primary":true,"text":[{"text":" async fn generate_revenue_forecast(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":47,"highlight_end":56}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/reports.rs","byte_start":7300,"byte_end":7309,"line_start":177,"line_end":177,"column_start":47,"column_end":56,"is_primary":true,"text":[{"text":" async fn generate_revenue_forecast(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":47,"highlight_end":56}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:177:47\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m177\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn generate_revenue_forecast(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":8543,"byte_end":8552,"line_start":204,"line_end":204,"column_start":48,"column_end":57,"is_primary":true,"text":[{"text":" async fn generate_expense_breakdown(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":48,"highlight_end":57}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/reports.rs","byte_start":8543,"byte_end":8552,"line_start":204,"line_end":204,"column_start":48,"column_end":57,"is_primary":true,"text":[{"text":" async fn generate_expense_breakdown(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":48,"highlight_end":57}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:204:48\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m204\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn generate_expense_breakdown(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"unused variable: `tenant_id`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":9626,"byte_end":9635,"line_start":226,"line_end":226,"column_start":50,"column_end":59,"is_primary":true,"text":[{"text":" async fn generate_cashflow_projection(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":50,"highlight_end":59}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"if this is intentional, prefix it with an underscore","code":null,"level":"help","spans":[{"file_name":"src/reports.rs","byte_start":9626,"byte_end":9635,"line_start":226,"line_end":226,"column_start":50,"column_end":59,"is_primary":true,"text":[{"text":" async fn generate_cashflow_projection(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {","highlight_start":50,"highlight_end":59}],"label":null,"suggested_replacement":"_tenant_id","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `tenant_id`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:226:50\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m226\u001b[0m \u001b[1m\u001b[94m|\u001b[0m async fn generate_cashflow_projection(&self, tenant_id: &str, params: &Value) -> Result<Value, String> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_tenant_id`\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"method `batch_compute` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/analytics.rs","byte_start":310,"byte_end":330,"line_start":16,"line_end":16,"column_start":1,"column_end":21,"is_primary":false,"text":[{"text":"impl AnalyticsEngine {","highlight_start":1,"highlight_end":21}],"label":"method in this implementation","suggested_replacement":null,"suggestion_applicability":null,"expansion":null},{"file_name":"src/analytics.rs","byte_start":4857,"byte_end":4870,"line_start":149,"line_end":149,"column_start":12,"column_end":25,"is_primary":true,"text":[{"text":" pub fn batch_compute(&self, tenant_id: &str, metrics: &[(&str, &str)]) -> Vec<AnalyticsResult> {","highlight_start":12,"highlight_end":25}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"`#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: method `batch_compute` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/analytics.rs:149:12\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m 16\u001b[0m \u001b[1m\u001b[94m|\u001b[0m impl AnalyticsEngine {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m--------------------\u001b[0m \u001b[1m\u001b[94mmethod in this implementation\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m149\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub fn batch_compute(&self, tenant_id: &str, metrics: &[(&str, &str)]) -> Vec<AnalyticsResult> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\n"}
|
||||
{"$message_type":"diagnostic","message":"fields `name` and `description` are never read","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/reports.rs","byte_start":179,"byte_end":193,"line_start":9,"line_end":9,"column_start":8,"column_end":22,"is_primary":false,"text":[{"text":"struct ReportTemplate {","highlight_start":8,"highlight_end":22}],"label":"fields in this struct","suggested_replacement":null,"suggestion_applicability":null,"expansion":null},{"file_name":"src/reports.rs","byte_start":200,"byte_end":204,"line_start":10,"line_end":10,"column_start":5,"column_end":9,"is_primary":true,"text":[{"text":" name: String,","highlight_start":5,"highlight_end":9}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null},{"file_name":"src/reports.rs","byte_start":218,"byte_end":229,"line_start":11,"line_end":11,"column_start":5,"column_end":16,"is_primary":true,"text":[{"text":" description: String,","highlight_start":5,"highlight_end":16}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: fields `name` and `description` are never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/reports.rs:10:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m 9\u001b[0m \u001b[1m\u001b[94m|\u001b[0m struct ReportTemplate {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m--------------\u001b[0m \u001b[1m\u001b[94mfields in this struct\u001b[0m\n\u001b[1m\u001b[94m10\u001b[0m \u001b[1m\u001b[94m|\u001b[0m name: String,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^\u001b[0m\n\u001b[1m\u001b[94m11\u001b[0m \u001b[1m\u001b[94m|\u001b[0m description: String,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"function `init_channel` is never used","code":{"code":"dead_code","explanation":null},"level":"warning","spans":[{"file_name":"src/ipc.rs","byte_start":215,"byte_end":227,"line_start":8,"line_end":8,"column_start":8,"column_end":20,"is_primary":true,"text":[{"text":"pub fn init_channel(name: &str) -> Result<(), String> {","highlight_start":8,"highlight_end":20}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: function `init_channel` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ipc.rs:8:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m8\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub fn init_channel(name: &str) -> Result<(), String> {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"27 warnings emitted","code":null,"level":"warning","spans":[],"children":[],"rendered":"\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: 27 warnings emitted\u001b[0m\n\n"}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
f1d40d367008c068
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"i128\", \"std\"]","target":8344828840634961491,"profile":2040997289075261528,"path":17947950383692024843,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/byteorder-6e3a8fd85d179480/dep-lib-byteorder","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
271f06d683869aea
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"extra-platforms\", \"serde\", \"std\"]","target":11402411492164584411,"profile":3654867079619179846,"path":16980282986469236506,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/bytes-7dc9380364cd2a34/dep-lib-bytes","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
c36d3712b8e31701
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"core\", \"rustc-dep-of-std\"]","target":13840298032947503755,"profile":2040997289075261528,"path":9433148093347736929,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/cfg-if-c28393f1568b0153/dep-lib-cfg_if","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
e1a2eee0808e7cc4
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"rng\"]","declared_features":"[\"cipher\", \"default\", \"legacy\", \"rng\", \"xchacha\", \"zeroize\"]","target":5186012452570817782,"profile":18050733770209708702,"path":11687202474411020191,"deps":[[7667230146095136825,"cfg_if",false,78781898221383107],[18359178603293420568,"rand_core",false,2843246380870605194]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/chacha20-d22823a185c48e25/dep-lib-chacha20","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
ebc3f9025c8aedc0
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"clock\", \"default\", \"iana-time-zone\", \"js-sys\", \"now\", \"oldtime\", \"serde\", \"std\", \"wasm-bindgen\", \"wasmbind\", \"winapi\", \"windows-link\"]","declared_features":"[\"__internal_bench\", \"alloc\", \"arbitrary\", \"clock\", \"core-error\", \"default\", \"defmt\", \"iana-time-zone\", \"js-sys\", \"libc\", \"now\", \"oldtime\", \"pure-rust-locales\", \"rkyv\", \"rkyv-16\", \"rkyv-32\", \"rkyv-64\", \"rkyv-validation\", \"serde\", \"std\", \"unstable-locales\", \"wasm-bindgen\", \"wasmbind\", \"winapi\", \"windows-link\"]","target":15315924755136109342,"profile":2040997289075261528,"path":17780376413348889854,"deps":[[5157631553186200874,"num_traits",false,14119788579692270633],[13548984313718623784,"serde",false,8151733579501977522],[16619627449254928351,"iana_time_zone",false,9712474382341530848]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/chrono-cff241d94d764653/dep-lib-chrono","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
1f3cafca33d2299d
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[]","target":7432811800008246249,"profile":15005971894838546436,"path":14113878691217321502,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/cmov-e22e852cf3fa7ce1/dep-lib-cmov","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
33eb5f50f822b777
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"arbitrary\", \"db\"]","target":15839317715723132186,"profile":2040997289075261528,"path":13813461199962814925,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/const-oid-fbddf381ae2612a4/dep-lib-const_oid","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
7e0dcda19b442a97
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[]","target":7407970971831147067,"profile":15005971894838546436,"path":1508821112638578062,"deps":[[7098700569944897890,"libc",false,10358284356093817449]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/cpufeatures-02fa6de1448eed7c/dep-lib-cpufeatures","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
8e2c706e35f3fe26
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[10684107345137278605,"build_script_build",false,405385505304535536]],"local":[{"RerunIfChanged":{"output":"release/build/crossbeam-deque-24893788c9d354ae/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
5a25749722a17c7f
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":15353977948366730291,"profile":14791228037615401302,"path":10580037843469392998,"deps":[[10684107345137278605,"build_script_build",false,2809950628337429646],[10951058209291271410,"crossbeam_utils",false,18382006368912275469],[13869114390706723416,"crossbeam_epoch",false,9559836231838556057]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-deque-56cc075aabc81ce0/dep-lib-crossbeam_deque","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
f0e1c420f837a005
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":5408242616063297496,"profile":1419616050453328851,"path":3163335187747278573,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-deque-dd9c30f432e6c432/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
995fa515675cab84
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"loom\", \"loom-crate\", \"nightly\", \"std\"]","target":16242420667881341737,"profile":14791228037615401302,"path":11685426848944331124,"deps":[[10951058209291271410,"crossbeam_utils",false,18382006368912275469],[13869114390706723416,"build_script_build",false,16122645801835036432]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-epoch-1a33398559ec6a44/dep-lib-crossbeam_epoch","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
10ffd171ef24bfdf
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[13869114390706723416,"build_script_build",false,10781594398831178862]],"local":[{"RerunIfChanged":{"output":"release/build/crossbeam-epoch-8deb7e5858c37858/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
6e74ac7efbea9f95
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"loom\", \"loom-crate\", \"nightly\", \"std\"]","target":5408242616063297496,"profile":1419616050453328851,"path":4544127582614795669,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-epoch-9bb466ec27e00e7c/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
0df8fb8566011aff
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":9626079250877207070,"profile":14791228037615401302,"path":11436926997345565096,"deps":[[10951058209291271410,"build_script_build",false,14200670855589481638]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-utils-1eb06fd8fb643741/dep-lib-crossbeam_utils","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
a64ca79fe7ea12c5
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[10951058209291271410,"build_script_build",false,1770077806496551786]],"local":[{"RerunIfChanged":{"output":"release/build/crossbeam-utils-9e612733194446e8/output","paths":["no_atomic.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
6a93070163949018
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":5408242616063297496,"profile":1419616050453328851,"path":2841676696308263227,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-utils-d404d0959e599037/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
6e06d2084f70a2d2
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"getrandom\", \"rand_core\", \"zeroize\"]","target":14002316677131120771,"profile":8917093484142751111,"path":14303971719399791221,"deps":[[3173661117269759064,"hybrid_array",false,18059322650750644876]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crypto-common-b4193356d4abfbfb/dep-lib-crypto_common","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
e65dee2c44f10962
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"alloc\", \"subtle\"]","target":14735723286394368586,"profile":15005971894838546436,"path":7161778167789485399,"deps":[[14821918413341411223,"cmov",false,11324813857885469727]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/ctutils-10dd7512d97fb618/dep-lib-ctutils","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
8a07f7113e02c671
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"arbitrary\", \"inline\", \"raw-api\", \"rayon\", \"serde\"]","target":7646408341754254191,"profile":2040997289075261528,"path":8119509999883745608,"deps":[[2555121257709722468,"lock_api",false,14023348759600241536],[5855319743879205494,"once_cell",false,13708832399288578193],[6545091685033313457,"parking_lot_core",false,17317473899768983122],[7667230146095136825,"cfg_if",false,78781898221383107],[13018563866916002725,"hashbrown",false,5717521732401734504]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/dashmap-b73f9415bc74b734/dep-lib-dashmap","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
a40906cee3085c65
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"block-api\", \"default\", \"mac\", \"oid\"]","declared_features":"[\"alloc\", \"blobby\", \"block-api\", \"default\", \"dev\", \"getrandom\", \"mac\", \"oid\", \"rand_core\", \"zeroize\"]","target":10850736035647688105,"profile":8917093484142751111,"path":8971369010967965637,"deps":[[2589336589600319205,"const_oid",false,8626402061147171635],[6101016705997077623,"common",false,15177817178944702062],[9917320985600281521,"ctutils",false,7064442765621222886],[18141537268335717567,"block_buffer",false,15610444207272609696]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/digest-6127948cdbc8b6c9/dep-lib-digest","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
e9ada86fc238175d
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[]","declared_features":"[\"default\", \"serde\", \"std\", \"use_std\"]","target":17124342308084364240,"profile":2040997289075261528,"path":15294895676438055135,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/either-c2268b586ceb397c/dep-lib-either","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
b5741924258248ab
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":17743456753391690785,"profile":8944999695620513791,"path":3360262050279122850,"deps":[[7098700569944897890,"libc",false,10358284356093817449]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/errno-06a2f72be8b8e935/dep-lib-errno","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
cdc5af4885cdbce1
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":15245709686714427328,"profile":2040997289075261528,"path":4507909794513511432,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/fallible-iterator-7d5ae483d5fc2396/dep-lib-fallible_iterator","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
6f0b77f85b841114
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":6496257856677244489,"profile":2040997289075261528,"path":11381800245154175600,"deps":[[6803352382179706244,"percent_encoding",false,4348505878887178983]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/form_urlencoded-b21a6453ec4c6027/dep-lib-form_urlencoded","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
9fadaa7f521ae9ab
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"default\", \"futures-sink\", \"sink\", \"std\"]","declared_features":"[\"alloc\", \"cfg-target-has-atomic\", \"default\", \"futures-sink\", \"sink\", \"std\", \"unstable\"]","target":13634065851578929263,"profile":18348216721672176038,"path":16039502854426308436,"deps":[[270634688040536827,"futures_sink",false,13060201866946825014],[302948626015856208,"futures_core",false,17808811113473154565]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/futures-channel-89600126d4fe9190/dep-lib-futures_channel","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
059e98425a9b25f7
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"cfg-target-has-atomic\", \"default\", \"portable-atomic\", \"std\", \"unstable\"]","target":9453135960607436725,"profile":18348216721672176038,"path":11758836047530020565,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/futures-core-54e00c79609f6de3/dep-lib-futures_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
364b42ee66283fb5
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":10827111567014737887,"profile":18348216721672176038,"path":5817484558020704007,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/futures-sink-005b1f0a7b5abe4a/dep-lib-futures_sink","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
020278dd36d56306
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\"]","declared_features":"[\"alloc\", \"cfg-target-has-atomic\", \"default\", \"std\", \"unstable\"]","target":13518091470260541623,"profile":18348216721672176038,"path":9883525045315444007,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/futures-task-bdcc3435c8e0275f/dep-lib-futures_task","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
7ce4340412e80dfa
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"alloc\", \"futures-sink\", \"sink\", \"slab\"]","declared_features":"[\"alloc\", \"async-await\", \"async-await-macro\", \"bilock\", \"cfg-target-has-atomic\", \"channel\", \"compat\", \"default\", \"futures-channel\", \"futures-io\", \"futures-macro\", \"futures-sink\", \"futures_01\", \"io\", \"io-compat\", \"libc\", \"memchr\", \"portable-atomic\", \"sink\", \"slab\", \"spin\", \"std\", \"tokio-io\", \"unstable\", \"write-all-vectored\"]","target":1788798584831431502,"profile":18348216721672176038,"path":8238611052005628129,"deps":[[270634688040536827,"futures_sink",false,13060201866946825014],[302948626015856208,"futures_core",false,17808811113473154565],[2251399859588827949,"pin_project_lite",false,4988885227890412228],[12256881686772805731,"futures_task",false,460446018542502402],[14895711841936801505,"slab",false,7991830269893888097]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/futures-util-8f80c1577197d149/dep-lib-futures_util","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -1 +0,0 @@
|
||||
5eeb9b9e26290224
|
||||
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"std\", \"sys_rng\"]","declared_features":"[\"std\", \"sys_rng\", \"wasm_js\"]","target":5479159445871601843,"profile":11558646924270836803,"path":15722209970639279736,"deps":[[7098700569944897890,"libc",false,10358284356093817449],[7667230146095136825,"cfg_if",false,78781898221383107],[17989731678791879549,"build_script_build",false,13854897316006317375],[18359178603293420568,"rand_core",false,2843246380870605194]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/getrandom-9b1825f45ddde54a/dep-lib-getrandom","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
dfc3b9ee620d01b3
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"rustc":3697274117413853022,"features":"[\"std\", \"sys_rng\"]","declared_features":"[\"std\", \"sys_rng\", \"wasm_js\"]","target":2835126046236718539,"profile":6350529014318243270,"path":14292317932849716997,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/getrandom-ae19c6dd532d364c/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This file has an mtime of when this was started.
|
||||
-1
@@ -1 +0,0 @@
|
||||
3fcd11d8b37b46c0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user