diff --git a/Cargo.lock b/Cargo.lock index 51b31965..95460cfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1441,6 +1441,7 @@ dependencies = [ "hex", "lazy_static", "leaky-bucket", + "markdown", "mime_guess", "mockall", "num_cpus", @@ -1517,6 +1518,15 @@ dependencies = [ "serde", ] +[[package]] +name = "markdown" +version = "1.0.0-alpha.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08be37c7b5269bb027f69cdf446c96099d1e8da723c0d5fb78856adc6a50680f" +dependencies = [ + "unicode-id", +] + [[package]] name = "md-5" version = "0.10.5" @@ -3045,6 +3055,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" +[[package]] +name = "unicode-id" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70b6494226b36008c8366c288d77190b3fad2eb4c10533139c1c1f461127f1a" + [[package]] name = "unicode-ident" version = "1.0.11" diff --git a/Cargo.toml b/Cargo.toml index c6a428c8..ff1cfb7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ futures = "0.3.28" hex = "0.4.3" lazy_static = "1.4.0" leaky-bucket = "1.0.1" +markdown = "1.0.0-alpha.12" mime_guess = "2.0.4" num_cpus = "1.16.0" octorust = "0.3.2" diff --git a/docs/config/guide.yml b/docs/config/guide.yml new file mode 100644 index 00000000..645967be --- /dev/null +++ b/docs/config/guide.yml @@ -0,0 +1,140 @@ +# Landscape2 guide +# +# This file allows defining the content of the landscape guide. + +# The landscape guide is organized into categories and subcategories. Each of +# these entities requires a name and some content. The content can be provided +# in markdown format. Categories and subcategories names are not required to +# match the ones defined in the landscape data file but, when they do, those +# categories/subcategories will be enriched with some extra information. + +# We recommend using headings of level 4-6 within the content blocks as levels +# 1-3 are reserved to illustrate the hierarchy of categories and subcategories. + +# The following example contains a subset of the CNCF landscape guide content: + +categories: + - name: "Introduction" + keywords: [] + content: | + If you've researched cloud native applications and technologies, you've probably come + across the [CNCF cloud native landscape](https://landscape.cncf.io). Unsurprisingly, + the sheer scale of it can be overwhelming. So many categories and so many technologies. + How do you make sense of it? + + As with anything else, if you break it down and analyze it one piece at a time, you'll + find it's not that complex and makes a lot of sense. In fact, the map is neatly organized + by functionality and, once you understand what each category represents, navigating it + becomes a lot easier. + + In this guide, we'll break this mammoth landscape down and provide a high-level overview + of its layers, columns, and categories. + + subcategories: + - name: "What is the cloud native landscape?" + content: | + The goal of the cloud native landscape is to compile and organize all cloud native open + source projects and proprietary products into categories, providing an overview of the + current ecosystem. Organizations that have a cloud native project or product can + [submit a PR](https://github.com/cncf/landscape/) to request it to be added to the + landscape. + + - name: "How to use this guide" + content: | + In this guide, you'll find one chapter per layer and column which discusses each category + within it. Categories are broken down into: what it is, the problem it addresses, how it + helps, and technical 101. While the first three sections assume no technical background, + the technical 101 is targeted to engineers just getting started with cloud native. We + also included a section for associated buzzwords and lists CNCF projects. + + > ##### INFOBOX + > + > When looking at the landscape, you'll note a few distinctions: + > * *Projects in large boxes* are CNCF-hosted open source projects. Some are still in + > the incubation phase (light blue/purple frame), while others are graduated + > projects (dark blue frame). + > * *Projects in small white boxes* are open source projects. + > * *Products in gray boxes* are proprietary products. + > + > Please note that new projects are continuously becoming part of the CNCF so + > always refer to the actual landscape - things are moving fast! + + - name: "Contribute to the CNCF Landscape" + content: | + Are you searching for an exciting project to contribute to within the CNCF ecosystem? + Look no further! The CNCF hosts a wide range of projects that span cloud-native computing. + To find the perfect project for your skills and interests, check out our comprehensive + contribution guide at [Getting Started](https://contribute.cncf.io/contributors/getting-started/). + It provides you step-by-step instructions on getting started and offers valuable insights for + both newcomers and experienced contributors. Join our vibrant community and make your mark on + cloud-native innovation today! + + - name: "Provisioning" + keywords: [] + content: | + Provisioning is the first layer in the cloud native landscape. It encompasses tools that + are used to *create and harden* the foundation on which cloud native apps are built. + You'll find tools to automatically configure, create, and manage the infrastructure, + as well as for scanning, signing, and storing container images. The layer also extends + to security with tools that enable policy setting and enforcement, embedded authentication + and authorization, and the handling of secrets distribution. That's a mouthful, so let's + discuss each category at a time. + + subcategories: + - name: "Automation & Configuration" + keywords: + - "Infrastructure-as-Code (IaC)" + - "Automation" + - "Declarative Configuration" + content: | + #### What it is + + Automation and configuration tools speed up the creation and configuration of compute + resources (virtual machines, networks, firewall rules, load balancers, etc.). Tools in + this category either handle different parts of the provisioning process or try to control + everything end-to-end. Most provide the ability to integrate with other projects and + products in the space. + + #### Problem it addresses + + Traditionally, IT processes relied on lengthy and labor intensive manual release cycles, + typically between three to six months. Those cycles came with lots of human processes and + controls that slowed down changes to production environments. These slow release cycles + and static environments aren't compatible with cloud native development. To deliver on + rapid development cycles, infrastructure must be provisioned dynamically and without + human intervention. + + #### How it helps + + Tools of this category allow engineers to build computing environments without human + intervention. By codifying the environment setup it becomes reproducible with the click + of a button. While manual setup is error prone, once codified, environment creation + matches the exact desired state -- a huge advantage. + + While tools may take different approaches, they all aim at reducing the required work + to provision resources through automation. + + #### Technical 101 + + As we move from old-style human-driven provisioning to a new on-demand scaling model + driven by the cloud, the patterns and tools we used before no longer meet our needs. + Most organizations can't afford a large 24x7 staff to create, configure, and manage + servers. Automated tools like Terraform reduce the level of effort required to scale + tens of servers and networks with hundreds of firewall rules. Tools like Puppet, Chef, + and Ansible provision and/or configure these new servers and applications + programmatically as they are spun up and allow them to be consumed by developers. + + Some tools interact directly with the infrastructure APIs provided by platforms like + AWS or vSphere, while others focus on configuring the individual machines to make them + part of a Kubernetes cluster. Many, like Chef and Terraform, can interoperate to provision + and configure the environment. Others, like OpenStack, exist to provide an + Infrastructure-as-a-Service (IaaS) environment that other tools could consume. + Fundamentally, you'll need one or more tools in this space as part of laying down the + computing environment, CPU, memory, storage, and networking, for your Kubernetes clusters. + You'll also need a subset of these to create and manage the Kubernetes clusters + themselves. + + There are now over 5 CNCF projects in this space, more if you count projects like Cluster + API which don't appear on the landscape. There is also a very robust set of other open + source and vendor provided tools. + diff --git a/docs/config/settings.yml b/docs/config/settings.yml index b16f559e..50d8aa36 100644 --- a/docs/config/settings.yml +++ b/docs/config/settings.yml @@ -1,4 +1,4 @@ -# Landscape2 settings file +# Landscape2 settings # # This settings file allows customizing some aspects of the landscape. diff --git a/src/build.rs b/src/build.rs index 63f8ac23..4a4f4c92 100644 --- a/src/build.rs +++ b/src/build.rs @@ -5,13 +5,14 @@ use crate::{ cache::Cache, crunchbase::collect_crunchbase_data, - data::{get_landscape_data, LandscapeData}, + data::LandscapeData, datasets::Datasets, github::collect_github_data, + guide::LandscapeGuide, logos::prepare_logo, projects::{generate_projects_csv, Project, ProjectsMd}, - settings::{get_landscape_settings, LandscapeSettings}, - BuildArgs, LogosSource, + settings::LandscapeSettings, + BuildArgs, GuideSource, LogosSource, }; use anyhow::{format_err, Result}; use askama::Template; @@ -62,15 +63,18 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { let cache = Cache::new(&args.cache_dir)?; // Get landscape data from the source provided - let mut landscape_data = get_landscape_data(&args.data_source).await?; + let mut landscape_data = LandscapeData::new(&args.data_source).await?; // Get landscape settings from the source provided - let settings = get_landscape_settings(&args.settings_source).await?; + let settings = LandscapeSettings::new(&args.settings_source).await?; // Add some extra information to the landscape based on the settings landscape_data.add_featured_items_data(&settings)?; landscape_data.add_member_subcategory(&settings.members_category); + // Prepare guide and copy it to the output directory + let includes_guide = prepare_guide(&args.guide_source, &args.output_dir).await?.is_some(); + // Prepare logos and copy them to the output directory prepare_logos(&cache, &args.logos_source, &mut landscape_data, &args.output_dir).await?; @@ -88,7 +92,7 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { let datasets = generate_datasets(&landscape_data, &settings, &args.output_dir)?; // Render index file and write it to the output directory - render_index(&datasets, &args.output_dir)?; + render_index(&datasets, includes_guide, &args.output_dir)?; // Copy web assets files to the output directory copy_web_assets(&args.output_dir)?; @@ -105,6 +109,8 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { /// Check web assets are present, to make sure the web app has been built. #[instrument(skip_all, err)] fn check_web_assets() -> Result<()> { + debug!("checking web assets are present"); + if !WebAssets::iter().any(|path| path.starts_with("assets/")) { return Err(format_err!( "web assets not found, please make sure they have been built" @@ -117,6 +123,8 @@ fn check_web_assets() -> Result<()> { /// Copy web assets files to the output directory. #[instrument(skip_all, err)] fn copy_web_assets(output_dir: &Path) -> Result<()> { + debug!("copying web assets to output directory"); + for asset_path in WebAssets::iter() { // The index document is a template that we'll render, so we don't want // to copy it as is. @@ -125,7 +133,6 @@ fn copy_web_assets(output_dir: &Path) -> Result<()> { } if let Some(embedded_file) = WebAssets::get(&asset_path) { - debug!(?asset_path, "copying file"); if let Some(parent_path) = Path::new(asset_path.as_ref()).parent() { fs::create_dir_all(output_dir.join(parent_path))?; } @@ -148,9 +155,8 @@ fn generate_datasets( output_dir: &Path, ) -> Result { debug!("generating datasets"); - let datasets = Datasets::new(landscape_data, settings)?; - debug!("copying datasets to output directory"); + let datasets = Datasets::new(landscape_data, settings)?; let datasets_path = output_dir.join(DATASETS_PATH); // Base @@ -167,7 +173,8 @@ fn generate_datasets( /// Generate the projects.md and projects.csv files from the landscape data. #[instrument(skip_all, err)] fn generate_projects_files(landscape_data: &LandscapeData, output_dir: &Path) -> Result<()> { - debug!("generating projects.* files"); + debug!("generating projects files"); + let projects: Vec = landscape_data.into(); // projects.md @@ -183,6 +190,20 @@ fn generate_projects_files(landscape_data: &LandscapeData, output_dir: &Path) -> Ok(()) } +/// Prepare guide and copy it to the output directory. +#[instrument(skip_all, err)] +async fn prepare_guide(guide_source: &GuideSource, output_dir: &Path) -> Result> { + debug!("preparing guide"); + + let Some(guide) = LandscapeGuide::new(guide_source).await? else { + return Ok(None); + }; + let path = output_dir.join(DATASETS_PATH).join("guide.json"); + File::create(path)?.write_all(&serde_json::to_vec(&guide)?)?; + + Ok(Some(())) +} + /// Prepare logos and copy them to the output directory, updating the logo /// reference on each landscape item. #[instrument(skip_all, err)] @@ -257,13 +278,19 @@ async fn prepare_logos( #[template(path = "index.html", escape = "none")] struct Index<'a> { pub datasets: &'a Datasets, + pub includes_guide: bool, } /// Render index file and write it to the output directory. #[instrument(skip_all, err)] -fn render_index(datasets: &Datasets, output_dir: &Path) -> Result<()> { +fn render_index(datasets: &Datasets, includes_guide: bool, output_dir: &Path) -> Result<()> { debug!("rendering index.html file"); - let index = Index { datasets }.render()?; + + let index = Index { + datasets, + includes_guide, + } + .render()?; let mut file = File::create(output_dir.join("index.html"))?; file.write_all(index.as_bytes())?; @@ -274,6 +301,8 @@ fn render_index(datasets: &Datasets, output_dir: &Path) -> Result<()> { /// paths inside it when needed. #[instrument(fields(?output_dir), skip_all, err)] fn setup_output_dir(output_dir: &Path) -> Result<()> { + debug!("setting up output directory"); + if !output_dir.exists() { debug!("creating output directory"); fs::create_dir_all(output_dir)?; diff --git a/src/data.rs b/src/data.rs index 47510c0b..e9d703f2 100644 --- a/src/data.rs +++ b/src/data.rs @@ -24,20 +24,6 @@ use uuid::Uuid; /// Format used for dates across the landscape data file. pub const DATE_FORMAT: &str = "%Y-%m-%d"; -/// Get landscape data from the source provided. -#[instrument(skip_all, err)] -pub(crate) async fn get_landscape_data(src: &DataSource) -> Result { - let data = if let Some(file) = &src.data_file { - debug!(?file, "getting landscape data from file"); - LandscapeData::new_from_file(file) - } else { - debug!(url = ?src.data_url.as_ref().unwrap(), "getting landscape data from url"); - LandscapeData::new_from_url(src.data_url.as_ref().unwrap()).await - }?; - - Ok(data) -} - /// Landscape data. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub(crate) struct LandscapeData { @@ -46,8 +32,26 @@ pub(crate) struct LandscapeData { } impl LandscapeData { + /// Create a new landscape data instance from the source provided. + #[instrument(skip_all, err)] + pub(crate) async fn new(src: &DataSource) -> Result { + // Try from file + if let Some(file) = &src.data_file { + debug!(?file, "getting landscape data from file"); + return LandscapeData::new_from_file(file); + }; + + // Try from url + if let Some(url) = &src.data_url { + debug!(?url, "getting landscape data from url"); + return LandscapeData::new_from_url(url).await; + }; + + Err(format_err!("data file or url not provided")) + } + /// Create a new landscape data instance from the file provided. - pub(crate) fn new_from_file(file: &Path) -> Result { + fn new_from_file(file: &Path) -> Result { let raw_data = fs::read_to_string(file)?; let legacy_data: legacy::LandscapeData = serde_yaml::from_str(&raw_data)?; legacy_data.validate()?; @@ -56,7 +60,7 @@ impl LandscapeData { } /// Create a new landscape data instance from the url provided. - pub(crate) async fn new_from_url(url: &str) -> Result { + async fn new_from_url(url: &str) -> Result { let resp = reqwest::get(url).await?; if resp.status() != StatusCode::OK { return Err(format_err!( diff --git a/src/guide.rs b/src/guide.rs new file mode 100644 index 00000000..09193072 --- /dev/null +++ b/src/guide.rs @@ -0,0 +1,112 @@ +//! This module defines the types used to represent the landscape guide content +//! that must be provided from a YAML file (guide.yml). + +use crate::GuideSource; +use anyhow::{format_err, Result}; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; +use tracing::{debug, instrument}; + +/// Landscape guide content. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(crate) struct LandscapeGuide { + #[serde(skip_serializing_if = "Option::is_none")] + pub categories: Option>, +} + +impl LandscapeGuide { + /// Create a new landscape guide instance from the source provided. + #[instrument(skip_all, err)] + pub(crate) async fn new(src: &GuideSource) -> Result> { + // Try from file + if let Some(file) = &src.guide_file { + debug!(?file, "getting landscape guide from file"); + return Ok(Some(LandscapeGuide::new_from_file(file)?)); + }; + + // Try from url + if let Some(url) = &src.guide_url { + debug!(?url, "getting landscape guide from url"); + return Ok(Some(LandscapeGuide::new_from_url(url).await?)); + }; + + Ok(None) + } + + /// Create a new landscape guide instance from the file provided. + fn new_from_file(file: &Path) -> Result { + let raw_data = fs::read_to_string(file)?; + let guide = LandscapeGuide::new_from_yaml(&raw_data)?; + + Ok(guide) + } + + /// Create a new landscape guide instance from the url provided. + async fn new_from_url(url: &str) -> Result { + let resp = reqwest::get(url).await?; + if resp.status() != StatusCode::OK { + return Err(format_err!( + "unexpected status code getting landscape guide file: {}", + resp.status() + )); + } + let raw_data = resp.text().await?; + let guide = LandscapeGuide::new_from_yaml(&raw_data)?; + + Ok(guide) + } + + /// Create a new landscape guide instance from the YAML string provided. + fn new_from_yaml(s: &str) -> Result { + // Parse YAML string + let mut guide: LandscapeGuide = serde_yaml::from_str(s)?; + + // Convert content fields from markdown to HTML + let options = markdown::Options { + compile: markdown::CompileOptions { + allow_dangerous_html: true, + ..markdown::CompileOptions::default() + }, + ..markdown::Options::default() + }; + if let Some(categories) = guide.categories.as_mut() { + for c in &mut *categories { + c.content = markdown::to_html_with_options(&c.content, &options) + .map_err(|err| format_err!("{err}"))?; + + if let Some(subcategories) = c.subcategories.as_mut() { + for sc in &mut *subcategories { + sc.content = markdown::to_html_with_options(&sc.content, &options) + .map_err(|err| format_err!("{err}"))?; + } + } + } + } + + Ok(guide) + } +} + +/// Guide category. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(crate) struct Category { + pub name: String, + pub content: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub keywords: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub subcategories: Option>, +} + +/// Guide subcategory. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(crate) struct Subcategory { + pub name: String, + pub content: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub keywords: Option>, +} diff --git a/src/logos.rs b/src/logos.rs index fa3a5eb3..cb32047b 100644 --- a/src/logos.rs +++ b/src/logos.rs @@ -81,10 +81,14 @@ async fn get_svg( logos_source: &LogosSource, file_name: &str, ) -> Result> { - let svg_data = if let Some(path) = &logos_source.logos_path { - fs::read(path.join(file_name))? - } else { - let logos_url = logos_source.logos_url.as_ref().unwrap().trim_end_matches('/'); + // Try from path + if let Some(path) = &logos_source.logos_path { + return fs::read(path.join(file_name)).map_err(Into::into); + }; + + // Try from url + if let Some(logos_url) = &logos_source.logos_url { + let logos_url = logos_url.trim_end_matches('/'); let logo_url = format!("{logos_url}/{file_name}"); let resp = http_client.get(logo_url).send().await?; if resp.status() != StatusCode::OK { @@ -93,10 +97,10 @@ async fn get_svg( resp.status() )); } - resp.bytes().await?.to_vec() + return Ok(resp.bytes().await?.to_vec()); }; - Ok(svg_data) + Err(format_err!("logos path or url not provided")) } /// Get SVG bounding box (smallest rectangle in which the object fits). diff --git a/src/main.rs b/src/main.rs index 64b59c96..87a396d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod crunchbase; mod data; mod datasets; mod github; +mod guide; mod logos; mod projects; mod s3; @@ -51,6 +52,10 @@ struct BuildArgs { #[command(flatten)] data_source: DataSource, + /// Guide source. + #[command(flatten)] + guide_source: GuideSource, + /// Logos source. #[command(flatten)] logos_source: LogosSource, @@ -77,6 +82,19 @@ struct DataSource { data_url: Option, } +/// Landscape guide location. +#[derive(Args)] +#[group(required = false, multiple = false)] +struct GuideSource { + /// Landscape guide file local path. + #[arg(long)] + guide_file: Option, + + /// Landscape guide file url. + #[arg(long)] + guide_url: Option, +} + /// Landscape logos location. #[derive(Args, Clone)] #[group(required = true, multiple = false)] diff --git a/src/settings.rs b/src/settings.rs index 63219722..a354e732 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -17,20 +17,6 @@ use serde::{Deserialize, Serialize}; use std::{fs, path::Path}; use tracing::{debug, instrument}; -/// Get landscape settings from the source provided. -#[instrument(skip_all, err)] -pub(crate) async fn get_landscape_settings(src: &SettingsSource) -> Result { - let settings = if let Some(file) = &src.settings_file { - debug!(?file, "getting landscape settings from file"); - LandscapeSettings::new_from_file(file) - } else { - debug!(url = ?src.settings_url.as_ref().unwrap(), "getting landscape settings from url"); - LandscapeSettings::new_from_url(src.settings_url.as_ref().unwrap()).await - }?; - - Ok(settings) -} - /// Landscape settings. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub(crate) struct LandscapeSettings { @@ -47,6 +33,49 @@ pub(crate) struct LandscapeSettings { pub members_category: Option, } +impl LandscapeSettings { + /// Create a new landscape settings instance from the source provided. + #[instrument(skip_all, err)] + pub(crate) async fn new(src: &SettingsSource) -> Result { + // Try from file + if let Some(file) = &src.settings_file { + debug!(?file, "getting landscape settings from file"); + return LandscapeSettings::new_from_file(file); + }; + + // Try from url + if let Some(url) = &src.settings_url { + debug!(?url, "getting landscape settings from url"); + return LandscapeSettings::new_from_url(url).await; + }; + + Err(format_err!("settings file or url not provided")) + } + + /// Create a new landscape settings instance from the file provided. + fn new_from_file(file: &Path) -> Result { + let raw_data = fs::read_to_string(file)?; + let settings: LandscapeSettings = serde_yaml::from_str(&raw_data)?; + + Ok(settings) + } + + /// Create a new landscape settings instance from the url provided. + async fn new_from_url(url: &str) -> Result { + let resp = reqwest::get(url).await?; + if resp.status() != StatusCode::OK { + return Err(format_err!( + "unexpected status code getting landscape settings file: {}", + resp.status() + )); + } + let raw_data = resp.text().await?; + let settings: LandscapeSettings = serde_yaml::from_str(&raw_data)?; + + Ok(settings) + } +} + /// Landscape group. A group provides a mechanism to organize sets of /// categories in the web application. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -75,28 +104,3 @@ pub(crate) struct FeaturedItemRuleOption { #[serde(skip_serializing_if = "Option::is_none")] pub label: Option, } - -impl LandscapeSettings { - /// Create a new landscape settings instance from the file provided. - pub(crate) fn new_from_file(file: &Path) -> Result { - let raw_data = fs::read_to_string(file)?; - let settings: LandscapeSettings = serde_yaml::from_str(&raw_data)?; - - Ok(settings) - } - - /// Create a new landscape settings instance from the url provided. - pub(crate) async fn new_from_url(url: &str) -> Result { - let resp = reqwest::get(url).await?; - if resp.status() != StatusCode::OK { - return Err(format_err!( - "unexpected status code getting landscape settings file: {}", - resp.status() - )); - } - let raw_data = resp.text().await?; - let settings: LandscapeSettings = serde_yaml::from_str(&raw_data)?; - - Ok(settings) - } -} diff --git a/src/validate.rs b/src/validate.rs index d74abad4..fa01edc1 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,13 +1,13 @@ //! This module defines the functionality of the validate CLI subcommand. -use crate::{data::get_landscape_data, DataSource}; +use crate::{data::LandscapeData, DataSource}; use anyhow::{Context, Result}; use tracing::instrument; /// Validate landscape data file. #[instrument(skip_all)] pub(crate) async fn validate_data(data_source: &DataSource) -> Result<()> { - get_landscape_data(data_source) + LandscapeData::new(data_source) .await .context("the landscape data file provided is not valid")?; diff --git a/web/index.html b/web/index.html index 2d1ec50a..ea396ace 100644 --- a/web/index.html +++ b/web/index.html @@ -13,6 +13,7 @@ <% } else { %> <% } %>