1. บทนำ
CEL คืออะไร
CEL คือภาษาการแสดงออกอย่างสมบูรณ์แบบที่ไม่ใช่ Tour ซึ่งออกแบบมาให้รวดเร็ว พกพาได้ และปลอดภัย คุณสามารถใช้ CEL โดยลำพังหรือฝังลงในผลิตภัณฑ์ขนาดใหญ่ก็ได้
CEL ได้รับการออกแบบมาให้เป็นภาษาที่เรียกใช้รหัสผู้ใช้ได้อย่างปลอดภัย แม้ว่าการเรียก eval() โดยการปิดบังด้วยโค้ด Python ของผู้ใช้จะเป็นอันตราย แต่คุณสามารถเรียกใช้โค้ด CEL ของผู้ใช้ได้อย่างปลอดภัย และเนื่องจาก CEL ป้องกันพฤติกรรมที่อาจทำให้ประสิทธิภาพลดลง จึงประเมินตามลำดับของนาโนวินาทีถึงไมโครวินาทีอย่างปลอดภัย เหมาะสำหรับแอปพลิเคชันที่ต้องการประสิทธิภาพ
CEL จะประเมินนิพจน์ซึ่งคล้ายกับฟังก์ชันบรรทัดเดียวหรือนิพจน์แลมบ์ดา แม้ว่ามักใช้ CEL สำหรับการตัดสินใจแบบบูลีน แต่ก็ยังใช้เพื่อสร้างออบเจ็กต์ที่ซับซ้อนมากขึ้นได้ เช่น ข้อความ JSON หรือข้อความ Protobuf
CEL เหมาะกับโปรเจ็กต์ของคุณไหม
เนื่องจาก CEL จะประเมินนิพจน์จาก AST ในหน่วยนาโนวินาทีไปจนถึงไมโครวินาที การใช้งานที่เหมาะที่สุดสำหรับ CEL คือแอปพลิเคชันที่มีเส้นทางที่สำคัญต่อประสิทธิภาพ ไม่ควรคอมไพล์โค้ด CEL ลงใน AST ในเส้นทางวิกฤต แอปพลิเคชันที่เหมาะสมคือแอปพลิเคชันที่มีการดำเนินการกำหนดค่าบ่อยและมีการแก้ไขไม่บ่อยนัก
ตัวอย่างเช่น การใช้นโยบายความปลอดภัยกับคำขอ HTTP แต่ละรายการไปยังบริการเป็นกรณีการใช้งานที่เหมาะกับ CEL เนื่องจากนโยบายความปลอดภัยมีการเปลี่ยนแปลงน้อยมาก และ CEL จะมีผลเพียงเล็กน้อยต่อเวลาในการตอบกลับ ในกรณีนี้ CEL จะแสดงผลบูลีนหากควรอนุญาตหรือไม่อนุญาตคำขอ แต่อาจแสดงข้อความที่ซับซ้อนกว่า
Codelab นี้ครอบคลุมอะไรบ้าง
ขั้นตอนแรกของ Codelab นี้จะบ่งบอกถึงแรงจูงใจในการใช้ CEL และแนวคิดหลักของ CEL ส่วนที่เหลือจะมีไว้สำหรับการเขียนโค้ดแบบฝึกหัดที่ครอบคลุมกรณีการใช้งานทั่วไป หากต้องการข้อมูลเชิงลึกเกี่ยวกับภาษา ความหมาย และฟีเจอร์ โปรดดูคำจำกัดความของภาษา CEL ใน GitHub และ CEL Go Docs
Codelab นี้มีเป้าหมายสำหรับนักพัฒนาซอฟต์แวร์ที่ต้องการเรียนรู้ CEL เพื่อใช้บริการที่รองรับ CEL อยู่แล้ว Codelab นี้ไม่ได้กล่าวถึงวิธีผสานรวม CEL เข้ากับโปรเจ็กต์ของคุณเอง
สิ่งที่คุณจะได้เรียนรู้
- แนวคิดหลักของ CEL
 - สวัสดีทุกคน: การใช้ CEL เพื่อประเมินสตริง
 - การสร้างตัวแปร
 - ทำความเข้าใจการลัดวงจรของ CEL ในการดำเนินการตรรกะ AND/OR
 - วิธีใช้ CEL เพื่อสร้าง JSON
 - วิธีใช้ CEL เพื่อสร้าง Protobuffers
 - การสร้างมาโคร
 - วิธีปรับแต่งนิพจน์ CEL
 
สิ่งที่คุณต้องมี
ข้อกำหนดเบื้องต้น
Codelab นี้สร้างขึ้นจากความเข้าใจขั้นพื้นฐานเกี่ยวกับบัฟเฟอร์โปรโตคอลและ Go Lang
หากคุณไม่คุ้นเคยกับ Protocol Buffers แบบฝึกหัดแรกจะช่วยให้คุณเข้าใจว่า CEL ทำงานอย่างไร แต่เนื่องจากตัวอย่างขั้นสูงกว่านั้นใช้ Protocol Buffers เป็นข้อมูลที่ป้อนใน CEL จึงอาจเข้าใจยากกว่า โปรดลองทำตามบทแนะนำเหล่านี้ก่อน โปรดทราบว่าบัฟเฟอร์โปรโตคอลไม่จำเป็นต้องใช้ CEL แต่มีการใช้กันอย่างแพร่หลายใน Codelab นี้
คุณทดสอบว่ามีการติดตั้งเวอร์ชันดังกล่าวได้โดยการเรียกใช้
go --help
2. หัวข้อสำคัญ
แอปพลิเคชัน
CEL เป็นเครื่องมือทั่วไปและมีการใช้งานสำหรับแอปพลิเคชันที่หลากหลาย ตั้งแต่การกำหนดเส้นทาง RPC ไปจนถึงการกำหนดนโยบายการรักษาความปลอดภัย CEL นั้นมีความยืดหยุ่น ใช้ได้กับแอปพลิเคชันมากมาย และเพิ่มประสิทธิภาพสำหรับเวิร์กโฟลว์การคอมไพล์ครั้งเดียวและประเมินได้จำนวนมาก
บริการและแอปพลิเคชันมากมายจะประเมินการกำหนดค่าเชิงประกาศ เช่น การควบคุมการเข้าถึงตามบทบาท (RBAC) เป็นการกำหนดค่าเชิงประกาศที่สร้างการตัดสินใจในการเข้าถึงบทบาทและกลุ่มผู้ใช้ หากการกำหนดค่าเชิงประกาศเป็นกรณีการใช้งาน 80% CEL จะเป็นเครื่องมือที่มีประโยชน์ในการปัดเศษ 20% ที่เหลือเมื่อผู้ใช้ต้องการพลังในการแสดงออกมากขึ้น
การรวบรวม
นิพจน์จะถูกคอมไพล์กับสภาพแวดล้อม ขั้นตอนการคอมไพล์จะสร้างแผนผังไวยากรณ์นามธรรม (AST) ในรูปแบบ Protobuf โดยทั่วไป นิพจน์ที่คอมไพล์แล้วจะได้รับการเก็บไว้สำหรับการใช้งานในอนาคตเพื่อให้การประเมินเป็นไปโดยเร็วที่สุด นิพจน์ที่คอมไพล์รายการเดียวจะประเมินได้ด้วยอินพุตหลายรายการ
นิพจน์
ผู้ใช้กำหนดนิพจน์ บริการและแอปพลิเคชันจะกำหนดสภาพแวดล้อมที่ใช้งาน ลายเซ็นของฟังก์ชันจะประกาศอินพุตและเขียนนอกนิพจน์ CEL ระบบจะนำเข้าไลบรารีของฟังก์ชันที่ใช้ได้กับ CEL โดยอัตโนมัติ
ในตัวอย่างต่อไปนี้ นิพจน์จะใช้ออบเจ็กต์คำขอ และคำขอมีโทเค็นการอ้างสิทธิ์ นิพจน์นี้จะแสดงค่าบูลีนที่ระบุว่าโทเค็นการอ้างสิทธิ์ยังคงใช้งานได้หรือไม่
// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
//   claims - authentication claims.
//   now    - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now
สภาพแวดล้อม
สภาพแวดล้อมจะกำหนดโดยบริการ บริการและแอปพลิเคชันที่ฝัง CEL จะประกาศสภาพแวดล้อมของนิพจน์ สภาพแวดล้อมคือคอลเล็กชันของตัวแปรและฟังก์ชันที่ใช้ในนิพจน์ได้
เครื่องมือตรวจสอบประเภท CEL จะใช้การประกาศแบบโปรโตเพื่อให้แน่ใจว่ามีการประกาศและใช้การอ้างอิงตัวระบุและฟังก์ชันทั้งหมดภายในนิพจน์อย่างถูกต้อง
การแยกวิเคราะห์นิพจน์ 3 ขั้นตอน
การประมวลผลนิพจน์มี 3 ขั้นตอน ได้แก่ แยกวิเคราะห์ ตรวจสอบ และประเมิน รูปแบบที่พบบ่อยที่สุดสำหรับ CEL คือระนาบควบคุมที่จะแยกวิเคราะห์และตรวจสอบนิพจน์ในเวลากำหนดค่า และจัดเก็บ AST

ขณะรันไทม์ ระนาบข้อมูลจะดึงและประเมิน AST ซ้ำๆ CEL ได้รับการเพิ่มประสิทธิภาพเพื่อประสิทธิภาพรันไทม์ แต่ไม่ควรแยกวิเคราะห์และตรวจสอบในเส้นทางโค้ดที่สำคัญของเวลาในการตอบสนอง

CEL จะแยกวิเคราะห์จากนิพจน์ที่มนุษย์อ่านได้เป็นแผนผังไวยากรณ์นามธรรมโดยใช้ไวยากรณ์ lexer / โปรแกรมแยกวิเคราะห์ ANTLR เฟสการแยกวิเคราะห์จะปล่อยโครงสร้างไวยากรณ์นามธรรมที่อิงตามโปรโต ซึ่งโหนด Expr แต่ละรายการใน AST จะมีรหัสจำนวนเต็มที่ใช้ในการจัดทำดัชนีเป็นข้อมูลเมตาที่สร้างขึ้นระหว่างการแยกวิเคราะห์และตรวจสอบ syntax.proto ที่สร้างขึ้นระหว่างการแยกวิเคราะห์แสดงถึงการนำเสนอแบบนามธรรมของสิ่งที่พิมพ์ในรูปแบบสตริงของนิพจน์
เมื่อมีการแยกวิเคราะห์นิพจน์แล้ว อาจมีการตรวจสอบนิพจน์นั้นกับสภาพแวดล้อมเพื่อให้แน่ใจว่ามีการประกาศตัวแปรและตัวระบุฟังก์ชันทั้งหมดในนิพจน์นั้นและใช้งานอย่างถูกต้อง เครื่องมือตรวจสอบประเภทจะสร้าง checked.proto ที่มีข้อมูลเมตาประเภท ตัวแปร และความละเอียดของฟังก์ชันที่ช่วยปรับปรุงประสิทธิภาพการประเมินได้อย่างมาก
ผู้ประเมิน CEL ต้องการ 3 สิ่งต่อไปนี้
- การเชื่อมโยงฟังก์ชันสำหรับส่วนขยายที่กำหนดเอง
 - การเชื่อมโยงตัวแปร
 - AST ในการประเมิน
 
การเชื่อมโยงฟังก์ชันและตัวแปรควรตรงกับที่ใช้ในการคอมไพล์ AST อินพุตใดๆ เหล่านี้สามารถนํามาใช้ซ้ำในการประเมินได้หลายรายการ เช่น AST ที่ได้รับการประเมินในการเชื่อมโยงตัวแปรหลายชุด หรือใช้ตัวแปรเดียวกับที่ใช้กับ AST หลายรายการ หรือการเชื่อมโยงฟังก์ชันที่ใช้ตลอดอายุของกระบวนการ (กรณีทั่วไป)
3. ตั้งค่า
โค้ดสำหรับ Codelab นี้อยู่ในโฟลเดอร์ codelab ของที่เก็บ cel-go มีวิธีแก้ปัญหาอยู่ในโฟลเดอร์ codelab/solution ของที่เก็บเดียวกัน
โคลนและซีดีลงในที่เก็บ:
git clone https://github.com/google/cel-go.git 
cd cel-go/codelab
เรียกใช้โค้ดโดยใช้ go run:
go run .
คุณควรจะเห็นผลลัพธ์ต่อไปนี้
=== Exercise 1: Hello World ===
=== Exercise 2: Variables ===
=== Exercise 3: Logical AND/OR ===
=== Exercise 4: Customization ===
=== Exercise 5: Building JSON ===
=== Exercise 6: Building Protos ===
=== Exercise 7: Macros ===
=== Exercise 8: Tuning ===
แพ็กเกจ CEL อยู่ตรงไหน
เปิด codelab/codelab.go ในเครื่องมือแก้ไข คุณควรจะเห็นฟังก์ชันหลักที่กระตุ้นการดำเนินการทำแบบฝึกหัดใน Codelab นี้ ตามด้วยฟังก์ชันตัวช่วย 3 บล็อก ผู้ช่วยเหลือชุดแรกจะให้ความช่วยเหลือในขั้นตอนการประเมิน CEL ดังนี้
- ฟังก์ชัน 
Compile: แยกวิเคราะห์และการตรวจสอบ ตลอดจนนิพจน์อินพุตกับสภาพแวดล้อม - ฟังก์ชัน 
Eval: ประเมินโปรแกรมที่คอมไพล์เทียบกับอินพุต - ฟังก์ชัน 
Report: จัดรูปแบบผลลัพธ์การประเมิน 
นอกจากนี้ยังมีผู้ช่วยเหลือ request และ auth เพื่ออำนวยความสะดวกในการจัดทำอินพุตสำหรับแบบฝึกหัดต่างๆ
แบบฝึกหัดจะอ้างอิงแพ็กเกจตามชื่อแพ็กเกจสั้นๆ หากคุณต้องการเจาะลึกรายละเอียด การแมปจากแพ็กเกจไปยังตำแหน่งต้นทางภายในที่เก็บ google/cel-go จะอยู่ด้านล่าง
แพ็กเกจ  | ตำแหน่งของแหล่งที่มา  | คำอธิบาย  | 
cel  | อินเทอร์เฟซระดับบนสุด  | |
อ้างอิง  | อินเทอร์เฟซข้อมูลอ้างอิง  | |
ประเภท  | ค่าประเภทรันไทม์  | 
4. สวัสดี
ตามธรรมเนียมของภาษาโปรแกรมทั้งหมด เราจะเริ่มด้วยการสร้างและประเมิน " Hello World!"
กำหนดค่าสภาพแวดล้อม
หาการประกาศของ exercise1 ในเครื่องมือแก้ไข แล้วป้อนข้อมูลต่อไปนี้เพื่อตั้งค่าสภาพแวดล้อม
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Will add the parse and check steps here
}
แอปพลิเคชัน CEL จะประเมินนิพจน์เทียบกับสภาพแวดล้อม env, err := cel.NewEnv() กำหนดค่าสภาพแวดล้อมมาตรฐาน
คุณปรับแต่งสภาพแวดล้อมได้โดยระบุตัวเลือก cel.EnvOption สำหรับการโทร ตัวเลือกเหล่านี้จะสามารถปิดใช้มาโคร ประกาศตัวแปรและฟังก์ชันที่กำหนดเอง ฯลฯ
สภาพแวดล้อม CEL มาตรฐานรองรับประเภท โอเปอเรเตอร์ ฟังก์ชัน และมาโครทั้งหมดที่กำหนดไว้ในข้อกำหนดภาษา
แยกวิเคราะห์และตรวจสอบนิพจน์
เมื่อกำหนดค่าสภาพแวดล้อมแล้ว คุณจะแยกวิเคราะห์และตรวจสอบนิพจน์ได้ เพิ่มข้อมูลต่อไปนี้ลงในฟังก์ชัน
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Check that the expression compiles and returns a String.
    ast, iss := env.Parse(`"Hello, World!"`)
    // Report syntactic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Type-check the expression for correctness.
    checked, iss := env.Check(ast)
    // Report semantic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Check the output type is a string.
    if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
        glog.Exitf(
            "Got %v, wanted %v output type",
            checked.OutputType(), cel.StringType,
        )
    }
    // Will add the planning step here
}
ค่า iss ที่การเรียก Parse และ Check แสดงผลคือรายการปัญหาที่อาจเป็นข้อผิดพลาด หาก iss.Err() ไม่ใช่ค่าศูนย์ แสดงว่ามีข้อผิดพลาดด้านไวยากรณ์หรืออรรถศาสตร์ และโปรแกรมจะดำเนินการต่อไม่ได้ เมื่อนิพจน์มีรูปแบบถูกต้อง ผลลัพธ์ของการเรียกเหล่านี้จะเป็นตัวดำเนินการ cel.Ast
หาค่านิพจน์
เมื่อแยกวิเคราะห์นิพจน์และตรวจสอบเป็น cel.Ast แล้ว สามารถแปลงเป็นโปรแกรมที่ประเมินได้ ซึ่งการเชื่อมโยงฟังก์ชันและโหมดการประเมินสามารถปรับแต่งได้ด้วยตัวเลือกฟังก์ชัน หมายเหตุ คุณยังสามารถอ่าน cel.Ast จาก Pro โดยใช้ฟังก์ชัน cel.CheckedExprToAst หรือ cel.ParsedExprToAst
เมื่อวางแผนcel.Programแล้ว ระบบจะประเมินข้อมูลนั้นเทียบกับข้อมูลที่ป้อนโดยโทรหา Eval ผลลัพธ์ของ Eval จะมีผลลัพธ์ รายละเอียดการประเมิน และสถานะข้อผิดพลาด
เพิ่มการวางแผนและโทรหา eval:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Check that the expression compiles and returns a String.
    ast, iss := env.Parse(`"Hello, World!"`)
    // Report syntactic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Type-check the expression for correctness.
    checked, iss := env.Check(ast)
    // Report semantic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Check the output type is a string.
    if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
        glog.Exitf(
            "Got %v, wanted %v output type",
            checked.OutputType(), cel.StringType)
    }
    // Plan the program.
    program, err := env.Program(checked)
    if err != nil {
        glog.Exitf("program error: %v", err)
    }
    // Evaluate the program without any additional arguments.
    eval(program, cel.NoVars())
    fmt.Println()
}
เพื่อความกระชับ เราจะไม่รวมกรณีข้อผิดพลาดที่ระบุไว้ข้างต้นจากแบบฝึกหัดในอนาคต
เรียกใช้โค้ด
เรียกใช้โค้ดอีกครั้งในบรรทัดคำสั่ง
go run .
คุณควรเห็นผลลัพธ์ต่อไปนี้พร้อมกับตัวยึดตำแหน่งสำหรับการออกกำลังกายในอนาคต
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. ใช้ตัวแปรในฟังก์ชัน
แอปพลิเคชัน CEL ส่วนใหญ่จะประกาศตัวแปรที่อ้างอิงภายในนิพจน์ได้ การประกาศตัวแปรจะระบุชื่อและประเภท ประเภทของตัวแปรอาจเป็นประเภท CEL Built-In, ประเภทบัฟเฟอร์โปรโตคอลที่รู้จัก หรือประเภทข้อความ Protobuf ใดก็ได้ ตราบใดที่มีการระบุตัวบ่งชี้ให้กับ CEL ด้วย
เพิ่มฟังก์ชัน
ในเครื่องมือแก้ไข ให้ค้นหาการประกาศของ exercise2 แล้วเพิ่มข้อมูลต่อไปนี้
// exercise2 shows how to declare and use variables in expressions.
//
// Given a request of type google.rpc.context.AttributeContext.Request
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
   env, err := cel.NewEnv(
    // Add cel.EnvOptions values here.
  )
  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)
  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}
เรียกใช้อีกครั้งและทำความเข้าใจข้อผิดพลาด
เรียกใช้โปรแกรมอีกครั้ง
go run .
คุณควรจะเห็นผลลัพธ์ต่อไปนี้
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
 | request.auth.claims.group == 'admin'
 | ^
เครื่องมือตรวจสอบประเภทจะสร้างข้อผิดพลาดสำหรับอ็อบเจกต์คำขอ ซึ่งจะรวมข้อมูลโค้ดแหล่งที่มาที่เกิดข้อผิดพลาดไว้ได้อย่างสะดวก
6. ประกาศตัวแปร
เพิ่ม EnvOptions
ในเครื่องมือแก้ไข ให้แก้ไขข้อผิดพลาดที่เกิดขึ้นโดยส่งการประกาศสำหรับออบเจ็กต์คำขอเป็นข้อความประเภท google.rpc.context.AttributeContext.Request ดังนี้
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
  env, err := cel.NewEnv(
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)
  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}
เรียกใช้อีกครั้งและทำความเข้าใจข้อผิดพลาด
การเรียกใช้โปรแกรมอีกครั้ง
go run .
คุณควรเห็นข้อผิดพลาดต่อไปนี้
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
 | request.auth.claims.group == 'admin'
 | .......^
หากต้องการใช้ตัวแปรที่อ้างถึงข้อความ protobuf เครื่องมือตรวจสอบประเภทจะต้องรู้จักตัวบอกประเภทด้วย
ใช้ cel.Types เพื่อระบุข้อบ่งชี้สำหรับคำขอในฟังก์ชัน
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
   env, err := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}), 
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)
  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}
เรียกใช้อีกครั้งเรียบร้อยแล้ว
เรียกใช้โปรแกรมอีกครั้ง
go run .
คุณควรจะเห็นสิ่งต่อไปนี้
=== Exercise 2: Variables ===
request.auth.claims.group == 'admin'
------ input ------
request = time: <
  seconds: 1569255569
>
auth: <
  principal: "user:me@acme.co"
  claims: <
    fields: <
      key: "group"
      value: <
        string_value: "admin"
      >
    >
  >
>
------ result ------
value: true (types.Bool)
ในการตรวจสอบ เราเพียงประกาศตัวแปรสำหรับข้อผิดพลาด กำหนดข้อบ่งชี้ประเภท แล้วอ้างอิงตัวแปรในการประเมินนิพจน์
7. ตรรกะ AND/OR
ฟีเจอร์ที่ไม่เหมือนใครอย่างหนึ่งของ CEL คือการใช้โอเปอเรเตอร์ตรรกะแบบสับเปลี่ยน ทั้ง 2 ฝั่งของสาขาแบบมีเงื่อนไขอาจทำให้การประเมินถูกตัดให้สั้นลง แม้อาจเกิดข้อผิดพลาดหรือมีอินพุตเพียงบางส่วน
กล่าวคือ CEL จะค้นหาลำดับการประเมินที่ให้ผลลัพธ์ทุกครั้งที่เป็นไปได้ โดยจะไม่สนใจข้อผิดพลาดหรือแม้แต่ข้อมูลที่ขาดหายไปที่อาจเกิดขึ้นในคำสั่งการประเมินอื่นๆ แอปพลิเคชันสามารถใช้พร็อพเพอร์ตี้นี้เพื่อลดค่าใช้จ่ายของการประเมิน โดยเลื่อนการรวบรวมอินพุตราคาแพงเมื่อผลลัพธ์สามารถเข้าถึงได้โดยไม่ต้องรอ
เราจะเพิ่มตัวอย่าง AND/OR จากนั้นลองใช้กับอินพุตอื่นเพื่อทำความเข้าใจวิธีการประเมินการลัดวงจรของ CEL
สร้างฟังก์ชัน
ในเครื่องมือแก้ไข ให้เพิ่มเนื้อหาต่อไปนี้ในแบบฝึกหัดที่ 3
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper 
// user, and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
}
ถัดไป ให้ใส่คำสั่ง "หรือ" ที่จะแสดงผลเป็น "จริง" หากผู้ใช้เป็นสมาชิกของกลุ่ม admin หรือมีตัวระบุอีเมลที่เฉพาะเจาะจง
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper 
// user, and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)
}
และสุดท้าย ให้เพิ่มเคส eval ที่ประเมินผู้ใช้ด้วยชุดการอ้างสิทธิ์ที่ว่างเปล่า ดังนี้
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
}
เรียกใช้โค้ดด้วยชุดการอ้างสิทธิ์ที่ว่างเปล่า
การเรียกใช้โปรแกรมอีกครั้ง คุณควรเห็นผลลัพธ์ใหม่ต่อไปนี้
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
    || request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
  seconds: 1569302377
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>
------ result ------
value: true (types.Bool)
อัปเดตกรณีการประเมิน
ถัดไป ให้อัปเดตกรณีการประเมินเพื่อส่งต่อผู้ใช้หลักรายอื่นที่มีชุดการอ้างสิทธิ์ที่ว่างเปล่า ดังนี้
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
}
เรียกใช้โค้ดที่มีเวลา
การเรียกใช้โปรแกรมอีกครั้ง
go run .
คุณควรเห็นข้อผิดพลาดต่อไปนี้
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
    || request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
  seconds: 1588989833
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>
------ result ------
value: true (types.Bool)
------ input ------
request = time: <
  seconds: 1588989833
>
auth: <
  principal: "other:me@acme.co"
  claims: <
  >
>
------ result ------
error: no such key: group
ใน Protobuf เราทราบว่าต้องพบกับฟิลด์และประเภทใดบ้าง ในค่าการแมปและ JSON เราไม่ทราบว่าจะมีคีย์หรือไม่ เนื่องจาก CEL ไม่มีค่าเริ่มต้นที่ปลอดภัยสำหรับคีย์ที่หายไป จึงไม่มีค่าเริ่มต้นเป็นข้อผิดพลาด
8. ฟังก์ชันที่กำหนดเอง
แม้ว่า CEL จะมีฟังก์ชันในตัวมากมาย แต่ก็อาจมีบางครั้งที่ฟังก์ชันที่กำหนดเองมีประโยชน์ เช่น สามารถใช้ฟังก์ชันที่กำหนดเองเพื่อปรับปรุงประสบการณ์ของผู้ใช้สำหรับเงื่อนไขทั่วไปหรือแสดงสถานะที่คำนึงถึงบริบท
ในแบบฝึกหัดนี้ เราจะศึกษาวิธีแสดงฟังก์ชันเพื่อรวมการตรวจสอบที่ใช้กันทั่วไปไว้ด้วยกัน
เรียกใช้ฟังก์ชันที่กำหนดเอง
ขั้นแรก ให้สร้างรหัสเพื่อตั้งค่าการลบล้างที่ชื่อ contains ซึ่งกำหนดว่ามีคีย์อยู่ในแผนที่หรือไม่และมีค่าเฉพาะหรือไม่ ปล่อยตัวยึดตำแหน่งสำหรับคำจำกัดความของฟังก์ชันและการเชื่อมโยงฟังก์ชัน
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The
  // custom map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
    // Add the custom function declaration and binding here with cel.Function()
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)
  // Construct the program plan and provide the 'contains' function impl.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(
    program,
    request(auth("user:me@acme.co", emptyClaims), time.Now()),
  )
  fmt.Println()
}  
เรียกใช้โค้ดและทำความเข้าใจข้อผิดพลาด
เรียกใช้โค้ดอีกครั้ง คุณควรพบข้อผิดพลาดต่อไปนี้
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
 | request.auth.claims.contains('group', 'admin')
 | ............................^
ในการแก้ไขข้อผิดพลาด เราต้องเพิ่มฟังก์ชัน contains ลงในรายการการประกาศที่ประกาศตัวแปรคำขอในปัจจุบัน
ประกาศประเภทที่ทำเป็นพารามิเตอร์โดยเพิ่ม 3 บรรทัดต่อไปนี้ (ซึ่งมีความซับซ้อนพอๆ กับที่ฟังก์ชันโอเวอร์โหลดจะเป็นสำหรับ CEL)
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value
  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
    // Add the custom function declaration and binding here with cel.Function()
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)
  // Construct the program plan.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  fmt.Println()
}
เพิ่มฟังก์ชันที่กำหนดเอง
ต่อไป เราจะเพิ่มฟังก์ชัน "มี" ใหม่ที่ใช้ประเภทพารามิเตอร์
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value
  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)
 
  // Env declaration.
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),   
    // Declare the custom contains function and its implementation.
    cel.Function("contains",
      cel.MemberOverload(
        "map_contains_key_value",
        []*cel.Type{mapAB, typeParamA, typeParamB},
        cel.BoolType,
        // Provide the implementation using cel.FunctionBinding()
      ),
    ),
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)
  // Construct the program plan and provide the 'contains' function impl.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  fmt.Println()
}
เรียกใช้โปรแกรมเพื่อทำความเข้าใจข้อผิดพลาด
เรียกใช้การออกกำลังกาย คุณควรพบข้อผิดพลาดต่อไปนี้เกี่ยวกับฟังก์ชันรันไทม์ที่ขาดหายไป
------ result ------
error: no such overload: contains
ติดตั้งใช้งานฟังก์ชันกับการประกาศ NewEnv โดยใช้ฟังก์ชัน cel.FunctionBinding() ดังนี้
// exercise4 demonstrates how to extend CEL with custom functions.
//
// Declare a contains member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value
  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)
  // Env declaration.
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),   
    // Declare the custom contains function and its implementation.
    cel.Function("contains",
      cel.MemberOverload(
        "map_contains_key_value",
        []*cel.Type{mapAB, typeParamA, typeParamB},
        cel.BoolType,
        cel.FunctionBinding(mapContainsKeyValue)),
    ),
  )
  ast := compile(env, 
    `request.auth.claims.contains('group', 'admin')`, 
    cel.BoolType)
  // Construct the program plan.
  // Output: false
  program, err := env.Program(ast)
  if err != nil {
    glog.Exit(err)
  }
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}
ตอนนี้โปรแกรมควรทำงานเป็นปกติ ดังนี้
=== Exercise 4: Custom Functions ===
request.auth.claims.contains('group', 'admin')
------ input ------
request = time: <
  seconds: 1569302377
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>
------ result ------
value: false (types.Bool)
จะเกิดอะไรขึ้นหากมีการอ้างสิทธิ์
หากต้องการเครดิตเพิ่มเติม ให้ลองตั้งค่าการอ้างสิทธิ์ของผู้ดูแลระบบในอินพุตเพื่อตรวจสอบว่ามีโอเวอร์โหลดด้วย แสดงค่า "จริง" เมื่อมีการอ้างสิทธิ์อยู่ คุณควรจะเห็นผลลัพธ์ต่อไปนี้
=== Exercise 4: Customization ===
request.auth.claims.contains('group','admin')
------ input ------
request = time: <
  seconds: 1588991010
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>
------ result ------
value: false (types.Bool)
------ input ------
request = time: <
  seconds: 1588991010
>
auth: <
  principal: "user:me@acme.co"
  claims: <
    fields: <
      key: "group"
      value: <
        string_value: "admin"
      >
    >
  >
>
------ result ------
value: true (types.Bool)
ก่อนที่จะดำเนินการต่อ คุณควรตรวจสอบฟังก์ชัน mapContainsKeyValue ด้วย
// mapContainsKeyValue implements the custom function:
//   map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
  // The declaration of the function ensures that only arguments which match
  // the mapContainsKey signature will be provided to the function.
  m := args[0].(traits.Mapper)
  // CEL has many interfaces for dealing with different type abstractions.
  // The traits.Mapper interface unifies field presence testing on proto
  // messages and maps.
  key := args[1]
  v, found := m.Find(key)
  // If not found and the value was non-nil, the value is an error per the
  // `Find` contract. Propagate it accordingly. Such an error might occur with
  // a map whose key-type is listed as 'dyn'.
  if !found {
    if v != nil {
      return types.ValOrErr(v, "unsupported key type")
    }
    // Return CEL False if the key was not found.
    return types.False
  }
  // Otherwise whether the value at the key equals the value provided.
  return v.Equal(args[2])
}
ลายเซ็นสำหรับฟังก์ชันที่กำหนดเองต้องใช้อาร์กิวเมนต์ของประเภท ref.Val เพื่อให้ส่วนขยายทำได้สะดวกที่สุด ข้อดีข้อเสียก็คือการที่ส่วนขยายใช้งานได้ง่ายจะเพิ่มภาระในการติดตั้งใช้งานที่ต้องจัดการมูลค่าทุกประเภทได้อย่างเหมาะสม เมื่อประเภทหรือจำนวนอาร์กิวเมนต์อินพุตไม่ตรงกับการประกาศฟังก์ชัน ระบบจะแสดงข้อผิดพลาด no such overload
cel.FunctionBinding() เพิ่มตัวป้องกันประเภทรันไทม์เพื่อให้แน่ใจว่าสัญญารันไทม์ตรงกับการประกาศที่ได้รับการตรวจสอบประเภทในสภาพแวดล้อมแล้ว
9. การสร้าง JSON
CEL ยังสร้างเอาต์พุตที่ไม่ใช่บูลีน เช่น JSON ได้ด้วย เพิ่มข้อมูลต่อไปนี้ลงในฟังก์ชัน
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      // Declare the 'now' variable as a Timestamp.
      // cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))
    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": &tpb.Timestamp{Seconds: time.Now().Unix()},
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}
เรียกใช้โค้ด
เรียกใช้โค้ดอีกครั้ง คุณควรพบข้อผิดพลาดต่อไปนี้
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
 |    'iat': now,
 | ..........^
... and more ...
เพิ่มการประกาศสำหรับตัวแปร now ประเภท cel.TimestampType ลงใน cel.NewEnv() และเรียกใช้อีกครั้ง
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))
     // Hint:
     // Convert `out` to JSON using the valueToJSON() helper method.
     // The valueToJSON() function calls ConvertToNative(&structpb.Value{})
     // to adapt the CEL value to a protobuf JSON representation and then
     // uses the jsonpb.Marshaler utilities on the output to render the JSON
     // string.
    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": time.Now(),
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}
เรียกใช้โค้ดอีกครั้ง โค้ดควรจะสำเร็จ:
=== Exercise 5: Building JSON ===
  {'aud': 'my-project',
   'exp': now + duration('300s'),
   'extra_claims': {
    'group': 'admin'
   },
   'iat': now,
   'iss': 'auth.acme.com:12350',
   'nbf': now,
   'sub': 'serviceAccount:delegate@acme.co'
   }
------ input ------
now = seconds: 1569302377
------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)
------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}
โปรแกรมจะทำงาน แต่ต้องแปลงค่าเอาต์พุต out เป็น JSON อย่างชัดเจน การแสดง CEL ภายในในกรณีนี้คือการแปลง JSON ได้ เนื่องจากจะหมายถึงประเภทที่ JSON รองรับเท่านั้น หรือที่มีการแมปโปรโตคอลกับ JSON ที่รู้จักกันดี
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
...
    fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
    fmt.Println()
}
เมื่อแปลงประเภทโดยใช้ฟังก์ชันตัวช่วย valueToJSON ภายในไฟล์ codelab.go แล้ว คุณควรเห็นผลลัพธ์เพิ่มเติมดังต่อไปนี้
------ type conversion ------
  {
   "aud": "my-project",
   "exp": "2019-10-13T05:54:29Z",
   "extra_claims": {
      "group": "admin"
     },
   "iat": "2019-10-13T05:49:29Z",
   "iss": "auth.acme.com:12350",
   "nbf": "2019-10-13T05:49:29Z",
   "sub": "serviceAccount:delegate@acme.co"
  }
10. การสร้าง Protos
CEL สามารถสร้างข้อความ Protobuf สำหรับข้อความทุกประเภทที่คอมไพล์ลงในแอปพลิเคชัน เพิ่มฟังก์ชันเพื่อสร้าง google.rpc.context.AttributeContext.Request จากอินพุต jwt
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
  fmt.Println("=== Exercise 6: Building Protos ===\n")
  // Construct an environment and indicate that the container for all references
  // within the expression is `google.rpc.context.AttributeContext`.
  requestType := &rpcpb.AttributeContext_Request{}
  env, _ := cel.NewEnv(
      // Add cel.Container() option for 'google.rpc.context.AttributeContext'
      cel.Types(requestType),
      // Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
      // and for 'now' as a timestamp.
  )
  // Compile the Request message construction expression and validate that
  // the resulting expression type matches the fully qualified message name.
  //
  // Note: the field names within the proto message types are not quoted as they
  // are well-defined names composed of valid identifier characters. Also, note
  // that when building nested proto objects, the message name needs to prefix 
  // the object construction.
  ast := compile(env, `
    Request{
        auth: Auth{
            principal: jwt.iss + '/' + jwt.sub,
            audiences: [jwt.aud],
            presenter: 'azp' in jwt ? jwt.azp : "",
            claims: jwt
        },
        time: now
    }`,
    cel.ObjectType("google.rpc.context.AttributeContext.Request"),
  )
  program, _ := env.Program(ast)
  // Construct the message. The result is a ref.Val that returns a dynamic
  // proto message.
  out, _, _ := eval(
      program,
      map[string]interface{}{
          "jwt": map[string]interface{}{
              "sub": "serviceAccount:delegate@acme.co",
              "aud": "my-project",
              "iss": "auth.acme.com:12350",
              "extra_claims": map[string]string{
                  "group": "admin",
              },
          },
          "now": time.Now(),
      },
  )
  // Hint: Unwrap the CEL value to a proto. Make sure to use the
  // `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
  // message to the concrete proto message type expected.
  fmt.Printf("------ type unwrap ------\n%v\n", out)
  fmt.Println()
}
เรียกใช้โค้ด
เรียกใช้โค้ดอีกครั้ง คุณควรพบข้อผิดพลาดต่อไปนี้
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
 |   Request{
 | .........^
โดยพื้นฐานแล้ว คอนเทนเนอร์นี้เทียบเท่ากับเนมสเปซหรือแพ็กเกจ แต่ก็อาจมีรายละเอียดได้ไม่ต่างจากชื่อข้อความ protobuf คอนเทนเนอร์ CEL ใช้กฎการแปลงเนมสเปซเดียวกันกับโปรโตคอลและ C++ เพื่อระบุตำแหน่งที่ประกาศตัวแปร ฟังก์ชัน หรือชื่อประเภท
ตัวตรวจสอบประเภทและตัวประเมินจะลองใช้ชื่อตัวระบุต่อไปนี้สำหรับตัวแปร ประเภท และฟังก์ชันทั้งหมดเมื่อให้คอนเทนเนอร์ google.rpc.context.AttributeContext
google.rpc.context.AttributeContext.<id>google.rpc.context.<id>google.rpc.<id>google.<id><id>
สำหรับชื่อสัมบูรณ์ ให้ใส่จุดนำหน้าตัวแปร ประเภท หรือการอ้างอิงฟังก์ชันไว้ด้านหน้า ในตัวอย่าง นิพจน์ .<id> จะค้นหาตัวระบุ <id> ระดับบนสุดเท่านั้น โดยไม่ต้องตรวจสอบภายในคอนเทนเนอร์ก่อน
ลองระบุตัวเลือก cel.Container("google.rpc.context.AttributeContext") ให้กับสภาพแวดล้อม CEL แล้วเรียกใช้อีกครั้งดังนี้
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
  fmt.Println("=== Exercise 6: Building Protos ===\n")
  // Construct an environment and indicate that the container for all references
  // within the expression is `google.rpc.context.AttributeContext`.
  requestType := &rpcpb.AttributeContext_Request{}
  env, _ := cel.NewEnv(
    // Add cel.Container() option for 'google.rpc.context.AttributeContext'
    cel.Container("google.rpc.context.AttributeContext.Request"),
    cel.Types(requestType),
    // Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
    // and for 'now' as a timestamp.
    // cel.Variable("now", cel.TimestampType),
    // cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
  )
 ...
}
คุณจะได้รับเอาต์พุตต่อไปนี้
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
 |     principal: jwt.iss + '/' + jwt.sub,
 | ...............^
... และข้อผิดพลาดอื่นๆ อีกมากมาย...
จากนั้นให้ประกาศตัวแปร jwt และ now แล้วโปรแกรมควรทำงานตามที่คาดไว้
=== Exercise 6: Building Protos ===
  Request{
   auth: Auth{
    principal: jwt.iss + '/' + jwt.sub,
    audiences: [jwt.aud],
    presenter: 'azp' in jwt ? jwt.azp : "",
    claims: jwt
   },
   time: now
  }
------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group": "admin"
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027
------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)
----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}
สำหรับเครดิตเพิ่มเติม คุณควรแยกประเภทโดยโทรหา out.Value() ในข้อความเพื่อดูว่าผลลัพธ์เปลี่ยนแปลงไปอย่างไร
11. มาโคร
มาโครสามารถใช้เพื่อจัดการโปรแกรม CEL ในเวลาแยกวิเคราะห์ได้ มาโครจะจับคู่ลายเซ็นการเรียกใช้ รวมถึงจัดการการเรียกอินพุตและอาร์กิวเมนต์เพื่อสร้าง AST ของนิพจน์ย่อยใหม่
มาโครสามารถใช้เพื่อนำตรรกะที่ซับซ้อนใน AST มาใช้ซึ่งเขียนใน CEL โดยตรงไม่ได้ ตัวอย่างเช่น มาโคร has จะเปิดใช้การทดสอบตัวตนภาคสนาม มาโครความเข้าใจอย่างเช่นที่มีอยู่และทั้งหมดจะแทนที่การเรียกฟังก์ชันด้วยการทำซ้ำโดยมีขอบเขตบนรายการอินพุตหรือแผนที่ ทั้งแนวคิดที่เป็นไปไม่ได้ในระดับไวยากรณ์ แต่เป็นไปได้ผ่านการขยายมาโคร
เพิ่มและเรียกใช้แบบฝึกหัดถัดไป:
// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
    fmt.Println("=== Exercise 7: Macros ===\n")
    env, _ := cel.NewEnv(
      cel.ClearMacros(),
      cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
    )
    ast := compile(env,
        `jwt.extra_claims.exists(c, c.startsWith('group'))
                && jwt.extra_claims
                      .filter(c, c.startsWith('group'))
                      .all(c, jwt.extra_claims[c]
                                 .all(g, g.endsWith('@acme.co')))`,
        cel.BoolType)
    program, _ := env.Program(ast)
    // Evaluate a complex-ish JWT with two groups that satisfy the criteria.
    // Output: true.
    eval(program,
        map[string]interface{}{
            "jwt": map[string]interface{}{
                "sub": "serviceAccount:delegate@acme.co",
                "aud": "my-project",
                "iss": "auth.acme.com:12350",
                "extra_claims": map[string][]string{
                    "group1": {"admin@acme.co", "analyst@acme.co"},
                    "labels": {"metadata", "prod", "pii"},
                    "groupN": {"forever@acme.co"},
                },
            },
        })
    fmt.Println()
}
คุณควรเห็นข้อผิดพลาดต่อไปนี้
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
 | jwt.extra_claims.exists(c, c.startsWith('group'))
 | ........................^
... และอื่นๆ อีกมากมาย ...
ข้อผิดพลาดเหล่านี้เกิดขึ้นเนื่องจากยังไม่ได้เปิดใช้งานมาโคร หากต้องการเปิดใช้มาโคร ให้นำ cel.ClearMacros() ออก แล้วเรียกใช้อีกครั้ง:
=== Exercise 7: Macros ===
jwt.extra_claims.exists(c, c.startsWith('group'))
    && jwt.extra_claims
       .filter(c, c.startsWith('group'))
       .all(c, jwt.extra_claims[c]
              .all(g, g.endsWith('@acme.co')))
------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group1": [
      "admin@acme.co",
      "analyst@acme.co"
    ],
    "groupN": [
      "forever@acme.co"
    ],
    "labels": [
      "metadata",
      "prod",
      "pii"
    ]
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}
------ result ------
value: true (types.Bool)
มาโครที่รองรับในปัจจุบันมีดังนี้
มาโคร  | ลายเซ็น  | คำอธิบาย  | 
ทั้งหมด  | r.all(var, cond)  | ทดสอบว่า cond ประเมินค่า true สำหรับ var all ในช่วง r หรือไม่  | 
มีอยู่  | r.exists(var, cond)  | ทดสอบว่า cond ประเมินค่า true สำหรับตัวแปรใดก็ได้ ในช่วง r  | 
exists_one  | r.exists_one(var, cond)  | ทดสอบว่า cond ประเมินค่าจริงสำหรับตัวแปรเพียงรายการเดียว ในช่วง r หรือไม่  | 
ตัวกรอง  | r.filter(var, cond)  | สำหรับรายการ ให้สร้างรายการใหม่ที่ตัวแปรองค์ประกอบแต่ละรายการในช่วง r เป็นไปตามเงื่อนไข สำหรับแผนที่ ให้สร้างรายการใหม่ที่แต่ละคีย์ตัวแปรในช่วง r เป็นไปตามเงื่อนไข  | 
แผนที่  | r.map(var, expr)  | สร้างรายการใหม่ที่แต่ละตัวแปรในช่วง r จะมีการแปลงโดย expr  | 
r.map(var, cond, expr)  | เหมือนกับแผนที่ 2 อาร์กิวเมนต์ แต่มีตัวกรองเงื่อนไขแบบมีเงื่อนไขก่อนที่ระบบจะเปลี่ยนรูปแบบค่า  | |
มี  | มี(ก.ข)  | การทดสอบการปรากฏของ b กับค่า a : สำหรับแผนที่ คำจำกัดความการทดสอบ JSON สำหรับ Proto ให้ทดสอบค่าพื้นฐานที่ไม่ใช่ค่าเริ่มต้น หรือช่องข้อความหรือช่องข้อความที่ตั้งไว้  | 
เมื่ออาร์กิวเมนต์ r เป็นช่วงประเภท map var จะเป็นคีย์แมป และสำหรับค่าประเภท list นั้น var จะเป็นค่าขององค์ประกอบรายการ มาโคร all, exists, exists_one, filter และ map จะเขียน AST ใหม่ซึ่งดำเนินการทำซ้ำแต่ละรายการที่มีขอบเขตโดยขนาดของอินพุต
ความเข้าใจที่มีขอบเขตช่วยให้มั่นใจได้ว่าโปรแกรม CEL จะไม่สมบูรณ์แบบ แต่จะประเมินตามลำดับเวลาโดยใช้อินพุต ใช้มาโครเหล่านี้เท่าที่จำเป็นหรือไม่ใช้เลย การใช้ความเข้าใจอย่างจริงจังมักจะเป็นตัวบ่งชี้ที่ดีว่าฟังก์ชันที่กำหนดเองจะให้ประสบการณ์ของผู้ใช้ที่ดีกว่าและประสิทธิภาพที่ดีกว่า
12. การจูนเครื่องยนต์
ขณะนี้มีฟีเจอร์บางอย่างที่มีใน CEL-Go โดยเฉพาะ แต่เป็นฟีเจอร์ที่บ่งบอกถึงแผนในอนาคตสำหรับการติดตั้งใช้งาน CEL อื่นๆ แบบฝึกหัดต่อไปนี้แสดงแผนของโปรแกรมต่างๆ สำหรับ AST เดียวกัน
// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
    fmt.Println("=== Exercise 8: Tuning ===\n")
    // Declare the x and 'y' variables as input into the expression.
    env, _ := cel.NewEnv(
        cel.Variable("x", cel.IntType),
        cel.Variable("y", cel.UintType),
    )
    ast := compile(env,
        `x in [1, 2, 3, 4, 5] && type(y) == uint`,
        cel.BoolType)
    // Try the different cel.EvalOptions flags when evaluating this AST for
    // the following use cases:
    // - cel.OptOptimize: optimize the expression performance.
    // - cel.OptExhaustiveEval: turn off short-circuiting.
    // - cel.OptTrackState: track state and compute a residual using the
    //   interpreter.PruneAst function.
    program, _ := env.Program(ast)
    eval(program, cel.NoVars())
    fmt.Println()
}
Optimize
เมื่อเปิด Flag การเพิ่มประสิทธิภาพ CEL จะใช้เวลาเพิ่มเติมในการสร้างรายการและแมปลิเทอรัลล่วงหน้าและเพิ่มประสิทธิภาพการเรียกบางรายการ เช่น โอเปอเรเตอร์ In เป็นการทดสอบการเป็นสมาชิกชุดจริง
    // Turn on optimization.
    trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
    program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
    // Try benchmarking with the optimization flag on and off.
    eval(program, trueVars)
เมื่อมีการประเมินโปรแกรมเดียวกันหลายครั้งโดยดูจากอินพุตที่ต่างกัน การเพิ่มประสิทธิภาพก็เป็นทางเลือกที่ดี แต่หากโปรแกรมได้รับการประเมินเพียงครั้งเดียว การเพิ่มประสิทธิภาพก็จะเพิ่มค่าใช้จ่ายในการดำเนินการ
การประเมินเมื่อเหนื่อยล้า
Exsurtive Eval มีประโยชน์สำหรับการแก้ไขข้อบกพร่องของลักษณะการทำงานการประเมินนิพจน์ เนื่องจากจะให้ข้อมูลเชิงลึกเกี่ยวกับค่าที่สังเกตได้ในแต่ละขั้นตอนในการประเมินนิพจน์
    // Turn on exhaustive eval to see what the evaluation state looks like.
    // The input is structure to show a false on the first branch, and true
    // on the second.
    falseVars := map[string]interface{}{"x": int64(6), "y": uint64(2)}
    program, _ = env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
    eval(program, falseVars)
คุณจะเห็นรายการสถานะการประเมินนิพจน์สำหรับรหัสนิพจน์แต่ละรายการดังนี้
------ eval states ------
1: 6 (types.Int)
2: false (types.Bool)
3: &{0xc0000336b0 [1 2 3 4 5] {0x89f020 0xc000434760 151}} (*types.baseList)
4: 1 (types.Int)
5: 2 (types.Int)
6: 3 (types.Int)
7: 4 (types.Int)
8: 5 (types.Int)
9: uint (*types.Type)
10: 2 (types.Uint)
11: true (types.Bool)
12: uint (*types.Type)
13: false (types.Bool)
รหัสนิพจน์ 2 สอดคล้องกับผลลัพธ์ของโอเปอเรเตอร์ in ในตอนแรก Branch และรหัสนิพจน์ 11 จะสอดคล้องกับโอเปอเรเตอร์ == ในค่าที่สอง ภายใต้การประเมินปกติ นิพจน์จะถูกตัดไฟฟ้าหลังจากคำนวณ 2 ครั้งแล้ว หาก y ไม่มีการ uint สถานะจะแสดงสาเหตุ 2 ประการที่ทำให้นิพจน์ล้มเหลว ไม่ใช่เพียง 1 ข้อ
13. เนื้อหาที่ครอบคลุมเรื่องใดบ้าง
หากต้องการใช้เครื่องมือนิพจน์ ให้ลองใช้ CEL CEL เหมาะสำหรับโปรเจ็กต์ที่จำเป็นต้องดำเนินการกับการกำหนดค่าของผู้ใช้ซึ่งจำเป็นต้องดำเนินการเพื่อเพิ่มประสิทธิภาพ
ในแบบฝึกหัดก่อนหน้า เราหวังว่าคุณจะมั่นใจที่จะส่งต่อข้อมูลไปยัง CEL และได้รับข้อมูลหรือการตัดสินใจกลับมา
เราหวังว่าคุณจะเข้าใจประเภทการดำเนินการที่คุณสามารถทำได้ ตั้งแต่การตัดสินใจแบบบูลีนไปจนถึงการสร้างข้อความ JSON และ Protobuffer
เราหวังว่าคุณจะเข้าใจวิธีใช้การแสดงออกและสิ่งที่พวกเขากำลังทำ และเราก็เข้าใจวิธีทั่วไปในการขยายการใช้งานเครื่องมือนี้ด้วย