jonathanstowe / JSON-Class

A Role to allow Raku objects to be constructed and serialised from/to JSON.
Artistic License 2.0
6 stars 6 forks source link

External opt-in #9

Open FCO opened 2 years ago

FCO commented 2 years ago

I was wondering, if I have a class that I want to use in 2 different places, but one is a external route and the other is an internal one. Then probably on both places I would want to "export" different attributes. Would have a way to do something like:

class Bla does JSON::Class[:externally-opt-in] {
   has $.private;
   has $.internal;
   has $.external;
}

my Bla $bla .= new: :1private, :2internal, :3external;

# on internal route:
$bla.to-json: :opt-in<internal external>; # { "internal": 2, "external": 3 }

# on external route:
$bla.to-json: :opt-in<external>; # { "external": 3 }

# and not specifying the attributes could die or return `null`

# or maybe even:

# on internal route:
my $internal-bla = $bla.opt-in: <internal external>;
$internal-bla.to-json; # { "internal": 2, "external": 3 }

# on external route:
my $external-bla = $bla.opt-in: <external>;
$external-bla.to-json; # { "external": 3 }

# or even:

# on internal route:
my %*JSON-Class-opt-in{Bla} = <internal external>;
$bla.to-json; # { "internal": 2, "external": 3 }

# on external route:
my %*JSON-Class-opt-in{Bla} = <external>;
$bla.to-json; # { "external": 3 }

I personally like the dynvar one because if I define it on my route, doesn't matter what I do with the object, it will never leak data... Or something like that? If it already has something like that, I'm sorry.

jonathanstowe commented 2 years ago

Good idea, I think I would probably move the definition of which attributes to the is json trait, So there would be something like:

class Bla does JSON::Class[:opt-in] {
   has $.private;
   has $.internal is json<internal>;
   has $.external is json;
}

Where they are essentially tags that must be specified for that attribute to serialised (no tag meaning unconditionally serialised,) possibly allowing a list.

I like the dynvar because it, e.g., would allow selecting attributes on some "authorisation context" (so some logged in users get some fields and others don't,) but I'd need to have a play with the body serializer to see how that would work best. I'd probably allow for both dynvar and argument to to-json for some flexibility.

I'll have a think about this, but definitely something that is worth pursuing.

Cheers.

FCO commented 2 years ago

About the tags on is json, I like it, but I also think it would be good to have the chance to define that outside. A class don't need to know every way it's going to be used, right?

jonathanstowe commented 2 years ago

Oh, I've just realised what you meant, I failed to realise that you literally meant to pass the names of the attributes :rofl:

That's doable but may run into difficulty where there are nested objects, one level is easy, I guess for subsequent levels it would be something like pass a pair in the list of attributes (the value being a list, etc...)

FCO commented 2 years ago

Hum! That makes completely sense, I haven't thought about that. Would that be feasible using json-path or json-pointer? I can totally see it as:

$post.to-json: <$.tiltle $.body $.author.* $.tags.* ˜.comments.*.body .comments.*.author.name>

but maybe that's too much...

another option would be:

my %*JSON-Class-opt-in{Post} = <title body author tags comments>;
my %*JSON-Class-opt-in{Person} = <name>;
my %*JSON-Class-opt-in{Comment} = <body author>;

$post.to-json

but maybe too complex also... and we could not differentiate the post's author from the comment's author.

FCO commented 2 years ago

What if?

my %*JSON-Class-opt-in{Post} = %( :title, :body, :author<external>, :tags<all>, :comments{ :body, :author{ :name } } ) # external is that previous discussed tag and all would be a uto generated tag that includes all attributes

# so
%*JSON-Class-opt-in{Bla} = "all"; # Would expose everything
%*JSON-Class-opt-in{Ble} = "external"; # Would expose only the attributes marked as external
%*JSON-Class-opt-in{Bli} = :attr; # or %( :attr ); Would expose only the attribute `attr`
%*JSON-Class-opt-in{Blo} = :blu{ :attr }; # Would expose only the attribute `attr` from the attribute `blu`

What do you think?

FCO commented 2 years ago

And just to let it clear:


class Bla { has $.a }
class Ble { has Bla @.as }

Bla.from-json(%( :123a )).to-json: :opt-in{ :a }; # Would return { "a": 123 }
# and
Ble.from-json(%( [:1a, :2a, :3a])).to-json: :opt-in{ :as{ :a } }; # Would return {"as": [{ "a": 1 }, { "a": 2 }, { "a": 3 }]}
jonathanstowe commented 2 years ago

Works for me, I'd probably leave the "all" case out as the default would be to serialise all the attributes that would be serialised as the code is now (either all those without json-skip without opt-in or all those with json or some other trait for the opt-in case.)

I do see them as slightly orthogonal things though, "group opt-in" and "explicit attribute select".

BTW most of the changes would be in the JSON::Marshal not here.