Merge: serialization: redef inspect for an output useful to humans
authorJean Privat <jean@pryen.org>
Mon, 18 Sep 2017 19:00:47 +0000 (15:00 -0400)
committerJean Privat <jean@pryen.org>
Mon, 18 Sep 2017 19:00:47 +0000 (15:00 -0400)
Intro a new serialization engine to pretty print the attributes of the receiver on calls to `inspect`. The format is designed to be helpful, reproducible and compact.

Note that, asides from direct calls to `inspect`, this will also affect the default behavior of `Serializable::to_s`. And this will not affect all Nit programs, but it will affect all those importing the serialization services, so most larger projects using either `json`, `nitcorn`, `gamnit`, `more_collections`, etc.

Simple immutable data are inspected as they would be written in Nit code:
~~~
assert 123.inspect == "123"
assert 1.5.inspect == "1.5"
assert 0xa1u8.inspect == "0xa1u8"
assert 'c'.inspect == "'c'"
assert "asdf\n".inspect == "\"asdf\\n\""
~~~

Inspections of mutable object show their dynamic type and an id unique to each call to `inspect`. A change from using their `object_id`.

Items of collections are flattened:

~~~
assert [1, 2, 3].inspect == "<Array[Int]#0 [1, 2, 3]>"

var set = new HashSet[Object].from([1, 1.5, "two": Object])
assert set.inspect == """<HashSet[Object]#0 [1, 1.5, "two"]>"""

var map = new Map[Int, String]
map[1] = "one"
map[2] = "two"
assert map.inspect == """<HashMap[Int, String]#0 {1:"one", 2:"two"}>"""
~~~

Inspecting other `Serializable` classes shows the first level attributes only:

~~~
class MyClass
    serialize

    var i: Int
    var o: nullable Object
end

var class_with_null = new MyClass(123)
assert class_with_null.to_s == class_with_null.inspect
assert class_with_null.to_s == "<MyClass#0 i:123, o:null>"

var class_with_other = new MyClass(456, class_with_null)
assert class_with_other.to_s == "<MyClass#0 i:456, o:<MyClass#1>>"

var class_with_cycle = new MyClass(789)
class_with_cycle.o = class_with_cycle
assert class_with_cycle.to_s == "<MyClass#0 i:789, o:<MyClass#0>>"
~~~

Inspections producing over 80 characters are cut short:

~~~
var long_class = new MyClass(123456789, "Some " + "very "*8 + "long string")
assert long_class.to_s == "<MyClass#0 i:123456789, o:\"Some very very very very very very very very long s…>"
~~~

Pull-Request: #2548
Reviewed-by: Jean Privat <jean@pryen.org>

16 files changed:
contrib/benitlux/src/client/views/home_views.nit
contrib/tnitter/src/tnitter_app.nit
examples/calculator/art/icon-sci.svg
examples/calculator/src/calculator.nit
lib/android/data_store.nit
lib/android/shared_preferences/shared_preferences_api10.nit
lib/app/data_store.nit
lib/app/examples/ui_example.nit
lib/app/http_request.nit
lib/app/ui.nit
lib/core/bytes.nit
lib/core/stream.nit
lib/core/text/flat.nit
lib/ios/data_store.nit
lib/linux/data_store.nit
src/highlight.nit

index 402c173..460e1be 100644 (file)
@@ -19,24 +19,9 @@ import beer_views
 import social_views
 import user_views
 
-redef class App
-       redef fun on_create
-       do
-               if debug then print "App::on_create"
-
-               # Create the main window
-               show_home
-               super
-       end
-
-       # Show the home/main windows
-       fun show_home
-       do
-               var window = new HomeWindow
-               window.refresh
-               push_window window
-       end
+redef fun root_window do return new HomeWindow
 
+redef class App
        redef fun on_log_in
        do
                super
index 8218004..760af3d 100644 (file)
@@ -38,18 +38,7 @@ import model
 # Delay in seconds before the next request after an error
 fun request_delay_on_error: Float do return 60.0
 
-redef class App
-       redef fun on_create
-       do
-               # Create the main window
-               push_window new TnitterWindow
-               super
-       end
-end
-
-# Main window
-class TnitterWindow
-       super Window
+redef class Window
 
        private var layout = new VerticalLayout(parent=self)
        private var list_posts = new ListLayout(parent=layout)
@@ -98,7 +87,7 @@ fun tnitter_server_uri: String do return "http://localhost:8080"
 abstract class AsyncTnitterRequest
        super AsyncHttpRequest
 
-       private var window: TnitterWindow
+       private var window: Window
 
        redef fun uri_root do return tnitter_server_uri
 
index 187bb2d..3358c6c 100644 (file)
@@ -13,8 +13,8 @@
    height="512"
    id="svg2"
    version="1.1"
-   inkscape:version="0.48.5 r10040"
-   sodipodi:docname="icon_sci.svg">
+   inkscape:version="0.92.1 r15371"
+   sodipodi:docname="icon-sci.svg">
   <defs
      id="defs4" />
   <sodipodi:namedview
@@ -25,7 +25,7 @@
      inkscape:pageopacity="0.0"
      inkscape:pageshadow="2"
      inkscape:zoom="1.4"
-     inkscape:cx="164.13268"
+     inkscape:cx="165.56125"
      inkscape:cy="278.8294"
      inkscape:document-units="px"
      inkscape:current-layer="layer1"
          id="path2999"
          style="" />
     </g>
-    <text
-       xml:space="preserve"
-       style="font-size:190.84666443px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
-       x="318.1875"
-       y="744.69647"
-       id="text2994"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan2996"
-         x="318.1875"
-         y="744.69647">√</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:190.84666443px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
-       x="72.978729"
-       y="725.84735"
-       id="text2998"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3000"
-         x="72.978729"
-         y="725.84735">π</tspan></text>
+    <path
+       inkscape:connector-curvature="0"
+       id="path4504"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+       d="M 376.61565,748.14397 H 364.2218 L 338.40905,675.6446 h -16.77364 v -13.32572 h 27.30375 l 21.52616,62.80794 47.71167,-136.14598 h 13.60528 z" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path4507"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+       d="m 181.35503,716.1555 q 4.93891,0 8.38682,-1.86373 v 13.13934 q -4.56616,2.42286 -12.58022,2.42286 -21.15342,0 -21.15342,-24.41495 V 639.6491 h -43.33188 v 88.34113 H 96.089068 V 639.6491 H 75.3084 v -7.26857 l 13.698467,-6.70945 H 192.63064 v 13.97802 h -20.03517 v 64.67168 q 0,11.83472 8.75956,11.83472 z" />
     <flowRoot
        xml:space="preserve"
        id="flowRoot3002"
            height="282.85715"
            x="-225"
            y="-62.285713" /></flowRegion><flowPara
-         id="flowPara3008"></flowPara></flowRoot>    <text
-       xml:space="preserve"
-       style="font-size:190.84666443px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
-       x="303.01755"
-       y="973.79059"
-       id="text3014"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3016"
-         x="303.01755"
-         y="973.79059">x²</tspan></text>
+         id="flowPara3008" /></flowRoot>    <path
+       inkscape:connector-curvature="0"
+       id="path4499"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+       d="m 335.60349,924.36988 -35.31781,-50.04133 h 19.1033 l 26.37188,38.57935 26.09232,-38.57935 h 18.91693 l -35.31782,50.04133 37.27474,52.27783 h -19.1033 l -27.86287,-40.81584 -28.23562,40.81584 h -18.91693 z" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path4501"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+       d="m 454.6031,922.04021 h -53.862 v -10.25055 l 21.99209,-21.43298 q 11.83473,-11.46198 15.09627,-16.68044 3.35473,-5.31165 3.35473,-11.74154 0,-6.05715 -3.5411,-9.13232 -3.44791,-3.07516 -9.2255,-3.07516 -5.77758,0 -10.43693,2.23648 -4.65934,2.23649 -9.59824,5.96396 l -6.70945,-8.75956 q 12.39385,-10.53012 26.931,-10.53012 12.30066,0 19.19649,6.15034 6.98901,6.05714 6.98901,16.40088 0,4.2866 -1.21143,8.01407 -1.11824,3.63429 -3.5411,7.45495 -2.42286,3.72747 -6.33671,8.01407 -3.82066,4.28659 -26.37187,25.53319 h 37.27474 z" />
   </g>
 </svg>
index 6cfb1aa..32799ca 100644 (file)
@@ -32,16 +32,7 @@ import calculator_logic
 # Show debug output?
 fun debug: Bool do return false
 
-redef class App
-       redef fun on_create
-       do
-               if debug then print "App::on_create"
-
-               # Create the main window
-               push_window new CalculatorWindow
-               super
-       end
-end
+redef fun root_window do return new CalculatorWindow
 
 # The main (and only) window of this calculator
 class CalculatorWindow
index 989b64c..cbef585 100644 (file)
 module data_store
 
 import app::data_store
-private import shared_preferences
+import shared_preferences
 
-redef class App
-       redef var data_store = new SharedPreferenceView
-end
-
-private class SharedPreferenceView
-       super DataStore
+redef class DataStore
 
        # The `SharedPreferences` used to implement the `DataStore`
        var shared_preferences = new SharedPreferences.privately(app, "data_store") is lazy
index f17661f..0464b67 100644 (file)
@@ -397,7 +397,16 @@ class SharedPreferences
                if serialized_string == "" then return null
 
                var deserializer = new JsonDeserializer(serialized_string)
-               return deserializer.deserialize
+               var deserialized = deserializer.deserialize
+
+               var errors = deserializer.errors
+               if errors.not_empty then
+                       # An update may have broken the versioning compatibility
+                       print_error "{class_name} error at deserialization: {errors.join(", ")}"
+                       return null # Let's be safe
+               end
+
+               return deserialized
        end
 end
 
index ebebe83..cb5ce2a 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Simple data storage services
+# Key/value storage services
 #
-# The implementation varies per platform.
+# The main services is `App::data_store`, a `DataStore` holding any
+# serializable Nit object.
 module data_store
 
 import app_base
@@ -28,12 +29,61 @@ import android::data_store is conditional(android)
 import ios::data_store is conditional(ios)
 
 redef class App
+
        # Services to store and load data
-       fun data_store: DataStore is abstract
+       #
+       # ~~~
+       # import app::ui
+       # import app::data_store
+       #
+       # class MyWindow
+       #     super Window
+       #
+       #     var state = "Simple string or any serializable class"
+       #
+       #     redef fun on_save_state do app.data_store["state"] = state
+       #
+       #     redef fun on_restore_state
+       #     do
+       #         var state = app.data_store["state"]
+       #         if state isa String then self.state = state
+       #     end
+       # end
+       # ~~~
+       var data_store = new DataStore is lazy
 end
 
 # Simple data storage facility
-interface DataStore
+#
+# Write values with `[]=` and read with `[]`.
+# ~~~
+# import linux::data_store # Needed for testing only
+#
+# class A
+#     serialize
+#
+#     var b = true
+#     var f = 1.234
+# end
+#
+# var data_store = new DataStore
+# data_store["one"] = 1
+# data_store["str"] = "Some string"
+# data_store["a"] = new A
+#
+# assert data_store["one"] == 1
+# assert data_store["str"] == "Some string"
+# assert data_store["a"].as(A).b
+# assert data_store["a"].as(A).f == 1.234
+# assert data_store["other"] == null
+# ~~~
+#
+# Set to `null` to clear a value.
+# ~~~
+# data_store["one"] = null
+# assert data_store["one"] == null
+# ~~~
+class DataStore
 
        # Get the object stored at `key`, or null if nothing is available
        fun [](key: String): nullable Object is abstract
index 43628af..5d01592 100644 (file)
@@ -16,7 +16,9 @@
 module ui_example is
        app_name "app.nit UI"
        app_namespace "org.nitlanguage.ui_example"
-       android_api_target 15
+       android_api_min 21
+       android_api_target 21
+       android_manifest_activity "android:theme=\"@android:style/Theme.Material\""
 end
 
 import app::ui
@@ -31,7 +33,7 @@ class UiExampleWindow
        var layout = new ListLayout(parent=self)
 
        # Some label
-       var some_label = new Label(parent=layout, text="This Window uses a ListLayout.")
+       var some_label = new Label(parent=layout, text="Sample Window using a ListLayout.")
 
        # A checkbox
        var checkbox = new CheckBox(parent=layout, text="A CheckBox")
@@ -82,11 +84,4 @@ class SecondWindow
        var another_label = new Label(parent=layout, text="Close it by tapping the back button.")
 end
 
-redef class App
-       redef fun on_create
-       do
-               # Create the main window
-               push_window new UiExampleWindow
-               super
-       end
-end
+redef fun root_window do return new UiExampleWindow
index 8ab0f1d..07d4bf3 100644 (file)
 # limitations under the License.
 
 # HTTP request services: `AsyncHttpRequest` and `Text::http_get`
+#
+# ~~~nitish
+# import app::http_request
+#
+# class MyHttpRequest
+#     super AsyncHttpRequest
+#
+#     redef fun uri do return "http://example.com/"
+#
+#     redef fun on_load(data, status) do print "Received: {data or else "null"}"
+#
+#     redef fun on_fail(error) do print "Connection error: {error}"
+# end
+#
+# var req = new MyHttpRequest
+# req.start
+# ~~~
 module http_request
 
 import app_base
@@ -122,9 +139,14 @@ abstract class AsyncHttpRequest
        fun after do end
 end
 
-# Minimal implementation of `AsyncHttpRequest` where `uri` is an attribute
+# Simple `AsyncHttpRequest` where `uri` is an attribute
+#
+# Prints on communication errors and when the remote server returns an
+# HTTP status code not in the 200s.
 #
-# Prints on communication errors and when the server returns an HTTP status code not in the 200s.
+# This class can be instantiated to execute a request where the response is
+# ignored by the application. Alternatively, it can be subclassed to implement
+# `on_load`.
 #
 # ~~~nitish
 # var request = new SimpleAsyncHttpRequest("http://example.com")
index 1fdf801..42e36ce 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Portable UI API for the _app.nit_ framework
+# Portable UI controls for mobiles apps
+#
+# ~~~
+# import app::ui
+#
+# class MyWindow
+#     super Window
+#
+#     var layout = new ListLayout(parent=self)
+#     var lbl = new Label(parent=layout, text="Hello world", align=0.5)
+#     var but = new Button(parent=layout, text="Press here")
+#
+#     redef fun on_event(event) do lbl.text = "Pressed!"
+# end
+#
+# redef fun root_window do return new MyWindow
+# ~~~
 module ui
 
 import app_base
@@ -30,14 +46,14 @@ redef class App
        # This attribute is set by `push_window`.
        var window: Window is noinit
 
-       # Make visible and push `window` on the top of `pop_window`
+       # Make `window` visible and push it on the top of the `window_stack`
        #
-       # This method must be called at least once within `App::on_create`.
-       # It can be called at any times while the app is active.
+       # This method can be called at any times while the app is active.
        fun push_window(window: Window)
        do
                window_stack.add window
                self.window = window
+               window.on_create
        end
 
        # Pop the current `window` from the stack and show the previous one
@@ -54,7 +70,11 @@ redef class App
        # Stack of active windows
        var window_stack = new Array[Window]
 
-       redef fun on_create do window.on_create
+       redef fun on_create
+       do
+               var window = root_window
+               push_window window
+       end
 
        redef fun on_resume do window.on_resume
 
@@ -67,6 +87,13 @@ redef class App
        redef fun on_save_state do window.on_save_state
 end
 
+# Hook to create the first window shown to the user
+#
+# By default, a `Window` is created, which can be refined to customize it.
+# However, most apps should refine this method to return a different window,
+# this way the app can have more than one window.
+fun root_window: Window do return new Window
+
 # An event created by an `AppComponent` and sent to `AppObserver`s
 interface AppEvent
 end
@@ -173,6 +200,9 @@ class CompositeControl
 end
 
 # A window, root of the `Control` tree
+#
+# Each window should hold a single control, usually a `CompositeControl`,
+# which in turn holds all the displayed controls.
 class Window
        super CompositeControl
 
@@ -183,7 +213,7 @@ class Window
        fun on_back_button do app.pop_window
 end
 
-# A viewable `Control`
+# A visible `Control`
 abstract class View
        super Control
 
@@ -193,7 +223,9 @@ abstract class View
        var enabled: nullable Bool is writable, abstract, autoinit
 end
 
-# A control with some `text`
+# A control displaying some `text`
+#
+# For a text-only control, see `Label`.
 abstract class TextView
        super View
 
@@ -233,17 +265,17 @@ class TextInput
        var is_password: nullable Bool is writable
 end
 
-# A pushable button, raises `ButtonPressEvent`
+# A pressable button, raises `ButtonPressEvent`
 class Button
        super TextView
 end
 
-# A text label
+# A simple text label
 class Label
        super TextView
 end
 
-# Toggle control with two states and a label
+# Toggle control between two states, also displays a label
 class CheckBox
        super TextView
 
index 5b5fceb..e375e60 100644 (file)
@@ -214,7 +214,7 @@ class Bytes
                return slice(st, ed - st + 1)
        end
 
-       # Returns a subset of the content of `self` starting at `from` and of length `count`
+       # Copy a subset of `self` starting at `from` and of `count` bytes
        #
        #     var b = "abcd".to_bytes
        #     assert b.slice(1, 2).hexdigest == "6263"
@@ -239,7 +239,7 @@ class Bytes
                return ret
        end
 
-       # Returns a copy of `self` starting at `from`
+       # Copy of `self` starting at `from`
        #
        #     var b = "abcd".to_bytes
        #     assert b.slice_from(1).hexdigest  == "626364"
@@ -453,18 +453,15 @@ class Bytes
                length += ln
        end
 
-       # Appends the bytes of `s` to `selftextextt`
-       fun append_text(s: Text) do
-               for i in s.substrings do
-                       append_ns(i.fast_cstring, i.byte_length)
-               end
-       end
+       # Appends the bytes of `str` to `self`
+       fun append_text(str: Text) do str.append_to_bytes self
 
        redef fun append_to(b) do b.append self
 
        redef fun enlarge(sz) do
                if capacity >= sz then return
                persisted = false
+               if capacity < 16 then capacity = 16
                while capacity < sz do capacity = capacity * 2 + 2
                var ns = new CString(capacity)
                items.copy_to(ns, length, 0, 0)
@@ -931,7 +928,7 @@ end
 redef class FlatText
        redef fun append_to_bytes(b) do
                var from = if self isa FlatString then first_byte else 0
-               b.append_ns_from(items, byte_length, from)
+               if isset _items then b.append_ns_from(items, byte_length, from)
        end
 end
 
index 80b96f0..b006398 100644 (file)
@@ -662,81 +662,172 @@ abstract class Duplex
        super Writer
 end
 
-# `Stream` that can be used to write to a `String`
+# Write to `bytes` in memory
 #
-# Mainly used for compatibility with Writer type and tests.
-class StringWriter
+# ~~~
+# var writer = new BytesWriter
+#
+# writer.write "Strings "
+# writer.write_char '&'
+# writer.write_byte 0x20u8
+# writer.write_bytes "bytes".to_bytes
+#
+# assert writer.to_s == "\\x53\\x74\\x72\\x69\\x6E\\x67\\x73\\x20\\x26\\x20\\x62\\x79\\x74\\x65\\x73"
+# assert writer.bytes.to_s == "Strings & bytes"
+# ~~~
+#
+# As with any binary data, UTF-8 code points encoded on two bytes or more
+# can be constructed byte by byte.
+#
+# ~~~
+# writer = new BytesWriter
+#
+# # Write just the character first half
+# writer.write_byte 0xC2u8
+# assert writer.to_s == "\\xC2"
+# assert writer.bytes.to_s == "�"
+#
+# # Complete the character
+# writer.write_byte 0xA2u8
+# assert writer.to_s == "\\xC2\\xA2"
+# assert writer.bytes.to_s == "¢"
+# ~~~
+class BytesWriter
        super Writer
 
-       private var content = new Buffer
-       redef fun to_s do return content.to_s
-       redef fun is_writable do return not closed
+       # Written memory
+       var bytes = new Bytes.empty
 
-       redef fun write_bytes(b) do
-               content.append(b.to_s)
-       end
+       redef fun to_s do return bytes.chexdigest
 
        redef fun write(str)
        do
-               assert not closed
-               content.append(str)
+               if closed then return
+               str.append_to_bytes bytes
        end
 
        redef fun write_char(c)
        do
-               assert not closed
-               content.add(c)
+               if closed then return
+               bytes.add_char c
+       end
+
+       redef fun write_byte(value)
+       do
+               if closed then return
+               bytes.add value
+       end
+
+       redef fun write_bytes(b)
+       do
+               if closed then return
+               bytes.append b
        end
 
        # Is the stream closed?
        protected var closed = false
 
        redef fun close do closed = true
+       redef fun is_writable do return not closed
 end
 
-# `Stream` used to read from a `String`
+# `Stream` writing to a `String`
 #
-# Mainly used for compatibility with Reader type and tests.
-class StringReader
+# This class has the same behavior as `BytesWriter`
+# except for `to_s` which decodes `bytes` to a string.
+#
+# ~~~
+# var writer = new StringWriter
+#
+# writer.write "Strings "
+# writer.write_char '&'
+# writer.write_byte 0x20u8
+# writer.write_bytes "bytes".to_bytes
+#
+# assert writer.to_s == "Strings & bytes"
+# ~~~
+class StringWriter
+       super BytesWriter
+
+       redef fun to_s do return bytes.to_s
+end
+
+# Read from `bytes` in memory
+#
+# ~~~
+# var reader = new BytesReader(b"a…b")
+# assert reader.read_char == 'a'
+# assert reader.read_byte == 0xE2u8 # 1st byte of '…'
+# assert reader.read_byte == 0x80u8 # 2nd byte of '…'
+# assert reader.read_char == '�' # Reads the last byte as an invalid char
+# assert reader.read_all_bytes == b"b"
+# ~~~
+class BytesReader
        super Reader
 
-       # The string to read from.
-       var source: String
+       # Source data to read
+       var bytes: Bytes
 
-       # The current position in the string (bytewise).
-       private var cursor: Int = 0
+       # The current position in `bytes`
+       private var cursor = 0
 
-       redef fun read_char do
-               if cursor < source.length then
-                       # Fix when supporting UTF-8
-                       var c = source[cursor]
-                       cursor += 1
-                       return c
-               else
-                       return null
-               end
-       end
+       redef fun read_char
+       do
+               if cursor >= bytes.length then return null
 
-       redef fun read_byte do
-               if cursor < source.length then
-                       var c = source.bytes[cursor]
-                       cursor += 1
-                       return c
-               else
-                       return null
-               end
+               var len = bytes.items.length_of_char_at(cursor)
+               var char = bytes.items.char_at(cursor)
+               cursor += len
+               return char
        end
 
-       redef fun close do
-               source = ""
+       redef fun read_byte
+       do
+               if cursor >= bytes.length then return null
+
+               var c = bytes[cursor]
+               cursor += 1
+               return c
        end
 
-       redef fun read_all_bytes do
-               var nslen = source.length - cursor
-               var nns = new CString(nslen)
-               source.copy_to_native(nns, nslen, cursor, 0)
-               return new Bytes(nns, nslen, nslen)
+       redef fun close do bytes = new Bytes.empty
+
+       redef fun read_all_bytes
+       do
+               var res = bytes.slice_from(cursor)
+               cursor = bytes.length
+               return res
        end
 
-       redef fun eof do return cursor >= source.byte_length
+       redef fun eof do return cursor >= bytes.length
+end
+
+# `Stream` reading from a `String` source
+#
+# This class has the same behavior as `BytesReader`
+# except for its constructor accepting a `String`.
+#
+# ~~~
+# var reader = new StringReader("a…b")
+# assert reader.read_char == 'a'
+# assert reader.read_byte == 0xE2u8 # 1st byte of '…'
+# assert reader.read_byte == 0x80u8 # 2nd byte of '…'
+# assert reader.read_char == '�' # Reads the last byte as an invalid char
+# assert reader.read_all == "b"
+# ~~~
+class StringReader
+       super BytesReader
+
+       autoinit source
+
+       # Source data to read
+       var source: String
+
+       init do bytes = source.to_bytes
+
+       redef fun close
+       do
+               source = ""
+               super
+       end
 end
index 06cdcfe..b728e42 100644 (file)
@@ -1205,7 +1205,7 @@ private class FlatBufferByteIterator
 
        var curr_pos: Int
 
-       init do target_items = target._items
+       init do if isset target._items then target_items = target._items
 
        redef fun index do return curr_pos
 
index a06586c..e4316ea 100644 (file)
@@ -19,12 +19,7 @@ import app::data_store
 import cocoa::foundation
 private import json
 
-redef class App
-       redef var data_store = new UserDefaultView
-end
-
-private class UserDefaultView
-       super DataStore
+redef class DataStore
 
        # The `NSUserDefaults` used to implement `DataStore`
        var user_defaults = new NSUserDefaults.standard_user_defaults is lazy
@@ -37,7 +32,16 @@ private class UserDefaultView
 
                # TODO report errors
                var deserializer = new JsonDeserializer(nsstr.to_s)
-               return deserializer.deserialize
+               var deserialized = deserializer.deserialize
+
+               var errors = deserializer.errors
+               if errors.not_empty then
+                       # An update may have broken the versioning compatibility
+                       print_error "{class_name} error at deserialization: {errors.join(", ")}"
+                       return null # Let's be safe
+               end
+
+               return deserialized
        end
 
        redef fun []=(key, value)
index 5924822..95ddeb1 100644 (file)
@@ -19,15 +19,10 @@ module data_store
 
 import app::data_store
 private import xdg_basedir
-private import sqlite3
+import sqlite3
 private import json
 
-redef class App
-       redef var data_store = new LinuxStore
-end
-
-private class LinuxStore
-       super DataStore
+redef class DataStore
 
        # File path of the Sqlite3 DB file
        fun db_file: String do return "data_store.db"
@@ -35,7 +30,7 @@ private class LinuxStore
        # Sqlite3 table used
        fun db_table: String do return "data_store"
 
-       var db_cache: nullable Sqlite3DB = null
+       private var db_cache: nullable Sqlite3DB = null
 
        # Database to use to implement the `DataStore`
        fun db: nullable Sqlite3DB
@@ -71,6 +66,7 @@ private class LinuxStore
 
                # Prepare SELECT statement
                var stmt = db.select("* FROM {db_table} WHERE key == {key.to_sql_string}")
+               if stmt == null then return null
 
                # Execute statment
                for row in stmt do
@@ -82,6 +78,13 @@ private class LinuxStore
                        var deserializer = new JsonDeserializer(serialized)
                        var deserialized = deserializer.deserialize
 
+                       var errors = deserializer.errors
+                       if errors.not_empty then
+                               # An update may have broken the versioning compatibility
+                               print_error "{class_name} error at deserialization: {errors.join(", ")}"
+                               return null # Let's be safe
+                       end
+
                        return deserialized
                end
 
index 3005aa7..f654c3e 100644 (file)
@@ -21,14 +21,16 @@ import pipeline
 import astutil
 import serialization
 
-# Fully process a content as a nit source file.
-fun hightlightcode(hl: HighlightVisitor, content: String): HLCode
+# Fully process `content` as a Nit source file.
+#
+# Set `print_errors = true` to print errors in the code to the console.
+fun hightlightcode(hl: HighlightVisitor, content: String, print_errors: nullable Bool): HLCode
 do
        # Prepare a stand-alone tool context
        var tc = new ToolContext
        tc.nit_dir = tc.locate_nit_dir # still use the common lib to have core
        tc.keep_going = true # no exit, obviously
-       tc.opt_warn.value = -1 # no output, obviously
+       if print_errors != true then tc.opt_warn.value = -1 # no output
 
        # Prepare an stand-alone model and model builder.
        # Unfortunately, models are enclosing and append-only.