1. Wstęp

Expr to dynamiczne rozwiązanie konfiguracyjne zaprojektowane dla języka Go, znane ze swojej prostoty składni i potężnych funkcji wydajnościowych. Rdzeń silnika wyrażeń Expr skupia się na bezpieczeństwie, szybkości i intuicyjności, co sprawia, że jest odpowiedni do scenariuszy takich jak kontrola dostępu, filtrowanie danych i zarządzanie zasobami. Gdy jest zastosowany do Go, Expr znacząco zwiększa zdolność aplikacji do obsługi dynamicznych reguł. W przeciwieństwie do interpreterów lub silników skryptów w innych językach, Expr stosuje statyczną kontrolę typów i generuje bajtkod do wykonania, zapewniając zarówno wydajność, jak i bezpieczeństwo.

2. Instalowanie Expr

Możesz zainstalować silnik wyrażeń Expr, używając narzędzia do zarządzania pakietami języka Go go get:

go get github.com/expr-lang/expr

To polecenie pobierze pliki biblioteki Expr i zainstaluje je w twoim projekcie Go, umożliwiając importowanie i używanie Expr w twoim kodzie Go.

3. Szybki start

3.1. Kompilowanie i Uruchamianie Podstawowych Wyrażeń

Zaczniemy od podstawowego przykładu: napisania prostego wyrażenia, skompilowania go, a następnie uruchomienia, aby uzyskać wynik.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// Kompilacja podstawowego wyrażenia dodawania
	program, err := expr.Compile(`2 + 2`)
	if err != nil {
		panic(err)
	}

	// Uruchamianie skompilowanego wyrażenia bez przekazywania środowiska, ponieważ tutaj nie są potrzebne zmienne
	output, err := expr.Run(program, nil)
	if err != nil {
		panic(err)
	}

	// Wydrukowanie wyniku
	fmt.Println(output)  // Wyświetla 4
}

W tym przykładzie wyrażenie 2 + 2 jest kompilowane do wykonywalnego bajtkodu, który następnie jest wykonywany w celu uzyskania wyniku.

3.2. Użycie Zmiennych w Wyrażeniach

Następnie stworzymy środowisko zawierające zmienne, napiszemy wyrażenie używające tych zmiennych, skompilujemy i uruchomimy to wyrażenie.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// Tworzenie środowiska z zmiennymi
	env := map[string]interface{}{
		"foo": 100,
		"bar": 200,
	}

	// Kompilacja wyrażenia z użyciem zmiennych ze środowiska
	program, err := expr.Compile(`foo + bar`, expr.Env(env))
	if err != nil {
		panic(err)
	}

	// Uruchamianie wyrażenia
	output, err := expr.Run(program, env)
	if err != nil {
		panic(err)
	}

	// Wydrukowanie wyniku
	fmt.Println(output)  // Wyświetla 300
}

W tym przykładzie środowisko env zawiera zmienne foo i bar. Wyrażenie foo + bar wnioskuje typy foo i bar z środowiska podczas kompilacji, a następnie używa wartości tych zmiennych w czasie wykonywania, aby obliczyć wynik wyrażenia.

4. Składnia Expr w Szczegółach

4.1. Zmienne i Literały

Silnik wyrażeń Expr obsługuje zwykłe literały typów danych, w tym liczby, ciągi znaków i wartości logiczne. Literały to wartości danych bezpośrednio zapisane w kodzie, takie jak 42, "hello" i true.

Liczby

W Expr możesz bezpośrednio zapisywać liczby całkowite i zmiennoprzecinkowe:

42      // Reprezentuje liczbę całkowitą 42
3.14    // Reprezentuje liczbę zmiennoprzecinkową 3.14

Ciągi znaków

Literały łańcuchów znaków są zamknięte w podwójnych cudzysłowach " lub tzw. backticks ``. Na przykład:

"hello, world" // Łańcuch znaków zamknięty w podwójnych cudzysłowach, obsługuje znaki ucieczki
`hello, world` // Łańcuch znaków zamknięty w backtickach, zachowuje format łańcucha znaków bez obsługi znaków ucieczki

Wartości logiczne

Istnieją tylko dwie wartości logiczne, true i false, reprezentujące logiczne prawda i fałsz:

true   // Wartość logiczna prawda
false  // Wartość logiczna fałsz

Zmienne

Expr pozwala również na definiowanie zmiennych w środowisku, a następnie odwoływanie się do tych zmiennych w wyrażeniu. Na przykład:

env := map[string]interface{}{
    "wiek": 25,
    "imie": "Alice",
}

Następnie w wyrażeniu możesz odwołać się do wiek i imie:

wiek > 18  // Sprawdź, czy wiek jest większy niż 18
imie == "Alice"  // Sprawdź, czy imie jest równe "Alice"

4.2. Operatory

Silnik wyrażeń Expr obsługuje różne operatory, w tym operatory arytmetyczne, logiczne, porównania i zbiorów, itp.

Operatory arytmetyczne i logiczne

Operatory arytmetyczne obejmują dodawanie (+), odejmowanie (-), mnożenie (*), dzielenie (/) i modulo (%). Operatory logiczne obejmują logiczne AND (&&), logiczne OR (||) i logiczne NOT (!), na przykład:

2 + 2 // Wynik to 4
7 % 3 // Wynik to 1
!true // Wynik to false
wiek >= 18 && imie == "Alice" // Sprawdź, czy wiek nie jest mniejszy niż 18 i czy imię jest równe "Alice"

Operatory porównania

Operatory porównania obejmują równy (==), różny od (!=), mniejszy niż (<), mniejszy lub równy (<=), większy niż (>), i większy lub równy (>=), używane do porównywania dwóch wartości:

wiek == 25 // Sprawdź, czy wiek jest równy 25
wiek != 18 // Sprawdź, czy wiek nie jest równy 18
wiek > 20  // Sprawdź, czy wiek jest większy niż 20

Operatory zbiorów

Expr udostępnia także operatory do pracy ze zbiorami, takie jak in do sprawdzania, czy element znajduje się w zbiorze. Zbiory mogą być tablicami, wycinkami (slices) lub mapami:

"user" in ["user", "admin"]  // true, ponieważ "user" znajduje się w tablicy
3 in {1: true, 2: false}     // false, ponieważ 3 nie jest kluczem w mapie

Istnieją także zaawansowane funkcje operacji na zbiorach, takie jak all, any, one i none, które wymagają użycia anonimowych funkcji (lambda):

all(tweets, {.Len <= 240})  // Sprawdź, czy pole Len we wszystkich tweetach nie przekracza 240
any(tweets, {.Len > 200})   // Sprawdź, czy istnieje pole Len w tweetach, które przekracza 200

Operator elementu

W języku wyrażeń Expr operator elementu pozwala nam uzyskać dostęp do właściwości struct w języku Go. Ta funkcja pozwala Expr bezpośrednio manipulować złożonymi strukturami danych, co czyni ją bardzo elastyczną i praktyczną.

Użycie operatora elementu jest bardzo proste, wystarczy użyć operatora . po którym podajemy nazwę właściwości. Na przykład, jeśli mamy następującą struct:

type User struct {
    Name string
    Age  int
}

Można napisać wyrażenie, aby uzyskać dostęp do właściwości Name struktury User w ten sposób:

env := map[string]interface{}{
    "user": User{Name: "Alice", Age: 25},
}

kod := `user.Name`

program, err := expr.Compile(kod, expr.Env(env))
if err != nil {
    panic(err)
}

wynik, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(wynik) // Wynik: Alice

Obsługa wartości nil

Podczas dostępu do właściwości może się zdarzyć, że obiekt jest nil. Expr zapewnia bezpieczny dostęp do właściwości, dzięki czemu nawet jeśli struktura lub zagnieżdżona właściwość jest nil, nie spowoduje to błędu w czasie wykonania.

Użyj operatora ?. do odwoływania się do właściwości. Jeśli obiekt jest nil, zwróci nil zamiast generować błąd.

author.User?.Name

Równoważne wyrażenie

author.User != nil ? author.User.Name : nil

Użycie operatora ?? służy głównie do zwracania wartości domyślnych:

author.User?.Name ?? "Anonymous"

Równoważne wyrażenie

author.User != nil ? author.User.Name : "Anonymous"

Operator potoku

Operator potoku (|) w Expr służy do przekazywania wyniku jednego wyrażenia jako parametru do innego wyrażenia. Jest to podobne do działania operatora potoku w powłoce Unix, umożliwiające łańcuchowe łączenie wielu modułów funkcjonalnych w procesie przetwarzania. W Expr użycie tego operatora może tworzyć wyrażenia bardziej klarowne i zwięzłe.

Na przykład, jeśli mamy funkcję do pobrania imienia użytkownika i szablon na powitanie:

env := map[string]interface{}{
    "user":      User{Name: "Bob", Age: 30},
    "get_name":  func(u User) string { return u.Name },
    "greet_msg": "Cześć, %s!",
}

code := `get_name(user) | sprintf(greet_msg)`

program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(output) // Wynik: Cześć, Bob!

W tym przykładzie najpierw pobieramy imię użytkownika przez get_name(user), a następnie przekazujemy imię do funkcji sprintf używając operatora potoku |, aby wygenerować ostateczną wiadomość powitalną.

Użycie operatora potoku może zmodularyzować nasz kod, poprawić jego ponowne wykorzystanie oraz ułatwić czytelność wyrażeń.

4.3 Funkcje

Expr obsługuje wbudowane funkcje oraz funkcje niestandardowe, co sprawia, że wyrażenia stają się bardziej potężne i elastyczne.

Użycie wbudowanych funkcji

Wbudowane funkcje takie jak len, all, none, any, itp. mogą być używane bezpośrednio w wyrażeniu.

// Przykład użycia wbudowanej funkcji
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// Uwaga: tutaj env musi zawierać zmienną users, a każdy użytkownik musi mieć właściwość Age
output, err := expr.Run(program, env)
fmt.Print(output) // Jeśli wszyscy użytkownicy w env są w wieku 18 lat lub starsi, zwróci true

Jak definiować i używać funkcji niestandardowych

W Expr możesz tworzyć funkcje niestandardowe, przekazując definicje funkcji w mapowaniu środowiskowym.

// Przykład funkcji niestandardowej
env := map[string]interface{}{
    "greet": func(name string) string {
        return fmt.Sprintf("Cześć, %s!", name)
    },
}

program, err := expr.Compile(`greet("World")`, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
fmt.Print(output) // Zwraca Cześć, World!

Podczas korzystania z funkcji w Expr, możesz zmodularyzować swój kod i włączyć w wyrażenia złożoną logikę. Poprzez połączenie zmiennych, operatorów i funkcji, Expr staje się potężnym i łatwym w użyciu narzędziem. Pamiętaj zawsze o zachowaniu bezpieczeństwa typów przy budowaniu środowiska Expr i uruchamianiu wyrażeń.

5. Dokumentacja wbudowanych funkcji

Silnik wyrażeń Expr udostępnia programistom bogaty zestaw wbudowanych funkcji do obsługi różnorodnych złożonych scenariuszy. Poniżej przedstawimy te wbudowane funkcje i ich użycie.

all

Funkcja all służy do sprawdzenia, czy wszystkie elementy w kolekcji spełniają określone warunki. Przyjmuje dwa parametry: kolekcję i wyrażenie warunkowe.

// Sprawdzenie, czy wszystkie tweety mają długość treści mniejszą niż 240 znaków
code := `all(tweets, len(.Content) < 240)`

any

Podobnie jak funkcja all, funkcja any służy do sprawdzenia, czy którykolwiek element w kolekcji spełnia warunek.

// Sprawdzenie, czy którykolwiek z tweeta ma długość treści większą niż 240 znaków
code := `any(tweets, len(.Content) > 240)`

none

Funkcja none służy do sprawdzenia, czy żaden z elementów w kolekcji nie spełnia warunku.

// Upewnienie się, że żaden z tweety nie jest oznaczony jako powtórzony
code := `none(tweets, .IsRepeated)`

one

Funkcja one służy do potwierdzenia, że tylko jeden element w kolekcji spełnia warunek.

// Sprawdzenie, czy tylko jeden tweet zawiera określone słowo kluczowe
code := `one(tweets, contains(.Content, "keyword"))`

filter

Funkcja filter służy do filtrowania elementów kolekcji spełniających określony warunek.

// Filtrowanie wszystkich oznaczonych jako priorytetowe tweety
code := `filter(tweets, .IsPriority)`

map

Funkcja map służy do transformowania elementów w kolekcji zgodnie z określoną metodą.

// Formatowanie czasu publikacji wszystkich tweety
code := `map(tweets, {.PublishTime: Format(.Date)})`

len

Funkcja len służy do zwracania długości kolekcji lub ciągu znaków.

// Pobieranie długości nazwy użytkownika
code := `len(username)`

contains

Funkcja contains służy do sprawdzania, czy ciąg znaków zawiera podciąg lub czy kolekcja zawiera określony element.

// Sprawdzenie, czy nazwa użytkownika zawiera niedozwolone znaki
code := `contains(username, "niedozwolone znaki")`

Wymienione powyżej funkcje są tylko częścią wbudowanych funkcji dostarczanych przez silnik wyrażeń Expr. Dzięki tym potężnym funkcjom możesz elastyczniej i efektywniej zarządzać danymi i logiką. Aby uzyskać bardziej szczegółową listę funkcji oraz instrukcje dotyczące używania, zapoznaj się z oficjalną dokumentacją Expr.