היום למדתי: לא הופכים OpenStruct ל JSON

13/03/2023

בכל מערכת יש את הרגע הזה שמימוש וממשק מתנגשים, מה שנקרא אבסטרקציה דולפת. אחד המקומות שזה קורה ב Ruby הוא המימוש של Open Struct.

הסיפור בקצרה - ברובי בדרך כלל הגישה למקום ב Hash נעשית עם סימן סוגריים מרובעים לדוגמה:

3.1.1 :001 > d = {foo: 10, bar: 20}
 => {:foo=>10, :bar=>20}
3.1.1 :002 > d[:foo]
 => 10
3.1.1 :003 >

אבל מתכנתים שבאים מ JavaScript רגילים לגשת ל Hash באמצעות אופרטור הנקודה. ברובי נקודה היא קריאה למתודה ו OpenStruct זו מעטפת סביב Hash שיוצרת מתודה עבור כל שדה מידע ב Hash. זה נראה ככה:

3.1.1 :005 > require 'ostruct'
 => true
3.1.1 :007 > dd = OpenStruct.new(d)
 => #<OpenStruct foo=10, bar=20>
3.1.1 :008 > dd.foo
 => 10
3.1.1 :009 > dd.bar
 => 20

עד לפה הכל טוב ואפשר גם להפעיל to_json ולקבל את כל השדות בחזרה בתור JSON:

3.1.1 :002 > require 'ostruct'
 => true
3.1.1 :003 > require 'json'
 => true
3.1.1 :004 > OpenStruct.new({ foo: 10, bar: 20 })
 => #<OpenStruct foo=10, bar=20>
3.1.1 :005 > OpenStruct.new({ foo: 10, bar: 20 }).to_json
 => "\"#<OpenStruct foo=10, bar=20>\""

אבל כשמעבירים את אותו קוד לריילס הקידוד ל JSON משתנה ומקבלים את התוצאה המפתיעה הבאה:

3.1.1 :001 > OpenStruct.new({ foo: 10, bar: 20 }).to_json
 => "{\"table\":{\"foo\":10,\"bar\":20}}"

מאיפה הגיע לשם ה table???

חיפוש ב Stack Overflow הביא אותי לתשובה הזאת. מסתבר שבמימוש של OpenStruct כל הפריטים נשמרים בתור ערכים של משתנה מחלקה בשם table, והקידוד ל JSON של ריילס פשוט מקודד את כל משתני המחלקה.

וכמובן אחרי שגילינו את האבסטרקציה הדולפת הפיתרון הוא מאוד פשוט:

3.1.1 :077 > OpenStruct.new({a: 10, b: 20}).to_h.to_json
 => "{\"a\":10,\"b\":20}"

מאחר ויש כבר פונקציה to_h שמחזירה את כל המידע בתור Hash רגיל, צריך רק לקרוא לה ולתרגם את ה Hash שהיא מחזירה ל JSON כמו שריילס כבר יודע לעשות.