///|
fn map_port_data(port : Port, transform : (Datum) -> Datum) -> Port {
  {
    ..port,
    data: port.data.map(transform),
    ports: port.ports.map(fn(child) { map_port_data(child, transform) }),
  }
}

///|
fn map_graph_data(graph : Graph, transform : (Datum) -> Datum) -> Graph {
  {
    ..graph,
    data: graph.data.map(transform),
    nodes: graph.nodes.map(fn(node) {
      {
        ..node,
        data: node.data.map(transform),
        ports: node.ports.map(fn(port) { map_port_data(port, transform) }),
        graph: node.graph.map(fn(nested) { map_graph_data(nested, transform) }),
      }
    }),
    edges: graph.edges.map(fn(edge) {
      {
        ..edge,
        data: edge.data.map(transform),
        graph: edge.graph.map(fn(nested) { map_graph_data(nested, transform) }),
      }
    }),
    hyperedges: graph.hyperedges.map(fn(hyperedge) {
      {
        ..hyperedge,
        data: hyperedge.data.map(transform),
        graph: hyperedge.graph.map(fn(nested) {
          map_graph_data(nested, transform)
        }),
      }
    }),
  }
}

///|
/// Rename a declaration and every corresponding data reference atomically.
pub fn Document::rename_key(
  self : Document,
  old_id : String,
  new_id : String,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  valid_token(new_id, "key.id")
  let catalog = key_map(self.keys)
  require(catalog.contains(old_id), "key.id", "key to rename does not exist")
  require(
    old_id == new_id || !catalog.contains(new_id),
    "key.id",
    "new key identifier already exists",
  )
  let keys = self.keys.map(fn(key) {
    if key.id == old_id {
      { ..key, id: new_id, }
    } else {
      key
    }
  })
  let transform = fn(datum : Datum) {
    if datum.key == old_id {
      { ..datum, key: new_id, }
    } else {
      datum
    }
  }
  Document::new(
    keys,
    self.graphs.map(fn(graph) { map_graph_data(graph, transform) }),
    data=self.data.map(transform),
    description?=self.description,
    extra=self.extra,
    namespaces=self.namespaces,
    limits~,
  )
}

///|
/// Replace a declaration, retaining its ID. Existing data must still satisfy its type and scope.
pub fn Document::replace_key(
  self : Document,
  key : Key,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  require(
    key_map(self.keys).contains(key.id),
    "key.id",
    "key to replace does not exist",
  )
  Document::new(
    self.keys.map(fn(existing) {
      if existing.id == key.id {
        key
      } else {
        existing
      }
    }),
    self.graphs,
    data=self.data,
    description?=self.description,
    extra=self.extra,
    namespaces=self.namespaces,
    limits~,
  )
}

///|
/// Removing an in-use key is intentionally forbidden, rather than silently discarding attributes.
pub fn Document::remove_key(
  self : Document,
  id : String,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  require(
    key_map(self.keys).contains(id),
    "key.id",
    "key to remove does not exist",
  )
  Document::new(
    self.keys.filter(fn(key) { key.id != id }),
    self.graphs,
    data=self.data,
    description?=self.description,
    extra=self.extra,
    namespaces=self.namespaces,
    limits~,
  )
}