julienduroure / jsoncanvas

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Compare with another implementation

clbarnes opened this issue · comments

Hi! I, too, saw a cool new file format and jumped to implement it in a great language. What I did not do was check whether anyone else was doing the same!

As you've won the crate name and there's certainly no need for 2 crates for what is currently an extremely niche format, I'll just leave my implementation here in case it has any value for you: https://github.com/clbarnes/jsoncanvas

In particular, I quite like how I did my node types - where the enum is at the top level and contains structs which have their own type-specific data, so the spec is discoverable to the library user rather than the user having to guess which node types have which data. The shared data is in a member struct and accessed through a trait which is also implemented by the different node types and the containing enum, using ambassador to delegate the trait.

I also use hex_color and url crates to strongly type those fields, and rely more heavily on serde macros to cut down on complexity.

I do like your use of the hashmap so that nodes and edges are easily findable by their ID - I was considering using a BTreeMap so that they stay in a deterministic order, if you're giving up insertion order either way.

Hello,

I'll just leave my implementation here in case it has any value for you

Of course it has value!
I am quite new using rust (and more new using serde), so your implement is quite useful!

I also use hex_color and url crates

Yes, I will implement it

serde macros

I don't know so much about it, I will have a look. Seems you code is much less verbose than mine for ser/deser using these macro!

using a BTreeMap so that they stay in a deterministic order, if you're giving up insertion order either way.

Currently, seems the specification does not really need to keep order. Not sure if this is something that can be useful for people using this library. Let's keep it with HashMap for now, and keep it in mind for later if needed

I also like your check that nodes exists for edge. I will add this check at edge insertion.

ambassador crate

I will have a look on it, didn't know this crate. This change quite a lot how data are managed.

Thanks again!

Hello,
Seems I manage & make changes regarding your propositions.
Feel free to have a look and comment. If you agree, I will close this ticket

Sure! Here are a few things you might be interested in:

  • Your *Id and Pixel* types could be pub so that others can make use of the aliases (and possibly gain some forward-compatibility if you ever change what those types are
  • Rather than impl Into<Node> for GroupNode etc., implement impl From<GroupNode> for Node as that automatically implements Into as well - see the docs for Into
  • As the type of node is specified in the node struct, you probably want to use an internally tagged enum rather than untagged
  • Does there need to be a None-type node? It's not in the spec as the type field is required.
  • I used i64 for location (infinite canvases could feasibly get very big and the memory savings for i32 are negligible given it must be able to go in and out of JSON). I also used unsigned integers for the dimensions because there's no reason to allow them to be negative.
  • On the JsonCanvas type, rather than implementing methods called to_string and from_string, you could look into the Display and FromStr traits. I'm not sure Display is a great fit for serialisation but it's probably what people expect when they see a to_string method. If you wanted to go all-in on convenience methods, you could also implement impl<R: Read> TryFrom<R> for JsonCanvas but then you could get into the weeds of different async IO types and so on, so it might be better for users to just adapt it themselves.
  • It's common to have e.g. get_nodes(&self) -> &HashMap<_> and get_nodes_mut(&mut self) -> &mut HashMap<_> just to play nice with the borrow checker as we're not sure yet how this library will be used!

A completely personal opinion: I am not a big fan of skip_serializing_if = "Option::is_none" (I think it interferes with clarity). But brevity might be preferred. If that's the case, you could also do skip_serializing_if = "HashMap::is_empty" on the JsonCanvas as technically nodes and edges are both optional fields and {} is a valid JsonCanvas (which is why I don't particularly like that pattern 😅 ).

Your Id and Pixel types could be pub so that others can make use of the aliases (and possibly gain some forward-compatibility if you ever change what those types are

Done

Rather than impl Into for GroupNode etc., implement impl From for Node as that automatically implements Into as well - see the docs for Into

Done. I read in documentation that they recommend to implement From instead of Into, but I am not sure to understand why one is better than the other

I used i64 for location (infinite canvases could feasibly get very big and the memory savings for i32 are negligible given it must be able to go in and out of JSON). I also used unsigned integers for the dimensions because there's no reason to allow them to be negative.

Done

As the type of node is specified in the node struct, you probably want to use an internally tagged enum rather than untagged

Not sure to understand your comment. I use tag here for type:

#[serde(tag = "type", rename_all = "camelCase")]

Does there need to be a None-type node? It's not in the spec as the type field is required.

I think I remove it already, when I switched to GenericNode system. Did I miss something?
https://github.com/julienduroure/jsoncanvas/blob/78b58b98aff63fc319ed73179b567932717b96b3/src/node.rs#L81C1-L81C16

It's common to have e.g. get_nodes(&self) -> &HashMap<> and get_nodes_mut(&mut self) -> &mut HashMap<> just to play nice with the borrow checker as we're not sure yet how this library will be used!

Done

On the JsonCanvas type, rather than implementing methods called to_string and from_string, you could look into the Display and FromStr traits. I'm not sure Display is a great fit for serialisation but it's probably what people expect when they see a to_string method. If you wanted to go all-in on convenience methods, you could also implement impl<R: Read> TryFrom for JsonCanvas but then you could get into the weeds of different async IO types and so on, so it might be better for users to just adapt it themselves.

Interresting. Let's keep it for another day, and enjoy the sunny sunday!

Thanks again :)

Not sure to understand your comment. I use tag here for type:

I think I remove it already, when I switched to GenericNode system.

My bad, I think I was looking at an old commit for both.

I read in documentation that they recommend to implement From instead of Into, but I am not sure to understand why one is better than the other

If you implement From manually, rust implements Into for you so you write one method and get both transformations. If you implement Into manually, rust does not implement From, so you write one method and only get one transformation. Specifically, this is because there is a generic implementation in the standard library which looks something like

impl<A, B> Into<A> for B where A: From<B> {
    fn into(self) -> A {
        A::from(self)
    }
}

I have a few question your you:

  • Display trait : seems to be ok
  • FromStr : Seems I need "use std::str::FromStr;" on each file where I want to use from_str method. Is there a way to avoid it?

Rather than using from_str directly you can use let my_struct: MyStruct = my_str.parse().unwrap();.

Ok, great!
Thanks :)

This time, I think everything is in a good state!
Just published 0.1.4

One more question: I tried to split the node file into multiple files (one by type), but seems I failed, because of the Ambassador crate, some delegation macro seems to not been seen correctly from a file to another. Any ideas?

Tricky! The compiler and rust-analyzer don't give helpful messages for this one. It's this issue: hobofan/ambassador#45 (comment) ; particularly weird because the pub use statement needs to come after the definition, which is pretty rare in rust. PR incoming!

I see. I tried to put the pub use before the definition, as usual, but without any success :)

Seems we can close this ticket, as we addressed all points