GeoServer Configuration Elixir Client

An Elixir library for interacting with GeoServer's REST API to manage workspaces, datastores, coverage stores, coverages, styles, and layer groups.

Prerequisites

Installation

Add to your mix.exs:

def deps do
  [
    {:geoserver_config, "~> 0.2.4"}
  ]
end

Connection

Every API function takes a GeoserverConfig.Connection as its first argument. Build one at application startup and pass it wherever needed.

From environment variables (read at runtime, never at compile time):

export GEOSERVER_BASE_URL="http://localhost:8080/geoserver/rest"
export GEOSERVER_USERNAME="admin"
export GEOSERVER_PASSWORD="geoserver"
conn = GeoserverConfig.Connection.from_env()

From explicit values:

conn = GeoserverConfig.Connection.new(
  "http://localhost:8080/geoserver/rest",
  "admin",
  "geoserver"
)

From your application's config (config/runtime.exs):

# config/runtime.exs
config :my_app, :geoserver,
  base_url: System.get_env("GEOSERVER_BASE_URL"),
  username: System.get_env("GEOSERVER_USERNAME"),
  password: System.get_env("GEOSERVER_PASSWORD")
conn = GeoserverConfig.Connection.from_application_env(:my_app)
# custom key: GeoserverConfig.Connection.from_application_env(:my_app, :geo_api)

Custom env prefix (useful for multiple GeoServer instances):

# reads STAGING_GEOSERVER_BASE_URL, STAGING_GEOSERVER_USERNAME, ...
conn = GeoserverConfig.Connection.from_env(prefix: "STAGING_GEOSERVER")

Workspace Operations

{:ok, workspaces} = GeoserverConfig.Workspaces.fetch_workspaces(conn)

{:ok, "new_ws"} = GeoserverConfig.Workspaces.create_workspace(conn, "new_ws")

{:ok, "new_name"} = GeoserverConfig.Workspaces.update_workspace(conn, "old_name", "new_name")

{:ok, "old_ws"} = GeoserverConfig.Workspaces.delete_workspace(conn, "old_ws")

Note: GeoServer may reject workspace renames depending on its version and configuration.

Datastore Operations

{:ok, stores} = GeoserverConfig.Datastores.list_datastores(conn, "workspace_name")

Shapefile:

{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
  conn,
  "workspace_name",
  "my_store",
  "shapefile",
  %{url: "file:///path/to/shapefile_directory"}
)

PostGIS:

{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
  conn,
  "workspace_name",
  "my_store",
  "postgis",
  %{
    host: "localhost",
    port: 5432,
    database: "db_name",
    user: "db_user",
    passwd: "db_password"
  }
)

GeoPackage:

{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
  conn,
  "workspace_name",
  "my_store",
  "geopkg",
  %{database: "file:///path/to/file.gpkg"}
)

Update:

{:ok, "my_store"} = GeoserverConfig.Datastores.update_datastore(
  conn,
  "workspace_name",
  "my_store",
  "shapefile",
  %{description: "New description", url: "file:///new/path"}
)

Delete (recurse: true also removes dependent feature types):

{:ok, "my_store"} = GeoserverConfig.Datastores.delete_datastore(conn, "workspace_name", "my_store", true)

Coverage Store Operations

{:ok, stores} = GeoserverConfig.Coveragestores.list_coveragestores(conn, "workspace_name")

Local GeoTIFF:

{:ok, "dem_store"} = GeoserverConfig.Coveragestores.create_coveragestore(
  conn,
  "workspace_name",
  "dem_store",
  "file:///path/to/geotiff.tif",
  "Optional description"
)

Cloud Optimized GeoTIFF (COG) via S3 or HTTP:

{:ok, "dem_store"} = GeoserverConfig.Coveragestores.create_coveragestore(
  conn,
  "workspace_name",
  "dem_store",
  "cog://https://path.to/your/file_cog.tif",
  "COG from HTTP",
  %{
    metadata: %{
      "entry" => %{
        "@key" => "CogSettings.Key",
        "cogSettings" => %{
          "useCachingStream" => false,
          "rangeReaderSettings" => "HTTP"
        }
      }
    },
    disableOnConnFailure: false
  }
)

Update:

{:ok, "dem_store"} = GeoserverConfig.Coveragestores.update_coveragestore(
  conn,
  "workspace_name",
  "dem_store",
  %{
    type: "GeoTIFF",
    enabled: true,
    url: "file:///new/path/to/file.tif",
    description: "Updated description"
  }
)

Delete (uses purge=true to remove related resources):

{:ok, "dem_store"} = GeoserverConfig.Coveragestores.delete_coveragestore(conn, "workspace_name", "dem_store")

Coverage Layer Operations

{:ok, coverages} = GeoserverConfig.Coverages.list_coverages(conn, "workspace_name", "dem_store")

Create:

{:ok, "dem_layer"} = GeoserverConfig.Coverages.create_coverage(
  conn,
  "workspace_name",
  "dem_store",
  "dem_layer",
  %{
    title: "DEM Layer",
    description: "Digital Elevation Model",
    abstract: "Raster coverage layer",
    srs: "EPSG:3301",
    native_crs: "EPSG:3301",
    native_bbox: %{minx: 369000.0, maxx: 740000.0, miny: 6377000.0, maxy: 6635000.0},
    latlon_bbox: %{minx: 21.664, maxx: 28.275, miny: 57.471, maxy: 59.831},
    grid: %{
      dimension: [3710, 2580],
      transform: [10.0, 0.0, 369000.0, 0.0, -10.0, 6635000.0]
    },
    metadata: %{
      "cacheAgeMax" => 3600,
      "cachingEnabled" => true
    }
  },
  "file:///path/to/geotiff.tif"
)

Delete (recurse: true also removes dependent resources):

{:ok, "dem_layer"} = GeoserverConfig.Coverages.delete_coverage(conn, "workspace_name", "dem_store", "dem_layer", true)

Style Operations

{:ok, styles} = GeoserverConfig.Styles.list_styles(conn)

{:ok, styles} = GeoserverConfig.Styles.list_styles_workspace_specific(conn, "workspace_name")

Get SLD content (pass nil as workspace for global styles):

{:ok, sld_xml} = GeoserverConfig.Styles.get_style(conn, "workspace_name", "style_name")
{:ok, sld_xml} = GeoserverConfig.Styles.get_style(conn, nil, "global_style")

# Save to disk (no connection needed)
{:ok, %{file_path: path, size: bytes}} = GeoserverConfig.Styles.write_sld_file("/tmp/style.sld", sld_xml)

Create:

{:ok, "my_style"} = GeoserverConfig.Styles.create_style(conn, %{
  name: "my_style",
  sld_content: "<StyledLayerDescriptor>...</StyledLayerDescriptor>",
  filename: "my_style.sld",
  workspace: "workspace_name"  # omit for a global style
})

Update:

{:ok, "my_style"} = GeoserverConfig.Styles.update_style(conn, %{
  name: "my_style",
  sld_content: "<StyledLayerDescriptor>...</StyledLayerDescriptor>",
  workspace: "workspace_name"
})

Delete:

{:ok, "my_style"} = GeoserverConfig.Styles.delete_style(conn, "my_style", "workspace_name", purge: true, recurse: true)

# Global style
{:ok, "my_style"} = GeoserverConfig.Styles.delete_style(conn, "my_style")

Assign Style to Layer

Verifies the style exists before assigning it:

# Style from the same or global scope
{:ok, msg} = GeoserverConfig.StyleAssignToLayer.assign_style_to_layer(
  conn,
  "workspace_name",
  "layer_name",
  "style_name"
)

# Style from a specific workspace
{:ok, msg} = GeoserverConfig.StyleAssignToLayer.assign_style_to_layer(
  conn,
  "workspace_name",
  "layer_name",
  "style_name",
  "style_workspace"
)

Layer Group Operations

{:ok, groups} = GeoserverConfig.LayerGroups.list_layer_groups(conn)

# Create from XML or a map
{:ok, _} = GeoserverConfig.LayerGroups.create_layer_group(conn, xml_string)
{:ok, _} = GeoserverConfig.LayerGroups.create_layer_group(conn, %{"layerGroup" => %{"name" => "my-group"}})

# Update
{:ok, _} = GeoserverConfig.LayerGroups.update_layer_group(conn, "my-group", updated_xml)

# Add / remove layers
{:ok, _} = GeoserverConfig.LayerGroups.add_layer_to_group(conn, "my-group", "ws:layer1", "ws:style1")
{:ok, _} = GeoserverConfig.LayerGroups.remove_layer_from_group(conn, "my-group", "ws:layer1")

{:ok, "my-group"} = GeoserverConfig.LayerGroups.delete_layer_group(conn, "my-group")

Error Handling

All functions return tagged tuples. Pattern match to handle each case:

case GeoserverConfig.Workspaces.fetch_workspaces(conn) do
  {:ok, workspaces} ->
    IO.inspect(workspaces)

  {:error, {:http_error, status, body}} ->
    IO.puts("GeoServer returned #{status}: #{inspect(body)}")

  {:error, {:not_found, name}} ->
    IO.puts("#{name} does not exist")

  {:error, {:request_failed, reason}} ->
    IO.puts("Transport error: #{inspect(reason)}")
end

Notes

License

MIT License