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);
+ };
+};