Crate sort_by_derive

source ·
Expand description

GitHub Crates.io docs.rs Continuous integration

sort_by_derive

This crate provides 3 derive macros SortBy, EnumAccessor and EnumSequence.

  • SortBy derives the traits Ord, PartialOrd, Eq, PartialEq and Hash on structs that can’t automatically derive those traits because they contain unorderable fields such as f32.
  • On enums and structs, SortBy can also implement a Ord trait that calls arbitrary methods - this is particularly useful in combination with enum variant accessor methods derived by EnumAccessor an EnumSequence
  • EnumAccessor derives accessor methods to common fields in variants - so you don’t need to write yourself match statements to access a field with the same name and type on different variants. This feature is similar to enum_dispatch, but takes a different approach where structs don’t need to implement a trait.
  • EnumSequence provides a enum_sequence method where the first variant returns 0, the second 1, etc. This is useful is you want to implement a custom sorting, while the order of declaration of variant is still relevant as a secondary ordering criteria.

Usage

SortBy

Fields that should be used for sorting are marked with the attribute #[sort_by]. Other fields will be ignored.

use std::cmp::Ordering;
use sort_by_derive::SortBy;

#[derive(SortBy)]
struct Something {
    #[sort_by]
    a: u16,
    b: u16
}

assert_eq!(Something{a: 2, b: 0}.cmp(&Something{a: 1, b: 1}), Ordering::Greater); // a is compared
assert_eq!(Something{a: 1, b: 0}.cmp(&Something{a: 1, b: 1}), Ordering::Equal); // b is ignored

EnumAccessor

This derive macro is similar to enum_dispatch. enum_dispatch requires structs to implement a common trait, which can be useful if a common set of functions applies to all variants. EnumAccessor takes the opposite approach: common fields and methods are declared at enum level, and you can have variants that don’t have a given field or method. This may be more practical if there is a large amount of variants and your only concern is accessing fields, because individual structs just hold data. This is typical for events - they represent a state change and are generally consumed as a whole, individual structs have no code of their own.

Field accessor

After adding derive(EnumAccessor) to the enum, fields are declared as accessor(field: type) attributes:

This will derive the accessor methods fn name(&self) -> &type; andfn name_mut(&mut self) -> &mut type;, and return a reference to the field of the same name on any variant.

use sort_by_derive::EnumAccessor;

#[derive(EnumAccessor)]
#[accessor(a: u16)]
#[accessor(b: u16)]
enum E {
    Variant1{a: u16, b: u16},
    Variant2{a: u16, b: u16, c: u32},
}

let v1 = E::Variant1{a: 1, b: 1};
let mut v2 = E::Variant2{a: 1, b: 1, c: 2};

// Accessor methods are generated for the specified members
assert_eq!(*v1.a(), 1);
assert_eq!(*v2.b(), 1);

// Mutable accessors are also generated
*v2.a_mut() = 2;
assert_eq!(*v2.a(), 2);

So you can take any E, all variants will have a, a_mut, b, b_mut

EnumSequence

Simply derive EnumSequence, and you get enum_sequence(&self) which returns a usize, starting from 0 and incrementing for each variant.

When using enums of enums, creating an accessor to the inner enum’s sequence may create a method name ambiguity. To mitigate this, a custom accessor name can be chosen by using as, for instance #[accessor(enum_sequence() as inner_sequence: usize)]

Note: this will create an extension trait {TypeName}EnumSequence ( i.e. the type T will get a new trait TEnumSequence ). This trait will have the same visibility as the type. When using this type from another module, make sure to bring the trait in scope with use {TypeName}EnumSequence.

Example
use sort_by_derive::EnumSequence;

#[derive(EnumSequence)]
enum ABC {
    A(u8),
    B(String),
    C { f: String, g: usize }
}

assert_eq!(ABC::B("hi!".into()).enum_sequence(), 1);

All together

Imagine the following :

#[derive(EnumSequence, EnumAccessor, SortBy, Debug)]
#[accessor(global_time: usize)]
#[accessor(channel: u8, except(CC))]
#[accessor(pitch: u8, except(CC, Unsupported))]
#[sort_by(global_time(), channel(), pitch(), enum_sequence())]
enum Note {
// ...
}

assert_eq!(
    Note::NoteOn {
        global_time: 0,
        pitch: 0,
        channel: 0,
    }.cmp(&Note::NoteOn {
        global_time: 0,
        pitch: 0,
        channel: 0,
    }),
    Ordering::Equal
);
assert_eq!(
    Note::NoteOn {
        global_time: 0,
        pitch: 2,
        channel: 2,
    }.cmp(&Note::NoteOff {
        global_time: 2,
        pitch: 0,
        channel: 0,
    }),
    Ordering::Less
);
assert_eq!(
    Note::NoteOn {
        global_time: 0,
        pitch: 2,
        channel: 0,
    }.cmp(&Note::NoteOff {
        global_time: 0,
        pitch: 0,
        channel: 2,
    }),
    Ordering::Less
);
assert_eq!(
    Note::NoteOn {
        global_time: 0,
        pitch: 0,
        channel: 0,
    }.cmp(&Note::NoteOff {
        global_time: 0,
        pitch: 0,
        channel: 2,
    }),
    Ordering::Less
);
assert_eq!(
    Note::NoteOn {
        global_time: 0,
        pitch: 0,
        channel: 0,
    }.cmp(&Note::NoteOff {
        global_time: 0,
        pitch: 0,
        channel: 0,
    }),
    Ordering::Less
);

Now I have a Note enum that will sort by global_time, channel, pitch, and lastly by variant order ( enum_sequence ). Note that None is always less than Some.

Conversely, separate structs such as NoteOn may derive from SortBy in order to ignore some fields ( ex: velocity may be a f32, so we can’t directly derive Ord ).

Derive Macros

  • This derive macro is similar to enum_dispatch. enum_dispatch requires structs to implement a common trait, which can be useful if a common set of functions applies to all variants. EnumAccessor takes the opposite approach: common fields and methods are declared at enum level, and you can have variants that don’t have a given field or method. This may be more practical if there is a large amount of variants and your only concern is accessing fields, because individual structs just hold data. This is typical for events - they represent a state change and are generally consumed as a whole, individual structs have no code of their own.
  • Simply derive EnumSequence, and you get enum_sequence(&self) which returns a usize, starting from 0 and incrementing for each variant.
  • Fields that should be used for sorting are marked with the attribute #[sort_by]. Other fields will be ignored.