local_methods <- function(..., .frame = caller_env()) {
  local_bindings(..., .env = global_env(), .frame = .frame)
}

local_foo_df <- function(frame = caller_env()) {
  local_methods(
    .frame = frame,
    group_by.foo_df = function(.data, ...) {
      out <- NextMethod()
      if (missing(...)) {
        class(out) <- c("foo_df", class(out))
      } else {
        class(out) <- c("grouped_foo_df", class(out))
      }
      out
    },
    ungroup.grouped_foo_df = function(x, ...) {
      out <- NextMethod()
      class(out) <- c("foo_df", class(out))
      out
    }
  )
}

new_ctor <- function(base_class) {
  function(x = list(), ..., class = NULL) {
    if (inherits(x, "tbl_df")) {
      tibble::new_tibble(x, class = c(class, base_class), nrow = nrow(x))
    } else if (is.data.frame(x)) {
      structure(x, class = c(class, base_class, "data.frame"), ...)
    } else {
      structure(x, class = c(class, base_class), ...)
    }
  }
}

foobar <- new_ctor("dplyr_foobar")
foobaz <- new_ctor("dplyr_foobaz")
quux <- new_ctor("dplyr_quux")

# For testing reconstructing methods that break invariants by adding
# new columns
new_dispatched_quux <- function(x) {
  out <- quux(x)
  out$dispatched <- rep(TRUE, nrow(out))
  out
}
