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

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

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

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

תבניות התאמה נתמכות

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

התאמת סכמה

אם אין סכימה בתבנית ה-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" לא נמצא ב-enum) null

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

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

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

השוואת UriMatchResult

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

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

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

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

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

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