22 个版本 (12 个重大更新)

0.13.0 2019 年 9 月 1 日
0.12.0 2019 年 6 月 9 日
0.11.3 2019 年 4 月 25 日
0.11.1 2019 年 1 月 24 日
0.3.0 2017 年 11 月 26 日

#93缓存

每月 39 次下载
用于 5 个库 (3 个直接)

BSD-3-Clause

83KB
683

暖意,热重载可加载和可重载的资源

Build Status dependency status crates.io docs.rs License

热重载,可加载和可重载的资源。

前言

资源是存在于存储中的对象,可以热重载——也就是说,它们可以在不与您交互的情况下更改。目前支持两种类型的资源

  • 文件系统资源,这些资源位于文件系统中,具有真实表示(即简称为 文件)。
  • 逻辑资源,这些资源是经过计算的,不直接需要任何 I/O。

资源通过 来引用。一个 是一个类型索引,它包含足够的信息来唯一标识存储中的资源。

这篇简短介绍将为您提供足够的信息和示例,让您开始使用 warmy。如果您想了解更多,请随意访问子模块的文档。

功能门

以下是可用的功能门的详尽列表

  • "arc":更改资源的内部表示以使用 ArcMutex,允许跨线程共享资源。这是一个正在等待更好的异步解决方案的补丁。
  • "json":提供了一个 Json 类型,您可以使用它作为加载方法来自动加载任何实现 serde::Deserialize 的类型,并将其编码为 JSON。您甚至不需要自己实现 Load默认启用
  • "ron-impl":提供了一种 Ron 类型,您可以将其用作加载方法来自动加载任何实现了 serde::Deserialize 的类型,并将其编码为 RON
  • "toml-impl":提供了一种 Toml 类型,您可以将其用作加载方法来自动加载任何实现了 serde::Deserialize 的类型,并将其编码为 TOML

资源加载

加载 是从给定位置获取对象的行为。该位置通常是您的文件系统,但它也可以是内存区域——映射文件或内存解析。在 warmy 中,加载是按 类型 实现的:这意味着您必须在类型上实现一个特质,以便该类型的任何对象都可以被加载。要实现的特质是 Load。我们感兴趣的有四个项目

  • Store,它负责保存和缓存资源。
  • Key 类型变量,用于告诉 warmy 您的存储知道如何表示哪种类型的资源以及键必须包含什么信息。
  • Load::Error 关联类型,即加载失败时使用的错误类型。
  • Load::load 方法,该方法用于在给定的存储中加载您的资源。

存储

一个 Store 负责保存和缓存资源。每个 Store 都与一个 根目录 相关联,该根目录是文件系统上的一个路径,所有文件系统资源都将从此路径获取。您可以通过提供一个 StoreOpt 来创建一个 Store,该 StoreOpt 用于自定义 Store —— 如果您现在不需要它也不关心它,只需使用 Store::default

use warmy::{SimpleKey, Store, StoreOpt};

let res = Store::<(), SimpleKey>::new(StoreOpt::default());

match res {
  Err(e) => {
    eprintln!("unable to create the store: {}", e);
  }

  Ok(store) => ()
}

如您所见,Store 有两个类型变量。这些类型变量指的是您想要与资源一起使用的 上下文 类型以及键的类型。现在我们将使用 () 作为上下文,因为我们不想使用上下文——但还有更多内容——并且使用通用的 SimpleKey 类型作为键。继续阅读。

Key 类型变量

关键类型必须实现 Key,这是 warmy 识别为键的类型类。从理论上讲,你不必担心这个特例,因为 warmy 已经提供了一些键类型。

如果你真的想实现 Key,请查看其文档以获取更多详细信息。

键是 warmy 的一个核心概念,因为它们是唯一表示资源(无论是在文件系统上还是在内存中)的对象。你将使用这些键来引用你的资源。

特殊情况:简单键

一个 简单键(也称为 SimpleKey)是一个用于表达常见情况的键,在这种情况下,你可能从文件系统和逻辑位置获取资源。它是为了方便提供的,这样你就不必编写该类型并实现 Key。在大多数情况下,它应该足够你使用——当然,如果你需要更多细节,你可以自由地定义你自己的键类型。

关联类型 Load::Error

此关联类型必须设置为你的加载实现可能生成的错误类型。例如,如果你使用 serde-json 加载某些内容,你可能想将其设置为 `serde_json::Error`。这种做法在 Rust 中非常常见;你不应该对此感到不舒服。

一般来说,你应该始终尽量使用精确且准确的错误类型。避免使用简单的类型,如 Stringu64,并优先使用详细的多项式数据类型。

Load::load 方法

这是入口点方法。必须实现 Load::load,这样 warmy 才知道如何读取资源。让我们为两种类型实现它:一种表示文件系统上的资源,另一种是从内存计算得出的。

use std::fmt;
use std::fs::File;
use std::io::{self, Read};
use warmy::{Load, Loaded, SimpleKey, Storage};

// Possible errors that might happen.
#[derive(Debug)]
enum Error {
  CannotLoadFromFS,
  CannotLoadFromLogical,
  IOError(io::Error)
}

impl fmt::Display for Error {
  fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
    match *self {
      Error::CannotLoadFromFS => f.write_str("cannot load from file system"),
      Error::CannotLoadFromLogical => f.write_str("cannot load from logical"),
      Error::IOError(ref e) => write!(f, "IO error: {}", e),
    }
  }
}

// The resource we want to take from a file.
struct FromFS(String);

// The resource we want to compute from memory.
struct FromMem(usize);

impl<C> Load<C, SimpleKey> for FromFS {
  type Error = Error;

  fn load(
    key: SimpleKey,
    storage: &mut Storage<C, SimpleKey>,
    _: &mut C
  ) -> Result<Loaded<Self, SimpleKey>, Self::Error> {
    // as we only accept filesystem here, we’ll ensure the key is a filesystem one
    match key {
      SimpleKey::Path(path) => {
        let mut fh = File::open(path).map_err(Error::IOError)?;
        let mut s = String::new();
        fh.read_to_string(&mut s);

        Ok(FromFS(s).into())
      }

      SimpleKey::Logical(_) => Err(Error::CannotLoadFromLogical)
    }
  }
}

impl<C> Load<C, SimpleKey> for FromMem {
  type Error = Error;

  fn load(
    key: SimpleKey,
    storage: &mut Storage<C, SimpleKey>,
    _: &mut C
  ) -> Result<Loaded<Self, SimpleKey>, Self::Error> {
    // ensure we only accept logical resources
    match key {
      SimpleKey::Logical(key) => {
        // this is a bit dummy, but why not?
        Ok(FromMem(key.len()).into())
      }

      SimpleKey::Path(_) => Err(Error::CannotLoadFromFS)
    }
  }
}

如你所见,这里有一些新概念

  • Loaded:你必须将你的对象包裹在这个类型中来表达依赖关系。因为它实现了 From<T> for Loaded<T>,你可以使用 .into() 来声明你没有任何依赖。
  • Storage:这是保存和缓存你的资源的最小结构。实际上,Store 是你将在客户端代码中处理的 接口结构

使用 Loaded 表达你的依赖

类型为 Loaded 的对象会向 warmy 提供关于您的依赖信息。在加载成功后——即您的资源成功 加载 ——您可以告诉 warmy 您加载的资源依赖于哪些资源。但这有点棘手,因为这里有一个重要的区别。

当您实现 Load::load 时,您会收到一个 Storage。您可以使用该 Storage 加载额外的资源并将它们收集到您的资源中。当这些额外的资源被重新加载时,如果您直接在对象中嵌入资源,您将自动看到自动化的资源——这正是这个crate的目的!然而,如果您没有对这些资源表达 依赖关系,您的旧资源将不会重新加载——它只会使用自动同步的资源,但不会重新加载自身。这有点棘手,但让我们举一个典型的例子,说明您可能想要使用依赖项和依赖图的情况。

  1. 您想要加载一个由几个值/资源聚合而成的对象。
  2. 您选择使用一个 逻辑资源 并猜测所有要加载的文件。
  3. 当您实现 Load::load 时,您会打开几个文件,将它们加载到内存中,组合它们,最后得到您的对象。
  4. 您从 Load::load 返回您的对象,没有依赖关系(即您使用 .into() 在它上面)。

这里将要发生的事情是,如果您的资源依赖的任何文件发生变化,因为存储中没有适当的资源,您的对象将看不到任何东西。一个典型的解决方案是将这些文件作为适当的资源加载,并将这些键放入返回的 Loaded 对象中,以表示您 依赖于这些键所引用的对象的重新加载。这有点棘手,但您最终会发现自己处于需要这种 Loaded 东西帮助您的情况。然后您将使用 Loaded::with_deps。有关更多信息,请参阅 Loaded 的文档。

有趣的事实:逻辑资源是为了解决该问题和依赖图而引入的。

让我们做一些事情吧!

当您实现了 Load,您就可以获取(缓存的)资源了。您有几种方法可以实现这个目标

  • Store::get,用于获取资源。这实际上会在第一次请求时加载它。如果请求过一次,它将使用缓存的版本。
  • 《get_proxied》是一个特殊的版本,类似于Store::get。如果初始加载(非缓存)失败(缺少资源,解析失败等),将使用代理 - 传递给Store::get_proxied。不过这个值是惰性的,所以如果加载成功,该值将不会被评估。

本教程将重点介绍Store::get

use std::fmt;
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use warmy::{Load, Loaded, SimpleKey, Store, StoreOpt, Storage};

// Possible errors that might happen.
#[derive(Debug)]
enum Error {
  CannotLoadFromFS,
  CannotLoadFromLogical,
  IOError(io::Error)
}

impl fmt::Display for Error {
  fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
    match *self {
      Error::CannotLoadFromFS => f.write_str("cannot load from file system"),
      Error::CannotLoadFromLogical => f.write_str("cannot load from logical"),
      Error::IOError(ref e) => write!(f, "IO error: {}", e),
    }
  }
}

// The resource we want to take from a file.
struct FromFS(String);

impl<C> Load<C, SimpleKey> for FromFS {
  type Error = Error;

  fn load(
    key: SimpleKey,
    storage: &mut Storage<C, SimpleKey>,
    _: &mut C
  ) -> Result<Loaded<Self, SimpleKey>, Self::Error> {
    // as we only accept filesystem here, we’ll ensure the key is a filesystem one
    match key {
      SimpleKey::Path(path) => {
        let mut fh = File::open(path).map_err(Error::IOError)?;
        let mut s = String::new();
        fh.read_to_string(&mut s);

        Ok(FromFS(s).into())
      }

      SimpleKey::Logical(_) => Err(Error::CannotLoadFromLogical)
    }
  }
}

fn main() {
  // we don’t need a context, so we’re using this mutable reference to unit
  let ctx = &mut ();
  let mut store: Store<(), SimpleKey> = Store::new(StoreOpt::default()).expect("store creation");

  let my_resource = store.get::<FromFS>(&Path::new("/foo/bar/zoo.json").into(), ctx);

  //

  // imagine that you’re in an event loop now and the resource has changed
  store.sync(ctx); // synchronize all resources (e.g. my_resource)
}

重新加载资源

warmy的大部分有趣概念是让您能够在不重新运行应用程序的情况下热重新加载资源。这通过两项操作实现:

Load::reload函数非常直接:当资源发生变化时调用。这种情况发生在:

  • 资源位于文件系统中(文件已更改)。
  • 或者它是已重新加载的依赖资源的依赖资源。

有关详细信息,请参阅Load::reload的文档。

上下文检查

上下文是一个特殊值,您可以在加载或重新加载时通过可变引用访问。如果您不需要,强烈建议在实现Load<C>时不要使用(),而将其保留为类型变量,以便更好地组合 - 即impl<C> Load<C>

如果您正在编写库并且需要访问上下文中的特定值,也建议不要直接将上下文类型变量设置为上下文类型。如果您这样做,没有人能够使用您的库,因为类型不会匹配 - 或者人们会接受仅限于您的类型。处理这种情况的典型方法是通过约束类型变量。为此目的引入了Inspect特质。例如

use std::fmt;
use std::io;
use warmy::{Inspect, Load, Loaded, SimpleKey, Store, StoreOpt, Storage};

// Possible errors that might happen.
#[derive(Debug)]
enum Error {
  CannotLoadFromFS,
  CannotLoadFromLogical,
  IOError(io::Error)
}

impl fmt::Display for Error {
  fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
    match *self {
      Error::CannotLoadFromFS => f.write_str("cannot load from file system"),
      Error::CannotLoadFromLogical => f.write_str("cannot load from logical"),
      Error::IOError(ref e) => write!(f, "IO error: {}", e),
    }
  }
}

struct Foo;

struct Ctx {
  nb_res_loaded: usize
}

impl<C> Load<C, SimpleKey> for Foo where Foo: for<'a> Inspect<'a, C, &'a mut Ctx> {
  type Error = Error;

  fn load(
    key: SimpleKey,
    storage: &mut Storage<C, SimpleKey>,
    ctx: &mut C
  ) -> Result<Loaded<Self, SimpleKey>, Self::Error> {
    Self::inspect(ctx).nb_res_loaded += 1; // magic happens here!

    Ok(Foo.into())
  }
}

fn main() {
  use warmy::{Res, Store, StoreOpt};

  let mut store: Store<Ctx, SimpleKey> = Store::new(StoreOpt::default()).unwrap();
  let mut ctx = Ctx { nb_res_loaded: 0 };

  let r: Res<Foo> = store.get(&"test-0".into(), &mut ctx).unwrap();
}

在这个例子中,因为我们想要的内容值与Store的上下文相同,所以一个通用的Inspect实现使您可以直接进行inspect。但是,如果您想更精确地检查它,例如使用&mut usize,您就需要为您的类型编写一个Inspect的实现。

use std::fmt;
use std::io;
use warmy::{Inspect, Load, Loaded, SimpleKey, Store, StoreOpt, Storage};

// Possible errors that might happen.
#[derive(Debug)]
enum Error {
  CannotLoadFromFS,
  CannotLoadFromLogical,
  IOError(io::Error)
}

impl fmt::Display for Error {
  fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
    match *self {
      Error::CannotLoadFromFS => f.write_str("cannot load from file system"),
      Error::CannotLoadFromLogical => f.write_str("cannot load from logical"),
      Error::IOError(ref e) => write!(f, "IO error: {}", e),
    }
  }
}

struct Foo;

struct Ctx {
  nb_res_loaded: usize
}

// this implementor states how the inspection should occur for Foo when the context has type
// Ctx: by targetting a mutable reference on a usize (i.e. the counter)
impl<'a> Inspect<'a, Ctx, &'a mut usize> for Foo {
  fn inspect(ctx: &mut Ctx) -> &mut usize {
    &mut ctx.nb_res_loaded
  }
}

// notice the usize instead of Ctx here
impl<C> Load<C, SimpleKey> for Foo where Foo: for<'a> Inspect<'a, C, &'a mut usize> {
  type Error = Error;

  fn load(
    key: SimpleKey,
    storage: &mut Storage<C, SimpleKey>,
    ctx: &mut C
  ) -> Result<Loaded<Self, SimpleKey>, Self::Error> {
    *Self::inspect(ctx) += 1; // direct access to the counter

    Ok(Foo.into())
  }
}

加载方法

warmy支持加载方法。这些方法用于指定加载给定类型对象的方式。默认情况下,Load是用默认方法实现的,即()。如果您需要更多方法,您可以在实现Load时将类型参数设置为其他值。

您还可以在这里找到一些集中的方法,但您绝对不必使用它们。

通用JSON支持

该包支持通用JSON实现。您可以通过Json类型使用它。

通用JSON支持通过"json"特性门进行。

通用JSON可以帮助您轻松地实现和进行实现。基本上,这意味着任何实现了serde::Deserialize的类型都可以通过warmy进行加载和热重载,无需您进行任何样板代码,只需让warmy获取给定的资源即可。这可以通过Store::get_byStore::get_proxied_by方法完成。

use serde::Deserialize;
use warmy::{Res, SimpleKey, Store, StoreOpt};
use warmy::json::Json;
use std::thread::sleep;
use std::time::Duration;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Dog {
  name: String,
  gender: Gender
}

impl Default for Dog {
  fn default() -> Self {
    Dog {
      name: "Norbert".to_owned(),
      gender: Gender::Male
    }
  }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
enum Gender {
  Female,
  Male
}

fn main() {
  let mut store: Store<(), SimpleKey> = Store::new(StoreOpt::default()).unwrap();
  let ctx = &mut ();

  let resource: Result<Res<Dog>, _> = store.get_by(&SimpleKey::from_path("/dog.json"), ctx, Json);

  match resource {
    Ok(dog) => {
      loop {
        store.sync(ctx);

        println!("Dog is {} and is a {:?}", dog.borrow().name, dog.borrow().gender);
        sleep(Duration::from_millis(1000));
      }
    }

    Err(e) => eprintln!("{}", e)
  }
}

通用TOML支持

该包还支持通用TOML实现。该实现可通过Toml类型访问。

通用TOML支持通过"toml-impl"特性门进行。

工作机制与通用JSON支持相同。

资源发现

资源发现通过一个简单的机制实现:每当文件系统上有新的资源可用时,就会调用您选择的闭包。这个闭包会传递您的StoreStorage以及其关联的上下文,使您能够动态地插入新的资源。

这与第一种选择略有不同:这使您能够用您尚未知道的资源填充存储——例如,一个纹理被保存在存储的根目录中,会自动添加并作出反应。

该功能可以通过在生成新的Store之前创建的StoreOpt对象来访问。有关如何使用资源发现机制的详细信息,请参阅StoreOpt::set_discoveryStoreOpt::discovery函数。

依赖项

~0.4–8.5MB
~63K SLoC