<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>emre şahin's digital garden 🍃 - Rust</title>
    <link>https://emresahin.net/categories/rust/</link>
    <description>Posts in the Rust category</description>
    <language>en</language>
    <managingEditor>contact@emresahin.net (Emre Şahin)</managingEditor>
    <lastBuildDate>Tue, 15 Sep 2026 19:46:32 +0000</lastBuildDate>
    <atom:link href="https://emresahin.net/categories/rust/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Devlog 10: Codecov, Doctests, and Cargo Publish Struggles</title>
      <published>2025-01-19T09:30:49+00:00</published>
      <updated>2025-01-19T09:30:49+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sun, 19 Jan 2025 09:30:49 +0000</pubDate>
      <link>https://emresahin.net/devlog-10/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-10/</guid>
      <description>🐢 Now we’re going into the real deep work. The only failure was the Codecov patch in Xvc. Let’s see what needs to be done. 🐇 It looks, from the coverage page , that our additions to HStore don’t have any tests. We can add some unit tests to new joins, maybe. 🐢 I don’t find unit tests particularly...</description>
      <category>Devlog</category>
      <category>XVC</category>
      <category>Rust</category>
      <category>cargo</category>
      <category>ecs</category>
      <category>Claude</category>
      <category>cargo-publish</category>
      <category>crates.io</category>
      <category>sqlite</category>
      <category>codecov</category>
      <category>doctests</category>
      <content:encoded><![CDATA[<p>🐢 Now we’re going into the real deep work. The only failure was the Codecov patch in Xvc. Let’s see what needs to be done.</p>
<p>🐇 It looks, from the <a href="https://app.codecov.io/gh/iesahin/xvc/pull/263?src=pr&amp;el=tree&amp;utm_medium=referral&amp;utm_source=github&amp;utm_content=comment&amp;utm_campaign=pr+comments&amp;utm_term=Emre+Sahin">coverage page</a>, that our additions to HStore don’t have any tests. We can add some unit tests to new joins, maybe.</p>
<p>🐢 I don’t find unit tests particularly useful, but let’s use GitHub Copilot to add tests for us.</p>
<p>🐇 Added a unit test and a doc test for <code>full_join</code>, and I think doc tests have more value. They provide documentation, and we can readily see how to use a function from its docs. Better to increase coverage with doc tests.</p>
<p>🐢 There are points that I should learn while writing doc tests. The imports must use the full path, not <code>crate::</code>. The tested struct also doesn’t have implicit imports.</p>
<p>🐇 The ceremony of adding keys and values is a bit too much. It may be worthwhile to add <code>insert</code> for any <code>Into&lt;XvcEntity&gt;</code>.</p>
<p>🐢 It will certainly save time if we don’t have to type <code>.into()</code> for each key :)</p>
<p>🐇 Pushed to test again. Should we have some means to test coverage locally?</p>
<p>🐢 I don’t think we need to consider coverage locally. It’s not worth our time.</p>
<p>🐇 Now while waiting for tests to be completed, what can we do?</p>
<pre><code>gh -R iesahin/xvc run list
completed	failure	v0.6.13	Rust-CI	v0.6.13	pull_request	12543831360	3m54s	2024-12-30T08:15:06Z
...
</code></pre>
<p>🐢 I don’t think it took too much time. Let’s view the results:</p>
<pre><code>gh -R iesahin/xvc run view 12543831360
...
  X Run Current Dev Tests
...
To see what failed, try: 
View this run on GitHub: https://github.com/iesahin/xvc/actions/runs/12543831360
</code></pre>
<p>🐢 The current dev tests fail for some reason. Let’s run these locally.</p>
<p>🐇 We’re missing <code>llvm-tools-preview</code> locally. How do we install this?</p>
<p>🐢 The command is:</p>
<pre><code>rustup component add llvm-tools-preview
info: component 'llvm-tools' for target 'aarch64-apple-darwin' is up to date
</code></pre>
<p>🐇 It’s already installed. We need to set the environment variables.</p>
<p>🐢 Instead, we can just turn off dev tests for the time being. We don’t need them. Our local tests pass.</p>
<p>🐇 Yeah, ok, we don’t need to solve each and every bit of these issues.</p>
<pre><code>gh -R iesahin/xvc run list
</code></pre>
<p>…
🐇 Ok. Let’s take a look at the run again.</p>
<pre><code>gh -R iesahin/xvc run list
completed	failure	v0.6.13	Rust-CI	v0.6.13	pull_request	12544020064	3m39s	2024-12-30T08:33:25Z

gh -R iesahin/xvc run view 12544020064
...
  X Test and Coverage
...

gh run view 12544020064 --log-failed
...
Test and Coverage (stable)	Test and Coverage	2024-12-30T08:36:58.8911690Z Error: ProcessError { stdout: "", stderr: "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\ncurl: (7) Failed to connect to e1.xvc.dev port 80 after 160 ms: Couldn't connect to server\n" }
...
</code></pre>
<p>🐢 We need to start <code>nginx</code> on the server. We forgot it yesterday.</p>
<p>🐇 Ah, yeah. After adding that dufs installation. Ok.</p>
<p>🐢 We also need to add a reverse proxy to dufs somehow, but this is for later.</p>
<p>🐇 For the use case, I don’t think it’s necessary. We can just adjust the port to a non-standard one if we need to use 443 for another thing, but let’s take a look at the tests again.</p>
<p>🐢 Let’s add another doc test. This time to <code>XvcStore</code>.</p>
<pre><code>cargo test -p xvc-ecs --doc
...
test result: ok. 8 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 2.87s
</code></pre>
<p>🐇 Sent the files again. Waiting for tests to finish.</p>
<p>🐢 Let’s check the keymaps file in the meantime.</p>
<p>🐇 I tried to read some documentation but didn’t see an error. Maybe we should just set it to non-lazy and avoid allowing remaps.</p>
<p>🐢 We already spent too much time with this.</p>
<p>🐇 Yes, let’s take a look at the tests again.</p>
<pre><code>gh -R iesahin/xvc run list
completed	success	v0.6.13	Rust-CI	v0.6.13	pull_request	12544291068	8m10s	2024-12-30T09:00:42Z
...
</code></pre>
<p>🐢 Oh, yeah, the merge is ready.</p>
<p>🐇 Patch coverage is still behind the target, though.</p>
<p>🐢 Yeah, but let’s release this one and make the next better covered. Also, I’m not sure if the doc tests had any effect on coverage.</p>
<p>🐇 If we look at the <a href="https://app.codecov.io/gh/iesahin/xvc/pull/263?src=pr&amp;el=tree&amp;utm_medium=referral&amp;utm_source=github&amp;utm_content=comment&amp;utm_campaign=pr+comments&amp;utm_term=Emre+Sahin">coverage page</a> again, we can see if the doc tests had any effect.</p>
<p>🐢 It seems codecov.io doesn’t consider coverage for doc tests. This is a bit weird, but let’s not spend more time on this.</p>
<p>🐇 Sure, let’s merge.</p>
<p>🐢 I think we forgot to bump the version in <code>Cargo.toml</code>. We’ll have to do that in main.</p>
<p>🐇 Oh, yeah. Let’s bump it and tag as well.</p>
<p>🐢 Now, we can wait for all files to be produced. What will we do next?</p>
<p>🐇 We can just release the Python version as well. It shouldn’t need any changes.</p>
<p>🐢 Umm, right. Maybe we can add a few tests as well.</p>
<p>🐇 Let’s bump the version first and see.</p>
<p>🐢 Bumped versions in <code>Cargo.toml</code> and ran <code>maturin develop</code>.</p>
<p>🐇 It seems ready now.</p>
<p>🐢 The main fails, though. The “Publish Crates” action looks for <code>libsqlite3</code>. Let’s take a look at it.</p>
<pre><code>gh -R iesahin/xvc run list
completed	failure	Release v0.6.13	Publish Crates	v0.6.13	push	12544753359	6m1s	2024-12-30T09:41:56Z
</code></pre>
<p>🐢 The issue is that the VM doesn’t have <code>libsqlite3-dev</code>. Let’s add it.</p>
<p>🐇 We need to start the job manually again. Let’s not tag this time.</p>
<p>🐢 Some of the packages were already published. Now they break. <code>crates.io</code> says they’re already published. Maybe we can check if a package is already published.</p>
<p>🐇 Let’s check if we can make <code>cargo publish</code> more forgiving.</p>
<p>🐢 There doesn’t seem to be an option. Let’s search “how to skip published packages in the workspace to avoid errors with cargo publish”.</p>
<p>🐇 Claude is bullshitting again. Let’s try a manual approach: how to skip already published packages.</p>
<p>🐢 It may be easier to just add a check if the package is published. How do we get the info?</p>
<p>🐇 Or we can just go on to the next package if the package is already available.</p>
<p>🐢 Let’s do this manually this time.</p>]]></content:encoded>
    </item>
    <item>
      <title>Perl Error Building rust-openssl with Maturin, Ubuntu, and Manylinux</title>
      <published>2023-11-02T09:19:26+00:00</published>
      <updated>2023-11-02T09:19:26+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 02 Nov 2023 09:19:26 +0000</pubDate>
      <link>https://emresahin.net/perl-error-building-rust-openssl-with-maturin--ubuntu-and-manylinux/</link>
      <guid isPermaLink="true">https://emresahin.net/perl-error-building-rust-openssl-with-maturin--ubuntu-and-manylinux/</guid>
      <description>While building Python packages for Xvc with Maturin, I was receiving an error in GitHub Actions CI for Linux packages. Can't locate IPC/Cmd.pm in @INC (@INC contains: /home/runner/work/xvc.py/xvc.py/target/x86_64-unknown-linux-gnu/release/build/openssl-sys-844a96d66ae533b1/out/openssl-build/build...</description>
      <category>Xvc</category>
      <category>CI/CD</category>
      <category>Rust</category>
      <category>Xvc</category>
      <category>Maturin</category>
      <category>GitHub Actions</category>
      <category>Perl</category>
      <category>Python</category>
      <category>OpenSSL</category>
      <category>CentOS</category>
      <category>Debian</category>
      <content:encoded><![CDATA[<p>While building <a href="https://github.com/iesahin/xvc.py">Python packages for Xvc</a> with Maturin, I was receiving an error in GitHub Actions CI for Linux packages.</p>
<pre><code class="language-text">Can't locate IPC/Cmd.pm in @INC (@INC contains: /home/runner/work/xvc.py/xvc.py/target/x86_64-unknown-linux-gnu/release/build/openssl-sys-844a96d66ae533b1/out/openssl-build/build/src/util/perl /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5 . /home/runner/work/xvc.py/xvc.py/target/x86_64-unknown-linux-gnu/release/build/openssl-sys-844a96d66ae533b1/out/openssl-build/build/src/external/perl/Text-Template-1.56/lib)
</code></pre>
<p>The issue seemed to be a missing Perl package. Building <code>rust-openssl</code> now appears to require the <code>perl-core</code> package.</p>
<p>I added <code>apt-get update &amp;&amp; apt-get install perl-core</code> to the CI configuration, but that didn’t work.</p>
<p>However, as <a href="https://github.com/sfackler/rust-openssl/issues/2036#issuecomment-1724324145">this GitHub issue comment suggests</a>, Maturin with its <code>manylinux</code> support uses different kinds of Docker containers to build the packages. We must detect whether it is a CentOS or Debian-based container and install the missing packages accordingly.</p>
<p>The <em>Building wheels</em> step in the configuration should look similar to:</p>
<pre><code class="language-yaml">- name: Build wheels
  uses: PyO3/maturin-action@v1
  with:
    target: ${{ matrix.target }}
    manylinux: auto
    args: --release --out dist
    before-script-linux: |
      # If we're running on RHEL/CentOS, install needed packages.
      if command -v yum &amp;&gt; /dev/null; then
          yum update -y &amp;&amp; yum install -y perl-core openssl openssl-devel pkgconfig libatomic

          # If we're running on i686, we need to symlink libatomic
          # in order to build openssl with the -latomic flag.
          if [[ ! -d "/usr/lib64" ]]; then
              ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so
          fi
      else
          # If we're running on a Debian-based system.
          apt update -y &amp;&amp; apt-get install -y libssl-dev openssl pkg-config
      fi
</code></pre>
<p>This will run the specified script before executing the Maturin build command in the container, ensuring all missing packages are installed.</p>]]></content:encoded>
    </item>
    <item>
      <title>Feature flags in Rust</title>
      <published>2022-10-01T14:09:00+00:00</published>
      <updated>2022-10-01T14:09:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 01 Oct 2022 14:09:00 +0000</pubDate>
      <link>https://emresahin.net/Feature-flags-in-Rust/</link>
      <guid isPermaLink="true">https://emresahin.net/Feature-flags-in-Rust/</guid>
      <description>Rust allows you to define feature flags to compile certain parts of a binary conditionally. The Cargo Book has an extensive section on them. The Cargo.toml file can have a [features] section. In this section, features can be defined, and they can enable other features. [features] my-feature = [] ...</description>
      <category>Rust</category>
      <category>Software Development</category>
      <category>Rust</category>
      <category>Cargo</category>
      <category>Feature Flags</category>
      <category>Conditional Compilation</category>
      <category>Configuration</category>
      <content:encoded><![CDATA[<p>Rust allows you to define feature flags to compile certain parts of a binary conditionally.
The Cargo Book has an extensive <a href="https://doc.rust-lang.org/cargo/reference/features.html">section</a> on them.</p>
<p>The <code>Cargo.toml</code> file can have a <code>[features]</code> section.
In this section, features can be defined, and they can enable other features.</p>
<pre><code class="language-toml">[features]
my-feature = []
another-feature = ["my-feature"]
</code></pre>
<p>These markers are used in the code as follows:</p>
<pre><code class="language-rust">
#[cfg(feature = "my-feature")]
pub mod my_module;
</code></pre>
<p>The default features are listed under the <code>default</code> key of the <code>[features]</code> section.
If it’s not defined, it’s considered empty, so <code>cargo build</code> supplies no features to the build system by default.</p>
<p>If you don’t want <code>default</code> features added automatically, you can use the <code>--no-default-features</code> option with <code>cargo</code>.</p>
<p>Starting from Rust 1.60, dependencies can be tied to features.
When you define a dependency in the <code>[dependencies]</code> section, add <code>optional = true</code> to its options. Then, in <code>[features]</code>, use the <code>dep:package</code> syntax.</p>
<pre><code class="language-toml">
[dependencies]

pack = { version = "1.0", optional = true }

[features]

my-feature = ["dep:pack"]
</code></pre>
<p>This way, Cargo doesn’t compile <code>pack</code> if <code>my-feature</code> is not enabled.</p>
<p>A caveat to using features is that they should be additive.
There shouldn’t be a <code>no-std</code> feature to remove the standard library dependency; instead, you should define a <code>std</code> feature and enable it by default.
The reason behind this is that Cargo ensures there is a single copy of a package with <em>all</em> the requested features enabled for the entire dependency graph.
If you have a <code>no-x</code> feature that removes functionality, it breaks the additive logic of Cargo’s feature resolution.</p>]]></content:encoded>
    </item>
    <item>
      <title>Rust `ends_with` and `strip_prefix` behavior differences in `Path` and `str`</title>
      <published>2022-07-06T16:03:51+00:00</published>
      <updated>2022-07-06T16:03:51+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Wed, 06 Jul 2022 16:03:51 +0000</pubDate>
      <link>https://emresahin.net/Rust--ends_with--and--strip_prefix--behavior-differences-in--Path--and--str-/</link>
      <guid isPermaLink="true">https://emresahin.net/Rust--ends_with--and--strip_prefix--behavior-differences-in--Path--and--str-/</guid>
      <description>While writing an ignore library, I encountered subtle bugs caused by Rust’s Path behavior. ends_with in Path is different from ends_with in str : If you try to check whether a Path is a directory by its final character, you’ll find that path.ends_with("/") returns false . Path::strip_prefix also ...</description>
      <category>development</category>
      <category>rust</category>
      <category>programming</category>
      <category>rust</category>
      <category>Path</category>
      <category>str</category>
      <category>std</category>
      <category>debugging</category>
      <content:encoded><![CDATA[<p>While writing an ignore library, I encountered subtle bugs caused by Rust’s <code>Path</code> behavior.</p>
<ul>
<li>
<p><strong><code>ends_with</code> in <code>Path</code> is different from <code>ends_with</code> in <code>str</code></strong>:
If you try to check whether a <code>Path</code> is a directory by its final character, you’ll find that <code>path.ends_with("/")</code> returns <code>false</code>.</p>
</li>
<li>
<p><strong><code>Path::strip_prefix</code> also consumes the final slash</strong>:
If you have a directory marker at the end, such as <code>/Users/emre/mydir/</code>, and pass it to <code>strip_prefix("/Users/emre")</code>, you’ll get <code>mydir</code> instead of <code>mydir/</code>.</p>
</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Creating a file system watcher with ignore rules</title>
      <published>2022-07-06T15:55:49+00:00</published>
      <updated>2022-07-06T15:55:49+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Wed, 06 Jul 2022 15:55:49 +0000</pubDate>
      <link>https://emresahin.net/Creating-a-file-system-watcher-with-ignore-rules/</link>
      <guid isPermaLink="true">https://emresahin.net/Creating-a-file-system-watcher-with-ignore-rules/</guid>
      <description>In a previous post, I described the development of a file system walker. It returns files from a directory recursively while considering ignore patterns. In this post, I’ll update it. Some of the requirements have changed, and I need new features: walk_serial currently takes a Sender and uses cha...</description>
      <category>Development</category>
      <category>Rust</category>
      <category>rust</category>
      <category>ignore</category>
      <category>filesystem</category>
      <category>watcher</category>
      <category>notify</category>
      <content:encoded><![CDATA[<p>In a previous post, I described the development of a file system walker. It
returns files from a directory recursively while considering ignore patterns.</p>
<p>In this post, I’ll update it. Some of the requirements have changed, and I need
new features:</p>
<ul>
<li>
<p><code>walk_serial</code> currently takes a <code>Sender</code> and uses channels to send the results. This is
confusing, as it doesn’t use any parallelism to traverse the file system.</p>
</li>
<li>
<p>There are long-running processes that modify the file system, which can affect
behavior. We need to be notified about file system changes while using
identical ignore rules.</p>
</li>
</ul>
<p>For the first change, I’ll convert the signature of <code>walk_serial</code> to:</p>
<pre><code class="language-rust">pub fn walk_serial(
    given: IgnoreRules,
    dir: &amp;Path,
    walk_options: &amp;WalkOptions,
    res_paths: &amp;mut Vec&lt;Result&lt;PathMetadata&gt;&gt;,
) -&gt; Result&lt;IgnoreRules&gt;</code></pre>
<p>In the first version, instead of <code>res_paths</code>, we had a <code>sender</code> through which the
results were sent.</p>
<p>Why don’t I simply return the paths instead of receiving a <code>mut Vec</code> parameter to fill?
While it might be simpler, I don’t like the idea of creating a new <code>Vec</code> for
each function call. The overhead of creating new vectors and merging them can
become a burden for large directory trees. Instead, each call of the function
works on the same vector and expands its capacity when necessary. Since the
current <code>Vec</code> strategy is to double the capacity when needed, there
are fewer memory allocations for complex and large directory hierarchies.</p>
<p>Other than this, instead of <code>Sender::send</code>, the function uses <code>Vec::push</code> to
add elements to the vector. The rest of the implementation is similar.</p>
<p>One major difference in this version is that the function returns an <code>IgnoreRules</code> value instead of <code>()</code>.
This contains the compiled ignore rules from all directories below the given directory.
We can use this returned value to further check the paths we get from the notifier we’ll develop.</p>
<h2 id="adding-a-file-system-notifier">Adding a file system notifier</h2>
<p>For the second change, we need to access the underlying OS machinery for
file system notifications. My research led me to the <code>notify</code> crate. Although it
seems to be undergoing a major overhaul as of this writing, it’s the only promising
option other than interfacing directly with the OS.</p>
<p>Again, we begin by defining the enums and structs for this problem.</p>
<p>What do we want? We want to collect file system events to update the
<code>Vec&lt;PathMetadata&gt;</code> that was built with <code>walk_serial</code> or <code>walk_parallel</code>.
<code>PathMetadata</code> was defined as:</p>
<pre><code class="language-rust">pub struct PathMetadata {
    pub path: PathBuf,
    pub metadata: Metadata,
}</code></pre>
<p>We could build a <code>HashMap&lt;PathBuf, Metadata&gt;</code> or keep this as a vector, but for
our purposes, we want to track new events. These events correspond to creating a
new file, updating metadata (size, timestamp), or deleting a file. We don’t need, for example, permission changes, as we primarily want to track which file
is newer or whose size has changed.</p>
<pre><code class="language-rust">pub enum PathEvent {
    Create { path: PathBuf, metadata: Metadata },
    Update { path: PathBuf, metadata: Metadata },
    Delete { path: PathBuf },
}</code></pre>
<p><a href="https://github.com/notify-rs/notify"><code>notify</code></a> provides an abstraction over different operating systems.
It’s called <code>RecommendedWatcher</code> and is used to provide a consistent interface for events.
A watcher is built by implementing the <code>EventHandler</code> trait.</p>
<p>Since we need to check whether paths are ignored before reporting them, the watcher should receive an <code>IgnoreRules</code> struct.</p>
<pre><code class="language-rust">pub fn make_watcher(
    ignore_rules: IgnoreRules,
) -&gt; Result&lt;(RecommendedWatcher, Receiver&lt;PathEvent&gt;)&gt; {</code></pre>
<p>It returns a <code>RecommendedWatcher</code> and a (crossbeam) <code>Receiver</code> for the <code>PathEvent</code> we defined above.
The implementation creates a channel, initializes the watcher, and starts watching.</p>
<pre><code class="language-rust">    let (sender, receiver) = bounded(10000);
    let root = ignore_rules.root.clone();
    let mut watcher = notify::recommended_watcher(PathEventHandler {
        ignore_rules,
        sender,
    })?;
    watcher.watch(&amp;root, RecursiveMode::Recursive)?;</code></pre>
<p><code>PathEventHandler</code> is the struct that implements the <code>EventHandler</code> trait.
The implementation requires a single function to be implemented:</p>
<pre><code class="language-rust">    fn handle_event(&amp;mut self, event: notify::Result&lt;Event&gt;)</code></pre>
<p><a href="https://docs.rs/notify/5.0.0-pre.15/notify/event/struct.Event.html"><code>notify::Event</code></a> is a hierarchical set of enums that describe the type of file system event.
The event handler is expected to discriminate between these events and react accordingly.</p>
<pre><code class="language-rust">pub struct Event {
    pub kind: EventKind,
    pub paths: Vec&lt;PathBuf&gt;,
    pub attrs: EventAttributes,
}</code></pre>
<p>We’re interested in filtering events by <code>EventKind</code>, checking whether the paths are ignored, and sending the path and metadata through a channel.
We have three kinds of events in <code>PathEvent</code>.
<code>PathEventHandler</code> implements event filtering and calls handlers for these <code>PathEvent</code>s.</p>
<p>The struct has functions that correspond to one or more <code>PathEvent</code> elements.
These are <code>create_event</code>, <code>write_event</code>, <code>remove_event</code>, and <code>rename_event</code>.
The first three correspond directly to members of <code>PathEvent</code>, and the latter sends both a <code>PathEvent::Delete</code> and a <code>PathEvent::Create</code>.</p>
<pre><code class="language-rust">match event.kind {
    notify::EventKind::Create(_) =&gt; self.create_event(event.paths[0].clone()),
    notify::EventKind::Modify(mk) =&gt; match mk {
        notify::event::ModifyKind::Any =&gt; todo!(),
        notify::event::ModifyKind::Data(_) =&gt; self.write_event(event.paths[0].clone()),
        notify::event::ModifyKind::Metadata(_) =&gt; {
            self.write_event(event.paths[0].clone())
        }
        notify::event::ModifyKind::Name(rk) =&gt; match rk {
            notify::event::RenameMode::Any =&gt; {}
            notify::event::RenameMode::To =&gt; self.create_event(event.paths[0].clone()),
            notify::event::RenameMode::From =&gt; {
                self.remove_event(event.paths[0].clone())
            }
            notify::event::RenameMode::Both =&gt; {
                self.rename_event(event.paths[0].clone(), event.paths[1].clone())
            }
            notify::event::RenameMode::Other =&gt; {}
        },
        notify::event::ModifyKind::Other =&gt; {}
    },
    notify::EventKind::Remove(_) =&gt; self.remove_event(event.paths[0].clone()),
    notify::EventKind::Any =&gt; {}
    notify::EventKind::Access(_) =&gt; {}
    notify::EventKind::Other =&gt; {}
}</code></pre>
<p>An example event handler should clarify how the others work:</p>
<pre><code class="language-rust">fn write_event(&amp;mut self, path: PathBuf) {
    match check_ignore(&amp;self.ignore_rules, &amp;path) {
        MatchResult::Whitelist | MatchResult::NoMatch =&gt; {
            self.sender
                .send(PathEvent::Create {
                    path: path.clone(),
                    metadata: path.metadata().map_err(Error::from).unwrap(),
                })
                .unwrap_or_else(|e| warn!("{}", e));
        }
        MatchResult::Ignore =&gt; {
            debug!("FS Notification Ignored: {}", path.to_string_lossy());
        }
    }
}</code></pre>
<p>Other event handlers work similarly. They send <code>PathEvent</code> values that can be used to update a list of paths and their metadata.</p>
<p>To use this watcher, you first retrieve the list of files with <code>walk_serial</code>.
It returns a snapshot of the directory and the <code>IgnoreRules</code> collected from the directories below it.
Then, you create a <code>watcher</code> with:</p>
<pre><code class="language-rust">let ignore_rules = walk_serial(...)?;
let (watcher, path_event_rec) = make_watcher(ignore_rules)?;</code></pre>
<p>After this, you can create another thread to monitor file system changes:</p>
<pre><code class="language-rust">crossbeam::scope(|s| {
    s.spawn(|_| {
        while let Ok(path_event) = path_event_rec.recv() {
            match path_event {
                PathEvent::Create { .. } =&gt; { /* ... */ }
                PathEvent::Delete { .. } =&gt; { /* ... */ }
                PathEvent::Update { .. } =&gt; { /* ... */ }
            }
        }
    });
});</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Creating OS-dependent temporary directories in Rust</title>
      <published>2022-06-11T06:02:43+00:00</published>
      <updated>2022-06-11T06:02:43+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 11 Jun 2022 06:02:43 +0000</pubDate>
      <link>https://emresahin.net/Creating-OS-dependent-temporary-directories-in-Rust/</link>
      <guid isPermaLink="true">https://emresahin.net/Creating-OS-dependent-temporary-directories-in-Rust/</guid>
      <description>There are a few crates in Rust for retrieving system-dependent directories, such as the user’s home or the system configuration directory. The one I prefer is directories-next . Recently, I needed a standard way to get the temporary directory. I checked the crate documentation but couldn’t find a...</description>
      <category>Rust</category>
      <category>Programming</category>
      <category>temporary directory</category>
      <category>crates</category>
      <category>standard library</category>
      <category>PathBuf</category>
      <content:encoded><![CDATA[<p>There are a few crates in Rust for retrieving system-dependent directories, such as the user’s home or the system configuration directory. The one I prefer is <a href="https://crates.io/crates/directories-next">directories-next</a>.</p>
<p>Recently, I needed a standard way to get the temporary directory. I checked the crate documentation but couldn’t find a proper function for this.</p>
<p>Then I noticed that <a href="https://doc.rust-lang.org/std/env/fn.temp_dir.html"><code>std::env::temp_dir()</code></a> returns a <code>PathBuf</code> pointing to the system’s temporary directory. This is a reminder to myself that we don’t always need extra packages for basic functionality.</p>]]></content:encoded>
    </item>
    <item>
      <title>Unit tests vs Integration Tests in Rust</title>
      <published>2022-06-11T05:55:58+00:00</published>
      <updated>2022-06-11T05:55:58+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 11 Jun 2022 05:55:58 +0000</pubDate>
      <link>https://emresahin.net/Unit-tests-vs-Integration-Tests-in-Rust/</link>
      <guid isPermaLink="true">https://emresahin.net/Unit-tests-vs-Integration-Tests-in-Rust/</guid>
      <description>One thing I’ve noticed since I started working with Rust is that Test-Driven Development (TDD) has become much more feasible. As a developer who is “absentminded by default,” I highly value tests. However, if the feedback cycle is slow, it becomes prohibitively expensive to test everything. This ...</description>
      <category>Rust</category>
      <category>Testing</category>
      <category>development</category>
      <category>unit tests</category>
      <category>integration tests</category>
      <category>Rust</category>
      <category>CI/CD</category>
      <category>TDD</category>
      <category>best practices</category>
      <content:encoded><![CDATA[<p>One thing I’ve noticed since I started working with Rust is that <em>Test-Driven Development</em> (TDD) has become much more feasible. As a developer who is “absentminded by default,” I highly value tests. However, if the feedback cycle is slow, it becomes prohibitively expensive to test everything. This is likely why unit tests are often favored over integration tests: they are more precise, faster to execute, and make rapid round-trip testing more achievable.</p>
<p>Another point is that Rust, being a statically typed language, requires far fewer unit tests than dynamic languages. Traditionally, unit tests are used to validate “input consumption” (ensuring input is processed correctly) and “well-formed output” (verifying that the output meets requirements under specific conditions). In Rust, many of these input-output concerns are resolved at compile time through the use of expressive types like structs and enums.</p>
<p>For these reasons, I find myself more inclined toward integration tests that reflect real-world requirements, rather than focusing solely on unit tests in artificial settings.</p>]]></content:encoded>
    </item>
    <item>
      <title>Developing a gitignore crate</title>
      <published>2022-06-09T02:48:03+00:00</published>
      <updated>2022-06-09T02:48:03+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 09 Jun 2022 02:48:03 +0000</pubDate>
      <link>https://emresahin.net/developing-a-gitignore-crate/</link>
      <guid isPermaLink="true">https://emresahin.net/developing-a-gitignore-crate/</guid>
      <description>I needed a file system ignore library for a utility I was writing. This is similar to Git’s .gitignore , but the files containing the rules can have different names, and ignore rules may be defined programmatically. I was using burntsushi’s ignore crate , which in turn uses globset by the same au...</description>
      <category>Rust</category>
      <category>Software Development</category>
      <category>rust</category>
      <category>gitignore</category>
      <category>architecture</category>
      <category>cli</category>
      <category>xvc</category>
      <content:encoded><![CDATA[<p>I needed a file system <em>ignore</em> library for a utility I was writing. This is similar
to Git’s <code>.gitignore</code>, but the files containing the rules can have different
names, and ignore rules may be defined programmatically.</p>
<p>I was using burntsushi’s <a href="https://crates.io/crates/ignore">ignore crate</a>, which in turn uses <a href="https://crates.io/crates/globset">globset</a> by the
same author. In a directory hierarchy, I was first collecting all <code>.ignore</code>
files, then trying to decide which files are ignored. Ripgrep’s <code>from</code>
parameter led me to believe this.</p>
<p>However, it looks like it has a bug that doesn’t consider the source directory of
the ignore file. Git has different semantics when <code>a/.gitignore</code> and
<code>a/b/.gitignore</code> have a line <code>mydir/</code>. In the second case, Git
doesn’t ignore <code>a/mydir/</code>, but Ripgrep seems to consider all elements from all
files as if they are from the root directory.</p>
<p>In practice, most people may be using ignore files in their root directory, but
there are valid use cases (like moving around directories with ignore patterns
in them) that are affected by such behavior.</p>
<p>I decided to tackle this. Ripgrep’s <a href="https://crates.io/crates/globset">globset</a> seems nice and fast, so
it was better to base the solution on top of that. My basic use case is walking
a directory hierarchy. There are solutions to this like <a href="https://docs.rs/walkdir/latest/walkdir/">walkdir</a> or <a href="https://docs.rs/jwalk/latest/jwalk/">jwalk</a>,
but they use ripgrep’s gitignore; hence, they have the same bug.</p>
<p>I usually begin by setting up data structures to understand the problem. This
allows me to understand the domain formally.</p>
<p>First, what are we trying to accomplish? Basically, we want to decide if a path
should be ignored (a) in the results and (b) in the directory traversal.
The gitignore specification not only allows <em>ignores</em>, but also has a way to add
paths to a whitelist to <em>unignore</em>. Hence, deciding whether to ignore a path is
like:</p>
<pre><code class="language-rust">let include = |path| {
    if whitelist_globs.match(path) {
       true
    } else if ignore_globs.match(path) {
       false
    } else {
       true }
}</code></pre>
<p>We also need to decide whether to traverse a directory:</p>
<pre><code class="language-rust">let traverse = |path| {
    if path.is_dir() &amp;&amp; !include(path) { false } else { true }
}</code></pre>
<p>We have the following possible <code>MatchResult</code>s between the ignore rules and a
path:</p>
<pre><code class="language-rust">pub enum MatchResult {
    NoMatch,
    Ignore,
    Whitelist,
}</code></pre>
<p>We need a comparison function to get a <code>MatchResult</code>. One side of the
comparison is <code>path</code>, and the other is a set of <code>GlobSet</code>s from the <a href="https://crates.io/crates/globset">globset</a>
crate. We’ll have two globsets: one for <code>ignores</code> and the other for
<code>whitelists</code>.</p>
<pre><code class="language-rust">pub struct IgnoreRules {
    root: PathBuf,

    patterns: Arc&lt;Vec&lt;Pattern&lt;Glob&gt;&gt;&gt;,

    whitelist_set: Arc&lt;GlobSet&gt;,
    ignore_set: Arc&lt;GlobSet&gt;,
}</code></pre>
<p>The data types for the sets are <code>Arc&lt;GlobSet&gt;</code>, as I planned to make the traversal
concurrent for each directory. However, as only one thread will be modifying
the rules (one <code>.gitignore</code> or <code>.ignore</code> file per directory), I felt no need to
wrap the sets with <code>Mutex</code>.</p>
<p>We also keep track of each pattern separately. <code>patterns</code> is a <code>Pattern</code> vector
that contains all globs individually. One reason is to rebuild the <code>GlobSet</code> as
we traverse directories: since we cannot add any more rules to the sets once they
have been built, we keep the patterns separately to add them to the sets of the
child directories.</p>
<p>Another reason is to keep track of the source, as this can be used for
debugging or reporting, similar to <a href="https://git-scm.com/docs/git-check-ignore"><code>git check-ignore</code></a>.</p>
<p><code>root</code> is the point where this traversal began.</p>
<p>Patterns are defined in a generic way to be instantiated with globs, strings,
regexes, or any other type.</p>
<pre><code class="language-rust">pub struct Pattern&lt;T&gt; {
    pattern: T,
    original: String,
    source: Source,
    effect: PatternEffect,
    relativity: PatternRelativity,
    path_kind: PathKind,
}</code></pre>
<p>The rationale for this generic approach is that the patterns need some kind of
transformation. They can be read from files as strings, then compiled into
globs or regexes.</p>
<pre class="mermaid">flowchart LR
     String--&gt;Ok(Glob)--&gt;Glob--&gt;Regex
</pre>

<p>The <code>source</code> field shows where this pattern was retrieved. It’s an enum with two
possible values for the time being:</p>
<pre><code class="language-rust">enum Source {
    File { path: PathBuf, line: usize },
    Global,
}</code></pre>
<p>The <a href="https://git-scm.com/docs/gitignore">gitignore specification</a> says:</p>
<blockquote>
<p>An optional prefix “!” which negates the pattern; any matching file excluded
by a previous pattern will become included again.</p>
</blockquote>
<p>We use the <code>effect</code> field to tag the pattern as either an <code>Ignore</code> or a
<code>Whitelist</code> pattern.</p>
<pre><code class="language-rust">pub enum PatternEffect {
    Ignore,
    Whitelist,
}</code></pre>
<p>The spec also makes a distinction between files and directories:</p>
<blockquote>
<p>If there is a separator at the end of the pattern then the pattern will only
match directories, otherwise the pattern can match both files and
directories.</p>
</blockquote>
<p>So we use <code>path_kind</code> to reflect this distinction:</p>
<pre><code class="language-rust">pub enum PathKind {
    Any,
    Directory,
}</code></pre>
<p>Some patterns can be relative to the directory they are found in:</p>
<blockquote>
<p>If there is a separator at the beginning or middle (or both) of the pattern,
then the pattern is relative to the directory level of the particular
.gitignore file itself. Otherwise the pattern may also match at any level
below the .gitignore level.</p>
</blockquote>
<p>And we show this with the <code>relativity</code> field:</p>
<pre><code class="language-rust">pub enum PatternRelativity {
    Anywhere,
    RelativeTo { directory: String },
}</code></pre>
<p>Note that we are converting from the specification to Rust types. In the future,
these enums can be extended to cover cases where these patterns are used in
other places, or we can merge some of them to reduce complexity. Currently, this
level of abstraction seems adequate.</p>
<p>To convert <code>Pattern&lt;String&gt;</code> to <code>Pattern&lt;Glob&gt;</code> (or <code>Pattern&lt;Result&lt;Glob, Error&gt;&gt;</code>), or any other type, we can use a <code>map</code> function similar to
<code>Option::map</code>:</p>
<pre><code class="language-rust">    fn map&lt;U, F&gt;(self, f: F) -&gt; Pattern&lt;U&gt;
    where
        F: FnOnce(T) -&gt; U,
    {
        let pat = Pattern::&lt;U&gt; {
            pattern: f(self.pattern),
            original: self.original,
            source: self.source,
            effect: self.effect,
            relativity: self.relativity,
            path_kind: self.path_kind,
        };
        pat
    }</code></pre>
<p><code>map</code> will make it easy to convert between various types of patterns.</p>
<p>Now, we change the perspective to a higher level. Let’s begin by writing a <em>file tree walker.</em></p>
<pre><code class="language-rust">pub fn walk_serial(
    given: IgnoreRules,
    dir: &amp;Path,
    walk_options: &amp;WalkOptions,
    sender: &amp;Sender&lt;WalkerResult&lt;PathMetadata&gt;&gt;,
) -&gt; WalkerResult&lt;()&gt; { todo!() }</code></pre>
<p>This function uses the given <code>IgnoreRules</code> and returns files and directories
via the channel. <code>PathMetadata</code> is just a bundle of <code>PathBuf</code> and its
<code>Metadata</code>.</p>
<pre><code class="language-rust">pub struct PathMetadata {
    path: PathBuf,
    metadata: Metadata,
}</code></pre>
<p><code>WalkOptions</code> will contain various options about this traversal. Currently, it only
has <code>ignore_filename</code> and <code>include_dirs</code>, which set whether only files are sent
through the channel, or if directories should be included too.</p>
<pre><code class="language-rust">pub struct WalkOptions&lt;'a&gt; {
    ignore_filename: Option&lt;&amp;'a str&gt;,
    include_dirs: bool,
}</code></pre>
<p>Initially, <code>IgnoreRules</code> contains only the files that should be ignored (or
whitelisted) globally. When a directory contains a <code>.gitignore</code> file (or any
file name that was set in <code>WalkOptions</code>), it checks for new ignore rules and sends
all files through the channel. If a directory is not ignored, it then calls
<code>walk_serial</code> recursively with it.</p>
<p>The final lines of the function are as follows:</p>
<pre><code class="language-rust">    for child_dir in child_dirs {
        walk_serial(dir_with_ignores.clone(), &amp;child_dir, walk_options, sender)?;
    }</code></pre>
<p>As it calls itself as a final operation after collecting and sending all child
files, this is an example of <em>tail recursion.</em></p>
<p>The output is sent through a channel because our final goal is to make all
this concurrent. Each new directory will create a new thread, and all files
will be collected from the channel. At this point, we are creating a serial
version of the walker with identical data structures to test it easily.</p>
<p>One gotcha using the channels in the serial version is that the channel size must
be enough to contain all file names. Effectively, this means it will be
<code>crossbeam_channel::unbounded</code> for <code>walk_serial</code>. We will (ab)use the channel’s
<code>Sender&lt;PathMetadata&gt;</code> as a <code>Vec&lt;PathMetadata&gt;</code> in <em>no parallel</em> mode. In
parallel settings, using <code>unbounded</code> channels is not recommended; if the
consumers of the channel don’t work as quickly, you may fill up the memory,
especially for longer-running processes.</p>
<p><code>walk_serial</code> will basically do four things:</p>
<ul>
<li>List all the elements in the given directory.</li>
<li>Check if there is a new ignore file and update the rules if necessary.</li>
<li>Filter the elements by ignore rules and send them to the channel.</li>
<li>Run <code>walk_serial</code> recursively for child directories if they are not ignored.</li>
</ul>
<h3 id="read-the-directory">Read the directory</h3>
<p>The first is done using <code>read_dir</code> of <code>Path</code>. It returns a set of elements.
Then the list is processed, and elements’ metadata is retrieved. This requires a
bit of error handling: <code>read_dir</code> may return an <code>Error</code>, each element in the
result can be an <code>Error</code>, and getting <code>metadata</code> may also result in an <code>Error</code>.</p>
<pre><code class="language-rust">    let elements = dir
        .read_dir()
        .map_err(|e| anyhow!("Error reading directory: {:?}, {:?}", dir, e))?;
    let mut child_dirs = Vec::&lt;PathBuf&gt;::new();
    let mut child_paths = Vec::&lt;PathMetadata&gt;::new();

    for entry in elements {
        match entry {
            Err(err) =&gt; sender.send(Err(WalkerError::from(anyhow!(
                "Error reading entry in dir {:?} {:?}",
                dir,
                err
            ))))?,
            Ok(entry) =&gt; match entry.metadata() {
                Err(err) =&gt; sender.send(Err(WalkerError::from(anyhow!(
                    "Error getting metadata {:?} {}",
                    entry,
                    err
                ))))?,
                Ok(md) =&gt; {
                    child_paths.push(PathMetadata {
                        path: entry.path(),
                        metadata: md.clone(),
                    });
                    if md.is_dir() {
                        child_dirs.push(entry.path());
                    }
                }
            },
        }
    }</code></pre>
<p>Note that we collect <code>PathMetadata</code> objects in the <code>child_paths</code> vector. If
there happens to be an error while retrieving, it’s sent to the caller to ignore or
to report to the user.</p>
<p>I usually handle errors first in <code>match</code> expressions because they are usually
more straightforward.</p>
<h3 id="check-if-there-are-new-ignore-rules">Check if there are new ignore rules</h3>
<p>After reading all the files in the directory, it checks if there actually is an
ignore filename in <code>given</code> and that a file exists with that name in the list we
just created. If there is no such file, we can use <code>given</code> rules to decide the
ignored elements.</p>
<p>If there is a file that may change the ignore rules, it reads it and updates
<code>patterns</code>. Then, if there are <em>new</em> ignore or whitelist rules, the respective
globsets are recreated. It checks whether we have new rules because compiling
globs is possibly an expensive operation.</p>
<h3 id="filtering-directory-elements">Filtering directory elements</h3>
<p>Filtering files and directories from the list checks two globsets. If an
element matches a whitelist rule, it’s included in the results without checking
if it <em>also</em> matches ignore rules. Hence, whitelisting has a priority. Otherwise,
it checks the ignore rules, and if they don’t match, the element is included in
the results.</p>
<p>If there are directories matching the ignore rules, they are not traversed, and
any possible <em>whitelisting</em> rule that was specified <em>within</em> that directory is
not taken into consideration.</p>
<h3 id="listing-child-directories">Listing child directories</h3>
<p>We saw how <code>walk_serial</code> calls itself for child directories above. After
sending all results to the channel, it calls itself for the child directories
and repeats.</p>
<h2 id="converting-patterns-from-string-to-glob">Converting patterns from String to Glob</h2>
<p>The <em>interesting</em> part about ignoring files is that we should modify patterns
into globs to add to a <code>GlobSet</code>. We cannot add the patterns directly to a <code>GlobSet</code>
because some of the rules make them <em>context dependent.</em> We parsed the patterns
to get <code>PatternRelativity</code> and <code>PathKind</code>, and these also affect how we add the
patterns to a <code>GlobSet</code>.</p>
<p>The heart of the conversion is a function that receives a <code>String</code>, filters it,
and transforms it to a list of <code>Pattern&lt;Glob&gt;</code>s.</p>
<pre><code class="language-rust">fn content_to_patterns(
    ignore_root: &amp;Path,
    source: Option&lt;&amp;Path&gt;,
    contents: &amp;str,
) -&gt; Vec&lt;Pattern&lt;WalkerResult&lt;Glob&gt;&gt;&gt; {</code></pre>
<p>As we also need to report the line number for patterns, we use <code>enumerate</code> on
the iterator:</p>
<pre><code class="language-rust">    let patterns: Vec&lt;Pattern&lt;WalkerResult&lt;Glob&gt;&gt;&gt; = content
        .lines()
        .enumerate()
        // A line starting with # serves as a comment. Put a backslash ("\") in front of the first hash for patterns that begin with a hash.
        .filter(|(_, line)| !(line.trim().is_empty() || line.starts_with("#")))</code></pre>
<p>The <code>filter</code> line is used to check for blank and comment lines. Then we trim the
trailing space unless the pattern ends with <code>\</code>.</p>
<pre><code class="language-rust">        .map(|(i, line)| {
            if !line.ends_with("\\ ") {
                (i, line.trim_end())
            } else {
                (i, line)
            }
        })</code></pre>
<p>The function receives the source file name as a parameter. We create the
<code>Source</code> using it and the line number:</p>
<pre><code class="language-rust">        .map(|(i, line)| {
            (
                line,
                match source {
                    Some(p) =&gt; Source::File {
                        path: p
                            .strip_prefix(ignore_root)
                            .expect("path must be within ignore_root")
                            .to_path_buf(),
                        line: (i + 1).into(),
                    },
                    None =&gt; Source::Global,
                },
            )
        })</code></pre>
<p>Then we build the pattern object, update the pattern to add it before the globset,
and build the globs. Building the glob may cause errors if it’s not well-formed.
They are handled as well.</p>
<pre><code class="language-rust">        .map(|(line, source)| build_pattern(source, line))
        .map(transform_pattern_for_glob)
        .map(|pc| pc.map(|s| Glob::new(&amp;s).map_err(WalkerError::from)))</code></pre>
<p>Two functions are left that may need attention. One is <code>build_pattern</code> that
converts a pattern line to a <code>Pattern&lt;String&gt;</code> object. The other is the
<code>transform_pattern_for_glob</code> function that modifies <code>Pattern&lt;String&gt;</code> before
building the glob.</p>
<p>After writing <code>walk_serial</code>, we’ll use all the machinery to write a parallel
version of it.</p>
<h3 id="parsing-pattern-strings-for-pattern-objects">Parsing pattern strings for pattern objects</h3>
<p>We create pattern objects from pattern strings following the <a href="https://git-scm.com/docs/gitignore">spec</a>. There are
three basic rules that we will follow:</p>
<ul>
<li>If the pattern starts with <code>!</code>, it’s a whitelist rule.</li>
<li>If the pattern ends with <code>/</code>, it matches only to directories.</li>
<li>If the pattern contains a non-final <code>/</code>, it’s relative to the directory.</li>
</ul>
<p>We’ll also check whether the line starts with <code>/</code> and remove that to prevent
double-slashes <code>//</code> in patterns. The resulting pattern in the object won’t
contain any of the artifacts that change its semantics, and we’ll add them when
building the globsets.</p>
<p>Let’s start by checking whether the line starts with <code>!</code>.</p>
<pre><code class="language-rust">    let begin_exclamation = original.starts_with("!");
    let line = if begin_exclamation || original.starts_with("\\!") {
        original[1..].to_owned()
    } else {
        original.to_owned()
    };</code></pre>
<p>Checking the presence of <code>/</code> at the beginning, at the end, and in intermediate
positions is also straightforward:</p>
<pre><code class="language-rust">    let end_slash = line.ends_with("/");
    let line = if end_slash {
        &amp;line[..line.len() - 1]
    } else {
        line
    };

    let begin_slash = line.starts_with("/");
    let non_final_slash = if line.len() &gt; 0 {
        line[..line.len() - 1].chars().find(|c| *c == '/').is_some()
    } else {
        false
    };</code></pre>
<p>We check the non-final slash separately from the beginning slash because we’ll
remove the initial slash if it exists.</p>
<pre><code class="language-rust">    let line = if begin_slash { &amp;line[1..] } else { line };</code></pre>
<p>Although there is almost 1-1 correspondence between the boolean conditions and
the enums, I create enums in separate statements for <em>self-documenting</em> code.</p>
<pre><code class="language-rust">    let effect = if begin_exclamation {
        PatternEffect::Whitelist
    } else {
        PatternEffect::Ignore
    };

    let path_kind = if end_slash {
        PathKind::Directory
    } else {
        PathKind::Any
    };

    let relativity = if non_final_slash {
        PatternRelativity::RelativeTo {
            directory: current_dir.to_owned(),
        }
    } else {
        PatternRelativity::Anywhere
    };</code></pre>
<p>After building these attributes of the pattern and stripping the pattern from the
initial and final <code>/</code>, and other semantic-changing elements, we build a pattern:</p>
<pre><code class="language-rust">
    let pattern = Pattern::&lt;String&gt; {
        pattern: line.to_owned(),
        original: original.to_owned(),
        source,
        effect,
        relativity,
        path_kind,
    };
</code></pre>
<h2 id="transforming-a-pattern-string-to-a-suitable-glob">Transforming a pattern string to a suitable glob</h2>
<p>Now, another transformation is needed to convert <code>Pattern&lt;String&gt;</code> to
<code>Pattern&lt;Glob&gt;</code>. In this case, we’ll need to recreate the pattern based on the
rules.</p>
<p>The transformation considers <code>relativity</code> and <code>path_kind</code> to generate a glob
that can be added to a <code>GlobSet</code>. Note that the function returns not a
compiled <code>Pattern&lt;Glob&gt;</code>, but a <code>Pattern&lt;String&gt;</code>, because after this
transformation, building the glob requires <code>Glob::new</code>.</p>
<pre><code class="language-rust">fn transform_pattern_for_glob(pattern: Pattern&lt;String&gt;) -&gt; Pattern&lt;String&gt; {
    let anything_anywhere = |p| format!("**/{p}");
    let anything_relative = |p, directory| format!("{directory}/**/{p}");
    let directory_anywhere = |p| format!("**{p}/");
    let directory_relative = |p, directory| format!("{directory}/**/{p}/");

    let transformed_pattern = match (&amp;pattern.path_kind, &amp;pattern.relativity) {
        (PathKind::Any, PatternRelativity::Anywhere) =&gt; anything_anywhere(pattern.pattern),
        (PathKind::Any, PatternRelativity::RelativeTo { directory }) =&gt; {
            anything_relative(pattern.pattern, directory)
        }
        (PathKind::Directory, PatternRelativity::Anywhere) =&gt; directory_anywhere(pattern.pattern),
        (PathKind::Directory, PatternRelativity::RelativeTo { directory }) =&gt; {
            directory_relative(pattern.pattern, directory)
        }
    };

    Pattern {
        pattern: transformed_pattern,
        ..pattern
    }
}</code></pre>
<p>I could merge the closures into the <code>match</code> and lower the number of lines, but adding
the closures increased <em>self-documentation</em>. Instead of multiple <code>if/else</code>
branches, I tend to use <code>match</code> with tuples to have a clearer picture. The
<em>corresponding</em> <code>if/else</code> code without closures would be like:</p>
<pre><code class="language-rust">
let transformed_pattern = if pattern.path_kind == PathKind::Any {
    if pattern.relativity == PatternRelativity::Anywhere {
        format!("**/{p}")
        } else if ...
        }
...</code></pre>
<p>And in my opinion, this latter format is more error-prone and less readable.</p>
<h2 id="combining-it-all-together">Combining it all together</h2>
<p>At this point, we can revisit <code>content_to_patterns</code> again:</p>
<pre><code class="language-rust linenos hl_lines=[&quot;13-14&quot;]">fn content_to_patterns(
    ignore_root: &amp;Path,
    source: Option&lt;&amp;Path&gt;,
    content: &amp;str,
) -&gt; Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt; {
    watch!(source);
    let patterns: Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt; = content
        .lines()
        .enumerate()
        ...
        .map(|(line, source)| build_pattern(source, line))
        .map(transform_pattern_for_glob)
        .map(|pc| pc.map(|s|
            Glob::new(&amp;s).map_err(Error::from)))
        .collect();

    patterns
}
</code></pre>
<p>The last step in the transformation is creating the <code>Glob</code> object, as in the
highlighted lines above. It runs <code>Pattern&lt;String&gt;.map</code> with <code>Glob::new</code> to get
a <code>Pattern&lt;Result&lt;Glob&gt;&gt;</code>. <code>Error</code> and <code>Result</code> are custom types defined within
the crate using the <a href="https://crates.io/crates/thiserror/latest/thiserror/"><code>thiserror</code></a> crate.</p>
<p>After we have a vector of <code>Pattern&lt;Result&lt;Glob&gt;&gt;</code>, we report the errors in
<code>clear_glob_errors</code> to get <code>Vec&lt;Pattern&lt;Glob&gt;&gt;</code> from this vector. The function is defined as:</p>
<pre><code class="language-rust linenos">fn clear_glob_errors(
    sender: &amp;Sender&lt;Result&lt;PathMetadata&gt;&gt;,
    new_patterns: Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt;,
) -&gt; Vec&lt;Pattern&lt;Glob&gt;&gt; {
    let new_glob_patterns: Vec&lt;Pattern&lt;Glob&gt;&gt; = new_patterns
        .into_iter()
        .filter_map(|p| match p.transpose() {
            Ok(p) =&gt; Some(p),
            Err(e) =&gt; {
                sender
                    .send(Err(Error::from(anyhow!("Error in glob pattern: {:?}", e))))
                    .expect("Error in channel");
                None
            }
        })
        .collect();
    new_glob_patterns
}</code></pre>
<p>The <code>sender</code> is the same channel that we use to send the results. This
communication channel is used to report <em>only the errors</em>, not <code>PathMetadata</code>;
hence, when we encounter an <code>Err(e)</code> in <code>new_patterns</code>, it’s reported to the same
channel that the results are expected. Handling the errors first seems like a
common theme here as well.</p>
<p>This is also one of the reasons <code>walk_serial</code> receives a channel instead of a
vector. Our usual use case is traversing all the directories in parallel, and
without channel-based communication in the serial version, we would need to
duplicate this function.</p>
<p>At this point, I completed the <code>walk_serial</code> functionality. I wrote tests. I’m using the
<a href="https://crates.io/crates/test-case"><code>test-case</code></a> crate for writing many tests encompassing different use cases.
I’m omitting them here for brevity.</p>
<h2 id="writing-a-parallel-walker">Writing a Parallel Walker</h2>
<p>Converting <code>walk_serial</code> to a parallel one is quite trivial. As we already used
channels in the serial version, we have all the components of a parallel
walker. For the sake of simplicity, I won’t use a thread pool and instead spawn a new
thread for each <em>task.</em> One question is what’s the unit task that we’ll use to
create tasks.</p>
<p><a href="https://docs.rs/jwalk/latest/jwalk/">jwalk</a> uses directories as points to spawn new tasks. That’s a sensible
approach, and I’ll use this as well. This means a single directory will be
processed by a single thread, even if it contains one million files, and 10
directories will be processed by 10 threads even if each contains one file. In
practice, I think we can assume <em>directories</em> are natural chunks for this
problem.</p>
<p>The interface for the parallel walker is identical to the serial version. This is
to replace one with the other when the need arises.<sup class="footnote-reference" id="fr-1-1"><a href="#footnote-1">1</a></sup></p>
<p>The only difference is in the final loop. <code>walk_serial</code> calls itself
recursively at the end, while <code>walk_parallel</code> calls itself in <em>separate threads</em>.</p>
<pre><code class="language-rust">
pub fn walk_parallel(
    given: IgnoreRules,
    dir: &amp;Path,
    walk_options: WalkOptions,
    sender: Sender&lt;Result&lt;PathMetadata&gt;&gt;,
) -&gt; Result&lt;()&gt; {
    ....
    crossbeam::scope(|s| {
        for child_dir in child_dirs {
            let dwi = dir_with_ignores.clone();
            let walk_options = walk_options.clone();
            let sender = sender.clone();
            s.spawn(move |_| walk_parallel(dwi, &amp;child_dir.path, walk_options, sender));
        }
    })
    .expect("Error in crossbeam scope in walk_parallel");

    Ok(())
}

</code></pre>
<h2 id="code">Code</h2>
<p>The code is soon to be released, and I’ll add a link to it. You can check <a href="https://github.com/iesahin">my
GitHub profile</a> if I forget.</p>
<hr>
<ol class="footnote-definition">
<li id="footnote-1">
<p>Update: I decided to change the <code>walk_serial</code> interface to use <code>Vec</code>
instead of <code>Sender</code> for simplicity. <a href="#fr-1-1">↩</a></p>
</li>
</ol>]]></content:encoded>
    </item>
  </channel>
</rss>
