git » sdk » commit d364e67

More form features

author Stephen Paul Weber
2025-11-19 14:14:47 UTC
committer Stephen Paul Weber
2025-11-19 14:14:47 UTC
parent 76d29aeeffd8589eb7bcbe3b01454481fd9a60ab

More form features

An OOB "form" that is just a title and URL
desc/options on fields
type on form

borogove/DataForm.hx +63 -1
borogove/Form.hx +52 -17
borogove/OOB.hx +15 -0
borogove/Register.hx +15 -5
npm/index.ts +2 -0

diff --git a/borogove/DataForm.hx b/borogove/DataForm.hx
index f4af46a..90ae52d 100644
--- a/borogove/DataForm.hx
+++ b/borogove/DataForm.hx
@@ -9,12 +9,17 @@ import HaxeCBridge;
 @:forward(toString)
 abstract DataForm(Stanza) from Stanza to Stanza {
 	public var title(get, never): Null<String>;
+	public var type(get, never): Null<String>;
 	public var fields(get, never): Array<Field>;
 
 	inline public function get_title() {
 		return this.getChildText("title");
 	}
 
+	inline public function get_type() {
+		return this.attr.get("type") ?? "form";
+	}
+
 	inline public function get_fields() {
 		return this.allTags("field");
 	}
@@ -31,9 +36,11 @@ abstract DataForm(Stanza) from Stanza to Stanza {
 abstract Field(Stanza) from Stanza to Stanza {
 	public var name(get, never): String;
 	public var label(get, never): Null<String>;
+	public var desc(get, never): Null<String>;
 	public var value(get, set): Array<String>;
-	public var type(get, never): String;
+	public var type(get, set): String;
 	public var datatype(get, never): String;
+	public var options(get, never): Array<Option>;
 	public var open(get, never): Bool;
 	public var rangeMin(get, never): Null<String>;
 	public var rangeMax(get, never): Null<String>;
@@ -48,6 +55,10 @@ abstract Field(Stanza) from Stanza to Stanza {
 		return this.attr.get("label");
 	}
 
+	inline public function get_desc() {
+		return this.getChildText("desc");
+	}
+
 	public function get_value() {
 		final isbool = (this : Field).datatype == "xs:boolean";
 		return this.allTags("value").map(v -> {
@@ -77,6 +88,10 @@ abstract Field(Stanza) from Stanza to Stanza {
 		return attr;
 	}
 
+	inline public function set_type(newType: String) {
+		return this.attr.set("type", newType);
+	}
+
 	public function get_datatype() {
 		final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate");
 		if (validate != null && validate.attr.get("datatype") != null) {
@@ -89,6 +104,10 @@ abstract Field(Stanza) from Stanza to Stanza {
 		return "xs:string";
 	}
 
+	public function get_options() {
+		return this.allTags("option");
+	}
+
 	inline public function get_open() {
 		final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate");
 		return validate?.getChild("open") != null;
@@ -122,6 +141,24 @@ abstract Field(Stanza) from Stanza to Stanza {
 	}
 }
 
+abstract Option(Stanza) from Stanza to Stanza {
+	public var label(get, never): Null<String>;
+	public var value(get, never): Null<String>;
+
+	inline public function get_label() {
+		return this.attr.get("label");
+	}
+
+	inline public function get_value() {
+		return this.getChildText("value");
+	}
+
+	@:to
+	inline public function toFormOption(): FormOption {
+		return this == null ? null : FormOption.fromOption(this);
+	}
+}
+
 @:expose
 #if cpp
 @:build(HaxeCBridge.expose())
@@ -130,10 +167,12 @@ abstract Field(Stanza) from Stanza to Stanza {
 class FormField {
 	public final name: String;
 	public final label: Null<String>;
+	public final desc: Null<String>;
 	public final value: Array<String>;
 	public final required: Bool;
 	public final type: String;
 	public final datatype: String;
+	public final options: Array<FormOption>;
 	public final open: Bool;
 	public final rangeMin: Null<String>;
 	public final rangeMax: Null<String>;
@@ -143,13 +182,36 @@ class FormField {
 	private function new(field: Field) {
 		name = field.name;
 		label = field.label;
+		desc = field.desc;
 		value = field.value;
 		required = field.required;
 		type = field.type;
 		datatype = field.datatype;
+		options = field.options.map(o -> o.toFormOption());
 		open = field.open;
 		rangeMin = field.rangeMin;
 		rangeMax = field.rangeMax;
 		regex = field.regex;
 	}
 }
+
+@:expose
+#if cpp
+@:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
+#end
+class FormOption {
+	public final label: Null<String>;
+	public final value: Null<String>;
+
+	@:allow(borogove)
+	private function new(label: Null<String>, value: Null<String>) {
+		this.label = label;
+		this.value = value;
+	}
+
+	@:allow(borogove)
+	private static function fromOption(option: Option) {
+		return new FormOption(option.label, option.value);
+	}
+}
diff --git a/borogove/Form.hx b/borogove/Form.hx
index 5b51684..03694c3 100644
--- a/borogove/Form.hx
+++ b/borogove/Form.hx
@@ -24,12 +24,14 @@ class FormItem {
 	public final text: Null<String>;
 	public final field: Null<FormField>;
 	public final section: Null<FormSection>;
+	public final status: Null<String>;
 
 	@:allow(borogove)
-	private function new(text: Null<String>, field: Null<FormField>, section: Null<FormSection>) {
+	private function new(text: Null<String>, field: Null<FormField>, section: Null<FormSection>, status: Null<String> = null) {
 		this.text = text;
 		this.field = field;
 		this.section = section;
+		this.status = status;
 	}
 }
 
@@ -51,18 +53,20 @@ class FormSubmitBuilder {
 	}
 
 	@:allow(borogove)
-	private function submit(form: DataForm) {
+	private function submit(form: Null<DataForm>) {
 		final toSubmit = new Stanza("x", { xmlns: "jabber:x:data", type: "submit" });
-		for (f in form.fields) {
-			if (!data.has(f.name) && f.value.length > 0) {
-				final tag = toSubmit.tag("field", { "var": f.name });
-				for (v in f.value) {
-					tag.textTag("value", v);
+		if (form != null) {
+			for (f in form.fields) {
+				if (!data.has(f.name) && f.value.length > 0) {
+					final tag = toSubmit.tag("field", { "var": f.name });
+					for (v in f.value) {
+						tag.textTag("value", v);
+					}
+					tag.up();
+				} else if (f.required && (!data.has(f.name) || data[f.name].length < 1)) {
+					trace("No value provided for required field", f.name);
+					return null;
 				}
-				tag.up();
-			} else if (f.required && (!data.has(f.name) || data[f.name].length < 1)) {
-				trace("No value provided for required field", f.name);
-				return null;
 			}
 		}
 		for (k => vs in data) {
@@ -85,28 +89,59 @@ typedef StringOrArray = haxe.extern.EitherType<String, Array<String>>;
 @:build(HaxeSwiftBridge.expose())
 #end
 class Form implements FormSection {
-	private final form: DataForm;
+	private final form: Null<DataForm>;
+	private final oob: Null<OOB>;
 
 	@:allow(borogove)
-	private function new(form: DataForm) {
+	private function new(form: Null<DataForm>, oob: Null<OOB>) {
+		if (form == null && oob == null) throw "Need a form or OOB";
 		this.form = form;
+		this.oob = oob;
+	}
+
+	/**
+		Is this form entirely results / read-only?
+	**/
+	public function isResult() {
+		if (form == null) return true;
+
+		return form.type == "result";
 	}
 
+	/**
+		Title of this form
+	**/
 	public function title() {
-		return form.title;
+		return form != null ? form.title : oob.desc;
 	}
 
+	/**
+		URL to use instead of this form
+	**/
+	public function url() {
+		return oob?.url;
+	}
+
+	/**
+		Items to render inside this form
+	**/
 	public function items() {
+		if (form == null) return [];
+
 		final s: Stanza = form;
 		final hasLayout = s.getChild("page", "http://jabber.org/protocol/xdata-layout") != null;
 		final items = [];
 		for (child in s.allTags()) {
 			if (child.name == "instructions" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) {
-				items.push(new FormItem(child.getText(), null, null));
+				items.push(new FormItem(child.getText(), null, null, child.attr.get("type")));
 			}
 			if (!hasLayout && child.name == "field" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) {
 				final fld: Null<Field> = child;
-				if (fld.type != "hidden") {
+				if (fld.type == "fixed" && fld.label == null) {
+					for (v in fld.value) {
+						items.push(new FormItem(v, null, null));
+					}
+				} else if (fld.type != "hidden") {
 					items.push(new FormItem(null, fld, null));
 				}
 			}
@@ -150,7 +185,7 @@ class Form implements FormSection {
 				builder.add(entry[0], entry[1]);
 			}
 		#end
-		} else {
+		} else if (data != null) {
 			for (k => v in ((cast data) : haxe.DynamicAccess<StringOrArray>)) {
 				if (Std.isOfType(v, String)) {
 					builder.add(k, v);
diff --git a/borogove/OOB.hx b/borogove/OOB.hx
new file mode 100644
index 0000000..dc91185
--- /dev/null
+++ b/borogove/OOB.hx
@@ -0,0 +1,15 @@
+package borogove;
+
+@:forward(toString)
+abstract OOB(Stanza) from Stanza to Stanza {
+	public var desc(get, never): Null<String>;
+	public var url(get, never): Null<String>;
+
+	inline public function get_desc() {
+		return this.getChildText("desc");
+	}
+
+	inline public function get_url() {
+		return this.getChildText("url");
+	}
+}
diff --git a/borogove/Register.hx b/borogove/Register.hx
index 68ce33c..1e2fe4d 100644
--- a/borogove/Register.hx
+++ b/borogove/Register.hx
@@ -66,17 +66,18 @@ class Register {
 	}
 
 	/**
-		Fetch registration form from the server.
+		Fetch registration form options from the server.
 		If you already know what fields your server wants, this is optional.
 	**/
-	public function getForm() {
+	public function getForm(): Promise<Array<Form>> {
 		return stream.register(domain, preAuth).then(reply -> {
 			final error = reply.getErrorText();
 			if (error != null) return Promise.reject(error);
 
 			final query = reply.getChild("query", "jabber:iq:register");
 			final form: DataForm = query.getChild("x", "jabber:x:data");
-			if (form == null) {
+			final oob: OOB = query.getChild("x", "jabber:x:oob");
+			if (form == null && oob == null) {
 				return Promise.reject("No form found");
 			}
 
@@ -86,8 +87,17 @@ class Register {
 				(fuser : Stanza).attr.set("type", "fixed");
 			}
 
-			this.form = new Form(form);
-			return Promise.resolve(this.form);
+			final results = [];
+			if (form != null) {
+				this.form = new Form(form, null);
+				results.push(this.form);
+			}
+			if (oob != null) {
+				final oobForm = new Form(null, oob);
+				results.push(oobForm);
+				if (this.form == null) this.form = oobForm;
+			}
+			return Promise.resolve(results);
 		});
 	}
 
diff --git a/npm/index.ts b/npm/index.ts
index e485a61..69635cf 100644
--- a/npm/index.ts
+++ b/npm/index.ts
@@ -16,9 +16,11 @@ export import Config = borogove.Config;
 export import CustomEmojiReaction = borogove.CustomEmojiReaction;
 export import DirectChat = borogove.DirectChat;
 export import EventEmitter = borogove.EventEmitter;
+export import Form = borogove.Form;
 export import FormSection = borogove.FormSection;
 export import FormItem = borogove.FormItem;
 export import FormField = borogove.FormField;
+export import FormOption = borogove.FormOption;
 export import Hash = borogove.Hash;
 export import Identicon = borogove.Identicon;
 export import Notification = borogove.Notification;