התאמה של קישורי עומק מסוג URI

כדי להתאים היררכיות של כתובות URI לתבניות ולחלץ ארגומנטים, משתמשים בפונקציה UriDeepLinkMatcher. הוא מסתמך על kotlinx.serialization כדי לבצע דה-סריאליזציה של ארגומנטים תואמים למחלקות המפתח שלכם.

כדי ליצור UriDeepLinkMatcher, צריך לספק תבנית DeepLinkUri ואת הסריאליזטור של המפתח המתאים:

למזהי URI לא היררכיים או לסכימות בהתאמה אישית (כמו tel:), אפשר לעיין במאמר בנושא יצירת כלים מותאמים אישית להשוואת קישורי עומק.

דפוסי התאמה נתמכים

UriDeepLinkMatcher מתאים לכתובות URI על סמך חמשת הרכיבים שלהן: סכימה, סמכות, נתיב, שאילתה ומקטע. בקטעים הבאים מתוארים התחביר הנתמך של התבניות, placeholders של ארגומנטים וכללי ההתאמה לכל רכיב.

התאמת סכמות

אם לא מצוינת סכימה בתבנית ה-URI, יש התאמה גם ל-http וגם ל-https. כדי להתאים לתוכנית ספציפית, צריך לכלול אותה בתבנית. יוצא מן הכלל: התבנית http תואמת גם למזהי URI של בקשות http וגם למזהי URI של בקשות https, בעוד שהתבנית https תואמת רק לבקשות https.

‫URI של התבנית URI של בקשה התאמה
www.example.com https://www.example.com
www.example.com http://www.example.com
http://www.example.com http://www.example.com
http://www.example.com https://www.example.com
https://www.example.com http://www.example.com
myapp://www.example.com myapp://www.example.com

התאמת סמכות

UriDeepLinkMatcher מבצע התאמה מדויקת לא תלוית-רישיות לרשות ה-URI (מארח ויציאה אופציונלית). אין תמיכה בפלייסולדרים או בתווים כלליים לחיפוש בסמכות, ולא מופקים ארגומנטים:

‫URI של התבנית URI של בקשה התאמה
example.com https://example.com
example.com https://EXAMPLE.COM
example.com https://sub.example.com
example.com https://www.example.com
example.com https://example.com:8080
example.com:8080 https://example.com:8080
example.com:8080 https://example.com

התאמת נתיבים

אפשר להשתמש בתבניות הנתיבים הבאות:

‫URI של התבנית URI של בקשה התאמה ארגומנטים שחולצו
www.example.com/users https://www.example.com/users ללא
www.example.com/users/{id} https://www.example.com/users/123 id: "123"
www.example.com/users/{first}-{last} https://www.example.com/users/john-doe first: "john", last: "doe"
www.example.com/users/{id}/profile https://www.example.com/users//profile id: "" (מחרוזת ריקה)
www.example.com/users/user_{id} https://www.example.com/users/user_123 id: "123"
www.example.com/users/{userId}/posts/{postId} https://www.example.com/users/123/posts/456 userId: "123", postId: "456"
www.example.com/users/.* https://www.example.com/users/john-doe ללא
www.example.com/users https://www.example.com/users/ ‫❌ (לוכסן בסוף יוצר פלח נוסף) לא רלוונטי

התאמה לשאילתות

הסדר של פרמטרי השאילתה ב-URI של הבקשה לא צריך להיות זהה לסדר ב-URI של התבנית. בנוסף, המערכת מתעלמת מפרמטרים שמופיעים ב-URI של הבקשה אבל לא ב-URI של התבנית.

המערכת תומכת בתבניות הבאות של פרמטרים של שאילתות:

‫URI של התבנית URI של בקשה ארגומנטים שחולצו
www.example.com/users?name={name} https://www.example.com/users?name=john name: "john"
www.example.com/users?name={name} https://www.example.com/users?name= name: "" (מחרוזת ריקה)
www.example.com/users?{rawQuery} https://www.example.com/users?anything&else rawQuery: ["anything", "else"]
www.example.com/users?type=user_{id} https://www.example.com/users?type=user_123 id: "123"
www.example.com/users?name={first}_{last} https://www.example.com/users?name=john_doe first: "john", last: "doe"
www.example.com/users?list={list} https://www.example.com/users?list=10&list=20 list: ["10", "20"]
www.example.com/users?name={name}&{other} https://www.example.com/users?name=john&tab=info name: "john", other: ["tab=info"]
www.example.com/users?type=user_.* https://www.example.com/users?type=user_admin type: "admin"

התאמת מקטעים

אלה סוגי התבניות של קטעי הטקסט שנתמכים:

‫URI של התבנית URI של בקשה ארגומנטים שחולצו
www.example.com/#section1 https://www.example.com/#section1 ללא
www.example.com/#section_{id} https://www.example.com/#section_123 id: "123"
www.example.com/#section_.* https://www.example.com/#section_123 ללא

סוגי נתונים נתמכים

UriDeepLinkMatcher תומך בביטול הסדרות של ארגומנטים של URI לסוגים פרימיטיביים, לספירות, לאוספים ולאובייקטים מותאמים אישית. סריאליזציה מתחלקת לשתי קטגוריות:

  • סדרת סטנדרטית: נעשה שימוש ב-kotlinx.serialization כדי לבצע דה-סריאליזציה אל:
    • פרימיטיבים (Boolean, ‏ Int, ‏ Long, ‏ Float, ‏ Double, ‏ Char, ‏ Byte,‏ Short) ו-String
    • טיפוסים בני מנייה (enum)
    • Set,‏ List או Array של פרימיטיבים, מחרוזות או סוגי enum
    • מחלקות @Serializable מוטמעות (שהמאפיינים שלהן משוטחים למחזיקי מקום של URI נפרדים)
  • סריאליזציה בהתאמה אישית עם DeepLinkSerializer: המרה בין String יחיד לבין אובייקטים מותאמים אישית, סוגים חיצוניים (כמו java.time.LocalDate) או אוספים מותאמים אישית עם תווי הפרדה.

סריאליזציה סטנדרטית

UriDeepLinkMatcher עובד מחוץ לקופסה עבור סוגים סטנדרטיים ומבנים שטוחים, בלי לדרוש הטמעות של סדרות מותאמות אישית.

פרימיטיבים ומחרוזות

UriDeepLinkMatcher מפענח באופן אוטומטי סוגים פרימיטיביים (Boolean, ‏ Int,‏ Long, ‏ Float, ‏ Double, ‏ Char, ‏ Byte, ‏ Short) ו-String:

טיפוסים בני מנייה (enum)

ההתאמה בין ערכי ה-enum לשמות של רכיבי ה-enum היא תלוית-רישיות:

אוספים של שאילתות חוזרות

פרמטרים של שאילתות עם מפתחות שחוזרים על עצמם (כמו ?id=10&id=20) עוברים באופן אוטומטי דה-סריאליזציה ל-List<T>, ל-Set<T> או ל-Array<T>, כאשר T הוא סוג פרימיטיבי, String או enum:

מחלקות @Serializable מוטמעות

כש-NavKey מכיל מאפיין שהסוג שלו הוא מחלקה אחרת של @Serializable,‏ UriDeepLinkMatcher משטח את המאפיינים שלו כך שכל מאפיין של המחלקה המקוננת ממופה ישירות לפרמטר URI נפרד עם אותו שם:

כדי לבצע דה-סריאליזציה של אובייקטים מותאמים אישית (כמו Filter(key = "brand", value = "pixel")), סוגים חיצוניים (כמו java.time.LocalDate) או מחרוזות מותאמות אישית עם תווי הפרדה (כמו ערכים מופרדים בפסיקים), צריך להרחיב את DeepLinkSerializer<T>.

DeepLinkSerializer<T> הוא KSerializer<T> מופשט שממיר בין String לבין T:

abstract class DeepLinkSerializer<T : Any> : KSerializer<T> {
    abstract val serialName: String
    abstract fun deserialize(value: String): T
    abstract fun serialize(value: T): String
}

לדוגמה, נבחן את ההגדרות Filter ו-FilterSerializer שמופיעות בקטעי הקוד הבאים:

אובייקטים מותאמים אישית בודדים

כדי לפענח אובייקט ממחרוזת פרמטרים של URI יחיד (כמו ?filter=brand:google), מוסיפים את ההערה @Serializable(with = ...) למאפיין:

אובייקטים מותאמים אישית בפרמטרים של שאילתה חוזרים

כדי לבצע דה-סריאליזציה של פרמטרים של שאילתה חוזרים לאוסף של אובייקטים מותאמים אישית (List<T>,‏ Set<T> או Array<T>), צריך להטמיע את DeepLinkSerializer<T> עבור סוג הרכיב T ולציין את ארגומנט הסוג של המאפיין באמצעות @Serializable(with = ...):

אוספים מופרדים בפרמטרים יחידים

כדי לנתח ערכים שמופרדים בפסיקים או בתו מפריד מותאם אישית (כמו ?ids=1,2,3) לאוסף, מטמיעים את DeepLinkSerializer עבור סוג האוסף כולו ומבצעים הערה של המאפיין באמצעות @Serializable(with = ...):

אימות של טיעונים ותוצאות של התאמה

UriDeepLinkMatcher מבחין בין אי התאמות (מחזיר null כדי שמנגנוני התאמה אחרים יוכלו לנסות להתאים) לבין תצורות לא נתמכות (מפעיל חריגה).

חוסר התאמה

חוסר התאמה מתרחש כש-URI של בקשה נכנסת לא עומד בדרישות של התבנית או הסוג:

  • פרמטרים נדרשים חסרים: מאפייני מפתח שלא יכולים להיות ריקים, ללא ערכי ברירת מחדל, שהפרמטרים התואמים שלהם ב-URI לא מופיעים ב-URI של הבקשה.
  • כשלים בניתוח סוגים: ערכי ארגומנטים שחולצו ולא ניתן לנתח אותם לסוג המאפיין הצפוי (לדוגמה, "abc" למאפיין Int).

אם אין התאמה, הפונקציה UriDeepLinkMatcher.match מחזירה null, וכך מאפשרת לבחון התאמות נוספות.

נניח שיש מחלקה מרכזית ורכיב התאמה שהוגדרו עם ערכי ברירת מחדל, אובייקטים מקוננים וסוגי נתונים מסוג enum:

בטבלה הבאה מוצגות תוצאות התאמה לכתובות URI שונות של בקשות:

URI של בקשה תוצאת הפענוח תוצאת המשחק
https://www.example.com/map/paris?zoom=15&style=dark&layer=SATELLITE הצלחה (כל הפרמטרים סופקו) UriMatchResult(MapKey("paris", 15, LayerOptions("dark", MapLayer.SATELLITE)))
https://www.example.com/map/paris?style=dark הצלחה (zoom מוגדר כברירת מחדל ל-12, ‏ layer ל-STANDARD) UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD)))
https://www.example.com/map/paris?zoom=&style=dark הפעולה הצליחה (פרמטר אופציונלי ריק של שאילתה משתמש בערך ברירת המחדל 12) UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD)))
https://www.example.com/map?style=dark חוסר התאמה (חסר הפרמטר הנדרש location) null
https://www.example.com/map/paris?zoom=close&style=dark חוסר התאמה ("close" הוא לא Int) null
https://www.example.com/map/paris?style=dark&layer=HYBRID חוסר התאמה (הערך "HYBRID" לא נמצא ברשימת הערכים) null

הגדרות שלא נתמכות

אם המחלקה המרכזית מכילה סוגי נתונים לא נתמכים, הפונקציה UriDeepLinkMatcher מעלה חריגה במהלך ההתאמה במקום להחזיר את הערך null.

  • מפות ואוספים רב-ממדיים: UriDeepLinkMatcher תומך רק באוספים חד-ממדיים של פרימיטיבים, מחרוזות, ספירות או סוגים מותאמים אישית עם הערה DeepLinkSerializer. סוגים Map יוצרים IllegalArgumentException, ואילו אוספים מקוננים (כמו List<List<String>>) יוצרים SerializationException.
  • אוספים של אובייקטים מותאמים אישית ללא הערות: אוספים של סוגים מותאמים אישית (כמו List<Filter>) מקפיצים הודעת שגיאה (throw) SerializationException אלא אם סוג הרכיב מסומן בהערה DeepLinkSerializer.
  • כיתות מקוננות לא שטוחות: אי אפשר למפות כיתות מקוננות @Serializable למחזיק מקום יחיד (כמו ?user={user}) בלי DeepLinkSerializer.

השוואת UriMatchResult

מופעי UriMatchResult מדורגים לפי הקריטריונים הבאים, בסדר הזה:

  1. סוג MatchResult: UriMatchResult מקבל דירוג גבוה יותר מסוגים אחרים של MatchResult MatchResult.
  2. נתיב מדויק: התאמות מדויקות של נתיבים מקבלות דירוג גבוה יותר מהתאמות של placeholder או wildcard.
  3. מספר הארגומנטים של הנתיב: התאמות עם יותר ארגומנטים של נתיב מקבלות דירוג גבוה יותר.
  4. נוכחות של ארגומנטים: התאמות שכוללות ארגומנטים מדורגות גבוה יותר מהתאמות שלא כוללות ארגומנטים.
  5. המספר הכולל של הארגומנטים: המספר הכולל של הארגומנטים (נתיב, שאילתה, מקטע) הוא הקריטריון האחרון להכרעה.

התאמה אישית של UriDeepLinkMatcher

UriDeepLinkMatcher הוא מחלקה מסוג open שאפשר ליצור ממנה מחלקת משנה כדי להתאים אישית את ההתנהגות של התאמת URI וחילוץ ארגומנטים:

  • matchRequest: נקודת כניסה להתאמה ברמה העליונה של DeepLinkRequest נכנס. אפשר לשנות את ההגדרה הזו כדי לבדוק את התוספים של הבקשה או להחיל תנאים מוקדמים מותאמים אישית לפני התאמת ה-URI.
  • matchUri: התאמה של DeepLinkUri לתבנית שהוגדרה. אפשר להגדיר חריגה כדי ליירט ולנרמל כתובות URI נכנסות (לדוגמה, לשכתב תת-דומיינים דינמיים או פורמטים של נתיבים מדור קודם) לפני שמפעילים את super.matchUri.
  • matchArguments: מבצעת דה-סריאליזציה של מפות הארגומנטים של הנתיב, השאילתה והמקטע שחולצו למופע של מפתח ניווט באמצעות serializer שסופק. אפשר לשנות את ההגדרה הזו כדי להוסיף ערכים דינמיים או לשנות את הארגומנטים לפני יצירת המופע של המפתח.

בדוגמה הבאה מוצג שימוש ב-subclassing של UriDeepLinkMatcher כדי לבצע נורמליזציה של קידומות של נתיבי כתובות ה-URL מדור קודם לפני ההתאמה: