NeoJSON and polymorphism

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
5 messages Options
Reply | Threaded
Open this post in threaded view
|

NeoJSON and polymorphism

Juraj Kubelka
Hi,

By studying the NeoJSON book chapter (Pharo Enterprise), I do not understand how to modify the following example:

-=-=-=-=-=-=-=-
"Let us say that we have an Attachment class..."

Object subclass: #Attachment
instanceVariableNames: 'url fileName'
classVariableNames: ''
package: 'NeoJSON-Use-Case'.

"...with url: and fileName: methods."
Attachment compile: 'url: anObject', String cr, String tab, 'url := anObject' classified: 'accessing'.
Attachment compile: 'fileName: anObject', String cr, String tab, 'fileName := anObject' classified: 'accessing'.
"Let's create a collection of two instances:"
collectionOne := { 
Attachment new 
fileName: 'chapter-one.txt'
yourself.
Attachment new 
fileName: 'image.png';
yourself.
}.

"And let's map it to a JSON structure:"
String streamContents: [ :aStream |
(NeoJSONWriter on: aStream)
for: #CollectionOfAttachments customDo: [ :mapping | 
mapping listOfElementSchema: Attachment ];
mapAllInstVarsFor: Attachment;
for: ZnUrl customDo: [ :mapping |
mapping encoder: [ :aZnUrl |
aZnUrl asString ] ];
nextPut: collectionOne as: #CollectionOfAttachments.
].

"And read the JSON structure:"
(NeoJSONReader on: '[{"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt"},{"url":"http://example.com/random-name.png","fileName":"image.png"}]' readStream)
for: #CollectionOfAttachments customDo: [ :mapping | 
mapping listOfElementSchema: Attachment ];
for: Attachment do: [ :mapping | 
mapping mapInstVar: 'fileName'.
(mapping mapInstVar: 'url') valueSchema: ZnUrl ];
for: ZnUrl customDo: [ :mapping |
mapping decoder: [ :string |
string asZnUrl ] ];
nextAs: #CollectionOfAttachments.

"======================="
“The previous example works perfectly, including the ZnUrl mapping (notice that url variables have ZnUrl instances).

Now, let's say that we want to distinguish PNG and TXT attachments.
For that reason we will create two Attachment subclasses..."

Attachment subclass: #PngAttachment
instanceVariableNames: ''
classVariableNames: ''
package: 'NeoJSON-Use-Case'.
Attachment subclass: #TxtAttachment
instanceVariableNames: ''
classVariableNames: ''
package: 'NeoJSON-Use-Case'.

"...with type methods that might be used in a new JSON structure:"
PngAttachment compile: 'type', String cr, String tab, '^ ''png''' classified: 'accessing'.
TxtAttachment compile: 'type', String cr, String tab, '^ ''txt''' classified: 'accessing'.

"Let's create a collection with the PNG and TXT instances:"
collectionTwo := { 
TxtAttachment new 
fileName: 'chapter-one.txt'
yourself.
PngAttachment new 
fileName: 'image.png';
yourself.
}.

"How can I modify NeoJSONWriter and NeoJSONReader mappings?

The JSON structure should (ideally) looks like this:"

'[
{“type”:”txt”,"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt”},
{“type”:”png”,"url":"http://example.com/random-name.png","fileName":"image.png”}
]'
-=-=-=-=-=-=-=-

I would like to keep the mapping isolated (without defining neoJsonOn: methods).

Thank you!
Juraj

Reply | Threaded
Open this post in threaded view
|

Re: NeoJSON and polymorphism

Sven Van Caekenberghe-2
Hi Juraj,

This would be a simpler form of the type/class tags that are often used in JSON encoding. Since there are many ways to do this, there cannot be one solution. NeoJSON mapping was not designed to cover these cases. Nor is JSON meant to do this. STON is one (rather elegant if I may say so) answer to this problem. Your example would be encoded as

[
  PngAttachement { #url:'http://example.com/random-name.txt', #fileName:'chapter-one.txt' },
  TxtAttachement { #url:'http://example.com/random-name.png', #fileName:'image.png' }
]

Still, the question of how to do this kind of dynamic decoding is an interesting challenge. Please update to the following commits:

===
Name: Neo-JSON-Core-SvenVanCaekenberghe.44
Author: SvenVanCaekenberghe
Time: 22 September 2017, 3:24:51.679449 pm
UUID: f1ebeade-2816-0d00-a04c-ae370e598362
Ancestors: Neo-JSON-Core-SvenVanCaekenberghe.42

Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)

Add NeoJSONStreamingWriter>>#writeElementAs:

Add NeoJSONMappingTests>>#testDynamicTyping as an example
===
Name: Neo-JSON-Tests-SvenVanCaekenberghe.41
Author: SvenVanCaekenberghe
Time: 22 September 2017, 3:25:09.881494 pm
UUID: a9a900e0-2816-0d00-a04d-a6fb0e598362
Ancestors: Neo-JSON-Tests-SvenVanCaekenberghe.39

Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)

Add NeoJSONStreamingWriter>>#writeElementAs:

Add NeoJSONMappingTests>>#testDynamicTyping as an example
===

Now you can do as follows:

NeoJSONMappingTests>>#testDynamicTyping
  | data customMapping json result |
  data := Array with: #foo->1 with: #(foo 2).
  "The idea is to map a key value combination as either a classic association or a simple pair,
   using key & value properties as well as a type property to distinguish between the two"
  customMapping := [ :mapper |
    mapper
      for: #AssocOrPair customDo: [ :mapping |
        mapping
          encoder: [ :x |
            x isArray
              ifTrue: [ { #type->#pair. #key->x first. #value->x second } asDictionary ]
              ifFalse: [ { #type->#assoc. #key->x key. #value->x value } asDictionary ] ];
          decoder: [ :x |
            (x at: #type) = #pair
              ifTrue: [ Array with: (x at: #key) with: (x at: #value) ]
              ifFalse: [ (x at: #key) -> (x at: #value)] ] ];
      for: #ArrayOfAssocOrPair customDo: [ :mapping |
        mapping listOfType: Array andElementSchema: #AssocOrPair ];
      yourself ].
  json := String streamContents: [ :out |
    (customMapping value: (NeoJSONWriter on: out)) nextPut: data as: #ArrayOfAssocOrPair ].
  result := (customMapping value: (NeoJSONReader on: json readStream)) nextAs: #ArrayOfAssocOrPair.
  self assert: result equals: data

Everything is virtual (not implemented as methods on actual classes) which makes the example self contained and non intrusive, but not very elegant, nor object oriented or extendable. Also, it is not as efficient as NeoJSON mapping was designed for, since it creates intermediate structures.

HTH,

Sven

> On 21 Sep 2017, at 22:07, Juraj Kubelka <[hidden email]> wrote:
>
> Hi,
>
> By studying the NeoJSON book chapter (Pharo Enterprise), I do not understand how to modify the following example:
>
> -=-=-=-=-=-=-=-
> "Let us say that we have an Attachment class..."
>
> Object subclass: #Attachment
> instanceVariableNames: 'url fileName'
> classVariableNames: ''
> package: 'NeoJSON-Use-Case'.
>
> "...with url: and fileName: methods."
> Attachment compile: 'url: anObject', String cr, String tab, 'url := anObject' classified: 'accessing'.
> Attachment compile: 'fileName: anObject', String cr, String tab, 'fileName := anObject' classified: 'accessing'.
>
> "Let's create a collection of two instances:"
> collectionOne := {
> Attachment new
> url: 'http://example.com/random-name.txt' asZnUrl;
> fileName: 'chapter-one.txt'
> yourself.
> Attachment new
> url: 'http://example.com/random-name.png' asZnUrl;
> fileName: 'image.png';
> yourself.
> }.
>
> "And let's map it to a JSON structure:"
> String streamContents: [ :aStream |
> (NeoJSONWriter on: aStream)
> for: #CollectionOfAttachments customDo: [ :mapping |
> mapping listOfElementSchema: Attachment ];
> mapAllInstVarsFor: Attachment;
> for: ZnUrl customDo: [ :mapping |
> mapping encoder: [ :aZnUrl |
> aZnUrl asString ] ];
> nextPut: collectionOne as: #CollectionOfAttachments.
> ].
>
> "And read the JSON structure:"
> (NeoJSONReader on: '[{"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt"},{"url":"http://example.com/random-name.png","fileName":"image.png"}]' readStream)
> for: #CollectionOfAttachments customDo: [ :mapping |
> mapping listOfElementSchema: Attachment ];
> for: Attachment do: [ :mapping |
> mapping mapInstVar: 'fileName'.
> (mapping mapInstVar: 'url') valueSchema: ZnUrl ];
> for: ZnUrl customDo: [ :mapping |
> mapping decoder: [ :string |
> string asZnUrl ] ];
> nextAs: #CollectionOfAttachments.
>
> "======================="
> “The previous example works perfectly, including the ZnUrl mapping (notice that url variables have ZnUrl instances).
>
> Now, let's say that we want to distinguish PNG and TXT attachments.
> For that reason we will create two Attachment subclasses..."
>
> Attachment subclass: #PngAttachment
> instanceVariableNames: ''
> classVariableNames: ''
> package: 'NeoJSON-Use-Case'.
>
> Attachment subclass: #TxtAttachment
> instanceVariableNames: ''
> classVariableNames: ''
> package: 'NeoJSON-Use-Case'.
>
> "...with type methods that might be used in a new JSON structure:"
> PngAttachment compile: 'type', String cr, String tab, '^ ''png''' classified: 'accessing'.
> TxtAttachment compile: 'type', String cr, String tab, '^ ''txt''' classified: 'accessing'.
>
> "Let's create a collection with the PNG and TXT instances:"
> collectionTwo := {
> TxtAttachment new
> url: 'http://example.com/random-name.txt' asZnUrl;
> fileName: 'chapter-one.txt'
> yourself.
> PngAttachment new
> url: 'http://example.com/random-name.png' asZnUrl;
> fileName: 'image.png';
> yourself.
> }.
>
> "How can I modify NeoJSONWriter and NeoJSONReader mappings?
>
> The JSON structure should (ideally) looks like this:"
>
> '[
> {“type”:”txt”,"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt”},
> {“type”:”png”,"url":"http://example.com/random-name.png","fileName":"image.png”}
> ]'
> -=-=-=-=-=-=-=-
>
> I would like to keep the mapping isolated (without defining neoJsonOn: methods).
>
> Thank you!
> Juraj
>


Reply | Threaded
Open this post in threaded view
|

Re: NeoJSON and polymorphism

Stephane Ducasse-3
Sven do you think that it is worth to create a new section with the
xample and the solution in the book?

On Sun, Sep 24, 2017 at 11:05 AM, Sven Van Caekenberghe <[hidden email]> wrote:

> Hi Juraj,
>
> This would be a simpler form of the type/class tags that are often used in JSON encoding. Since there are many ways to do this, there cannot be one solution. NeoJSON mapping was not designed to cover these cases. Nor is JSON meant to do this. STON is one (rather elegant if I may say so) answer to this problem. Your example would be encoded as
>
> [
>   PngAttachement { #url:'http://example.com/random-name.txt', #fileName:'chapter-one.txt' },
>   TxtAttachement { #url:'http://example.com/random-name.png', #fileName:'image.png' }
> ]
>
> Still, the question of how to do this kind of dynamic decoding is an interesting challenge. Please update to the following commits:
>
> ===
> Name: Neo-JSON-Core-SvenVanCaekenberghe.44
> Author: SvenVanCaekenberghe
> Time: 22 September 2017, 3:24:51.679449 pm
> UUID: f1ebeade-2816-0d00-a04c-ae370e598362
> Ancestors: Neo-JSON-Core-SvenVanCaekenberghe.42
>
> Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)
>
> Add NeoJSONStreamingWriter>>#writeElementAs:
>
> Add NeoJSONMappingTests>>#testDynamicTyping as an example
> ===
> Name: Neo-JSON-Tests-SvenVanCaekenberghe.41
> Author: SvenVanCaekenberghe
> Time: 22 September 2017, 3:25:09.881494 pm
> UUID: a9a900e0-2816-0d00-a04d-a6fb0e598362
> Ancestors: Neo-JSON-Tests-SvenVanCaekenberghe.39
>
> Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)
>
> Add NeoJSONStreamingWriter>>#writeElementAs:
>
> Add NeoJSONMappingTests>>#testDynamicTyping as an example
> ===
>
> Now you can do as follows:
>
> NeoJSONMappingTests>>#testDynamicTyping
>   | data customMapping json result |
>   data := Array with: #foo->1 with: #(foo 2).
>   "The idea is to map a key value combination as either a classic association or a simple pair,
>    using key & value properties as well as a type property to distinguish between the two"
>   customMapping := [ :mapper |
>     mapper
>       for: #AssocOrPair customDo: [ :mapping |
>         mapping
>           encoder: [ :x |
>             x isArray
>               ifTrue: [ { #type->#pair. #key->x first. #value->x second } asDictionary ]
>               ifFalse: [ { #type->#assoc. #key->x key. #value->x value } asDictionary ] ];
>           decoder: [ :x |
>             (x at: #type) = #pair
>               ifTrue: [ Array with: (x at: #key) with: (x at: #value) ]
>               ifFalse: [ (x at: #key) -> (x at: #value)] ] ];
>       for: #ArrayOfAssocOrPair customDo: [ :mapping |
>         mapping listOfType: Array andElementSchema: #AssocOrPair ];
>       yourself ].
>   json := String streamContents: [ :out |
>     (customMapping value: (NeoJSONWriter on: out)) nextPut: data as: #ArrayOfAssocOrPair ].
>   result := (customMapping value: (NeoJSONReader on: json readStream)) nextAs: #ArrayOfAssocOrPair.
>   self assert: result equals: data
>
> Everything is virtual (not implemented as methods on actual classes) which makes the example self contained and non intrusive, but not very elegant, nor object oriented or extendable. Also, it is not as efficient as NeoJSON mapping was designed for, since it creates intermediate structures.
>
> HTH,
>
> Sven
>
>> On 21 Sep 2017, at 22:07, Juraj Kubelka <[hidden email]> wrote:
>>
>> Hi,
>>
>> By studying the NeoJSON book chapter (Pharo Enterprise), I do not understand how to modify the following example:
>>
>> -=-=-=-=-=-=-=-
>> "Let us say that we have an Attachment class..."
>>
>> Object subclass: #Attachment
>>       instanceVariableNames: 'url fileName'
>>       classVariableNames: ''
>>       package: 'NeoJSON-Use-Case'.
>>
>> "...with url: and fileName: methods."
>> Attachment compile: 'url: anObject', String cr, String tab, 'url := anObject' classified: 'accessing'.
>> Attachment compile: 'fileName: anObject', String cr, String tab, 'fileName := anObject' classified: 'accessing'.
>>
>> "Let's create a collection of two instances:"
>> collectionOne := {
>>       Attachment new
>>               url: 'http://example.com/random-name.txt' asZnUrl;
>>               fileName: 'chapter-one.txt'
>>               yourself.
>>       Attachment new
>>               url: 'http://example.com/random-name.png' asZnUrl;
>>               fileName: 'image.png';
>>               yourself.
>> }.
>>
>> "And let's map it to a JSON structure:"
>> String streamContents: [ :aStream |
>>       (NeoJSONWriter on: aStream)
>>               for: #CollectionOfAttachments customDo: [ :mapping |
>>                       mapping listOfElementSchema: Attachment ];
>>               mapAllInstVarsFor: Attachment;
>>               for: ZnUrl customDo: [ :mapping |
>>                       mapping encoder: [ :aZnUrl |
>>                               aZnUrl asString ] ];
>>               nextPut: collectionOne as: #CollectionOfAttachments.
>> ].
>>
>> "And read the JSON structure:"
>> (NeoJSONReader on: '[{"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt"},{"url":"http://example.com/random-name.png","fileName":"image.png"}]' readStream)
>>       for: #CollectionOfAttachments customDo: [ :mapping |
>>                       mapping listOfElementSchema: Attachment ];
>>       for: Attachment do: [ :mapping |
>>               mapping mapInstVar: 'fileName'.
>>               (mapping mapInstVar: 'url') valueSchema: ZnUrl ];
>>       for: ZnUrl customDo: [ :mapping |
>>               mapping decoder: [ :string |
>>                       string asZnUrl ] ];
>>       nextAs: #CollectionOfAttachments.
>>
>> "======================="
>> “The previous example works perfectly, including the ZnUrl mapping (notice that url variables have ZnUrl instances).
>>
>> Now, let's say that we want to distinguish PNG and TXT attachments.
>> For that reason we will create two Attachment subclasses..."
>>
>> Attachment subclass: #PngAttachment
>>       instanceVariableNames: ''
>>       classVariableNames: ''
>>       package: 'NeoJSON-Use-Case'.
>>
>> Attachment subclass: #TxtAttachment
>>       instanceVariableNames: ''
>>       classVariableNames: ''
>>       package: 'NeoJSON-Use-Case'.
>>
>> "...with type methods that might be used in a new JSON structure:"
>> PngAttachment compile: 'type', String cr, String tab, '^ ''png''' classified: 'accessing'.
>> TxtAttachment compile: 'type', String cr, String tab, '^ ''txt''' classified: 'accessing'.
>>
>> "Let's create a collection with the PNG and TXT instances:"
>> collectionTwo := {
>>       TxtAttachment new
>>               url: 'http://example.com/random-name.txt' asZnUrl;
>>               fileName: 'chapter-one.txt'
>>               yourself.
>>       PngAttachment new
>>               url: 'http://example.com/random-name.png' asZnUrl;
>>               fileName: 'image.png';
>>               yourself.
>> }.
>>
>> "How can I modify NeoJSONWriter and NeoJSONReader mappings?
>>
>> The JSON structure should (ideally) looks like this:"
>>
>> '[
>>       {“type”:”txt”,"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt”},
>>       {“type”:”png”,"url":"http://example.com/random-name.png","fileName":"image.png”}
>> ]'
>> -=-=-=-=-=-=-=-
>>
>> I would like to keep the mapping isolated (without defining neoJsonOn: methods).
>>
>> Thank you!
>> Juraj
>>
>
>

Reply | Threaded
Open this post in threaded view
|

Re: NeoJSON and polymorphism

Juraj Kubelka
In reply to this post by Sven Van Caekenberghe-2
Hi Sven,

thank you!

In the NeoJSON repository, you likely want to merge Neo-JSON-Core-SvenVanCaekenberghe.43 and 44. And the same for the test cases.

> On Sep 24, 2017, at 06:05, Sven Van Caekenberghe <[hidden email]> wrote:
>
> Hi Juraj,
>
> This would be a simpler form of the type/class tags that are often used in JSON encoding. Since there are many ways to do this, there cannot be one solution. NeoJSON mapping was not designed to cover these cases. Nor is JSON meant to do this. STON is one (rather elegant if I may say so) answer to this problem. Your example would be encoded as
>
> [
>  PngAttachement { #url:'http://example.com/random-name.txt', #fileName:'chapter-one.txt' },
>  TxtAttachement { #url:'http://example.com/random-name.png', #fileName:'image.png' }
> ]

About STON usage, I wonder how it works in terms of a data structure evolution. Currently I use JSON format to store a data permanently. The data are supposed to be accesible for a long time (years). I am pretty sure that the protocol will evolve and we will end up with several JSON versions. I think that having different JSON mapping schemes will allow to map old JSON versions to a latest object structure (or to several different versions if necessary).

For the example above, think of the scenario that PngAttachment does not exist anymore. In a simple scenario there could be an ImageAttachment class instead with the same instance variables. In a more complex scenario, there could be a AttachmentTwo { #type: AttachmentTwoType { … instances … }, #path: AttachmentTwoPath { … instances … } }.

I can use JSON mappings that transform the old JSON version to the new class structure. I am not sure if STON is that flexible.
What do you think? Do I miss something?

>
> Still, the question of how to do this kind of dynamic decoding is an interesting challenge. Please update to the following commits:
>
> ===
> Name: Neo-JSON-Core-SvenVanCaekenberghe.44
> Author: SvenVanCaekenberghe
> Time: 22 September 2017, 3:24:51.679449 pm
> UUID: f1ebeade-2816-0d00-a04c-ae370e598362
> Ancestors: Neo-JSON-Core-SvenVanCaekenberghe.42
>
> Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)
>
> Add NeoJSONStreamingWriter>>#writeElementAs:
>
> Add NeoJSONMappingTests>>#testDynamicTyping as an example
> ===
> Name: Neo-JSON-Tests-SvenVanCaekenberghe.41
> Author: SvenVanCaekenberghe
> Time: 22 September 2017, 3:25:09.881494 pm
> UUID: a9a900e0-2816-0d00-a04d-a6fb0e598362
> Ancestors: Neo-JSON-Tests-SvenVanCaekenberghe.39
>
> Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)
>
> Add NeoJSONStreamingWriter>>#writeElementAs:
>
> Add NeoJSONMappingTests>>#testDynamicTyping as an example
> ===
>
> Now you can do as follows:
>
> NeoJSONMappingTests>>#testDynamicTyping
>  | data customMapping json result |
>  data := Array with: #foo->1 with: #(foo 2).
>  "The idea is to map a key value combination as either a classic association or a simple pair,
>   using key & value properties as well as a type property to distinguish between the two"
>  customMapping := [ :mapper |
>    mapper
>      for: #AssocOrPair customDo: [ :mapping |
>        mapping
>          encoder: [ :x |
>            x isArray
>              ifTrue: [ { #type->#pair. #key->x first. #value->x second } asDictionary ]
>              ifFalse: [ { #type->#assoc. #key->x key. #value->x value } asDictionary ] ];
>          decoder: [ :x |
>            (x at: #type) = #pair
>              ifTrue: [ Array with: (x at: #key) with: (x at: #value) ]
>              ifFalse: [ (x at: #key) -> (x at: #value)] ] ];
>      for: #ArrayOfAssocOrPair customDo: [ :mapping |
>        mapping listOfType: Array andElementSchema: #AssocOrPair ];
>      yourself ].
>  json := String streamContents: [ :out |
>    (customMapping value: (NeoJSONWriter on: out)) nextPut: data as: #ArrayOfAssocOrPair ].
>  result := (customMapping value: (NeoJSONReader on: json readStream)) nextAs: #ArrayOfAssocOrPair.
>  self assert: result equals: data
>
> Everything is virtual (not implemented as methods on actual classes) which makes the example self contained and non intrusive, but not very elegant, nor object oriented or extendable. Also, it is not as efficient as NeoJSON mapping was designed for, since it creates intermediate structures.

Nice example. If #decoder: and #encoder: are used, is it still possible to use inner mappings? For example in my previous example, one instance variable is a ZnUrl instance that is stored as a String. Can I say “use ZnUrl mapping in for this intermediate structure”? Well, I understand that I can hard code it directly in the the #decoder: and #encoder: messages using #asZnUrl and #asString messages. I am thinking about a more complicated structures that involves more mapping definitions.

Thanks!
Juraj

>
> HTH,
>
> Sven
>
>> On 21 Sep 2017, at 22:07, Juraj Kubelka <[hidden email]> wrote:
>>
>> Hi,
>>
>> By studying the NeoJSON book chapter (Pharo Enterprise), I do not understand how to modify the following example:
>>
>> -=-=-=-=-=-=-=-
>> "Let us say that we have an Attachment class..."
>>
>> Object subclass: #Attachment
>> instanceVariableNames: 'url fileName'
>> classVariableNames: ''
>> package: 'NeoJSON-Use-Case'.
>>
>> "...with url: and fileName: methods."
>> Attachment compile: 'url: anObject', String cr, String tab, 'url := anObject' classified: 'accessing'.
>> Attachment compile: 'fileName: anObject', String cr, String tab, 'fileName := anObject' classified: 'accessing'.
>>
>> "Let's create a collection of two instances:"
>> collectionOne := {
>> Attachment new
>> url: 'http://example.com/random-name.txt' asZnUrl;
>> fileName: 'chapter-one.txt'
>> yourself.
>> Attachment new
>> url: 'http://example.com/random-name.png' asZnUrl;
>> fileName: 'image.png';
>> yourself.
>> }.
>>
>> "And let's map it to a JSON structure:"
>> String streamContents: [ :aStream |
>> (NeoJSONWriter on: aStream)
>> for: #CollectionOfAttachments customDo: [ :mapping |
>> mapping listOfElementSchema: Attachment ];
>> mapAllInstVarsFor: Attachment;
>> for: ZnUrl customDo: [ :mapping |
>> mapping encoder: [ :aZnUrl |
>> aZnUrl asString ] ];
>> nextPut: collectionOne as: #CollectionOfAttachments.
>> ].
>>
>> "And read the JSON structure:"
>> (NeoJSONReader on: '[{"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt"},{"url":"http://example.com/random-name.png","fileName":"image.png"}]' readStream)
>> for: #CollectionOfAttachments customDo: [ :mapping |
>> mapping listOfElementSchema: Attachment ];
>> for: Attachment do: [ :mapping |
>> mapping mapInstVar: 'fileName'.
>> (mapping mapInstVar: 'url') valueSchema: ZnUrl ];
>> for: ZnUrl customDo: [ :mapping |
>> mapping decoder: [ :string |
>> string asZnUrl ] ];
>> nextAs: #CollectionOfAttachments.
>>
>> "======================="
>> “The previous example works perfectly, including the ZnUrl mapping (notice that url variables have ZnUrl instances).
>>
>> Now, let's say that we want to distinguish PNG and TXT attachments.
>> For that reason we will create two Attachment subclasses..."
>>
>> Attachment subclass: #PngAttachment
>> instanceVariableNames: ''
>> classVariableNames: ''
>> package: 'NeoJSON-Use-Case'.
>>
>> Attachment subclass: #TxtAttachment
>> instanceVariableNames: ''
>> classVariableNames: ''
>> package: 'NeoJSON-Use-Case'.
>>
>> "...with type methods that might be used in a new JSON structure:"
>> PngAttachment compile: 'type', String cr, String tab, '^ ''png''' classified: 'accessing'.
>> TxtAttachment compile: 'type', String cr, String tab, '^ ''txt''' classified: 'accessing'.
>>
>> "Let's create a collection with the PNG and TXT instances:"
>> collectionTwo := {
>> TxtAttachment new
>> url: 'http://example.com/random-name.txt' asZnUrl;
>> fileName: 'chapter-one.txt'
>> yourself.
>> PngAttachment new
>> url: 'http://example.com/random-name.png' asZnUrl;
>> fileName: 'image.png';
>> yourself.
>> }.
>>
>> "How can I modify NeoJSONWriter and NeoJSONReader mappings?
>>
>> The JSON structure should (ideally) looks like this:"
>>
>> '[
>> {“type”:”txt”,"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt”},
>> {“type”:”png”,"url":"http://example.com/random-name.png","fileName":"image.png”}
>> ]'
>> -=-=-=-=-=-=-=-
>>
>> I would like to keep the mapping isolated (without defining neoJsonOn: methods).
>>
>> Thank you!
>> Juraj
>>
>
>


Reply | Threaded
Open this post in threaded view
|

Re: NeoJSON and polymorphism

Sven Van Caekenberghe-2

> On 25 Sep 2017, at 17:14, Juraj Kubelka <[hidden email]> wrote:
>
> Hi Sven,
>
> thank you!
>
> In the NeoJSON repository, you likely want to merge Neo-JSON-Core-SvenVanCaekenberghe.43 and 44. And the same for the test cases.

I did, thanks !

>> On Sep 24, 2017, at 06:05, Sven Van Caekenberghe <[hidden email]> wrote:
>>
>> Hi Juraj,
>>
>> This would be a simpler form of the type/class tags that are often used in JSON encoding. Since there are many ways to do this, there cannot be one solution. NeoJSON mapping was not designed to cover these cases. Nor is JSON meant to do this. STON is one (rather elegant if I may say so) answer to this problem. Your example would be encoded as
>>
>> [
>> PngAttachement { #url:'http://example.com/random-name.txt', #fileName:'chapter-one.txt' },
>> TxtAttachement { #url:'http://example.com/random-name.png', #fileName:'image.png' }
>> ]
>
> About STON usage, I wonder how it works in terms of a data structure evolution. Currently I use JSON format to store a data permanently. The data are supposed to be accesible for a long time (years). I am pretty sure that the protocol will evolve and we will end up with several JSON versions. I think that having different JSON mapping schemes will allow to map old JSON versions to a latest object structure (or to several different versions if necessary).
>
> For the example above, think of the scenario that PngAttachment does not exist anymore. In a simple scenario there could be an ImageAttachment class instead with the same instance variables. In a more complex scenario, there could be a AttachmentTwo { #type: AttachmentTwoType { … instances … }, #path: AttachmentTwoPath { … instances … } }.
>
> I can use JSON mappings that transform the old JSON version to the new class structure. I am not sure if STON is that flexible.
> What do you think? Do I miss something?

STON has no options for more than one mapping (the 'system' one).

Schema evolution is always a hard problem, using temporary structures seems the simplest solution.

The STON specification has not changed significantly since I first wrote it years ago. The implementation and the number of cases handled has though.

>> Still, the question of how to do this kind of dynamic decoding is an interesting challenge. Please update to the following commits:
>>
>> ===
>> Name: Neo-JSON-Core-SvenVanCaekenberghe.44
>> Author: SvenVanCaekenberghe
>> Time: 22 September 2017, 3:24:51.679449 pm
>> UUID: f1ebeade-2816-0d00-a04c-ae370e598362
>> Ancestors: Neo-JSON-Core-SvenVanCaekenberghe.42
>>
>> Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)
>>
>> Add NeoJSONStreamingWriter>>#writeElementAs:
>>
>> Add NeoJSONMappingTests>>#testDynamicTyping as an example
>> ===
>> Name: Neo-JSON-Tests-SvenVanCaekenberghe.41
>> Author: SvenVanCaekenberghe
>> Time: 22 September 2017, 3:25:09.881494 pm
>> UUID: a9a900e0-2816-0d00-a04d-a6fb0e598362
>> Ancestors: Neo-JSON-Tests-SvenVanCaekenberghe.39
>>
>> Implement some missing code in NeoJSONCustomMapping (the writing part of #listOfElementSchema: #listOfType:andElementSchema: #mapWithValueSchema:)
>>
>> Add NeoJSONStreamingWriter>>#writeElementAs:
>>
>> Add NeoJSONMappingTests>>#testDynamicTyping as an example
>> ===
>>
>> Now you can do as follows:
>>
>> NeoJSONMappingTests>>#testDynamicTyping
>> | data customMapping json result |
>> data := Array with: #foo->1 with: #(foo 2).
>> "The idea is to map a key value combination as either a classic association or a simple pair,
>>  using key & value properties as well as a type property to distinguish between the two"
>> customMapping := [ :mapper |
>>   mapper
>>     for: #AssocOrPair customDo: [ :mapping |
>>       mapping
>>         encoder: [ :x |
>>           x isArray
>>             ifTrue: [ { #type->#pair. #key->x first. #value->x second } asDictionary ]
>>             ifFalse: [ { #type->#assoc. #key->x key. #value->x value } asDictionary ] ];
>>         decoder: [ :x |
>>           (x at: #type) = #pair
>>             ifTrue: [ Array with: (x at: #key) with: (x at: #value) ]
>>             ifFalse: [ (x at: #key) -> (x at: #value)] ] ];
>>     for: #ArrayOfAssocOrPair customDo: [ :mapping |
>>       mapping listOfType: Array andElementSchema: #AssocOrPair ];
>>     yourself ].
>> json := String streamContents: [ :out |
>>   (customMapping value: (NeoJSONWriter on: out)) nextPut: data as: #ArrayOfAssocOrPair ].
>> result := (customMapping value: (NeoJSONReader on: json readStream)) nextAs: #ArrayOfAssocOrPair.
>> self assert: result equals: data
>>
>> Everything is virtual (not implemented as methods on actual classes) which makes the example self contained and non intrusive, but not very elegant, nor object oriented or extendable. Also, it is not as efficient as NeoJSON mapping was designed for, since it creates intermediate structures.
>
> Nice example. If #decoder: and #encoder: are used, is it still possible to use inner mappings? For example in my previous example, one instance variable is a ZnUrl instance that is stored as a String. Can I say “use ZnUrl mapping in for this intermediate structure”? Well, I understand that I can hard code it directly in the the #decoder: and #encoder: messages using #asZnUrl and #asString messages. I am thinking about a more complicated structures that involves more mapping definitions.

Yes, other mappings should be respected.

Sven

> Thanks!
> Juraj
>
>>
>> HTH,
>>
>> Sven
>>
>>> On 21 Sep 2017, at 22:07, Juraj Kubelka <[hidden email]> wrote:
>>>
>>> Hi,
>>>
>>> By studying the NeoJSON book chapter (Pharo Enterprise), I do not understand how to modify the following example:
>>>
>>> -=-=-=-=-=-=-=-
>>> "Let us say that we have an Attachment class..."
>>>
>>> Object subclass: #Attachment
>>> instanceVariableNames: 'url fileName'
>>> classVariableNames: ''
>>> package: 'NeoJSON-Use-Case'.
>>>
>>> "...with url: and fileName: methods."
>>> Attachment compile: 'url: anObject', String cr, String tab, 'url := anObject' classified: 'accessing'.
>>> Attachment compile: 'fileName: anObject', String cr, String tab, 'fileName := anObject' classified: 'accessing'.
>>>
>>> "Let's create a collection of two instances:"
>>> collectionOne := {
>>> Attachment new
>>> url: 'http://example.com/random-name.txt' asZnUrl;
>>> fileName: 'chapter-one.txt'
>>> yourself.
>>> Attachment new
>>> url: 'http://example.com/random-name.png' asZnUrl;
>>> fileName: 'image.png';
>>> yourself.
>>> }.
>>>
>>> "And let's map it to a JSON structure:"
>>> String streamContents: [ :aStream |
>>> (NeoJSONWriter on: aStream)
>>> for: #CollectionOfAttachments customDo: [ :mapping |
>>> mapping listOfElementSchema: Attachment ];
>>> mapAllInstVarsFor: Attachment;
>>> for: ZnUrl customDo: [ :mapping |
>>> mapping encoder: [ :aZnUrl |
>>> aZnUrl asString ] ];
>>> nextPut: collectionOne as: #CollectionOfAttachments.
>>> ].
>>>
>>> "And read the JSON structure:"
>>> (NeoJSONReader on: '[{"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt"},{"url":"http://example.com/random-name.png","fileName":"image.png"}]' readStream)
>>> for: #CollectionOfAttachments customDo: [ :mapping |
>>> mapping listOfElementSchema: Attachment ];
>>> for: Attachment do: [ :mapping |
>>> mapping mapInstVar: 'fileName'.
>>> (mapping mapInstVar: 'url') valueSchema: ZnUrl ];
>>> for: ZnUrl customDo: [ :mapping |
>>> mapping decoder: [ :string |
>>> string asZnUrl ] ];
>>> nextAs: #CollectionOfAttachments.
>>>
>>> "======================="
>>> “The previous example works perfectly, including the ZnUrl mapping (notice that url variables have ZnUrl instances).
>>>
>>> Now, let's say that we want to distinguish PNG and TXT attachments.
>>> For that reason we will create two Attachment subclasses..."
>>>
>>> Attachment subclass: #PngAttachment
>>> instanceVariableNames: ''
>>> classVariableNames: ''
>>> package: 'NeoJSON-Use-Case'.
>>>
>>> Attachment subclass: #TxtAttachment
>>> instanceVariableNames: ''
>>> classVariableNames: ''
>>> package: 'NeoJSON-Use-Case'.
>>>
>>> "...with type methods that might be used in a new JSON structure:"
>>> PngAttachment compile: 'type', String cr, String tab, '^ ''png''' classified: 'accessing'.
>>> TxtAttachment compile: 'type', String cr, String tab, '^ ''txt''' classified: 'accessing'.
>>>
>>> "Let's create a collection with the PNG and TXT instances:"
>>> collectionTwo := {
>>> TxtAttachment new
>>> url: 'http://example.com/random-name.txt' asZnUrl;
>>> fileName: 'chapter-one.txt'
>>> yourself.
>>> PngAttachment new
>>> url: 'http://example.com/random-name.png' asZnUrl;
>>> fileName: 'image.png';
>>> yourself.
>>> }.
>>>
>>> "How can I modify NeoJSONWriter and NeoJSONReader mappings?
>>>
>>> The JSON structure should (ideally) looks like this:"
>>>
>>> '[
>>> {“type”:”txt”,"url":"http://example.com/random-name.txt","fileName":"chapter-one.txt”},
>>> {“type”:”png”,"url":"http://example.com/random-name.png","fileName":"image.png”}
>>> ]'
>>> -=-=-=-=-=-=-=-
>>>
>>> I would like to keep the mapping isolated (without defining neoJsonOn: methods).
>>>
>>> Thank you!
>>> Juraj