dropbox / djinni

A tool for generating cross-language type declarations and interface bindings.
Apache License 2.0
2.88k stars 488 forks source link

Djinni's optional<enum> generates an NSNumber on iOS #429

Closed bilsou closed 5 years ago

bilsou commented 5 years ago

When using a Djinni's optional<enum> in a record type, the generated Objective-C property becomes an NSNumber

measurements_status = enum {
    no_results;
    results_found;
}
measurements = record {
    measurementsStatus: optional<measurements_status>;
}

generates on ObjC

@property (nonatomic, readonly, nullable) NSNumber * measurementsStatus;

After some digging, it might be related to line 118 in ObjcMarshal.scala but I might have been mistaken

  def toObjcType(ty: TypeRef): (String, Boolean) = toObjcType(ty.resolved, false)
  def toObjcType(ty: TypeRef, needRef: Boolean): (String, Boolean) = toObjcType(ty.resolved, needRef)
  def toObjcType(tm: MExpr): (String, Boolean) = toObjcType(tm, false)
  def toObjcType(tm: MExpr, needRef: Boolean): (String, Boolean) = {
    def args(tm: MExpr) = if (tm.args.isEmpty) "" else tm.args.map(toBoxedParamType).mkString("<", ", ", ">")
    def f(tm: MExpr, needRef: Boolean): (String, Boolean) = {
      tm.base match {
        case MOptional =>
          // We use "nil" for the empty optional.
          assert(tm.args.size == 1)
          val arg = tm.args.head
          arg.base match {
            case MOptional => throw new AssertionError("nested optional?")
            case m => f(arg, true)
          }
        case o =>
          val base = o match {
            case p: MPrimitive => if (needRef) (p.objcBoxed, true) else (p.objcName, false)
            case MString => ("NSString", true)
            case MDate => ("NSDate", true)
            case MBinary => ("NSData", true)
            case MOptional => throw new AssertionError("optional should have been special cased")
            case MList => ("NSArray" + args(tm), true)
            case MSet => ("NSSet" + args(tm), true)
            case MMap => ("NSDictionary" + args(tm), true)
            case d: MDef => d.defType match {
              case DEnum => if (needRef) ("NSNumber", true) else (idObjc.ty(d.name), false)
              case DRecord => (idObjc.ty(d.name), true)
              case DInterface =>
                val ext = d.body.asInstanceOf[Interface].ext
                if (!ext.objc)
                  (idObjc.ty(d.name), true)
                else
                  (s"id<${idObjc.ty(d.name)}>", false)
            }
            case e: MExtern => e.body match {
              case i: Interface => if(i.ext.objc) (s"id<${e.objc.typename}>", false) else (e.objc.typename, true)
              case _ => if(needRef) (e.objc.boxed, true) else (e.objc.typename, e.objc.pointer)
            }
            case p: MParam => throw new AssertionError("Parameter should not happen at Obj-C top level")
          }
          base
      }
    }
    f(tm, needRef)
  }

Any help would be much appreciated

artwyman commented 5 years ago

That behavior is by-design. What were you expecting instead? I don't know of another obvious way to represent an optional enum in Objective-C in a safe way. Boxed types allow nullability, and and NSNumber is expected way to box enums in Objective-C.

bilsou commented 5 years ago

Apologies, I have been using Swift full-time lately and I just forgot that an NSEnum cannot be nullable on Obj-C contrarily to a Swift enum. It is a pain, but nothing Djinni can do about indeed. Thanks again

artwyman commented 5 years ago

Yeah, direct bridging to Swift would be a nice feature add now that ObjC is on its way out, but I don't know of anyone currently putting the the effort into that for Djinni.