Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ jobs:
working-directory: ComplianceSuite
- run: make test-compliance || true # NB: This doesn't succeed yet!

build-minimal:
name: Build minimal (all traits disabled)
runs-on: ubuntu-latest
container: "swift:6.2"
permissions:
contents: read
steps:
- name: Show Swift version
run: swift --version
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install dependencies
run: |
apt-get update
apt-get install -y libjemalloc-dev make
- name: Build (minimal, all traits disabled)
run: make build-minimal

diff:
name: diff compliance tests (vs main)
runs-on: macos-latest
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ perf:
.PHONY: build
build:
swift build


# Minimum viable build: all package traits disabled.
.PHONY: build-minimal
build-minimal:
swift build --disable-default-traits

.PHONY: build-release
build-release:
swift build -c release
Expand Down
15 changes: 14 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ let package = Package(
targets: ["CLI"]
),
],
traits: [
// Enabled by default. Library-only consumers can opt out to drop the CLI and its
// dependencies from resolution.
.default(enabledTraits: ["CLI"]),
Trait(
name: "CLI",
description: "Builds the swift-opa-cli executable and its dependencies."
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0")
],
Expand Down Expand Up @@ -77,7 +86,11 @@ let package = Package(
name: "CLI",
dependencies: [
"Rego",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(
name: "ArgumentParser",
package: "swift-argument-parser",
condition: .when(traits: ["CLI"])
),
]
),
]
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ let package = Package(
)
```

### Traits and Minimal Builds

Swift-OPA ships a `swift-opa-cli` executable that depends on
[swift-argument-parser](https://github.com/apple/swift-argument-parser). This is enabled
by default via the `CLI` package trait.

Library-only consumers who only want the VM can disable the trait to drop the CLI and
its dependencies entirely on newer Swift toolchain versions:

```swift
dependencies: [
.package(
url: "https://github.com/open-policy-agent/swift-opa",
branch: "main",
traits: [] // disables the default "CLI" trait; swift-argument-parser is not fetched
),
],
```

## Usage

The main entry point for policy evaluation is the `OPA.Engine`. An engine can evaluate policies packaged in one or more
Expand Down
60 changes: 31 additions & 29 deletions Sources/CLI/BenchCommand.swift
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
import AST
import ArgumentParser
import Foundation
import Rego
#if CLI
import AST
import ArgumentParser
import Foundation
import Rego

struct BenchCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "bench",
abstract: "Benchmark a Rego query"
)
struct BenchCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "bench",
abstract: "Benchmark a Rego query"
)

@OptionGroup
var evalOptions: EvalOptions
@Option(name: [.short, .customLong("count")], help: "iteration count")
var count: UInt = 10_000
@OptionGroup
var evalOptions: EvalOptions
@Option(name: [.short, .customLong("count")], help: "iteration count")
var count: UInt = 10_000

mutating func run() async throws {
// Initialize a Rego.Engine initially configured with our bundles from the CLI options.
var regoEngine = Rego.OPA.Engine(bundlePaths: self.evalOptions.bundlePaths)
mutating func run() async throws {
// Initialize a Rego.Engine initially configured with our bundles from the CLI options.
var regoEngine = Rego.OPA.Engine(bundlePaths: self.evalOptions.bundlePaths)

// Prepare does as much pre-processing as possible to get ready to evaluate queries.
// This only needs to be done once when loading the engine and after updating it.
let preparedQuery = try await regoEngine.prepareForEvaluation(query: self.evalOptions.query)
// Prepare does as much pre-processing as possible to get ready to evaluate queries.
// This only needs to be done once when loading the engine and after updating it.
let preparedQuery = try await regoEngine.prepareForEvaluation(query: self.evalOptions.query)

let report = try await measureAsync(iterations: Int(count)) {
let _ = try await preparedQuery.evaluate(
input: self.evalOptions.inputValue,
strictBuiltins: self.evalOptions.strictBuiltinErrors
)
}
let report = try await measureAsync(iterations: Int(count)) {
let _ = try await preparedQuery.evaluate(
input: self.evalOptions.inputValue,
strictBuiltins: self.evalOptions.strictBuiltinErrors
)
}

// <name> <iterations> <value> <unit> [<value> <unit>...]
// https://go.googlesource.com/proposal/+/master/design/14313-benchmark-format.md
print(report.formatted(.gobench))
// <name> <iterations> <value> <unit> [<value> <unit>...]
// https://go.googlesource.com/proposal/+/master/design/14313-benchmark-format.md
print(report.formatted(.gobench))
}
}
}
#endif
29 changes: 20 additions & 9 deletions Sources/CLI/CLI.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import ArgumentParser
#if CLI
import ArgumentParser

@main
struct CLIRootCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "swift-opa-cli",
abstract: "An example command line showing swift-opa in action.",
subcommands: [EvalCommand.self, BenchCommand.self, CapabilitiesCommand.self]
)
}
@main
struct CLIRootCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "swift-opa-cli",
abstract: "An example command line showing swift-opa in action.",
subcommands: [EvalCommand.self, BenchCommand.self, CapabilitiesCommand.self]
)
}
#else
// The executable is built without its swift-argument-parser dependency when the
// "CLI" trait is disabled. This stub keeps the target linkable in that configuration.
@main
struct CLIRootCommand {
static func main() {
print("swift-opa-cli was built without the \"CLI\" trait enabled.")
}
}
#endif
99 changes: 51 additions & 48 deletions Sources/CLI/CapabilitiesCommand.swift
Original file line number Diff line number Diff line change
@@ -1,51 +1,54 @@
import ArgumentParser
import Foundation
import Rego

struct CapabilitiesCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "capabilities",
abstract: "Output capabilities JSON for supported builtins.",
shouldDisplay: false
)

@Argument(help: "Path to the capabilities.json file")
var capabilitiesPath: String

func run() async throws {
let supportedBuiltins = Set(BuiltinRegistry.getSupportedBuiltinNames())

guard let capabilitiesData = FileManager.default.contents(atPath: capabilitiesPath) else {
throw CapabilitiesError.capabilitiesFileNotFound(path: capabilitiesPath)
}

guard let capabilitiesDict = try JSONSerialization.jsonObject(with: capabilitiesData) as? [String: Any] else {
throw CapabilitiesError.invalidCapabilitiesFormat
}

guard let builtinsArray = capabilitiesDict["builtins"] as? [[String: Any]] else {
throw CapabilitiesError.invalidCapabilitiesFormat
}

let filteredBuiltins = builtinsArray.filter { builtin in
guard let name = builtin["name"] as? String else { return false }
return supportedBuiltins.contains(name)
}

var filteredCapabilities = capabilitiesDict
filteredCapabilities["builtins"] = filteredBuiltins
filteredCapabilities.removeValue(forKey: "wasm_abi_versions")

let jsonData = try JSONSerialization.data(
withJSONObject: filteredCapabilities, options: [.prettyPrinted, .sortedKeys])

if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
#if CLI
import ArgumentParser
import Foundation
import Rego

struct CapabilitiesCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "capabilities",
abstract: "Output capabilities JSON for supported builtins.",
shouldDisplay: false
)

@Argument(help: "Path to the capabilities.json file")
var capabilitiesPath: String

func run() async throws {
let supportedBuiltins = Set(BuiltinRegistry.getSupportedBuiltinNames())

guard let capabilitiesData = FileManager.default.contents(atPath: capabilitiesPath) else {
throw CapabilitiesError.capabilitiesFileNotFound(path: capabilitiesPath)
}

guard let capabilitiesDict = try JSONSerialization.jsonObject(with: capabilitiesData) as? [String: Any]
else {
throw CapabilitiesError.invalidCapabilitiesFormat
}

guard let builtinsArray = capabilitiesDict["builtins"] as? [[String: Any]] else {
throw CapabilitiesError.invalidCapabilitiesFormat
}

let filteredBuiltins = builtinsArray.filter { builtin in
guard let name = builtin["name"] as? String else { return false }
return supportedBuiltins.contains(name)
}

var filteredCapabilities = capabilitiesDict
filteredCapabilities["builtins"] = filteredBuiltins
filteredCapabilities.removeValue(forKey: "wasm_abi_versions")

let jsonData = try JSONSerialization.data(
withJSONObject: filteredCapabilities, options: [.prettyPrinted, .sortedKeys])

if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
}
}
}

enum CapabilitiesError: Error {
case capabilitiesFileNotFound(path: String)
case invalidCapabilitiesFormat
}
enum CapabilitiesError: Error {
case capabilitiesFileNotFound(path: String)
case invalidCapabilitiesFormat
}
#endif
Loading
Loading