diff --git a/etc/build.props b/etc/build.props --- a/etc/build.props +++ b/etc/build.props @@ -1,17 +1,18 @@ title = AFP/Tools module = $AFP_BASE/tools/lib/classes/afp_tools.jar requirements = \ env:ISABELLE_SCALA_JAR sources = \ tools/admin/afp_build_hugo.scala \ tools/admin/afp_build_python.scala \ tools/migration/afp_migrate_metadata.scala \ tools/afp_build_site.scala \ tools/afp_check_roots.scala \ tools/afp_dependencies.scala \ + tools/afp_structure.scala \ tools/afp_tool.scala \ tools/metadata.scala \ tools/python.scala \ tools/toml.scala \ tools/utils.scala services = afp.Tools diff --git a/tools/afp_structure.scala b/tools/afp_structure.scala new file mode 100644 --- /dev/null +++ b/tools/afp_structure.scala @@ -0,0 +1,108 @@ +package afp + + +import isabelle._ + + +class AFP_Structure private(base_dir: Path) +{ + /* files */ + + private val metadata_dir = base_dir + Path.basic("metadata") + + private val thys_dir = base_dir + Path.basic("thys") + + private val authors_file = metadata_dir + Path.basic("authors.toml") + + private val releases_file = metadata_dir + Path.basic("releases.toml") + + private val topics_file = metadata_dir + Path.basic("topics.toml") + + private val entries_dir = metadata_dir + Path.basic("entries") + + private def entry_file(name: Metadata.Entry.Name): Path = entries_dir + Path.basic(name + ".toml") + + + /* load */ + + private def load[A](file: Path, parser: afp.TOML.T => A): A = + { + val content = File.read(file) + val toml = TOML.parse(content) + parser(toml) + } + + def load_authors: List[Metadata.Author] = load(authors_file, Metadata.TOML.to_authors) + + def load_releases: List[Metadata.Release] = load(releases_file, Metadata.TOML.to_releases) + + def load_topics: List[Metadata.Topic] = load(topics_file, Metadata.TOML.to_topics) + + def load_entry(name: Metadata.Entry.Name, + authors: Map[Metadata.Author.ID, Metadata.Author], + topics: Map[Metadata.Topic.ID, Metadata.Topic], + releases: Map[Metadata.Entry.Name, List[Metadata.Release]]): Metadata.Entry = + { + val entry_releases = releases.getOrElse(name, error("No releases for entry " + name)) + load(entry_file(name), toml => Metadata.TOML.to_entry(toml ++ TOML.T("name" -> name), authors, topics, entry_releases)) + } + + + /* save */ + + private def save(file: Path, content: afp.TOML.T): Unit = + { + file.file.mkdirs() + File.write(file, TOML.Format(content)) + } + + def save_authors(authors: List[Metadata.Author]): Unit = + save(authors_file, Metadata.TOML.from_authors(authors)) + + def save_releases(releases: List[Metadata.Release]): Unit = + save(releases_file, Metadata.TOML.from_releases(releases)) + + def save_topics(topics: List[Metadata.Topic]): Unit = + save(topics_file, Metadata.TOML.from_topics(topics)) + + def save_entry(entry: Metadata.Entry): Unit = + save(entry_file(entry.name), Metadata.TOML.from_entry(entry)) + + + /* sessions */ + + def entries: List[Metadata.Entry.Name] = + { + val Entry = """([a-zA-Z0-9+_-]+)\.toml""".r + val file_entries = File.read_dir(entries_dir).map { + case Entry(name) => name + case f => error("Unrecognized metadata entry: " + f) + } + val session_entries = Sessions.parse_roots(thys_dir + Path.basic("ROOTS")) + + val session_set = session_entries.toSet + val metadata_set = file_entries.toSet + + if (session_set != metadata_set) { + val inter = session_set.intersect(metadata_set) + val session_without_metadata = + if (session_set.subsetOf(inter)) "" + else "No metadata for session roots: " + commas_quote(session_set -- inter) + val metadata_without_session = + if (metadata_set.subsetOf(inter)) "" + else "Not session roots for metadata entries: " + commas_quote(metadata_set -- inter) + error("Metadata does not match sessions:\n" + session_without_metadata + metadata_without_session) + } else session_entries + } + + def sessions_structure: Sessions.Structure = + Sessions.load_structure(options = Options.init(), select_dirs = List(thys_dir)) + + def entry_sessions(name: Metadata.Entry.Name): List[Sessions.Session_Entry] = + Sessions.parse_root(thys_dir + Path.make(List(name, "ROOT"))).collect { case e: Sessions.Session_Entry => e } +} + +object AFP_Structure +{ + def apply(base_dir: Path = Path.explode("$AFP_BASE")): AFP_Structure = new AFP_Structure(base_dir.absolute) +}