Merge: nitweb: better namespaces
authorJean Privat <jean@pryen.org>
Thu, 1 Sep 2016 18:50:21 +0000 (14:50 -0400)
committerJean Privat <jean@pryen.org>
Thu, 1 Sep 2016 18:50:21 +0000 (14:50 -0400)
Before this PR, the namespace composition (adding html things to the MEntity::full_name) was handled by Angular. Namespaces are a pain in the nit to generate, even more when you don't have access to the model... So the solution was to delegate that work to the API.

With this PR, nitweb attach a Namespace object to each MEntity so Angular knows how to render it.
The idea is pretty simple, a namespace is an array of either:

* a string for "::", "$" ...
* an mentity reference like `Array`
* another namespace for recursive definitions

Demo is here: http://nitweb.moz-code.org/

Some examples:

* `core`: http://nitweb.moz-code.org/doc/core
* `core>`: http://nitweb.moz-code.org/doc/core>
* `core::core`: http://nitweb.moz-code.org/doc/core::core
* `core::Array`: http://nitweb.moz-code.org/doc/core::Array
* `core::Array::from`: http://nitweb.moz-code.org/doc/core::Array::from

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

14 files changed:
contrib/mnit_test/src/test_audio.nit
lib/android/audio.nit
lib/app/audio.nit
lib/json/serialization.nit
lib/linux/audio.nit
lib/serialization/serialization.nit
src/frontend/glsl_validation.nit
src/frontend/serialization_phase.nit
src/modelize/modelize_property.nit
src/semantize/typing.nit
tests/sav/niti/test_json_deserialization_plain_alt2.res
tests/sav/test_json_deserialization_plain.res
tests/sav/test_json_deserialization_plain_alt2.res
tests/test_json_deserialization_plain.nit

index 9ef5d92..ae94e70 100644 (file)
@@ -22,10 +22,10 @@ import android::audio
 
 redef class App
        # Sound
-       var soundsp: Sound
+       var soundsp: Sound is noinit
 
        # Music
-       var soundmp: Music
+       var soundmp: Music is noinit
 
        # Sound
        var easy_soundsp = new Sound("testsound")
@@ -47,8 +47,8 @@ redef class App
                super
                default_mediaplayer.looping = true
                if test_assets then
-                       soundsp = load_sound("testsound.og")
-                       soundmp = load_music("xylofon.og")
+                       soundsp = new Sound("testsound.og")
+                       soundmp = new Music("xylofon.og")
                        soundmp.play
                end
                if test_ressources then
index e985891..86e019a 100644 (file)
@@ -280,7 +280,7 @@ class SoundPool
                var resval = nsoundpool.load_path(path.to_java_string, priority)
                sys.jni_env.pop_local_frame
                if  resval == -1 then
-                       self.error = new Error("Unable to load sound from path : " + path)
+                       self.error = new Error("Unable to load sound from path: " + path)
                        return new Sound.priv_init(null, -1, self, self.error)
                else
                        return new Sound.priv_init(null, resval, self, null)
@@ -293,12 +293,12 @@ class SoundPool
                return nsoundpool.play(id, left_volume, right_volume, priority, looping, rate)
        end
 
-       # Load a sound by its name in the resources, the sound must be in the `res/raw` folder
-       fun load_name(resource_manager: ResourcesManager, context: NativeActivity, sound: String): Sound do
-               var id = resource_manager.raw_id(sound)
+       # Load a sound by its `name` in the resources, the sound must be in the `res/raw` folder
+       fun load_name(resource_manager: ResourcesManager, context: NativeActivity, name: String): Sound do
+               var id = resource_manager.raw_id(name)
                var resval = nsoundpool.load_id(context, id, priority)
                if  resval == -1 then
-                       self.error = new Error("Unable to load sound from resources : " + sound)
+                       self.error = new Error("Unable to load sound from resources: " + name)
                        return new Sound.priv_init(null, -1, self, self.error)
                else
                        return new Sound.priv_init(null, resval, self, null)
@@ -498,7 +498,7 @@ redef class PlayableAudio
        # Used when the app pause all sounds or resume all sounds
        var paused: Bool = false
 
-       redef init do add_to_sounds(self)
+       redef init do sounds.add self
 end
 
 redef class Sound
@@ -525,16 +525,16 @@ redef class Sound
 
        redef fun load do
                if is_loaded then return
-               var retval_resources = app.default_soundpool.load_name_rid(app.resource_manager, app.native_activity, self.name.strip_extension)
+               var retval_resources = app.default_soundpool.load_name_rid(app.resource_manager, app.native_activity, path.strip_extension)
                if retval_resources == -1 then
-                       self.error = new Error("failed to load" + self.name)
-                       var nam = app.asset_manager.open_fd(self.name)
+                       self.error = new Error("Failed to load " + path)
+                       var nam = app.asset_manager.open_fd(path)
                        if nam.is_java_null then
-                               self.error = new Error("Failed to get file descriptor for " + self.name)
+                               self.error = new Error("Failed to get file descriptor for " + path)
                        else
                                var retval_assets = app.default_soundpool.load_asset_fd_rid(nam)
                                if retval_assets == -1 then
-                                       self.error = new Error("Failed to load" + self.name)
+                                       self.error = new Error("Failed to load " + path)
                                else
                                        self.soundpool_id = retval_assets
                                        self.soundpool = app.default_soundpool
@@ -549,6 +549,9 @@ redef class Sound
                        self.soundpool.error = null
                end
                is_loaded = true
+
+               var error = error
+               if error != null then print_error error
        end
 
        redef fun play do
@@ -591,12 +594,12 @@ redef class Music
 
        redef fun load do
                if is_loaded then return
-               var mp_sound_resources = app.default_mediaplayer.load_sound(app.resource_manager.raw_id(self.name.strip_extension), app.native_activity)
+               var mp_sound_resources = app.default_mediaplayer.load_sound(app.resource_manager.raw_id(path.strip_extension), app.native_activity)
                if mp_sound_resources.error != null then
                        self.error = mp_sound_resources.error
-                       var nam = app.asset_manager.open_fd(self.name)
+                       var nam = app.asset_manager.open_fd(path)
                        if nam.is_java_null then
-                               self.error = new Error("Failed to get file descriptor for " + self.name)
+                               self.error = new Error("Failed to get file descriptor for " + path)
                        else
                                var mp_sound_assets = app.default_mediaplayer.data_source_fd(nam)
                                if mp_sound_assets.error != null then
@@ -613,6 +616,9 @@ redef class Music
                        self.media_player.error = null
                end
                is_loaded = true
+
+               var error = error
+               if error != null then print_error error
        end
 
        redef fun play do
@@ -658,38 +664,18 @@ redef class App
                App_native_activity(self).setVolumeControlStream(AudioManager.STREAM_MUSIC);
        `}
 
-       # Retrieves a sound with a soundpool in the `assets` folder using its name.
-       # Used to play short songs, can play multiple sounds simultaneously
-       redef fun load_sound(path) do
-               var fd = asset_manager.open_fd(path)
-               if not fd.is_java_null then
-                       return add_to_sounds(default_soundpool.load_asset_fd(fd)).as(Sound)
-               else
-                       var error = new Error("Failed to load Sound {path}")
-                       return new Sound.priv_init(null, -1, default_soundpool, error)
-               end
-       end
-
-       # Retrieves a music with a media player in the `assets` folder using its name.
-       # Used to play long sounds or musics, can't play multiple sounds simultaneously
-       redef fun load_music(path) do
-               var fd = asset_manager.open_fd(path)
-               if not fd.is_java_null then
-                       return add_to_sounds(default_mediaplayer.data_source_fd(fd)).as(Music)
-               else
-                       var error = new Error("Failed to load music {path}")
-                       return new Music.priv_init(null, default_mediaplayer, error)
-               end
-       end
-
        # Same as `load_sound` but load the sound from the `res/raw` folder
        fun load_sound_from_res(sound_name: String): Sound do
-               return add_to_sounds(default_soundpool.load_name(resource_manager,self.native_activity, sound_name)).as(Sound)
+               var sound = default_soundpool.load_name(resource_manager,self.native_activity, sound_name)
+               sys.sounds.add sound
+               return sound
        end
 
        # Same as `load_music` but load the sound from the `res/raw` folder
        fun load_music_from_res(music: String): Music do
-               return add_to_sounds(default_mediaplayer.load_sound(resource_manager.raw_id(music), self.native_activity)).as(Music)
+               var sound = default_mediaplayer.load_sound(resource_manager.raw_id(music), self.native_activity)
+               sys.sounds.add sound
+               return sound
        end
 
        redef fun on_pause do
@@ -728,10 +714,4 @@ redef class Sys
        # Sounds handled by the application, when you load a sound, it's added to this list.
        # This array is used in `pause` and `resume`
        private var sounds = new Array[PlayableAudio]
-
-       # Factorizes `sounds.add` to use it in `load_music`, `load_sound`, `load_music_from_res` and `load_sound_from_res`
-       private fun add_to_sounds(sound: PlayableAudio): PlayableAudio do
-               sounds.add(sound)
-               return sound
-       end
 end
index 7f22c78..17310d8 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# App audio abstraction
-# Default behaviour is loading the audio from the `assets` folder of the project with its name and extension
-# Platforms implementations can modify this comportement
+# Services to load and play `Sound` and `Music` from the assets folder
 #
-# Once the application has started (after `App.setup`)
-# use `App.load_sound` to get a sound
-# then `Sound.play` to play it
+# Get a handle to a sound using `new Sound` or `new Music` at any time.
+# Call `load` at or after `App::on_create` or leave it to be loaded
+# on demand by the first call to `play`.
 module audio
 
 import app_base
@@ -33,43 +31,34 @@ import android::audio is conditional(android)
 # Abstraction of a playable Audio
 abstract class PlayableAudio
 
-       # Name of this playable audio
-       var name: String
+       # Path to the audio file in the assets folder
+       var path: String
 
-       # Error which is not null if an error happened
+       # Last error on this sound, if any
        var error: nullable Error = null is writable
 
-       # Is this already loaded ?
+       # Is `self` already loaded?
        protected var is_loaded = false is writable
 
        # Load this playable audio
        fun load is abstract
 
-       # Plays the sound
+       # Play the sound
        fun play is abstract
 
-       # Pauses the sound
+       # Pause the sound
        fun pause is abstract
 
-       # Resumes the sound
+       # Resume the sound
        fun resume is abstract
 end
 
-# Abstraction of a short sound
+# Short sound
 class Sound
        super PlayableAudio
 end
 
-# Abstraction of a long song, that can bee looped
+# Long sound that can bee looped
 class Music
        super PlayableAudio
 end
-
-redef class App
-
-       # Load a sound
-       fun load_sound(name: String): Sound is abstract
-
-       # Load a music
-       fun load_music(name: String): Music is abstract
-end
index e26b8ec..983f50b 100644 (file)
 # assert deserializer.errors.is_empty # If false, `obj` is invalid
 # print object
 # ~~~
+#
+# ### Missing attributes and default values
+#
+# When reading JSON, some attributes expected by Nit classes may be missing.
+# The JSON object may come from an external API using optional attributes or
+# from a previous version of your program without the attributes.
+# When an attribute is not found, the deserialization engine acts in one of three ways:
+#
+# 1. If the attribute has a default value or if it is annotated by `lazy`,
+#    the engine leave the attribute to the default value. No error is raised.
+# 2. If the static type of the attribute is nullable, the engine sets
+#    the attribute to `null`. No error is raised.
+# 3. Otherwise, the engine raises an error and does not set the attribute.
+#    The caller must check for `errors` and must not read from the attribute.
+#
+# ~~~nitish
+# import json::serialization
+#
+# class MyConfig
+#     serialize
+#
+#     var width: Int # Must be in JSON or an error is raised
+#     var height = 4
+#     var volume_level = 8 is lazy
+#     var player_name: nullable String
+#     var tmp_dir: nullable String = "/tmp" is lazy
+# end
+#
+# # ---
+# # JSON object with all expected attributes -> OK
+# var plain_json = """
+# {
+#     "width": 11,
+#     "height": 22,
+#     "volume_level": 33,
+#     "player_name": "Alice",
+#     "tmp_dir": null
+# }"""
+# var deserializer = new JsonDeserializer(plain_json)
+# var obj = new MyConfig.from_deserializer(deserializer)
+#
+# assert deserializer.errors.is_empty
+# assert obj.width == 11
+# assert obj.height == 22
+# assert obj.volume_level == 33
+# assert obj.player_name == "Alice"
+# assert obj.tmp_dir == null
+#
+# # ---
+# # JSON object missing optional attributes -> OK
+# plain_json = """
+# {
+#     "width": 11
+# }"""
+# deserializer = new JsonDeserializer(plain_json)
+# obj = new MyConfig.from_deserializer(deserializer)
+#
+# assert deserializer.errors.is_empty
+# assert obj.width == 11
+# assert obj.height == 4
+# assert obj.volume_level == 8
+# assert obj.player_name == null
+# assert obj.tmp_dir == "/tmp"
+#
+# # ---
+# # JSON object missing the mandatory attribute -> Error
+# plain_json = """
+# {
+#     "player_name": "Bob",
+# }"""
+# deserializer = new JsonDeserializer(plain_json)
+# obj = new MyConfig.from_deserializer(deserializer)
+#
+# # There's an error, `obj` is partial
+# assert deserializer.errors.length == 1
+#
+# # Still, we can access valid attributes
+# assert obj.player_name == "Bob"
+# ~~~
 module serialization
 
 import ::serialization::caching
@@ -295,13 +374,15 @@ class JsonDeserializer
                        if not root isa Error then
                                errors.add new Error("Deserialization Error: parsed JSON value is not an object.")
                        end
+                       deserialize_attribute_missing = false
                        return null
                end
 
                var current = path.last
 
                if not current.keys.has(name) then
-                       errors.add new Error("Deserialization Error: JSON object has not attribute '{name}'.")
+                       # Let the generated code / caller of `deserialize_attribute` raise the missing attribute error
+                       deserialize_attribute_missing = true
                        return null
                end
 
@@ -310,6 +391,8 @@ class JsonDeserializer
                attributes_path.add name
                var res = convert_object(value, static_type)
                attributes_path.pop
+
+               deserialize_attribute_missing = false
                return res
        end
 
index 297c7e9..09ddfff 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Linux audio services
+# Linux audio implementation
 module audio
 
 import app::audio
 import linux
 
-# Simple audio asset
 redef class Sound
 
        redef fun play do
-               if name.has_suffix(".wav") then
-                       sys.system "aplay -q {app.assets_dir}{name} &"
-               else if name.has_suffix(".mp3") then
-                       sys.system "mpg123 -q {app.assets_dir}{name} &"
+               if path.has_suffix(".wav") then
+                       sys.system "aplay -q {app.assets_dir}{path} &"
+               else if path.has_suffix(".mp3") then
+                       sys.system "mpg123 -q {app.assets_dir}{path} &"
                end
        end
 
@@ -39,10 +38,10 @@ end
 redef class Music
 
        redef fun play do
-               if name.has_suffix(".wav") then
-                       sys.system "aplay -q {app.assets_dir}{name} &"
-               else if name.has_suffix(".mp3") then
-                       sys.system "mpg123 -q {app.assets_dir}{name} &"
+               if path.has_suffix(".wav") then
+                       sys.system "aplay -q {app.assets_dir}{path} &"
+               else if path.has_suffix(".mp3") then
+                       sys.system "mpg123 -q {app.assets_dir}{path} &"
                end
        end
 
@@ -50,15 +49,3 @@ redef class Music
        redef fun pause do end
        redef fun resume do end
 end
-
-redef class App
-       redef fun load_sound(name)
-       do
-               return new Sound(name)
-       end
-
-       redef fun load_music(name)
-       do
-               return new Music(name)
-       end
-end
index 5630b34..db02122 100644 (file)
@@ -100,9 +100,15 @@ abstract class Deserializer
        # The `static_type` can be used as last resort if the deserialized object
        # desn't have any metadata declaring the dynamic type.
        #
+       # Return the deserialized value or null on error, and set
+       # `deserialize_attribute_missing` to whether the attribute was missing.
+       #
        # Internal method to be implemented by the engines.
        fun deserialize_attribute(name: String, static_type: nullable String): nullable Object is abstract
 
+       # Was the attribute queried by the last call to `deserialize_attribute` missing?
+       var deserialize_attribute_missing = false
+
        # Register a newly allocated object (even if not completely built)
        #
        # Internal method called by objects in creation, to be implemented by the engines.
index 2d61ce0..f6f3760 100644 (file)
@@ -58,9 +58,7 @@ private class GLSLValidationPhase
 
                # Do not double check if tool is in path
                var in_path = tool_is_in_path
-               if in_path != null then
-                       if not in_path then return
-               else
+               if in_path == null then
                        # Is _glslangValidator_ installed?
                        var proc_which = new ProcessReader("which", "glslangValidator")
                        proc_which.wait
@@ -68,11 +66,12 @@ private class GLSLValidationPhase
                        var status = proc_which.status
                        in_path = status == 0
                        tool_is_in_path = in_path
-                       if not in_path then
-                               toolcontext.warning(nat.location, "glslvalidator",
-                                       "Warning: program `glslangValidator` not in PATH, cannot validate this shader.")
-                               return
-                       end
+               end
+
+               if not in_path then
+                       toolcontext.advice(nat.location, "glslvalidator",
+                               "Warning: program `glslangValidator` not in PATH, cannot validate this shader.")
+                       return
                end
 
                # Get the shader source
index b9cab9d..9eb01a4 100644 (file)
@@ -312,18 +312,29 @@ do
                                code.add """
        self.{{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
 """
-                       else code.add """
+                       else
+                               code.add """
        var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
-       if not {{{name}}} isa {{{type_name}}} then
-               # Check if it was a subjectent error
-               v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}")
+       if v.deserialize_attribute_missing then
+"""
+                               # What to do when an attribute is missing?
+                               if attribute.has_value then
+                                       # Leave it to the default value
+                               else if mtype isa MNullableType then
+                                       code.add """
+               self.{{{name}}} = null"""
+                               else code.add """
+               v.errors.add new Error("Deserialization Error: attribute `{class_name}::{{{name}}}` missing from JSON object")"""
 
-               # Clear subjacent error
+                               code.add """
+       else if not {{{name}}} isa {{{type_name}}} then
+               v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}")
                if v.keep_going == false then return
        else
                self.{{{name}}} = {{{name}}}
        end
 """
+                       end
                end
 
                code.add "end"
index 9465dbb..44d551d 100644 (file)
@@ -1158,8 +1158,8 @@ redef class AAttrPropdef
        # Is the node tagged optional?
        var is_optional = false
 
-       # Has the node a default value?
-       # Could be through `n_expr` or `n_block`
+       # Does the node have a default value?
+       # Could be through `n_expr`, `n_block` or `is_lazy`
        var has_value = false
 
        # The guard associated to a lazy attribute.
index bec49a8..9e2d69a 100644 (file)
@@ -102,9 +102,9 @@ private class TypeVisitor
        end
 
        # Check that `sub` is a subtype of `sup`.
-       # If `sub` is not a valid suptype, then display an error on `node` an return null.
-       # If `sub` is a safe subtype of `sup` then return `sub`.
-       # If `sub` is an unsafe subtype (ie an implicit cast is required), then return `sup`.
+       # If `sub` is not a valid suptype, then display an error on `node` and return `null`.
+       # If `sub` is a safe subtype of `sup`, then return `sub`.
+       # If `sub` is an unsafe subtype (i.e., an implicit cast is required), then return `sup`.
        #
        # The point of the return type is to determinate the usable type on an expression when `autocast` is true:
        # If the suptype is safe, then the return type is the one on the expression typed by `sub`.
index 102d57b..a6ae660 100644 (file)
@@ -1,3 +1,3 @@
 Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:27)
 # JSON: {"__class": "MyClass", "i": 123, "o": null}
-# Errors: 'Deserialization Error: JSON object has not attribute 's'.', 'Deserialization Error: Wrong type on `MyClass::s` expected `String`, got `null`', 'Deserialization Error: JSON object has not attribute 'f'.', 'Deserialization Error: Wrong type on `MyClass::f` expected `Float`, got `null`', 'Deserialization Error: JSON object has not attribute 'a'.', 'Deserialization Error: Wrong type on `MyClass::a` expected `Array[String]`, got `null`'
+# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object'
index 71dc9e7..62b3c82 100644 (file)
@@ -11,7 +11,6 @@
 # Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>
 
 # JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"]}
-# Errors: 'Deserialization Error: JSON object has not attribute 'o'.'
 # Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>
 
 # JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"], "o":
index 54e5a4d..4b53f0f 100644 (file)
@@ -1,3 +1,3 @@
 Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:22)
 # JSON: {"__class": "MyClass", "i": 123, "o": null}
-# Errors: 'Deserialization Error: JSON object has not attribute 's'.', 'Deserialization Error: Wrong type on `MyClass::s` expected `String`, got `null`', 'Deserialization Error: JSON object has not attribute 'f'.', 'Deserialization Error: Wrong type on `MyClass::f` expected `Float`, got `null`', 'Deserialization Error: JSON object has not attribute 'a'.', 'Deserialization Error: Wrong type on `MyClass::a` expected `Array[String]`, got `null`'
+# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object'
index e577eda..7358c85 100644 (file)
@@ -49,7 +49,7 @@ tests.add """
 tests.add """
 {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "o": null, "a": ["one", "two"], "Some random attribute": 777}"""
 
-# Skipping `o` will cause an error but the attribute will be set to `null`
+# Skipping `o` will set the attribute to `null`
 tests.add """
 {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"]}"""