You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/06/17 15:55:10 UTC

(opendal) 01/01: docs(core): make the Rust Getting Started example runnable and CI-checked

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/docs-runnable-examples
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit cea4f0a7fbd1692911efabf8d4ecbbba7c394239
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Wed Jun 17 23:54:59 2026 +0800

    docs(core): make the Rust Getting Started example runnable and CI-checked
    
    Add a getting-started example crate that runs against the in-memory service with no credentials, a remark plugin that embeds its quickstart region into the website guide, and a ci_core run_examples job that compiles and runs it. The published snippet is now a slice of a program CI actually runs, so a broken example fails CI instead of shipping bad docs.
    
    This establishes the mechanism; further snippets and bindings follow the same pattern (ANCHOR-marked example + file= include + a run step in each binding's CI).
---
 .github/workflows/ci_core.yml              |  14 ++++
 core/Cargo.lock                            |   8 ++
 core/examples/getting-started/Cargo.toml   |  28 +++++++
 core/examples/getting-started/src/main.rs  |  45 +++++++++++
 website/docs/10-core/02-getting-started.md |  23 +-----
 website/docusaurus.config.js               |   1 +
 website/plugins/remark-include-code.js     | 115 +++++++++++++++++++++++++++++
 7 files changed, 212 insertions(+), 22 deletions(-)

diff --git a/.github/workflows/ci_core.yml b/.github/workflows/ci_core.yml
index 2cd06aaca..8ac7f76fa 100644
--- a/.github/workflows/ci_core.yml
+++ b/.github/workflows/ci_core.yml
@@ -110,6 +110,20 @@ jobs:
         working-directory: core
         run: cargo build --locked
 
+  run_examples:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+      - name: Setup Rust toolchain
+        uses: ./.github/actions/setup
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+      # Examples back the snippets shown in the website guide. Running them keeps
+      # the docs honest: a broken example fails CI instead of shipping bad docs.
+      - name: Run guide examples
+        working-directory: core
+        run: cargo run -p opendal-examples-getting-started
+
   build_all_features:
     runs-on: ubuntu-latest
     steps:
diff --git a/core/Cargo.lock b/core/Cargo.lock
index 3d31931e3..f031fd848 100644
--- a/core/Cargo.lock
+++ b/core/Cargo.lock
@@ -6469,6 +6469,14 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "opendal-examples-getting-started"
+version = "0.57.0"
+dependencies = [
+ "opendal",
+ "tokio",
+]
+
 [[package]]
 name = "opendal-examples-multipart-upload"
 version = "0.57.0"
diff --git a/core/examples/getting-started/Cargo.toml b/core/examples/getting-started/Cargo.toml
new file mode 100644
index 000000000..c66dd64bc
--- /dev/null
+++ b/core/examples/getting-started/Cargo.toml
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+name = "opendal-examples-getting-started"
+publish = false
+
+edition.workspace = true
+rust-version.workspace = true
+version.workspace = true
+
+[dependencies]
+opendal = { path = "../.." }
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
diff --git a/core/examples/getting-started/src/main.rs b/core/examples/getting-started/src/main.rs
new file mode 100644
index 000000000..eea16acb9
--- /dev/null
+++ b/core/examples/getting-started/src/main.rs
@@ -0,0 +1,45 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! The Getting Started example for the Rust core, embedded into the website
+//! guide. It runs against the in-memory service with no credentials, so CI can
+//! execute it directly. The region between the ANCHOR markers is what the docs
+//! show — keep it copy-pasteable.
+
+// ANCHOR: quickstart
+use opendal::Operator;
+use opendal::Result;
+use opendal::services;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    // Configure a service, then build an operator from it.
+    let op = Operator::new(services::Memory::default())?;
+
+    // The same verbs work on every service.
+    op.write("hello.txt", "Hello, World!").await?;
+
+    let bytes = op.read("hello.txt").await?;
+    println!("read {} bytes", bytes.len());
+
+    let meta = op.stat("hello.txt").await?;
+    println!("size = {} bytes", meta.content_length());
+
+    op.delete("hello.txt").await?;
+    Ok(())
+}
+// ANCHOR_END: quickstart
diff --git a/website/docs/10-core/02-getting-started.md b/website/docs/10-core/02-getting-started.md
index 29d8b85bf..a884d7e21 100644
--- a/website/docs/10-core/02-getting-started.md
+++ b/website/docs/10-core/02-getting-started.md
@@ -19,28 +19,7 @@ opendal = "0.57"
 tokio = { version = "1", features = ["full"] }
 ```
 
-```rust
-use opendal::services;
-use opendal::Operator;
-use opendal::Result;
-
-#[tokio::main]
-async fn main() -> Result<()> {
-    // Configure a service, then build an operator from it.
-    let op = Operator::new(services::Memory::default())?;
-
-    // The same verbs work on every service.
-    op.write("hello.txt", "Hello, World!").await?;
-
-    let bytes = op.read("hello.txt").await?;
-    println!("read {} bytes", bytes.len());
-
-    let meta = op.stat("hello.txt").await?;
-    println!("size = {} bytes", meta.content_length());
-
-    op.delete("hello.txt").await?;
-    Ok(())
-}
+```rust file=core/examples/getting-started/src/main.rs region=quickstart
 ```
 
 ## Point it at a real backend
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index fdc2e33c4..b62ac9383 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -101,6 +101,7 @@ const config = {
           editUrl: "https://github.com/apache/opendal/tree/main/website/",
           showLastUpdateAuthor: true,
           showLastUpdateTime: true,
+          remarkPlugins: [require("./plugins/remark-include-code")],
         },
         blog: {
           showReadingTime: true,
diff --git a/website/plugins/remark-include-code.js b/website/plugins/remark-include-code.js
new file mode 100644
index 000000000..6de6a5046
--- /dev/null
+++ b/website/plugins/remark-include-code.js
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// A remark plugin that fills fenced code blocks from real example files, so the
+// snippets shown in the docs are slices of code that CI actually compiles and
+// runs. Usage in markdown:
+//
+//   ```rust file=core/examples/getting-started/src/main.rs region=quickstart
+//   ```
+//
+// `file` is resolved from the repository root. `region` (optional) extracts the
+// lines between `ANCHOR: <name>` and `ANCHOR_END: <name>` markers in the file —
+// language-agnostic, so it works with //, #, --, etc. Without a region, the
+// whole file is used with any leading license-header comment block stripped.
+
+const fs = require("fs");
+const path = require("path");
+
+// website/plugins -> website -> repository root.
+const ROOT = path.resolve(__dirname, "..", "..");
+
+function parseMeta(meta) {
+  const out = {};
+  for (const m of meta.matchAll(/(\w+)=("[^"]*"|\S+)/g)) {
+    out[m[1]] = m[2].replace(/^"|"$/g, "");
+  }
+  return out;
+}
+
+function dedent(lines) {
+  const indents = lines
+    .filter((l) => l.trim().length > 0)
+    .map((l) => l.match(/^[ \t]*/)[0].length);
+  const min = indents.length ? Math.min(...indents) : 0;
+  return lines.map((l) => l.slice(min));
+}
+
+function extractRegion(content, region, file) {
+  const lines = content.split("\n");
+  const start = new RegExp(`ANCHOR:\\s*${region}\\b`);
+  const end = new RegExp(`ANCHOR_END:\\s*${region}\\b`);
+  let from = -1;
+  let to = -1;
+  for (let i = 0; i < lines.length; i++) {
+    if (from === -1 && start.test(lines[i])) {
+      from = i + 1;
+    } else if (from !== -1 && end.test(lines[i])) {
+      to = i;
+      break;
+    }
+  }
+  if (from === -1 || to === -1) {
+    throw new Error(`remark-include-code: region "${region}" not found in ${file}`);
+  }
+  return dedent(lines.slice(from, to)).join("\n").replace(/^\n+|\s+$/g, "");
+}
+
+function stripLicenseHeader(content) {
+  // Drop a leading block of comment/blank lines (the ASF header) so it does not
+  // show up in the rendered snippet.
+  const lines = content.split("\n");
+  let i = 0;
+  while (
+    i < lines.length &&
+    (lines[i].trim() === "" || /^\s*(\/\/|#|--|\/\*|\*)/.test(lines[i]))
+  ) {
+    i++;
+  }
+  return lines.slice(i).join("\n").replace(/^\n+|\s+$/g, "");
+}
+
+function transform(node) {
+  if (
+    node.type === "code" &&
+    typeof node.meta === "string" &&
+    /(^|\s)file=/.test(node.meta)
+  ) {
+    const opts = parseMeta(node.meta);
+    const abs = path.resolve(ROOT, opts.file);
+    const content = fs.readFileSync(abs, "utf8");
+    node.value = opts.region
+      ? extractRegion(content, opts.region, opts.file)
+      : stripLicenseHeader(content);
+    const meta = node.meta
+      .replace(/\s*file=("[^"]*"|\S+)/, "")
+      .replace(/\s*region=("[^"]*"|\S+)/, "")
+      .trim();
+    node.meta = meta.length ? meta : null;
+  }
+  if (Array.isArray(node.children)) {
+    node.children.forEach(transform);
+  }
+}
+
+module.exports = function remarkIncludeCode() {
+  return (tree) => {
+    transform(tree);
+  };
+};