تطوير API في Go Using Goa

نشرت: 2022-03-11

تطوير API هو موضوع ساخن في الوقت الحاضر. هناك عدد كبير من الطرق التي يمكنك من خلالها تطوير وتقديم واجهة برمجة تطبيقات ، وقد طورت الشركات الكبرى حلولاً ضخمة لمساعدتك في تمهيد تطبيق ما بسرعة.

ومع ذلك ، تفتقر معظم هذه الخيارات إلى ميزة أساسية: إدارة دورة حياة التطوير. لذلك ، يقضي المطورون بعض الدورات في إنشاء واجهات برمجة تطبيقات مفيدة وقوية ولكن ينتهي بهم الأمر إلى الكفاح مع التطور العضوي المتوقع لشفراتهم والآثار المترتبة على تغيير بسيط في واجهة برمجة التطبيقات في الكود المصدري.

في عام 2016 ، أنشأ رافائيل سيمون Goa ، وهو إطار عمل لتطوير API في Golang مع دورة حياة تضع تصميم API أولاً. في Goa ، لا يتم وصف تعريف API الخاص بك على أنه رمز فحسب ، بل هو أيضًا المصدر الذي يتم اشتقاق كود الخادم ، ورمز العميل ، والوثائق منه. هذا يعني أن الكود الخاص بك موصوف في تعريف API الخاص بك باستخدام لغة Golang Domain Specific Language (DSL) ، ثم يتم إنشاؤه باستخدام goa cli ، ويتم تنفيذه بشكل منفصل عن كود مصدر التطبيق الخاص بك.

هذا هو سبب تألق جوا. إنه حل مع عقد دورة حياة تطوير محدد جيدًا ويعتمد على أفضل الممارسات عند إنشاء الكود (مثل تقسيم المجالات والمخاوف المختلفة في طبقات ، لذلك لا تتداخل جوانب النقل مع جوانب العمل في التطبيق) ، باتباع نمط معماري نظيف حيث يمكن تكوينه يتم إنشاء الوحدات النمطية لطبقات النقل ونقطة النهاية ومنطق الأعمال في تطبيقك.

تتضمن بعض ميزات Goa ، كما حددها الموقع الرسمي ، ما يلي:

  • التركيب . تعتبر الحزمة وخوارزميات إنشاء التعليمات البرمجية والتعليمات البرمجية التي تم إنشاؤها كلها معيارية.
  • حيادية النقل . يعني فصل طبقة النقل عن تنفيذ الخدمة الفعلي أن نفس الخدمة يمكن أن تعرض نقاط النهاية التي يمكن الوصول إليها عبر وسائل نقل متعددة مثل HTTP و / أو gRPC.
  • فصل الاهتمامات . يتم عزل تنفيذ الخدمة الفعلي عن كود النقل.
  • استخدام أنواع مكتبة Go القياسية . هذا يجعل من السهل التعامل مع التعليمات البرمجية الخارجية.

في هذه المقالة ، سوف أقوم بإنشاء تطبيق وسأوجهك خلال مراحل دورة حياة تطوير API. يدير التطبيق تفاصيل حول العملاء ، مثل الاسم والعنوان ورقم الهاتف ووسائل التواصل الاجتماعي وما إلى ذلك. في النهاية ، سنحاول تمديده وإضافة ميزات جديدة لممارسة دورة حياة التطوير الخاصة به.

لذلك دعونا نبدأ!

تحضير منطقة التطوير الخاصة بك

خطوتنا الأولى هي بدء المستودع وتمكين دعم وحدات Go:

 mkdir -p clients/design cd clients go mod init clients

في النهاية ، يجب أن يكون هيكل الريبو الخاص بك كما يلي:

 $ tree . ├── design └── go.mod

تصميم API الخاص بك

مصدر الحقيقة لواجهة برمجة التطبيقات الخاصة بك هو تعريف التصميم الخاص بك. كما تنص الوثائق ، "تتيح لك Goa التفكير في واجهات برمجة التطبيقات الخاصة بك بشكل مستقل عن أي مخاوف تتعلق بالتنفيذ ثم مراجعة هذا التصميم مع جميع أصحاب المصلحة قبل كتابة التنفيذ." هذا يعني أن كل عنصر من عناصر واجهة برمجة التطبيقات يتم تعريفه هنا أولاً ، قبل إنشاء رمز التطبيق الفعلي. لكن يكفي الحديث!

افتح الملف clients/design/design.go وأضف المحتوى أدناه:

 /* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Method("add", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Field(2, "ClientName", String, "Client ID") Required("ClientID", "ClientName") }) Result(Empty) Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Response(StatusCreated) }) }) Method("get", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Required("ClientID") }) Result(ClientManagement) Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Response(StatusOK) }) }) Method("show", func() { Result(CollectionOf(ClientManagement)) HTTP(func() { GET("/api/v1/client") Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Required("ClientID", "ClientName") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned when " + "the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") })

أول شيء يمكنك ملاحظته هو أن DSL أعلاه عبارة عن مجموعة من وظائف Go التي يمكن تكوينها لوصف واجهة برمجة تطبيقات للخدمة عن بُعد. تتكون الوظائف باستخدام وسيطات دالة مجهولة. في وظائف DSL ، لدينا مجموعة فرعية من الوظائف التي لا يُفترض أن تظهر ضمن وظائف أخرى ، والتي نسميها DSL ذات المستوى الأعلى. أدناه ، لديك مجموعة جزئية من وظائف DSL وهيكلها:

وظائف DSL

لذلك ، لدينا في تصميمنا الأولي DSL من المستوى الأعلى لواجهة برمجة التطبيقات (API) يصف واجهة برمجة التطبيقات (API) الخاصة بالعميل ، وواحد من خدمات DSL عالية المستوى التي تصف خدمة API الرئيسية ، clients ، وخدمة ملف واجهة برمجة التطبيقات ، واثنين من نوع DSL من المستوى الأعلى لوصف نوع عرض الكائن المستخدم في حمولة النقل.

وظيفة API هي خدمة DSL اختيارية من المستوى الأعلى تسرد الخصائص العامة لواجهة برمجة التطبيقات مثل الاسم والوصف وأيضًا خادم واحد أو أكثر من المحتمل أن يعرض مجموعات مختلفة من الخدمات. في حالتنا ، يكفي خادم واحد ، ولكن يمكنك أيضًا تقديم خدمات مختلفة في مستويات مختلفة: التطوير والاختبار والإنتاج ، على سبيل المثال.

تحدد وظيفة Service مجموعة من الأساليب التي يُحتمل أن تُعيِّن موردًا في النقل. قد تحدد الخدمة أيضًا استجابات الخطأ الشائعة. يتم وصف طرق الخدمة باستخدام Method . تحدد هذه الوظيفة أنواع حمولة الأسلوب (الإدخال) والنتيجة (المخرجات). إذا حذفت الحمولة أو نوع النتيجة ، فسيتم استخدام النوع المضمن فارغ ، والذي يعيّن جسمًا فارغًا في HTTP.

أخيرًا ، تحدد وظيفتا Type أو ResultType الأنواع المعرفة من قبل المستخدم ، والفرق الرئيسي هو أن نوع النتيجة يحدد أيضًا مجموعة من "العروض".

في مثالنا ، وصفنا واجهة برمجة التطبيقات وشرحنا كيف يجب أن تخدم ، كما أنشأنا ما يلي:

  • خدمة تسمى clients
  • ثلاث طرق: add (لإنشاء عميل واحد) ، get (لاسترجاع عميل واحد) ، show (لسرد جميع العملاء)
  • أنواعنا المخصصة ، والتي ستكون مفيدة عندما نتكامل مع قاعدة بيانات ، ونوع خطأ مخصص

الآن بعد أن تم وصف تطبيقنا ، يمكننا إنشاء الكود المعياري. يأخذ الأمر التالي مسار استيراد حزمة التصميم كوسيطة. يقبل أيضًا المسار إلى دليل الإخراج كعلامة اختيارية:

 goa gen clients/design

يقوم الأمر بإخراج أسماء الملفات التي ينشئها. هناك ، gen الدليل العام على الدليل الفرعي لاسم التطبيق الذي يضم رمز خدمة النقل المستقل. يصف الدليل الفرعي http نقل HTTP (لدينا رمز الخادم والعميل مع منطق لترميز وفك تشفير الطلبات والاستجابات ، ورمز CLI لإنشاء طلبات HTTP من سطر الأوامر). يحتوي أيضًا على ملفات مواصفات Open API 2.0 بتنسيقات JSON و YAML.

يمكنك نسخ محتوى ملف swagger ولصقه في أي محرر Swagger عبر الإنترنت (مثل ذلك الموجود في swagger.io) لتصور وثائق مواصفات API الخاصة بك. أنها تدعم كل من تنسيقات YAML و JSON.

نحن الآن جاهزون لخطوتنا التالية في دورة حياة التطوير.

تنفيذ API الخاص بك

بعد إنشاء الكود المعياري الخاص بك ، حان الوقت لإضافة بعض منطق الأعمال إليه. في هذه المرحلة ، هذا هو الشكل الذي يجب أن تبدو عليه التعليمات البرمجية الخاصة بك:

تطوير API في Go

حيث يتم الاحتفاظ بكل ملف أعلاه وتحديثه بواسطة Goa كلما قمنا بتنفيذ CLI. وهكذا ، مع تطور الهندسة المعمارية ، سيتبع تصميمك التطور ، وكذلك كود المصدر الخاص بك. لتنفيذ التطبيق ، نقوم بتنفيذ الأمر أدناه (سيُنشئ تنفيذًا أساسيًا للخدمة جنبًا إلى جنب مع ملفات الخادم القابلة للبناء التي تعمل على تشغيل goroutines لبدء خادم HTTP وملفات العميل التي يمكنها تقديم طلبات إلى هذا الخادم):

 goa example clients/design

سيؤدي هذا إلى إنشاء مجلد cmd مع مصادر قابلة للبناء من الخادم والعميل. سيكون هناك تطبيقك ، وتلك هي الملفات التي يجب أن تحتفظ بها بنفسك بعد أن تقوم Goa بإنشائها أولاً.

توضح وثائق Goa ما يلي: "يُنشئ هذا الأمر نقطة انطلاق للخدمة للمساعدة في تطوير bootstrap - ولا يُقصد على وجه الخصوص إعادة تشغيله عندما يتغير التصميم."

الآن ، سيبدو الرمز الخاص بك كما يلي:

تطوير API في Go: مجلد cmd

حيث يعد client.go مع تنفيذ وهمي لطريقتين get و show . دعونا نضيف بعض منطق الأعمال إليها!

للتبسيط ، سنستخدم SQLite بدلاً من قاعدة البيانات في الذاكرة و Gorm باعتباره ORM. أنشئ ملف sqlite.go وأضف المحتوى أدناه - سيضيف منطق قاعدة البيانات لإنشاء سجلات في قاعدة البيانات وسرد واحدًا و / أو عدة صفوف من قاعدة البيانات:

 package clients import ( "clients/gen/client" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Client *client.ClientManagement // InitDB is the function that starts a database file and table structures // if not created then returns db object for next functions func InitDB() *gorm.DB { // Opening file db, err := gorm.Open("sqlite3", "./data.db") // Display SQL queries db.LogMode(true) // Error if err != nil { panic(err) } // Creating the table if it doesn't exist var TableStruct = client.ClientManagement{} if !db.HasTable(TableStruct) { db.CreateTable(TableStruct) db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(TableStruct) } return db } // GetClient retrieves one client by its ID func GetClient(clientID string) (client.ClientManagement, error) { db := InitDB() defer db.Close() var clients client.ClientManagement db.Where("client_id = ?", clientID).First(&clients) return clients, err } // CreateClient created a client row in DB func CreateClient(client Client) error { db := InitDB() defer db.Close() err := db.Create(&client).Error return err } // ListClients retrieves the clients stored in Database func ListClients() (client.ClientManagementCollection, error) { db := InitDB() defer db.Close() var clients client.ClientManagementCollection err := db.Find(&clients).Error return clients, err }

بعد ذلك ، نقوم بتحرير client.go لتحديث جميع الطرق في Client Service ، وتنفيذ استدعاءات قاعدة البيانات وإنشاء استجابات API:

 // Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }

الجزء الأول من طلبنا جاهز ليتم تجميعه. قم بتشغيل الأمر التالي لإنشاء ثنائيات الخادم والعميل:

 go build ./cmd/clients go build ./cmd/clients-cli

لتشغيل الخادم ، ما عليك سوى تشغيل ./clients . اتركها تعمل الآن. يجب أن تراها تعمل بنجاح ، مثل ما يلي:

 $ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP server listening on "localhost:8080"

نحن على استعداد لإجراء بعض الاختبارات في تطبيقنا. لنجرب جميع الطرق باستخدام cli:

 $ ./clients-cli client add --body '{"ClientName": "Cool Company"}' \ --client-id "1" $ ./clients-cli client get --client-id "1" { "ClientID": "1", "ClientName": "Cool Company" } $ ./clients-cli client show [ { "ClientID": "1", "ClientName": "Cool Company" } ]

إذا تلقيت أي خطأ ، فتحقق من سجلات الخادم للتأكد من أن منطق SQLite ORM جيد وأنك لا تواجه أي أخطاء في قاعدة البيانات مثل عدم تهيئة قاعدة البيانات أو عدم إرجاع الاستعلامات إلى أي صفوف.

تمديد API الخاص بك

يدعم إطار العمل تطوير المكونات الإضافية لتوسيع واجهة برمجة التطبيقات وإضافة المزيد من الميزات بسهولة. يوجد في Goa مستودع للمكونات الإضافية التي أنشأها المجتمع.

كما أوضحت سابقًا ، كجزء من دورة حياة التطوير ، يمكننا الاعتماد على مجموعة الأدوات لتوسيع تطبيقنا من خلال العودة إلى تعريف التصميم ، وتحديثه ، وتحديث الكود الذي تم إنشاؤه. دعنا نعرض كيف يمكن أن تساعد المكونات الإضافية عن طريق إضافة CORS والمصادقة إلى واجهة برمجة التطبيقات.

قم بتحديث clients/design/design.go إلى المحتوى أدناه:

 /* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" cors "goa.design/plugins/v3/cors/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") cors.Origin("/.*localhost.*/", func() { cors.Headers("X-Authorization", "X-Time", "X-Api-Version", "Content-Type", "Origin", "Authorization") cors.Methods("GET", "POST", "OPTIONS") cors.Expose("Content-Type", "Origin") cors.MaxAge(100) cors.Credentials() }) Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("add", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Field(3, "ClientName", String, "Client ID") Field(4, "ContactName", String, "Contact Name") Field(5, "ContactEmail", String, "Contact Email") Field(6, "ContactMobile", Int, "Contact Mobile Number") Required("token", "ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) Security(JWTAuth, func() { Scope("api:write") }) Result(Empty) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusCreated) }) }) Method("get", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Required("token", "ClientID") }) Security(JWTAuth, func() { Scope("api:read") }) Result(ClientManagement) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Method("show", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Required("token") }) Security(JWTAuth, func() { Scope("api:read") }) Result(CollectionOf(ClientManagement)) Error("invalid-scopes", String, "Token scopes are invalid") HTTP(func() { GET("/api/v1/client") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to // configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") Attribute("ContactName", String, "Name of the Contact.", func() { Example("John Doe") }) Field(4, "ContactEmail") Field(5, "ContactMobile") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") Attribute("ContactName") Attribute("ContactEmail") Attribute("ContactMobile") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Attribute("ContactName", String, "Name of the Client Contact.", func() { Example("John Doe") }) Attribute("ContactEmail", String, "Email of the Client Contact", func() { Example("[email protected]") }) Attribute("ContactMobile", Int, "Mobile number of the Client Contact", func() { Example(12365474235) }) Required("ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned " + "when the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") }) // Creds is a custom type for replying Tokens var Creds = Type("Creds", func() { Field(1, "jwt", String, "JWT token", func() { Example("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9" + "lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHD" + "cEfxjoYZgeFONFh7HgQ") }) Required("jwt") }) // JWTAuth is the JWTSecurity DSL function for adding JWT support in the API var JWTAuth = JWTSecurity("jwt", func() { Description(`Secures endpoint by requiring a valid JWT token retrieved via the signin endpoint. Supports scopes "api:read" and "api:write".`) Scope("api:read", "Read-only access") Scope("api:write", "Read and write access") }) // BasicAuth is the BasicAuth DSL function for // adding basic auth support in the API var BasicAuth = BasicAuthSecurity("basic", func() { Description("Basic authentication used to " + "authenticate security principal during signin") Scope("api:read", "Read-only access") }) // Signin Service is the service used to authenticate users and assign JWT tokens for their sessions var _ = Service("signin", func() { Description("The Signin service authenticates users and validate tokens") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("authenticate", func() { Description("Creates a valid JWT") Security(BasicAuth) Payload(func() { Description("Credentials used to authenticate to retrieve JWT token") UsernameField(1, "username", String, "Username used to perform signin", func() { Example("user") }) PasswordField(2, "password", String, "Password used to perform signin", func() { Example("password") }) Required("username", "password") }) Result(Creds) HTTP(func() { POST("/signin/authenticate") Response(StatusOK) }) }) })

يمكنك ملاحظة اختلافين رئيسيين في التصميم الجديد. لقد حددنا نطاقًا أمنيًا في خدمة client حتى نتمكن من التحقق مما إذا كان المستخدم مصرحًا له باستدعاء الخدمة ، وحددنا خدمة ثانية تسمى تسجيل الدخول ، والتي signin لمصادقة المستخدمين وإنشاء رموز ويب JSON (JWT) ، والتي ستستخدم خدمة client للسماح بالمكالمات. لقد أضفنا أيضًا المزيد من الحقول إلى نوع العميل المخصص لدينا. هذه حالة شائعة عند تطوير واجهة برمجة التطبيقات - الحاجة إلى إعادة تشكيل البيانات أو إعادة هيكلتها.

في التصميم ، قد تبدو هذه التغييرات بسيطة ، ولكن انعكاسًا لها ، هناك الكثير من الحد الأدنى من الميزات المطلوبة لتحقيق ما هو موصوف في التصميم. خذ ، على سبيل المثال ، المخططات المعمارية للمصادقة والترخيص باستخدام طرق API الخاصة بنا:

API Development in Go: مخططات معمارية للمصادقة والترخيص

هذه كلها ميزات جديدة لا تتوفر في الكود لدينا حتى الآن. مرة أخرى ، هذا هو المكان الذي تضيف فيه Goa قيمة أكبر لجهودك التنموية. دعنا ننفذ هذه الميزات في جانب النقل عن طريق إعادة إنشاء كود المصدر مرة أخرى بالأمر أدناه:

 goa gen clients/design

في هذه المرحلة ، إذا كنت تستخدم Git ، فستلاحظ وجود ملفات جديدة ، مع ظهور ملفات أخرى على أنها محدثة. هذا لأن Goa قامت بتحديث الكود المعياري بسلاسة وفقًا لذلك ، دون تدخلنا.

الآن ، نحن بحاجة إلى تنفيذ رمز جانب الخدمة. في تطبيق حقيقي ، ستقوم بتحديث التطبيق يدويًا بعد تحديث مصدرك ليعكس جميع تغييرات التصميم. هذه هي الطريقة التي توصي بها Goa أن نتابع ، ولكن للإيجاز ، سأقوم بحذف وإعادة إنشاء التطبيق النموذجي للوصول بنا إلى هناك بشكل أسرع. قم بتشغيل الأوامر أدناه لحذف التطبيق النموذجي وإعادة إنشائه:

 rm -rf cmd client.go goa example clients/design

مع ذلك ، يجب أن تبدو التعليمات البرمجية الخاصة بك كما يلي:

تطوير API في Go: تجديد

يمكننا أن نرى ملفًا واحدًا جديدًا في تطبيقنا كمثال: signin.go ، والذي يحتوي على منطق خدمة تسجيل الدخول. ومع ذلك ، يمكننا أن نرى أن client.go قد تم تحديثه أيضًا بوظيفة JWTAuth للتحقق من الرموز المميزة. يتطابق هذا مع ما كتبناه في التصميم ، لذلك سيتم اعتراض كل استدعاء لأي طريقة في العميل للتحقق من صحة الرمز وإعادة توجيهه فقط إذا تم التصريح به بواسطة رمز مميز ونطاق صحيح.

لذلك ، سنقوم بتحديث الطرق في خدمة تسجيل الدخول الخاصة بنا داخل signin.go من أجل إضافة منطق لإنشاء الرموز المميزة التي ستنشئها واجهة برمجة التطبيقات للمستخدمين المصادق عليهم. انسخ والصق السياق التالي في signin.go :

 package clients import ( signin "clients/gen/signin" "context" "log" "time" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) // signin service example implementation. // The example methods log the requests and return zero values. type signinsrvc struct { logger *log.Logger } // NewSignin returns the signin service implementation. func NewSignin(logger *log.Logger) signin.Service { return &signinsrvc{logger} } // BasicAuth implements the authorization logic for service "signin" for the // "basic" security scheme. func (s *signinsrvc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) { if user != "gopher" && pass != "academy" { return ctx, signin. Unauthorized("invalid username and password combination") } return ctx, nil } // Creates a valid JWT func (s *signinsrvc) Authenticate(ctx context.Context, p *signin.AuthenticatePayload) (res *signin.Creds, err error) { // create JWT token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), "iat": time.Now().Unix(), "exp": time.Now().Add(time.Duration(9) * time.Minute).Unix(), "scopes": []string{"api:read", "api:write"}, }) s.logger.Printf("user '%s' logged in", p.Username) // note that if "SignedString" returns an error then it is returned as // an internal error to the client t, err := token.SignedString(Key) if err != nil { return nil, err } res = &signin.Creds{ JWT: t, } return }

أخيرًا ، نظرًا لأننا أضفنا المزيد من الحقول إلى النوع المخصص لدينا ، نحتاج إلى تحديث طريقة الإضافة في خدمة العميل في client.go لتعكس هذه التغييرات. انسخ والصق التالي لتحديث client.go :

 package clients import ( client "clients/gen/client" "context" "log" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) var ( // Key is the key used in JWT authentication Key = []byte("secret") ) // client service example implementation. // The example methods log the requests and return zero values. type clientsrvc struct { logger *log.Logger } // NewClient returns the client service implementation. func NewClient(logger *log.Logger) client.Service { return &clientsrvc{logger} } // JWTAuth implements the authorization logic for service "client" for the // "jwt" security scheme. func (s *clientsrvc) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) { claims := make(jwt.MapClaims) // authorize request // 1. parse JWT token, token key is hardcoded to "secret" in this example _, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { return Key, nil }) if err != nil { s.logger.Print("Unable to obtain claim from token, it's invalid") return ctx, client.Unauthorized("invalid token") } s.logger.Print("claims retrieved, validating against scope") s.logger.Print(claims) // 2. validate provided "scopes" claim if claims["scopes"] == nil { s.logger.Print("Unable to get scope since the scope is empty") return ctx, client.InvalidScopes("invalid scopes in token") } scopes, ok := claims["scopes"].([]interface{}) if !ok { s.logger.Print("An error occurred when retrieving the scopes") s.logger.Print(ok) return ctx, client.InvalidScopes("invalid scopes in token") } scopesInToken := make([]string, len(scopes)) for _, scp := range scopes { scopesInToken = append(scopesInToken, scp.(string)) } if err := scheme.Validate(scopesInToken); err != nil { s.logger.Print("Unable to parse token, check error below") return ctx, client.InvalidScopes(err.Error()) } return ctx, nil } // Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, ContactName: p.ContactName, ContactEmail: p.ContactEmail, ContactMobile: p.ContactMobile, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context, p *client.ShowPayload) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }

وهذا كل شيء! دعنا نعيد ترجمة التطبيق ونختبره مرة أخرى. قم بتشغيل الأوامر أدناه لإزالة الثنائيات القديمة وتجميع الثنائيات الجديدة:

 rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli

قم بتشغيل ./clients مرة أخرى واتركه يعمل. يجب أن تراها تعمل بنجاح ، ولكن هذه المرة ، مع تنفيذ الطرق الجديدة:

 $ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /openapi.json [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP "Authenticate" mounted on POST /signin/authenticate [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /signin/authenticate [clients] 00:00:01 HTTP server listening on "localhost:8080"

للاختبار ، دعنا ننفذ جميع طرق API باستخدام cli - لاحظ أننا نستخدم بيانات الاعتماد المشفرة:

 $ ./clients-cli signin authenticate \ --username "gopher" --password "academy" { "JWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4 \ MSwibmJmIjoxNDQ0NDc4NDAwLCJzY29wZXMiOlsiY \ XBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" } $ ./clients-cli client add --body \ '{"ClientName": "Cool Company", \ "ContactName": "Jane Masters", \ "ContactEmail": "[email protected]", \ "ContactMobile": 13426547654 }' \ --client-id "1" --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" $ ./clients-cli client get --client-id "1" \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } $ ./clients-cli client show \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" [ { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } ]

وها نحن ذا! لدينا تطبيق مبسط مع المصادقة المناسبة ، وتفويض النطاق ، ومساحة للنمو التطوري. بعد ذلك ، يمكنك تطوير استراتيجية المصادقة الخاصة بك باستخدام الخدمات السحابية أو أي مزود هوية آخر من اختيارك. يمكنك أيضًا إنشاء مكونات إضافية لقاعدة البيانات أو نظام المراسلة المفضل لديك ، أو حتى التكامل مع واجهات برمجة التطبيقات الأخرى بسهولة.

تحقق من مشروع Goa GitHub لمزيد من المكونات الإضافية ، والأمثلة (توضح إمكانات محددة للإطار) ، وغيرها من الموارد المفيدة.

يكفي هذا لليوم. أتمنى أن تكون قد استمتعت باللعب مع جوا وقراءة هذا المقال. إذا كان لديك أي ملاحظات حول المحتوى ، فلا تتردد في التواصل مع GitHub أو Twitter أو LinkedIn.

أيضًا ، نتسكع في قناة #goa على Gophers Slack ، لذا تعال وقل مرحبًا!

لمزيد من المعلومات حول Golang ، راجع Go Programming Language: دروس تمهيدية لـ Golang ومنطق منظم جيدًا: برنامج تعليمي لـ Golang OOP.