WebReflection/introspected

Feature request: read access callback

McShelby opened this issue · 12 comments

We have some C++ code that serializes our objects via an Archive class to either XML or JSON.
Because JSON is new to the Archive serializer it wasn't a concern when most of our object serialization code was written.

The problem now is:
If the object has an empty array property we do not generate any JSON for it.
If the object has at least one element in this array property it generates a JSON array with the proper elements.

C++ Example Code:

const bool generate_array = true;
for( auto e : container ){
	Archive.BeginElement( "my_element", generate_array );
	// ... element content
	Archive.EndElement();
}

Resulting JSON for an empty array property:

{
}

Resulting JSON for an array property filled with one number:

{
	"my_element": [ 666 ]
}

Using such an object on the JS side is rather tedious, because accessing the array property may result in errors if the array was empty on C++ side. You can work around that by adding empty array properties if the property doesn't exist. But that's error prone and may be forgotten by the programmer.

Therefore I thought of adding an empty array using Introspected once an undefined property is accessed. By now I only achieved this by editing the Introspected code itself replacing the "default" code path in the get() function.

Is this a valid use case? Do I miss some obvious point in the API? Or could this functionality be added to the library?

can you please tell me what are you doing and what you'd expect that is not working?

I'm not sure I've even understood the use case.

Thanks

Just suppose that the JSON object has an array property "my_element" if at least one item in the array is present. If the array is empty the property is undefined resulting in the above mentioned JSON examples.

The client code may look like this:

var s; // '{}' or '{"my_element":[666]}'
var my_object = JSON.parse( s );
// I want to get rid of the following line
my_object.my_element = my_object.my_element || [];
if( my_object.my_element.length ){
	// do something
}

Goal is to get rid of the marked line, because it may clutter the code or just simply be forgotten. If the property wasn't present this will result in an error one line later saying "my_element is undefined".

The idea was to use Introspected here, but...:

var o1 = { my_element: [] }; // for this example just an empty array!
var o2 = Introspected( {} );
if( !!o1.my_element.length != !!o2.my_element.length ){
	console.log("Darn!");
}

Because o2.my_lement.length is a proxy object, a boolean check always returns truthy, but the native array in o1 returns falsy. Therefore I still can't get rid of the marked line from the first example, because checking the length property may behave differently for Introspected vs. plain objects.

Now my idea was to tell Introspected that every time it tries to access a non existant property that it just creates an empty array (because that's the only case a property may not exist).

Therefore I hacked the Introspected code and replaced some of the code in its get() method. But that felt somehow ugly. Maybe there is a different way to solve this without hacking the Introspected code and by just using the API?

This is the bit I don't get

Now my idea was to tell Introspected that every time it tries to access a non existant property that it just creates an empty array (because that's the only case a property may not exist).

Introspected already creates an empty object, which is why you can do the following:

var o2 = Introspected( {} );

o2.bla.bla.bla.bla.bla.bla = 'ok';

In that case all properties don't exist at all. You can JSON.stringify that too, it will produce:

{"bla":{"bla":{"bla":{"bla":{"bla":{"bla":"ok"}}}}}}

What you want is to create Arrays instead of objects. Now, consider this:

var o2 = [];
o2.bla = [];
o2.bla.bla = [];
o2.bla.bla.bla = 'ok';

// guess what's the outcome here ...
JSON.stringify(o2); // "[]"

Maybe there is a different way to solve this without hacking the Introspected code and by just using the API?

There is strictly nothing to solve in introspected. By contract, it notifies you when you set/change data.
That's what this library was created for.

What you want instead, is a library that automagically decides if whatever you access is something ... unpredictable. The length is not a special property, it's not an Array only thing, maybe you wanted to compare the length of a string set at that point.

How can this library know what you are expecting there, if nothing is defined?

What could help you is an utility that gives you last property, if available, or a default value, if not.

const value = (o, p, d) => p in o ? o[p] : d;

// if you want to assign a default if absent, use this instead
const value = (o, p, d) => p in o ? o[p] : (o[p] = d);

At this point your logic would look more like:

var o1 = { my_element: [] }; // for this example just an empty array!
var o2 = Introspected( {} );
if( !!o1.my_element.length != !!value(o2, 'my_element', []).length ){
	console.log("Darn!");
}

As long as you reach the last part of your path through a string instead of a getter, you are good to go with any path, at any depth, and any default.

// check if obj.any.depth.any.path exists and returns it
// or return a `{deafult: true}` object instead
value(obj.any.depth.any, 'path', {deafult: true});

Would this solve your issue?

How can this library know what you are expecting there, if nothing is defined?

Well that's exactly the point here. The current implementation makes an assumption in its get() function by creating an object

create(null)

For my use case, the library is plain wrong cause I need an array here.

Why not replacing the call to create(null) with some kind of factory callback that can be given in the Introspected constructor as additional, optional parameter. If not given it simply does its usual create(null) wrapped in a default callback.

Signature of the factory callback would be (target, prop) and returns the (whatever) wanted result.

The current implementation makes an assumption in its get() function by creating an object

No. The current library returns a Proxy.

For my use case, the library is plain wrong cause I need an array here.

No. The library documentation, purpose, and contract, is to let you access any existent property at any depth and notify whenever a property is changed or set.

It has 100% code coverage for what it promises so it's very unfair to state the library is wrong.

Why not replacing the call to create(null) with some kind of factory callback ...

Please show me an example on how that would work.

The library documentation, purpose, and contract, is to let you access any existent property at any depth and notify whenever a property is changed or set.

Well. I could provide an example but with my factory function the resulting object would not adhere to the contract. You're right here. I was using the library for something it wasn't made for.

I haven't seen a statement (or just missed it) what the libray does it the property is not existing. For me it's quite surprising that it will add this property. Was this meant to be that way?

It hasn't been surprising for its users 'till today. It's about handling states and changes, not about accessing properties.

You can simply use a Proxy for that and return whatever you think makes sense as a default.

Here, an Array as a default, makes no sense whatsoever, and a Proxy is needed in any case.

I haven't seen a statement (or just missed it)

This is the first line and the only description of the project.

If you'd like to be notified about any possible change that could happen to a JSON compatible model / data / object / array / structure, including the possibility to retrieve the exact full path of the object that changed and eventually walk through it, you've reached your destination.

Imho just reading a not existing property shouldn't cause it to be added to the object. But that's what currently happens as seen in your JSON.stringify example from above.

Again ... reading doesn't add anything. setting is what is this library about.

var o=Introspected({});
JSON.stringify(o); // "{}"
console.log( o.klaus.theo.gaertner ); // Proxy{}
JSON.stringify(o); // "{"klaus":{"theo":{"gaertner":{}}}}"

Just read access but nevertheless the object seems to be changed for JSON.stringify. But maybe I am not allowed to stringify it? Or I still don't get it at all...

Or I still don't get it at all...

most likely, I'm afraid.

This library is described already. I wont' point at this link again.
#7 (comment)

This libarry is about setting values and be notified.

This is the bit you don't get.

you don't access randomly values in the wild, you access paths you know you want / need to change and you get notified.

What you want, is to learn how a Proxy works and create your own wrapper.

Your proposed solution wouldn't work here, you still need to address that path you reached.

You return an Array? good, that will result into {"klaus":{"theo":{"gaertner":[]}}}

Would you be happy once that happens? I don't think so, you have another use case.

Just go with any other library out there that gives you callbacks on accessing paths, or use what this library provides , including the ability to know if a path exists already or not.

That is Introspected.pathValue and it's on the readme.
https://github.com/WebReflection/introspected#api

Im off this discussion because this project is not sustainable and I don't have extra time these days to take care of it. It works, it's tested, it does one thing and it does that well.

Best Regards