From 1ef88f6abc4c2c9f70a3cf8652ca72504bad3b36 Mon Sep 17 00:00:00 2001 From: Jonas Klasen Date: Mon, 30 Mar 2026 16:14:39 +0000 Subject: [PATCH 1/8] update for windows --- .github/workflows/CI.yml | 8 +++----- src/graph/contract.jl | 20 ++++++++++---------- src/graph/customize.jl | 20 ++++++++++---------- src/graph/extract.jl | 26 +++++++++++++------------- src/graph/partition.jl | 20 ++++++++++---------- 5 files changed, 46 insertions(+), 48 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d07f847..bd80167 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,8 +20,7 @@ jobs: # - "nightly" os: - ubuntu-latest - # turn of windows for now until someone fixes the Cmd issues - # - windows-latest + - windows-latest - macos-latest arch: - x64 @@ -29,9 +28,8 @@ jobs: exclude: - os: ubuntu-latest arch: aarch64 - # # windows does not support aarch64 yet - # - os: windows-latest - # arch: aarch64 + - os: windows-latest + arch: aarch64 # libosrmc does not support x64 on macOS yet - os: macos-latest arch: x64 diff --git a/src/graph/contract.jl b/src/graph/contract.jl index 3c5af04..332a49c 100644 --- a/src/graph/contract.jl +++ b/src/graph/contract.jl @@ -30,46 +30,46 @@ function contract( parse_conditionals_from_now::Int64 = 0, time_zone_file::String = "", ) - cmd = `$(OSRM_jll.osrm_contract())` + args = String[] # Verbosity - convert enum to string verbosity_str = verbosity_enum_to_string(verbosity) if verbosity_str != "INFO" # Only add if non-default - cmd = `$cmd --verbosity $verbosity_str` + push!(args, "--verbosity", verbosity_str) end # Threads if threads !== nothing - cmd = `$cmd --threads $(string(threads))` + push!(args, "--threads", string(threads)) end # Segment speed files (can be multiple) for path in segment_speed_file - cmd = `$cmd --segment-speed-file $path` + push!(args, "--segment-speed-file", path) end # Turn penalty files (can be multiple) for path in turn_penalty_file - cmd = `$cmd --turn-penalty-file $path` + push!(args, "--turn-penalty-file", path) end # Edge weight updates over factor if edge_weight_updates_over_factor != 0.0 - cmd = `$cmd --edge-weight-updates-over-factor $(string(edge_weight_updates_over_factor))` + push!(args, "--edge-weight-updates-over-factor", string(edge_weight_updates_over_factor)) end # Parse conditionals from now if parse_conditionals_from_now != 0 - cmd = `$cmd --parse-conditionals-from-now $(string(parse_conditionals_from_now))` + push!(args, "--parse-conditionals-from-now", string(parse_conditionals_from_now)) end # Time zone file if !isempty(time_zone_file) - cmd = `$cmd --time-zone-file $time_zone_file` + push!(args, "--time-zone-file", time_zone_file) end # Input file (positional, goes last) - cmd = `$cmd $osrm_base_path` + push!(args, osrm_base_path) - return run(cmd) + return run(`$(OSRM_jll.osrm_contract()) $args`) end diff --git a/src/graph/customize.jl b/src/graph/customize.jl index 8400383..d639e12 100644 --- a/src/graph/customize.jl +++ b/src/graph/customize.jl @@ -30,46 +30,46 @@ function customize( parse_conditionals_from_now::Int64 = 0, time_zone_file::String = "", ) - cmd = `$(OSRM_jll.osrm_customize())` + args = String[] # Verbosity - convert enum to string verbosity_str = verbosity_enum_to_string(verbosity) if verbosity_str != "INFO" # Only add if non-default - cmd = `$cmd --verbosity $verbosity_str` + push!(args, "--verbosity", verbosity_str) end # Threads if threads !== nothing - cmd = `$cmd --threads $(string(threads))` + push!(args, "--threads", string(threads)) end # Segment speed files (can be multiple) for path in segment_speed_file - cmd = `$cmd --segment-speed-file $path` + push!(args, "--segment-speed-file", path) end # Turn penalty files (can be multiple) for path in turn_penalty_file - cmd = `$cmd --turn-penalty-file $path` + push!(args, "--turn-penalty-file", path) end # Edge weight updates over factor if edge_weight_updates_over_factor != 0.0 - cmd = `$cmd --edge-weight-updates-over-factor $(string(edge_weight_updates_over_factor))` + push!(args, "--edge-weight-updates-over-factor", string(edge_weight_updates_over_factor)) end # Parse conditionals from now if parse_conditionals_from_now != 0 - cmd = `$cmd --parse-conditionals-from-now $(string(parse_conditionals_from_now))` + push!(args, "--parse-conditionals-from-now", string(parse_conditionals_from_now)) end # Time zone file if !isempty(time_zone_file) - cmd = `$cmd --time-zone-file $time_zone_file` + push!(args, "--time-zone-file", time_zone_file) end # Input file (positional, goes last) - cmd = `$cmd $osrm_base_path` + push!(args, osrm_base_path) - return run(cmd) + return run(`$(OSRM_jll.osrm_customize()) $args`) end diff --git a/src/graph/extract.jl b/src/graph/extract.jl index b74e39b..ffaca9e 100644 --- a/src/graph/extract.jl +++ b/src/graph/extract.jl @@ -38,7 +38,7 @@ function extract( disable_location_cache::Bool = false, dump_nbg_graph::Bool = false ) - cmd = `$(OSRM_jll.osrm_extract())` + args = String[] # Profile (required) if isa(profile, Profile) @@ -46,53 +46,53 @@ function extract( else profile_path_val = profile end - cmd = `$cmd --profile $profile_path_val` + push!(args, "--profile", profile_path_val) # Verbosity - convert enum to string verbosity_str = verbosity_enum_to_string(verbosity) if verbosity_str != "INFO" # Only add if non-default - cmd = `$cmd --verbosity $verbosity_str` + push!(args, "--verbosity", verbosity_str) end # Data version if !isempty(data_version) - cmd = `$cmd --data-version $data_version` + push!(args, "--data-version", data_version) end # Threads if threads !== nothing - cmd = `$cmd --threads $(string(threads))` + push!(args, "--threads", string(threads)) end # Small component size if small_component_size != 1000 # Only add if non-default - cmd = `$cmd --small-component-size $(string(small_component_size))` + push!(args, "--small-component-size", string(small_component_size)) end # Boolean flags (only add if true) if with_osm_metadata - cmd = `$cmd --with-osm-metadata` + push!(args, "--with-osm-metadata") end if parse_conditional_restrictions - cmd = `$cmd --parse-conditional-restrictions` + push!(args, "--parse-conditional-restrictions") end if disable_location_cache - cmd = `$cmd --disable-location-cache` + push!(args, "--disable-location-cache") end if dump_nbg_graph - cmd = `$cmd --dump-nbg-graph` + push!(args, "--dump-nbg-graph") end # Location-dependent data (can be multiple) for path in location_dependent_data - cmd = `$cmd --location-dependent-data $path` + push!(args, "--location-dependent-data", path) end # Input file (positional, goes last) - cmd = `$cmd $osm_path` + push!(args, osm_path) - return run(cmd) + return run(`$(OSRM_jll.osrm_extract()) $args`) end diff --git a/src/graph/partition.jl b/src/graph/partition.jl index db315f9..ddb202c 100644 --- a/src/graph/partition.jl +++ b/src/graph/partition.jl @@ -31,48 +31,48 @@ function partition( small_component_size::Int = 1000, max_cell_sizes::Vector{Int} = [128, 4096, 65536, 2097152], ) - cmd = `$(OSRM_jll.osrm_partition())` + args = String[] # Verbosity - convert enum to string verbosity_str = verbosity_enum_to_string(verbosity) if verbosity_str != "INFO" # Only add if non-default - cmd = `$cmd --verbosity $verbosity_str` + push!(args, "--verbosity", verbosity_str) end # Threads if threads !== nothing - cmd = `$cmd --threads $(string(threads))` + push!(args, "--threads", string(threads)) end # Balance if balance != 1.2 # Only add if non-default - cmd = `$cmd --balance $(string(balance))` + push!(args, "--balance", string(balance)) end # Boundary if boundary != 0.25 # Only add if non-default - cmd = `$cmd --boundary $(string(boundary))` + push!(args, "--boundary", string(boundary)) end # Optimizing cuts if optimizing_cuts != 10 # Only add if non-default - cmd = `$cmd --optimizing-cuts $(string(optimizing_cuts))` + push!(args, "--optimizing-cuts", string(optimizing_cuts)) end # Small component size if small_component_size != 1000 # Only add if non-default - cmd = `$cmd --small-component-size $(string(small_component_size))` + push!(args, "--small-component-size", string(small_component_size)) end # Max cell sizes - convert vector to comma-separated string default_max_cell_sizes = [128, 4096, 65536, 2097152] if max_cell_sizes != default_max_cell_sizes max_cell_sizes_str = join(string.(max_cell_sizes), ",") - cmd = `$cmd --max-cell-sizes $max_cell_sizes_str` + push!(args, "--max-cell-sizes", max_cell_sizes_str) end # Input file (positional, goes last) - cmd = `$cmd $osrm_base_path` + push!(args, osrm_base_path) - return run(cmd) + return run(`$(OSRM_jll.osrm_partition()) $args`) end From 5c2aeb40fe090dbf6dcaaaa48d8104135afeee75 Mon Sep 17 00:00:00 2001 From: Jonas Klasen Date: Fri, 10 Apr 2026 19:19:42 +0200 Subject: [PATCH 2/8] use privat OSRM_jll --- Project.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Project.toml b/Project.toml index 51d3452..6a251a5 100644 --- a/Project.toml +++ b/Project.toml @@ -17,3 +17,7 @@ FlatBuffers = "0.6.2" OSRM_jll = "~6.0.0" julia = "1.11.0" libosrmc_jll = "=6.0.2" + +[sources.OSRM_jll] +url = "https://github.com/jrklasen/OSRM_jll.jl.git" +rev = "main" From 9eaf7fb00494f2dd49bb8c1dd035eb8bfb706fed Mon Sep 17 00:00:00 2001 From: Jonas Klasen Date: Mon, 13 Apr 2026 09:45:08 +0200 Subject: [PATCH 3/8] use the latest libosrmc build --- Project.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6a251a5..a80f7e6 100644 --- a/Project.toml +++ b/Project.toml @@ -16,8 +16,12 @@ EnumX = "1.0.5" FlatBuffers = "0.6.2" OSRM_jll = "~6.0.0" julia = "1.11.0" -libosrmc_jll = "=6.0.2" +libosrmc_jll = "=6.0.3" [sources.OSRM_jll] url = "https://github.com/jrklasen/OSRM_jll.jl.git" rev = "main" + +[sources.libosrmc_jll] +url = "https://github.com/jrklasen/libosrmc_jll.jl.git" +rev = "main" From 453c9fc1a57a27f532f57319ddd78ef2e0003206 Mon Sep 17 00:00:00 2001 From: Jonas Klasen Date: Mon, 13 Apr 2026 21:06:06 +0200 Subject: [PATCH 4/8] Update OSRM_jll and libosrmc_jll versions to 26.4.0 in Project.toml --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index a80f7e6..92af962 100644 --- a/Project.toml +++ b/Project.toml @@ -14,9 +14,9 @@ libosrmc_jll = "b012619c-76c7-5216-bb6b-491eaf671aba" [compat] EnumX = "1.0.5" FlatBuffers = "0.6.2" -OSRM_jll = "~6.0.0" +OSRM_jll = "~26.4.0" julia = "1.11.0" -libosrmc_jll = "=6.0.3" +libosrmc_jll = "=26.4.0" [sources.OSRM_jll] url = "https://github.com/jrklasen/OSRM_jll.jl.git" From 43a3d6abc3d22b951719c865812f28b3753dfa12 Mon Sep 17 00:00:00 2001 From: Jonas Klasen Date: Fri, 17 Apr 2026 08:43:52 +0200 Subject: [PATCH 5/8] Add OVERVIEW_BY_LEGS option to Overview enum in OSRM --- Project.toml | 13 ++++--------- src/OpenSourceRoutingMachine.jl | 2 +- src/shared.jl | 5 +++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Project.toml b/Project.toml index 92af962..a837a2d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,8 +1,8 @@ name = "OpenSourceRoutingMachine" uuid = "19e5a071-0e58-4e20-9e36-393e8533d14d" license = "MIT" -authors = ["MOVIRO GmbH "] version = "0.2.1" +authors = ["MOVIRO GmbH "] [deps] EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56" @@ -11,17 +11,12 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" OSRM_jll = "a74304e1-5a13-544f-adb7-0572470cee19" libosrmc_jll = "b012619c-76c7-5216-bb6b-491eaf671aba" +[sources] +libosrmc_jll = {rev = "main", url = "https://github.com/jrklasen/libosrmc_jll.jl"} + [compat] EnumX = "1.0.5" FlatBuffers = "0.6.2" OSRM_jll = "~26.4.0" julia = "1.11.0" libosrmc_jll = "=26.4.0" - -[sources.OSRM_jll] -url = "https://github.com/jrklasen/OSRM_jll.jl.git" -rev = "main" - -[sources.libosrmc_jll] -url = "https://github.com/jrklasen/libosrmc_jll.jl.git" -rev = "main" diff --git a/src/OpenSourceRoutingMachine.jl b/src/OpenSourceRoutingMachine.jl index 093f955..9bfc213 100644 --- a/src/OpenSourceRoutingMachine.jl +++ b/src/OpenSourceRoutingMachine.jl @@ -79,7 +79,7 @@ export Algorithm, ALGORITHM_CH, ALGORITHM_MLD, Snapping, SNAPPING_DEFAULT, SNAPPING_ANY, Approach, APPROACH_CURB, APPROACH_UNRESTRICTED, APPROACH_OPPOSITE, Geometries, GEOMETRIES_POLYLINE, GEOMETRIES_POLYLINE6, GEOMETRIES_GEOJSON, - Overview, OVERVIEW_SIMPLIFIED, OVERVIEW_FULL, OVERVIEW_FALSE, + Overview, OVERVIEW_SIMPLIFIED, OVERVIEW_FULL, OVERVIEW_FALSE, OVERVIEW_BY_LEGS, Annotations, ANNOTATIONS_NONE, ANNOTATIONS_DURATION, ANNOTATIONS_NODES, ANNOTATIONS_DISTANCE, ANNOTATIONS_WEIGHT, ANNOTATIONS_DATASOURCES, ANNOTATIONS_SPEED, ANNOTATIONS_ALL, diff --git a/src/shared.jl b/src/shared.jl index b264629..4882ffc 100644 --- a/src/shared.jl +++ b/src/shared.jl @@ -48,13 +48,14 @@ EnumX.@enum( """ Overview -Controls how much geometry detail OSRM should include (`OVERVIEW_SIMPLIFIED`, `OVERVIEW_FULL`, `OVERVIEW_FALSE`). +Controls how much geometry detail OSRM should include (`OVERVIEW_SIMPLIFIED`, `OVERVIEW_FULL`, `OVERVIEW_FALSE`, `OVERVIEW_BY_LEGS`). """ EnumX.@enum( Overview::Int32, OVERVIEW_SIMPLIFIED = 0, OVERVIEW_FULL = 1, - OVERVIEW_FALSE = 2 + OVERVIEW_FALSE = 2, + OVERVIEW_BY_LEGS = 3 ) """ From aafaf6a7115a376532d0c35aebd83f84886235f8 Mon Sep 17 00:00:00 2001 From: Jonas Klasen Date: Fri, 17 Apr 2026 18:08:46 +0200 Subject: [PATCH 6/8] update flatbuffer code gen --- gen/README.md | 34 +++---- gen/flatbuffers/route.fbs | 2 + gen/generate.jl | 29 +++--- gen/src/Generator.jl | 15 +-- gen/src/dependencies.jl | 160 +++++++++++++++----------------- gen/src/download.jl | 35 +++---- gen/src/generation.jl | 186 +++++++++++++++++-------------------- gen/src/parsing.jl | 188 ++++++++++++++++---------------------- gen/src/types.jl | 81 ++++++++++------ src/types.jl | 2 + 10 files changed, 342 insertions(+), 390 deletions(-) diff --git a/gen/README.md b/gen/README.md index 7bd932f..7ba57ee 100644 --- a/gen/README.md +++ b/gen/README.md @@ -1,34 +1,30 @@ # FlatBuffers Type Generator -Tools for automatically generating Julia type definitions from OSRM's [FlatBuffers](https://flatbuffers.dev/) schema files. The OSRM backend uses FlatBuffers to serialize API responses, and this generator parses the `.fbs` schema files to produce equivalent Julia type definitions. - -## Components - -- **`Generator.jl`**: Main module containing code for downloading schema files from the OSRM GitHub repository, parsing `.fbs` files to extract enums/structs/tables, mapping FlatBuffers types to Julia equivalents, and generating Julia code with proper dependency resolution. - -- **`generate.jl`**: Convenience script uses the Module to download schemas and generates `../src/types.jl`. Configured for OSRM `v6.0.0` by default; update `OSRM_VERSION` in the script for other versions. +Generates Julia type definitions from OSRM's [FlatBuffers](https://flatbuffers.dev/) schema files. The OSRM backend uses FlatBuffers to serialize API responses; this generator parses the `.fbs` schemas and produces equivalent Julia types for use with `FlatBuffers.jl`. ## Usage -Run from this directory: - ```bash +cd gen julia generate.jl ``` -This downloads schema files (`fbresult.fbs`, `route.fbs`, `table.fbs`, `position.fbs`, `waypoint.fbs`) to `flatbuffers/`, parses them and their includes, and generates `../src/types.jl`. +This downloads schema files to `flatbuffers/`, parses them, and generates `../src/types.jl`. -## How It Works +## Structure -1. **Download**: Fetches `.fbs` files from the OSRM repository at the specified version tag. -2. **Parse**: Extracts enums, structs, and tables, tracking include relationships. -3. **Generate**: Resolves dependencies, topologically sorts types, and generates Julia code with `@cenum` definitions, immutable structs, and mutable table structs. -4. **Output**: Writes generated code to `src/types.jl`. +| File | Responsibility | +|------|---------------| +| `generate.jl` | Entry script — configuration and orchestration | +| `src/Generator.jl` | Module wrapper | +| `src/types.jl` | Type maps (FBS -> Julia) and IR structs | +| `src/download.jl` | Download `.fbs` files from GitHub | +| `src/parsing.jl` | Parse `.fbs` files into IR | +| `src/dependencies.jl` | Topological sorting of type definitions | +| `src/generation.jl` | Emit Julia source code from IR | -## Updating for New Versions +## Updating for a new OSRM version -1. Edit `OSRM_VERSION` in `generate.jl` (e.g., `"v6.1.0"`) +1. Edit `OSRM_VERSION` in `generate.jl` (currently `v26.4.0`) 2. Run `julia generate.jl` 3. Review `src/types.jl` for breaking changes - -The generator handles includes automatically and ensures types are properly referenced across files. diff --git a/gen/flatbuffers/route.fbs b/gen/flatbuffers/route.fbs index 902bfa1..bbdd5fc 100644 --- a/gen/flatbuffers/route.fbs +++ b/gen/flatbuffers/route.fbs @@ -96,6 +96,8 @@ table Leg { summary: string; annotations: Annotation; steps: [Step]; + polyline: string; + coordinates: [Position]; } table RouteObject { diff --git a/gen/generate.jl b/gen/generate.jl index 406340a..4b4742d 100644 --- a/gen/generate.jl +++ b/gen/generate.jl @@ -3,9 +3,8 @@ include("src/Generator.jl") using .Generator: download_flatbuffers, generate_julia_code -# Configuration constants -const OSRM_VERSION = "v6.0.0" - +# --- Configuration --- +const OSRM_VERSION = "v26.4.0" const BASE_URL = "https://raw.githubusercontent.com/Project-OSRM/osrm-backend" const FBS_SUBDIR = "include/engine/api/flatbuffers" const DOWNLOAD_FILES = [ @@ -16,33 +15,29 @@ const DOWNLOAD_FILES = [ "waypoint.fbs", ] -# Path constants relative to script location +# --- Paths (relative to this script) --- const SCRIPT_DIR = @__DIR__ const FLATBUFFERS_DIR = joinpath(SCRIPT_DIR, "flatbuffers") const SRC_DIR = joinpath(SCRIPT_DIR, "..", "src") - - const INPUT_FILE = joinpath(FLATBUFFERS_DIR, "fbresult.fbs") const OUTPUT_FILE = joinpath(SRC_DIR, "types.jl") -# Step 1: Download flatbuffer files +# --- Step 1: Download --- println("Step 1: Downloading FlatBuffer schema files...") println("-"^60) -success = download_flatbuffers(OSRM_VERSION; base_url = BASE_URL, subdir = FBS_SUBDIR, files = DOWNLOAD_FILES, output_dir = FLATBUFFERS_DIR) -if !success - println() - println("Error: Failed to download some FlatBuffer files") +if !download_flatbuffers(OSRM_VERSION; base_url = BASE_URL, subdir = FBS_SUBDIR, files = DOWNLOAD_FILES, output_dir = FLATBUFFERS_DIR) + println("\nError: Failed to download some FlatBuffer files") + exit(1) end -# Step 2: Generate Julia code +# --- Step 2: Generate --- println("Step 2: Generating Julia code...") println("-"^60) -success = generate_julia_code(INPUT_FILE, OUTPUT_FILE) -if !success - println() - println("Error: Failed to generate Julia code") +if !generate_julia_code(INPUT_FILE, OUTPUT_FILE) + println("\nError: Failed to generate Julia code") + exit(1) end println("="^60) -println("Successfully completed all steps!") +println("Done.") println("="^60) diff --git a/gen/src/Generator.jl b/gen/src/Generator.jl index 2c3ab44..422c456 100644 --- a/gen/src/Generator.jl +++ b/gen/src/Generator.jl @@ -1,29 +1,22 @@ """ Generator -Module for generating Julia code from FlatBuffers schema files. +Module for generating Julia type definitions from FlatBuffers schema files. -This module provides functionality to download FlatBuffers schema files from the OSRM -GitHub repository, parse `.fbs` files to extract enums, structs, and tables, map -FlatBuffers types to Julia equivalents, and generate Julia code with proper dependency -resolution. - -The main entry points are: -- [`download_flatbuffers`](@ref): Download schema files from a GitHub repository -- [`generate_julia_code`](@ref): Generate Julia type definitions from FlatBuffers schemas +Provides two entry points: +- [`download_flatbuffers`](@ref): Download `.fbs` schema files from the OSRM GitHub repository +- [`generate_julia_code`](@ref): Parse schemas and generate Julia code with dependency resolution """ module Generator using Downloads -# Include all module files in dependency order include("types.jl") include("download.jl") include("parsing.jl") include("dependencies.jl") include("generation.jl") -# Exports export download_flatbuffers, generate_julia_code end # module Generator diff --git a/gen/src/dependencies.jl b/gen/src/dependencies.jl index 2b8f3a3..9855d87 100644 --- a/gen/src/dependencies.jl +++ b/gen/src/dependencies.jl @@ -1,16 +1,18 @@ -"""Get all files related to a given file (the file itself and all files it includes recursively).""" +""" + get_related_files(parser::FBSParser, filename::String) -> Set{String} + +Return `filename` and all files it transitively includes. +""" function get_related_files(parser::FBSParser, filename::String)::Set{String} related = Set{String}([filename]) - to_process = [filename] - - while !isempty(to_process) - current = pop!(to_process) - if haskey(parser.file_includes, current) - for included_file in parser.file_includes[current] - if !(included_file in related) - push!(related, included_file) - push!(to_process, included_file) - end + queue = [filename] + + while !isempty(queue) + current = pop!(queue) + for included in get(parser.file_includes, current, Set{String}()) + if included ∉ related + push!(related, included) + push!(queue, included) end end end @@ -18,118 +20,104 @@ function get_related_files(parser::FBSParser, filename::String)::Set{String} return related end -"""Extract struct and table dependencies from a type string.""" -function extract_type_dependencies(parser::FBSParser, fbs_type)::Set{String} - dependencies = Set{String}() +""" + extract_type_dependencies(parser::FBSParser, fbs_type::AbstractString) -> Set{String} + +Return the set of user-defined struct/table names that `fbs_type` depends on. +Primitive types and enums are not considered dependencies (they have no ordering constraints). +""" +function extract_type_dependencies(parser::FBSParser, fbs_type::AbstractString)::Set{String} fbs_type = strip(String(fbs_type)) + deps = Set{String}() - # Handle arrays: [Type] -> Type + # Unwrap array type: [Type] -> Type if startswith(fbs_type, '[') && endswith(fbs_type, ']') - inner_type = String(strip(fbs_type[2:(end - 1)])) - return extract_type_dependencies(parser, inner_type) + return extract_type_dependencies(parser, fbs_type[2:(end - 1)]) end - # Check if it's a basic type (no dependency) - if haskey(TYPE_MAP, fbs_type) - return dependencies - end + # Skip primitives + haskey(FBS_TYPE_MAP, fbs_type) && return deps - # Check if it's a struct dependency - if haskey(parser.structs, fbs_type) - push!(dependencies, fbs_type) + # Record struct/table dependencies + if haskey(parser.structs, fbs_type) || haskey(parser.tables, fbs_type) + push!(deps, fbs_type) end - # Check if it's a table dependency - if haskey(parser.tables, fbs_type) - push!(dependencies, fbs_type) - end - - return dependencies + return deps end -"""Get all struct and table dependencies for a given struct or table.""" -function get_dependencies(def::Union{StructDef, TableDef}, parser::FBSParser)::Set{String} - dependencies = Set{String}() +"""Collect all struct/table dependencies for a struct or table definition.""" +function _get_field_dependencies(def::Union{StructDef, TableDef}, parser::FBSParser)::Set{String} + deps = Set{String}() for field in def.fields - field_type = String(field["type"]) - deps = extract_type_dependencies(parser, field_type) - union!(dependencies, deps) + union!(deps, extract_type_dependencies(parser, field.type)) end - return dependencies + return deps end -"""Generic topological sort using Kahn's algorithm. +""" + topological_sort(names, get_deps; in_degree_names=names) -> Vector{String} -Args: - parser: FBSParser instance - names: Names to sort - get_deps_func: Function to get dependencies for a name - valid_deps: Set of valid dependency names to consider - in_degree_deps: Set of dependency names that count for in-degree (defaults to valid_deps) +Kahn's algorithm. `get_deps(name)` returns dependency names for `name`. +Only dependencies present in `names` are considered. +`in_degree_names` controls which dependencies count toward in-degree +(e.g. exclude structs when sorting tables, since structs are already emitted). +Warns on cycles and appends remaining nodes in input order. """ -function topological_sort(parser::FBSParser, names::Vector{String}, get_deps_func::Function, valid_deps::Set{String}, in_degree_deps::Set{String} = valid_deps)::Vector{String} - # Build dependency graph +function topological_sort( + names::Vector{String}, + get_deps::Function; + in_degree_names::Set{String} = Set(names), +)::Vector{String} + name_set = Set(names) + + # Build adjacency: name -> set of dependencies within `names` graph = Dict{String, Set{String}}() for name in names - deps = get_deps_func(parser, name) - filtered_deps = Set{String}() - for dep in deps - if dep in valid_deps && dep != name - push!(filtered_deps, dep) - end - end - graph[name] = filtered_deps - end - - # Calculate in-degrees (only counting dependencies in in_degree_deps) - in_degree = Dict{String, Int}() - for name in names - in_degree[name] = length(filter(d -> d in in_degree_deps, graph[name])) + graph[name] = filter(d -> d ∈ name_set && d != name, get_deps(name)) end - # Start with nodes that have no dependencies - queue = String[name for name in names if in_degree[name] == 0] + # In-degree: count only deps in `in_degree_names` + in_degree = Dict(name => count(d -> d ∈ in_degree_names, graph[name]) for name in names) + queue = [name for name in names if in_degree[name] == 0] result = String[] + while !isempty(queue) current = popfirst!(queue) push!(result, current) - # Decrease in-degree for nodes that depend on current (only if current counts for in-degree) - if current in in_degree_deps - for (name, deps) in graph - if current in deps && name != current - in_degree[name] -= 1 - if in_degree[name] == 0 && name ∉ result - push!(queue, name) - end + # Only update in-degrees if current is in the counted set + current ∉ in_degree_names && continue + for (name, deps) in graph + if current ∈ deps && name ∉ result + in_degree[name] -= 1 + if in_degree[name] == 0 + push!(queue, name) end end end end - # Add any remaining nodes (handles cycles gracefully) - for name in names - if name ∉ result - push!(result, name) - end + # Append any remaining nodes (cycle present) + remaining = [name for name in names if name ∉ result] + if !isempty(remaining) + @warn "Dependency cycle detected among: $(join(remaining, ", ")). Appending in input order." + append!(result, remaining) end return result end -"""Topologically sort structs so dependencies come first.""" +"""Sort struct names so that dependencies come before dependents.""" function topological_sort_structs(parser::FBSParser, struct_names::Vector{String})::Vector{String} - get_deps = (p, name) -> get_dependencies(p.structs[name], p) - valid_deps = Set(struct_names) - return topological_sort(parser, struct_names, get_deps, valid_deps) + get_deps = name -> _get_field_dependencies(parser.structs[name], parser) + return topological_sort(struct_names, get_deps) end -"""Topologically sort tables so dependencies (structs and other tables) come first.""" -function topological_sort_tables(parser::FBSParser, table_names::Vector{String}, struct_names::Vector{String})::Vector{String} - get_deps = (p, name) -> get_dependencies(p.tables[name], p) - valid_deps = union(Set(struct_names), Set(table_names)) - table_set = Set(table_names) - # Only count table dependencies for in-degree (structs are defined first) - return topological_sort(parser, table_names, get_deps, valid_deps, table_set) +"""Sort table names so that table-to-table dependencies come before dependents. +Struct dependencies are ignored here because all structs are emitted first.""" +function topological_sort_tables(parser::FBSParser, table_names::Vector{String})::Vector{String} + get_deps = name -> _get_field_dependencies(parser.tables[name], parser) + return topological_sort(table_names, get_deps) end diff --git a/gen/src/download.jl b/gen/src/download.jl index a291b91..c30382c 100644 --- a/gen/src/download.jl +++ b/gen/src/download.jl @@ -1,32 +1,35 @@ """ - download_flatbuffers(version::String; base_url::String, subdir::String, files::Vector{String}, output_dir::String) -> Bool + download_flatbuffers(version; base_url, subdir, files, output_dir) -> Bool -Download FlatBuffer schema files (.fbs) from the OSRM backend GitHub repository -for a specific version. Returns true if all downloads succeeded, false otherwise. +Download FlatBuffer schema files (`.fbs`) from the OSRM backend GitHub repository +for a specific version tag. Returns `true` if all downloads succeeded. -Arguments: -- version: OSRM version tag (e.g., "v6.0.0") -- base_url: Base URL for the GitHub repository (keyword argument) -- subdir: Subdirectory path in the repository (keyword argument) -- files: List of .fbs filenames to download (keyword argument) -- output_dir: Output directory for the downloaded files (keyword argument) +# Arguments +- `version::String`: OSRM version tag (e.g. `"v26.4.0"`) +- `base_url::String`: Base URL for the raw GitHub content +- `subdir::String`: Subdirectory path within the repository +- `files::Vector{String}`: List of `.fbs` filenames to download +- `output_dir::String`: Local directory to write downloaded files to """ -function download_flatbuffers(version::String; base_url::String, subdir::String, files::Vector{String}, output_dir::String)::Bool - # Create output directory if it doesn't exist +function download_flatbuffers( + version::String; + base_url::String, + subdir::String, + files::Vector{String}, + output_dir::String, +)::Bool mkpath(output_dir) - # Download each file failed_files = String[] for file in files url = "$base_url/$version/$subdir/$file" output_path = joinpath(output_dir, file) - try println("Downloading $file...") Downloads.download(url, output_path) - println(" ✓ Successfully downloaded $file") + println(" [OK] $file") catch e - println(" ✗ Failed to download $file: $e") + println(" [FAIL] $file: $e") push!(failed_files, file) end end @@ -36,7 +39,7 @@ function download_flatbuffers(version::String; base_url::String, subdir::String, println("Successfully downloaded all $(length(files)) FlatBuffer files") return true else - println("Warning: Failed to download $(length(failed_files)) file(s):") + println("Failed to download $(length(failed_files)) file(s):") for file in failed_files println(" - $file") end diff --git a/gen/src/generation.jl b/gen/src/generation.jl index 26882e3..88aba53 100644 --- a/gen/src/generation.jl +++ b/gen/src/generation.jl @@ -1,157 +1,141 @@ -"""Convert PascalCase to snake_case.""" -function pascal_to_snake_case(name::String)::String - if isempty(name) - return "" - end - - result = "" - chars = collect(name) - - for i in 1:length(chars) - c = chars[i] +"""Convert PascalCase to UPPER_SNAKE_CASE (e.g. `"NewName"` -> `"NEW_NAME"`).""" +function _pascal_to_upper_snake(name::String)::String + isempty(name) && return "" + buf = IOBuffer() + for (i, c) in enumerate(name) if isuppercase(c) && i > 1 - # Insert underscore if: - # 1. Previous char was lowercase (e.g., "NewName" -> "new_name") - # 2. Previous char was uppercase but next char is lowercase (e.g., "UTurn" -> "u_turn") - prev_lower = islowercase(chars[i - 1]) - next_lower = i < length(chars) && islowercase(chars[i + 1]) - if prev_lower || (isuppercase(chars[i - 1]) && next_lower) - result *= "_" + prev_lower = islowercase(name[i - 1]) + next_lower = i < length(name) && islowercase(name[i + 1]) + if prev_lower || (isuppercase(name[i - 1]) && next_lower) + write(buf, '_') end end - result *= lowercase(c) + write(buf, uppercase(c)) end - return result + return String(take!(buf)) end -"""Generate code for a single enum.""" -function generate_enum_code(parser::FBSParser, enum_name::String, lines::Vector{String}) +"""Generate Julia code for a single enum definition. Returns a string.""" +function _generate_enum(parser::FBSParser, enum_name::String)::String enum_def = parser.enums[enum_name] - enum_type = get(BASE_TYPE_MAP, enum_def.base_type, "Int32") + julia_base_type = get(ENUM_BASE_TYPE_MAP, enum_def.base_type, "Int32") + prefix = _pascal_to_upper_snake(enum_name) + + buf = IOBuffer() + println(buf, "EnumX.@enum(") + println(buf, " $enum_name::$julia_base_type,") - push!(lines, "EnumX.@enum(") - push!(lines, " $enum_name::$enum_type,") current_value = 0 - enum_prefix = uppercase(pascal_to_snake_case(enum_name)) for (i, val) in enumerate(enum_def.values) - val_name = val["name"] - val_value = val["value"] - if val_value !== nothing - current_value = parse(Int, val_value) - end - val_snake = uppercase(pascal_to_snake_case(val_name)) - # Add comma after all but the last value - if i < length(enum_def.values) - push!(lines, " $(enum_prefix)_$(val_snake) = $current_value,") - else - push!(lines, " $(enum_prefix)_$(val_snake) = $current_value") + if val.value !== nothing + current_value = val.value end + val_snake = _pascal_to_upper_snake(val.name) + trailing = i < length(enum_def.values) ? "," : "" + println(buf, " $(prefix)_$(val_snake) = $current_value$trailing") current_value += 1 end - push!(lines, ")") - return push!(lines, "") + + println(buf, ")") + println(buf) + return String(take!(buf)) end -"""Generate code for a single struct.""" -function generate_struct_code(parser::FBSParser, struct_name::String, lines::Vector{String}) +"""Generate Julia code for a single struct definition. Returns a string.""" +function _generate_struct(parser::FBSParser, struct_name::String)::String struct_def = parser.structs[struct_name] - push!(lines, "FlatBuffers.@STRUCT struct $struct_name") + buf = IOBuffer() + println(buf, "FlatBuffers.@STRUCT struct $struct_name") for field in struct_def.fields - field_type = String(field["type"]) - julia_type = resolve_type(parser, field_type) - field_name = String(field["name"]) - push!(lines, " $field_name::$julia_type") + julia_type = resolve_type(parser, field.type) + println(buf, " $(field.name)::$julia_type") end - push!(lines, "end") - return push!(lines, "") + println(buf, "end") + println(buf) + return String(take!(buf)) end -"""Generate code for a single table.""" -function generate_table_code(parser::FBSParser, table_name::String, lines::Vector{String}) +"""Generate Julia code for a single table definition. Returns a string.""" +function _generate_table(parser::FBSParser, table_name::String)::String table_def = parser.tables[table_name] - push!(lines, "FlatBuffers.@with_kw mutable struct $table_name") + buf = IOBuffer() + println(buf, "FlatBuffers.@with_kw mutable struct $table_name") for field in table_def.fields - field_type = String(field["type"]) - julia_type = resolve_type(parser, field_type) - field_name = String(field["name"]) - - if field["default"] !== nothing - default_val = String(field["default"]) - push!(lines, " $field_name::$julia_type = $default_val") + julia_type = resolve_type(parser, field.type) + if field.default !== nothing + println(buf, " $(field.name)::$julia_type = $(field.default)") elseif startswith(julia_type, "Vector{") - push!(lines, " $field_name::$julia_type = []") + println(buf, " $(field.name)::$julia_type = []") else - push!(lines, " $field_name::Union{$julia_type, Nothing} = nothing") + println(buf, " $(field.name)::Union{$julia_type, Nothing} = nothing") end end - push!(lines, "end") - return push!(lines, "") + println(buf, "end") + println(buf) + return String(take!(buf)) end -"""Generate Julia code from parsed definitions, including only types from target_files.""" +""" + generate_code(parser::FBSParser, target_files::Set{String}) -> String + +Generate complete Julia source code for all types defined in `target_files`. +Enums are sorted alphabetically; structs and tables are topologically sorted. +""" function generate_code(parser::FBSParser, target_files::Set{String})::String - lines = String[] - push!(lines, "# Auto-generated Julia code from FlatBuffers schema files") - push!(lines, "# DO NOT EDIT MANUALLY - Generated by Generator.jl") - push!(lines, "") - push!(lines, "using FlatBuffers") - push!(lines, "using EnumX") - push!(lines, "") - - # Generate enums - enum_names = [name for (name, file) in parser.enum_files if file in target_files] + buf = IOBuffer() + println(buf, "# Auto-generated Julia code from FlatBuffers schema files") + println(buf, "# DO NOT EDIT MANUALLY - Generated by Generator.jl") + println(buf) + println(buf, "using FlatBuffers") + println(buf, "using EnumX") + println(buf) + + # Enums (alphabetical order) + enum_names = sort([name for (name, file) in parser.enum_files if file in target_files]) if !isempty(enum_names) - push!(lines, "# Enums") - for enum_name in sort(enum_names) - generate_enum_code(parser, enum_name, lines) + println(buf, "# Enums") + for enum_name in enum_names + print(buf, _generate_enum(parser, enum_name)) end end - # Generate structs + # Structs (topological order) struct_names = [name for (name, file) in parser.struct_files if file in target_files] if !isempty(struct_names) - push!(lines, "# Structs (immutable value types)") + println(buf, "# Structs (immutable value types)") for struct_name in topological_sort_structs(parser, struct_names) - generate_struct_code(parser, struct_name, lines) + print(buf, _generate_struct(parser, struct_name)) end end - # Generate tables + # Tables (topological order) table_names = [name for (name, file) in parser.table_files if file in target_files] if !isempty(table_names) - push!(lines, "# Table (mutable reference types)") - for table_name in topological_sort_tables(parser, table_names, struct_names) - generate_table_code(parser, table_name, lines) + println(buf, "# Table (mutable reference types)") + for table_name in topological_sort_tables(parser, table_names) + print(buf, _generate_table(parser, table_name)) end end - return join(lines, "\n") + return rstrip(String(take!(buf))) * "\n" end """ generate_julia_code(input_file::String, output_file::String) -> Bool -Generate Julia code from FlatBuffers schema files. Processes input_file and all its includes. -Returns true if generation succeeded, false otherwise. - -Arguments: -- input_file: Full path to the root .fbs file to process (e.g., "/path/to/fbresult.fbs") -- output_file: Full path to the output Julia file (e.g., "/path/to/output.jl") +Parse the FlatBuffers schema at `input_file` (and its includes) and write +generated Julia type definitions to `output_file`. +Returns `true` on success, `false` on failure. """ function generate_julia_code(input_file::String, output_file::String)::Bool - # Extract schema directory from input file path schema_dir = dirname(input_file) input_filename = basename(input_file) - - # Ensure output directory exists - output_dir = dirname(output_file) - mkpath(output_dir) + mkpath(dirname(output_file)) if !isdir(schema_dir) println("Error: Schema directory not found: $schema_dir") return false end - if !isfile(input_file) println("Error: Input file not found: $input_file") return false @@ -162,22 +146,16 @@ function generate_julia_code(input_file::String, output_file::String)::Bool println("Output file: $output_file") println() - # Parse all files to build complete type registry (needed for includes) parser = FBSParser(schema_dir) - parse_all(parser) + parse_all!(parser) println("Found $(length(parser.enums)) enums, $(length(parser.structs)) structs, $(length(parser.tables)) tables") println() - # Get all related files (input_filename and its includes) - # Note: get_related_files expects filename relative to schema_dir related_files = get_related_files(parser, input_filename) - - # Generate code for input_file and all its dependencies code = generate_code(parser, related_files) - - # Write output file write(output_file, code) + println("Generated: $output_file") println() println("Successfully generated $(basename(output_file))") diff --git a/gen/src/parsing.jl b/gen/src/parsing.jl index e867abc..6677d18 100644 --- a/gen/src/parsing.jl +++ b/gen/src/parsing.jl @@ -1,165 +1,133 @@ -# Parsing helper functions -"""Resolve file path, trying schema_dir first, then relative path.""" -function resolve_filepath(parser::FBSParser, filename::String)::String +"""Resolve a schema filename to its full path, checking schema_dir first.""" +function _resolve_filepath(parser::FBSParser, filename::String)::String filepath = joinpath(parser.schema_dir, filename) - if !isfile(filepath) && isfile(filename) - filepath = filename - elseif !isfile(filepath) - error("Schema file not found: $filename") + if isfile(filepath) + return filepath + elseif isfile(filename) + return filename + else + error("Schema file not found: $filename (searched $(parser.schema_dir))") end - return filepath end -"""Parse a .fbs file and return its content.""" -function parse_file(parser::FBSParser, filename::String)::String - filepath = resolve_filepath(parser, filename) - - if filepath in parser.parsed_files - return "" # Already parsed - end - - push!(parser.parsed_files, filepath) +"""Read a schema file and return its content as a string.""" +function _read_schema_file(parser::FBSParser, filename::String)::String + filepath = _resolve_filepath(parser, filename) return read(filepath, String) end -"""Convert FlatBuffers type to Julia type.""" -function resolve_type(parser::FBSParser, fbs_type)::String - fbs_type = String(fbs_type) +""" + resolve_type(parser::FBSParser, fbs_type::String) -> String + +Convert a FlatBuffers type string to the corresponding Julia type string. +Handles arrays (`[Type]` -> `Vector{Type}`), primitives, and user-defined types. +""" +function resolve_type(parser::FBSParser, fbs_type::AbstractString)::String + fbs_type = strip(String(fbs_type)) - # Handle arrays + # Handle arrays: [Type] -> Vector{Type} if startswith(fbs_type, '[') && endswith(fbs_type, ']') inner_type = strip(fbs_type[2:(end - 1)]) return "Vector{$(resolve_type(parser, inner_type))}" end - fbs_type = strip(fbs_type) - - # Handle basic types - if haskey(TYPE_MAP, fbs_type) - return TYPE_MAP[fbs_type] - end - - # Handle enums, structs, and tables (return the name) - if haskey(parser.enums, fbs_type) || haskey(parser.structs, fbs_type) || haskey(parser.tables, fbs_type) - return fbs_type + # Primitive / built-in type + if haskey(FBS_TYPE_MAP, fbs_type) + return FBS_TYPE_MAP[fbs_type] end - # Default: assume it's a user-defined type + # User-defined type (enum, struct, or table) — return name as-is return fbs_type end -"""Parse an enum definition.""" -function parse_enum(content::String, name::String)::EnumDef - # Extract base type - base_type_match = match(r"enum\s+\w+:\s*(\w+)", content) - base_type = base_type_match !== nothing ? String(base_type_match.captures[1]) : "byte" - - # Extract values - values = Dict{String, Union{String, Nothing}}[] - value_pattern = r"(\w+)(?:\s*=\s*(\d+))?" - for match in eachmatch(value_pattern, content) - val_name = String(match.captures[1]) - val_num = length(match.captures) > 1 && match.captures[2] !== nothing ? String(match.captures[2]) : nothing - push!(values, Dict("name" => val_name, "value" => val_num)) +"""Parse enum values from the body between `{...}`. Returns `EnumDef`.""" +function _parse_enum(body::String, name::String, base_type::String)::EnumDef + values = EnumValueDef[] + for m in eachmatch(r"(\w+)(?:\s*=\s*(\d+))?", body) + val_name = String(m.captures[1]) + val_num = m.captures[2] !== nothing ? parse(Int, m.captures[2]) : nothing + push!(values, EnumValueDef(val_name, val_num)) end - return EnumDef(name, base_type, values) end -"""Parse a struct definition.""" -function parse_struct(content::String, name::String)::StructDef - fields = Dict{String, Union{String, Nothing}}[] - # Match field definitions: name: type; - field_pattern = r"(\w+):\s*([^;]+);" - for match in eachmatch(field_pattern, content) - field_name = String(match.captures[1]) - field_type = strip(String(match.captures[2])) - push!(fields, Dict("name" => field_name, "type" => field_type, "default" => nothing)) +"""Parse struct fields from the body between `{...}`. Returns `StructDef`.""" +function _parse_struct(body::String, name::String)::StructDef + fields = FieldDef[] + for m in eachmatch(r"(\w+):\s*([^;]+);", body) + field_name = String(m.captures[1]) + field_type = strip(String(m.captures[2])) + push!(fields, FieldDef(field_name, field_type, nothing)) end - return StructDef(name, fields) end -"""Parse a table definition.""" -function parse_table(content::String, name::String)::TableDef - fields = Dict{String, Union{String, Nothing}}[] - # Match field definitions: name: type [= default]; - field_pattern = r"(\w+):\s*([^=;]+)(?:\s*=\s*([^;]+))?;" - for match in eachmatch(field_pattern, content) - field_name = String(match.captures[1]) - field_type = strip(String(match.captures[2])) - default_value = length(match.captures) > 2 && match.captures[3] !== nothing ? strip(String(match.captures[3])) : nothing - push!(fields, Dict("name" => field_name, "type" => field_type, "default" => default_value)) +"""Parse table fields (with optional defaults) from the body between `{...}`. Returns `TableDef`.""" +function _parse_table(body::String, name::String)::TableDef + fields = FieldDef[] + for m in eachmatch(r"(\w+):\s*([^=;]+)(?:\s*=\s*([^;]+))?;", body) + field_name = String(m.captures[1]) + field_type = strip(String(m.captures[2])) + default_value = m.captures[3] !== nothing ? strip(String(m.captures[3])) : nothing + push!(fields, FieldDef(field_name, field_type, default_value)) end - return TableDef(name, fields) end -"""Recursively parse a file and its includes.""" -function parse_recursive!(parser::FBSParser, filename::String) - filepath = resolve_filepath(parser, filename) +""" + parse_recursive!(parser::FBSParser, filename::String) - if filepath in parser.parsed_files - return - end +Parse a `.fbs` file and all its includes, populating `parser` with the definitions found. +Files that have already been parsed are skipped. +""" +function parse_recursive!(parser::FBSParser, filename::String) + filepath = _resolve_filepath(parser, filename) - content = parse_file(parser, filename) - if isempty(content) - return - end + # Skip already-parsed files + filepath in parser.parsed_files && return + push!(parser.parsed_files, filepath) - # Parse namespace - ns_match = match(r"namespace\s+([\w.]+);", content) - if ns_match !== nothing - parser.namespace = String(ns_match.captures[1]) - end + content = _read_schema_file(parser, filename) - # Track includes for this file + # Track and recurse into includes includes = Set{String}() - include_pattern = r"include\s+\"([^\"]+)\";" - for match in eachmatch(include_pattern, content) - include_file = String(match.captures[1]) + for m in eachmatch(r"include\s+\"([^\"]+)\";", content) + include_file = String(m.captures[1]) push!(includes, include_file) parse_recursive!(parser, include_file) end parser.file_includes[filename] = includes - # Parse enums - enum_pattern = r"enum\s+(\w+)(?::\s*(\w+))?\s*\{(.*?)\}"s - for match in eachmatch(enum_pattern, content) - enum_name = String(match.captures[1]) - enum_content = String(match.captures[3]) - parser.enums[enum_name] = parse_enum(enum_content, enum_name) + # Parse enums — capture name, optional base type, and body + for m in eachmatch(r"enum\s+(\w+)(?::\s*(\w+))?\s*\{(.*?)\}"s, content) + enum_name = String(m.captures[1]) + base_type = m.captures[2] !== nothing ? String(m.captures[2]) : "byte" + enum_body = String(m.captures[3]) + parser.enums[enum_name] = _parse_enum(enum_body, enum_name, base_type) parser.enum_files[enum_name] = filename end # Parse structs - struct_pattern = r"struct\s+(\w+)\s*\{(.*?)\}"s - for match in eachmatch(struct_pattern, content) - struct_name = String(match.captures[1]) - struct_content = String(match.captures[2]) - parser.structs[struct_name] = parse_struct(struct_content, struct_name) + for m in eachmatch(r"struct\s+(\w+)\s*\{(.*?)\}"s, content) + struct_name = String(m.captures[1]) + parser.structs[struct_name] = _parse_struct(String(m.captures[2]), struct_name) parser.struct_files[struct_name] = filename end # Parse tables - table_pattern = r"table\s+(\w+)\s*\{(.*?)\}"s - for match in eachmatch(table_pattern, content) - table_name = String(match.captures[1]) - table_content = String(match.captures[2]) - parser.tables[table_name] = parse_table(table_content, table_name) + for m in eachmatch(r"table\s+(\w+)\s*\{(.*?)\}"s, content) + table_name = String(m.captures[1]) + parser.tables[table_name] = _parse_table(String(m.captures[2]), table_name) parser.table_files[table_name] = filename end - return -end -"""Parse all schema files in the directory.""" -function parse_all(parser::FBSParser) - files_to_parse = filter(f -> endswith(f, ".fbs"), readdir(parser.schema_dir; join = false)) + return nothing +end - # Parse all files to build complete type registry - for filename in files_to_parse +"""Parse all `.fbs` files in the parser's schema directory.""" +function parse_all!(parser::FBSParser) + for filename in filter(f -> endswith(f, ".fbs"), readdir(parser.schema_dir; join = false)) parse_recursive!(parser, filename) end - return + return nothing end diff --git a/gen/src/types.jl b/gen/src/types.jl index 6f6fe72..d266805 100644 --- a/gen/src/types.jl +++ b/gen/src/types.jl @@ -1,5 +1,10 @@ -# Type mapping from FlatBuffers to Julia -const TYPE_MAP = Dict( +# FlatBuffers-to-Julia type mapping and intermediate representation. +# +# FBS_TYPE_MAP resolves field types (byte is signed Int8 per FBS spec). +# ENUM_BASE_TYPE_MAP resolves enum base types (byte is unsigned UInt8, matching OSRM convention). + +"""Map from FlatBuffers scalar/string type names to Julia type names.""" +const FBS_TYPE_MAP = Dict{String, String}( "bool" => "Bool", "byte" => "Int8", "ubyte" => "UInt8", @@ -7,10 +12,10 @@ const TYPE_MAP = Dict( "ushort" => "UInt16", "int" => "Int32", "uint" => "UInt32", - "uint32" => "UInt32", - "uint64" => "UInt64", "int32" => "Int32", + "uint32" => "UInt32", "int64" => "Int64", + "uint64" => "UInt64", "long" => "Int64", "ulong" => "UInt64", "float" => "Float32", @@ -18,36 +23,68 @@ const TYPE_MAP = Dict( "string" => "String", ) -# Data structures for parsing -mutable struct EnumDef +"""Map from FlatBuffers enum base type names to Julia type names.""" +const ENUM_BASE_TYPE_MAP = Dict{String, String}( + "byte" => "UInt8", + "ubyte" => "UInt8", + "short" => "Int16", + "ushort" => "UInt16", + "int" => "Int32", + "uint" => "UInt32", +) + +# --- Intermediate representation for parsed .fbs definitions --- + +"""A single value in an enum definition (e.g. `Turn = 0`).""" +struct EnumValueDef + name::String + value::Union{Int, Nothing} +end + +"""A single field in a struct or table definition.""" +struct FieldDef + name::String + type::String + default::Union{String, Nothing} +end + +"""A parsed FlatBuffers enum.""" +struct EnumDef name::String base_type::String - values::Vector{Dict{String, Union{String, Nothing}}} + values::Vector{EnumValueDef} end -mutable struct StructDef +"""A parsed FlatBuffers struct (immutable value type).""" +struct StructDef name::String - fields::Vector{Dict{String, Union{String, Nothing}}} + fields::Vector{FieldDef} end -mutable struct TableDef +"""A parsed FlatBuffers table (mutable reference type).""" +struct TableDef name::String - fields::Vector{Dict{String, Union{String, Nothing}}} + fields::Vector{FieldDef} end +""" + FBSParser(schema_dir::String) + +Accumulates parsed definitions from one or more `.fbs` files. +Tracks which file defines each type and include relationships between files. +""" mutable struct FBSParser schema_dir::String parsed_files::Set{String} enums::Dict{String, EnumDef} structs::Dict{String, StructDef} tables::Dict{String, TableDef} - namespace::Union{String, Nothing} # Track which file defines each type - enum_files::Dict{String, String} # enum_name -> filename - struct_files::Dict{String, String} # struct_name -> filename - table_files::Dict{String, String} # table_name -> filename + enum_files::Dict{String, String} + struct_files::Dict{String, String} + table_files::Dict{String, String} # Track include relationships - file_includes::Dict{String, Set{String}} # filename -> Set of included filenames + file_includes::Dict{String, Set{String}} FBSParser(schema_dir::String) = new( schema_dir, @@ -55,19 +92,9 @@ mutable struct FBSParser Dict{String, EnumDef}(), Dict{String, StructDef}(), Dict{String, TableDef}(), - nothing, Dict{String, String}(), Dict{String, String}(), Dict{String, String}(), - Dict{String, Set{String}}() + Dict{String, Set{String}}(), ) end - -const BASE_TYPE_MAP = Dict( - "byte" => "UInt8", - "ubyte" => "UInt8", - "short" => "Int16", - "ushort" => "UInt16", - "int" => "Int32", - "uint" => "UInt32", -) diff --git a/src/types.jl b/src/types.jl index 50de7f8..709fa03 100644 --- a/src/types.jl +++ b/src/types.jl @@ -140,6 +140,8 @@ FlatBuffers.@with_kw mutable struct Leg summary::Union{String, Nothing} = nothing annotations::Union{Annotation, Nothing} = nothing steps::Vector{Step} = [] + polyline::Union{String, Nothing} = nothing + coordinates::Vector{Position} = [] end FlatBuffers.@with_kw mutable struct RouteObject From d358fd8c864daedd5cfb37916a132ea47fa3c9c9 Mon Sep 17 00:00:00 2001 From: Jonas Klasen <2886182+jrklasen@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:19:42 +0200 Subject: [PATCH 7/8] Update gen/src/download.jl Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- gen/src/download.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gen/src/download.jl b/gen/src/download.jl index c30382c..26628db 100644 --- a/gen/src/download.jl +++ b/gen/src/download.jl @@ -12,12 +12,12 @@ for a specific version tag. Returns `true` if all downloads succeeded. - `output_dir::String`: Local directory to write downloaded files to """ function download_flatbuffers( - version::String; - base_url::String, - subdir::String, - files::Vector{String}, - output_dir::String, -)::Bool + version::String; + base_url::String, + subdir::String, + files::Vector{String}, + output_dir::String, + )::Bool mkpath(output_dir) failed_files = String[] From 3580dea6ef501c923b002ec799af037b190624f4 Mon Sep 17 00:00:00 2001 From: Jonas Klasen <2886182+jrklasen@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:19:55 +0200 Subject: [PATCH 8/8] Update gen/src/dependencies.jl Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- gen/src/dependencies.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gen/src/dependencies.jl b/gen/src/dependencies.jl index 9855d87..cb9882f 100644 --- a/gen/src/dependencies.jl +++ b/gen/src/dependencies.jl @@ -65,10 +65,10 @@ Only dependencies present in `names` are considered. Warns on cycles and appends remaining nodes in input order. """ function topological_sort( - names::Vector{String}, - get_deps::Function; - in_degree_names::Set{String} = Set(names), -)::Vector{String} + names::Vector{String}, + get_deps::Function; + in_degree_names::Set{String} = Set(names), + )::Vector{String} name_set = Set(names) # Build adjacency: name -> set of dependencies within `names`