I finally had some time to think about JsInterop and how to abstract
browser APIs.
I will try to summarize my thought process and different options that I
have played with so that you could get a better understanding where I'm
coming from and I hope it will provide good documentation for future.
So I started by playing with the static functions, constructors and how to
represent that with the current JsInterop semantics.
First I started with the original approach: use @JsType on an interface and
use some simple conventions to generate the rest:
@JsType(prototype = "Object")interface JsObject extends Auto_JsObject {
static JsObject of(Object obj) {
return obj instanceof JsObject ? (JsObject) obj : null;
}
interface Static {
String[] keys(JsObject obj);
JsObject defineProperties(JsObject obj, JsObject props);
}
interface constructors {
void constructor(String param);
}
boolean hasOwnProperty(String prop);
boolean isPrototypeOf(JsObject prop);
}
then with the annotation processor following code will be generated:
interface Auto_JsObject {
public static JsObject newInstance(String param) {
return js("new Object($0)", param);
}
public static String[] keys(JsObject obj) {
return js("Object.keys($0)", obj);
}
public static JsObject defineProperties(JsObject obj, JsObject props) {
...
}
@PrototypeOfJsType(JsObject.class)
public static class prototype {
JsObject constructor(String param) { return null}
boolean hasOwnProperty(String prop) { return false; };
boolean isPrototypeOf(JsObject prop) { return false; };
}
}
And it looks like following when used:
MyObject extends JsObject.prototype {}
JsObject.keys( ... );
JsObject.newInstance( ... );
JsObject.of(new Object());
One of the advantages of this is; I can additionally generate a special
method named statics() that returns an instance of JsObject.Static where we
forward all calls to the static methods. This is pretty useful for people
who care about pure unit testing.
Although this solution is not ideal due to need for extending a separate
class (i.e. JsObject.prototype), it looks reasonably good at first sight.
But there is one major gotcha: it doesn't work!
The reason is, according to here
<http://docs.oracle.com/javase/tutorial/java/IandI/override.html> unlike
static methods in real classes, static methods in interfaces are not
'inherited' by subclasses.
So I stepped backed and started questioning what we are doing here.
We basically creating a DSL but this DSL is trying to enhance the very same
class where it is used. This model isn't directly supported with annotation
processors (that's why we do the trick of extending generated class) and we
already decided not to use Lombok hacks so it looks like we can't do much
here.
So I'm thinking; as we are already defining a DSL why not make it more
explicit in a separate class and let it generate the actual API?
Here we go:
@JsApi("Object") // A new annotation for the DSL, not required to be
in the SDKinterface __JsObject { // Used underscore so it doesn't show
up in code completion
String[] keys(JsObject obj);
JsObject defineProperties(JsObject obj, JsObject props);
static JsObject of(Object obj) {
return obj instanceof JsObject ? (JsObject) obj : null;
}
interface prototype {
boolean hasOwnProperty(String prop);
boolean isPrototypeOf(JsObject prop);
}
interface constructors {
void constructor(String param);
}
}
generates following:
@JsType(prototype = "Object")interface JsObject {
static JsObject newInstance(String param) {
return js("new Object($0)", param);
}
static String[] keys(JsObject obj) {
return js("Object.keys($0)", obj);
};
static JsObject defineProperties(JsObject obj, JsObject props) {
...
}
static JsObject of(Object obj) {
return __JsObject.of(obj);
}
boolean hasOwnProperty(String prop);
boolean isPrototypeOf(JsObject prop);
public static JsObject defineProperties(JsObject obj, JsObject props) {
...
}
@PrototypeOfJsType(JsObject.class)
public static class prototype {
JsObject constructor(String param) { return null;}
boolean hasOwnProperty(String prop) { return false; };
boolean isPrototypeOf(JsObject prop) { return false; };
}
}
So this is basically a working version of the previous one.
One may argue that if we are committing to use some kind of DSL why not use
something else like IDL or xml etc. There are 3 main reasons to use java +
APT instead of others:
First, our user base already familiar with java so they don't need to learn
anything new other than a few simple naming conventions. Also developers
need to able to write java code snippets for helper functions (see
JsObject.of) which plays well with this approach.
Second, IDE will give free syntax highlighting, checks and auto completion
for whole process.
Third, logistics for APT based code generation is already provided by IDEs,
build systems and very likely will be provided by superdev in the future.
Long story short; this is quite feasible and viable option but there are
still some stuff I don't like about it:
- JsObject.protoype is still an annoying artifact that all API consumers
need to know about.
- I don't like the compiler silently ignoring some method bodies (i.e.
JsObject.prototype::*).
- There is plenty of code generation going on which causes extra
indirections.
So I stepped back one more time and decided give a short break to my strong
prejudice on native methods and created a solution around native methods:
@JsType(prototype="Object")public class JsObject {
@JsConstructor
public static native JsObject newInstance(String param);
public static native String[] keys(JsObject obj);
public static native JsObject defineProperties(JsObject obj, JsObject props);
public static JsObject of(Object obj) {
return obj instanceof JsObject ? (JsObject) obj : null;
}
public native boolean hasOwnProperty(String prop);
public native boolean isPrototypeOf(JsObject prop);
}
This solution is perfect from a purist point of view. No code generation
involved; we basically utilize the host language's extension point created
solely for this purpose. Here we are saying that this methods exist in the
runtime and implemented natively by the platform.
Pros:
It is intuitive for java developers. There is no magic prototype class to
extend from, no conventions or DSL, no code generators. It is more explicit
and it is very clear where the native boundary is when you look at the
code. Probably it is even more pleasing to implement in the compiler.
All the cons are around the testability aspect. For JRE testing, native
methods are a headache. Also we no longer generate an interface for static
methods. We are basically going back to square one with respect to
testability.
But on the other hand it is not horrible. For people who insists on JRE
testing; we can provide a classloader that stubs all native methods or they
can use a more powerful mocking library like powermock or gwt-mockito.
I would like to first get high level feedback here before discussing this
internally. I'll update the design doc if we conclude on something.
- Goktug
--
You received this message because you are subscribed to the Google Groups "GWT
Contributors" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/google-web-toolkit-contributors/CAN%3DyUA2DvnfqtL1F4zd_ydM4KdWx9bBXCdtY7RGs_vaVvh88%2Bg%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.