لینک کوتاه مطلب : https://hsgar.com/?p=5315

پیاده سازی Microsoft REST API فیلتر

سازمان‌ها سعی می‌کنند دستورالعمل‌های Microsoft REST API را  به عنوان پایه‌ای برای سرویس‌هایشان تطبیق دهند. به طور کلی این ایده خوبی است و دلایل آن به قرار زیر است:

  1. همه استانداردها را دوست دارند
  2. خوب است که روی شانه غول هایی بایستیم که شیوه های خوبی را توسعه دادند که با گذشت زمان ثابت شده است

دنبال کردن بعضی چیزها ساده است، و برخی دیگر پیچیده، مانند $filter.

چند وقت پیش به دنبال نمونه‌های خوبی از اینکه چطور $filter می تواند برای مطابقت با الزامات دستورالعمل پیاده سازی شود گشتم. چنین مثالی پیدا نکردم پس در اینجا آنچه در ذهن دارم می‌‌آورم.

با نگاه کردن به $filter شما ممکن است یک موازی با فیلتر OData رسم کنید و حق با شما خواهد بود. مایکروسافت قطعا از OData الهام گرفته است زیرا:

  • آنها آغازگر OData هستند
  • ASP.NET (مایکروسافت) حتی دارای یک ابزار خارج از جعبه برای ایجاد سرور OData است.

اما دستورالعمل ها عمومی هستند، آنها باید برای هر ابزاری قابل اجرا باشند. همانطور که می دانیم، بسیاری از شرکت ها با استفاده از جاوا ساخته می شوند.

در این پست به شما نشان خواهم داد که چگونه با آن کار کنید $filter خارج از ابزار مایکروسافت

مشکل

با نگاهی به الزامات، می‌توانیم چالش‌های زیر را ببینیم که باید حل شوند:

  • فیلتر مجموعه ای از عملیات دارد که باید پشتیبانی شوند
  • عملگرهای منطقی، مقایسه و گروه بندی باید برای پشتیبانی از عملیات اجرا شود

داده ها باید به صورت زیر قابل پرس و جو باشند:

GET https://api.contoso.com/v1.0/products?$filter=(name eq 'Milk' or name eq 'Eggs') and price lt 2.55

هدف راه حل ترجمه پرس و جو زیر است:

(name eq 'Milk' or name eq 'Eggs') and price lt 2.55

به برخی از پرس و جوهای پایگاه داده:

WHERE (name='Milk' OR name='Eggs') AND price < 2.55

نوشتن تجزیه کننده

برای حل چنین مشکلی (ترجمه یک چیز به چیز دیگر) تجزیه کننده استفاده می شود.

تجزیه کننده ها معمولا درختی را می سازند که بتوان از آن عبور کرد. می‌توانیم از آن عبور کنیم، نشانه‌ها را استخراج کنیم و یک پرس و جو ایجاد کنیم. توکن ها گره هایی در یک درخت هستند. این وظیفه لکسر است برای درک اینکه چه گره هایی (توکن ها) در یک رشته وجود دارد.

در مبحث کامپایلرها غوطه ور نمی شوم در این پست اما زمانی که Lexers و/یا Parser ها ذکر می شوند، معمولاً به این معنی است که موضوع Compliers لمس شده است.

درختی که می‌خواهیم برای رشته بالا بسازیم باید نزدیک به این باشد:

            -------and------
           /                
      ----or----            lt
     /                   /    
    eq          eq     price  2.55
  /          /    
name 'Milk' name 'Eggs'

سپس، برای Mongo، می‌توانیم پرس و جوی زیر را با راه رفتن این درخت بسازیم:

{ $and: [
    { $or: [
        { name: { $eq: "Milk" } },
        { name: { $eq: "Eggs" } }
      ]
    },
    { price: { $lt: 2.55 } }
  ]
}

به نظر سرگرم کننده است، ها؟ اما قبل از ادامه مطلب اجازه دهید یک سوال بپرسم. آیا می خواهیم برای این کار کامپایور خودمان را بنویسیم؟ نه!

با این حال، ما باید به نحوی زبان (گرامر) را تعریف کنیم، آن را به ابزاری تغذیه کنیم که سپس قادر به درک نحوه ساخت درخت از رشته داده شده باشد. به نظر می رسد امروزه یک کار یادگیری ماشینی است، درست است؟ نه!

به ANTLR خوش آمدید

خوشبختانه، ANTLR (ابزار دیگری برای تشخیص زبان) وجود دارد. که آنچه ما نیاز داریم را انجام می دهد:

  1. ما می توانیم زبان را توصیف کنیم
  2. به ابزار یک متن بدهید
  3. درخت را خواهد ساخت
  4. ما قادر خواهیم بود آن را راه بریم

ابتدا باید گرامر را برای خود ایجاد کنیم $filter زبان گرامر در ANTLR به شکلی شبیه به Backus-Naur (BNF) توصیف می‌شود. بدون غوطه ور شدن در جزئیات، با دستور زبان زیر به پایان می رسیم. شما باید آن را از پایین به بالا بخوانید. اگر علاقه مند به جزئیات بیشتر در مورد آنچه در اینجا اتفاق می افتد هستید، به شدت پیشنهاد می کنم آموزش فوق العاده ANTLR Mega را ببینید.. در سطح بالا بعد از قطعه توضیح خواهم داد.

grammar Filter;

filter: expr+ EOF ;

expr
 : OPAR expr CPAR
 | NOT PROPERTY COMPARISON VALUE
 | PROPERTY COMPARISON VALUE
 | expr AND expr
 | expr OR expr
 ;

OPAR: '(' ;
CPAR: ')' ;

OR  : 'or' ;
AND : 'and' ;

NOT : 'not' ;

COMPARISON: (GT | GE | LT | LE | EQ | NE);

GT : 'gt' ;
GE : 'ge' ;
LT : 'lt' ;
LE : 'le' ;
EQ : 'eq' ;
NE : 'ne' ;

VALUE
 : TRUE
 | FALSE
 | INT
 | FLOAT
 | STRING
 ;

TRUE
 : 'true'
 ;

FALSE
 : 'false'
 ;

PROPERTY
 : ALLOWED_CHARACTERS+ ('.' ALLOWED_CHARACTERS+)*
 ;

fragment ALLOWED_CHARACTERS : [a-zA-Z0-9_-];

STRING
 : ''' (~['rn] | '''')* '''
 ;

INT
 : DIGIT+
 ;

FLOAT
 : DIGIT+ '.' DIGIT*
 | '.' DIGIT+
 ;

fragment DIGIT : [0-9] ;

SPACE
 : [ trn] -> skip
 ;

الهام گرفتن از OData $filter نحو در جستجوی شناختی Azure من به دستور زبانی رسیدم که در بالا می بینید. این دستور زبان توضیح می دهد که:

  1. فاصله ها، برگه ها و خطوط جدید برای نشانه گذاری نادیده گرفته می شوند
  2. رشته ها باید نقل قول شوند ' و اگر می خواهید از یک نقل قول در یک رشته فرار کنید، به عنوان مثال Mary's، باید آن را دو برابر کنید (Mary''s)
  3. اعداد می توانند باشند 0، 0.0 یا .0
  4. بولی ها هستند true و false
  5. بیان اشکال مختلفی دارد و می توانند عبارات تودرتو داشته باشند

زمان کدنویسی است!

راه اندازی محیط

از آنجا که ANTLR ابزار مبتنی بر جاوا است، از زبان جاوا بهتری استفاده خواهم کرد که Kotlin است.

تنظیم این است:

  • کاتلین
  • گریدل
  • (اختیاری) IntelliJ IDEA (من از نسخه انجمن استفاده می کنم)

پس از تولید ساختار پروژه Kotlin پیش فرض، اجازه دهید ANTLR را اضافه کنیم. خود را گسترش دهید build.gradle.kts فایل با خطوط زیر:

plugins {
  antlr
}

dependencies {
  antlr("org.antlr:antlr4:4.10.1)
}

tasks.generateGrammarSource {
  maxHeapSize = "64m"
  arguments = arguments + listOf("-long-messages")
}

tasks.named("compileTestKotlin") {
  dependsOn(":generateTestGrammarSource")
}

tasks.named("compileKotlin") {
  dependsOn(":generateGrammarSource")
}

ما وابستگی ANTRL را اضافه کرده‌ایم و به Gradle دستور داده‌ایم تا هنگام کامپایل پروژه یا کامپایل تست‌ها، کلاس‌هایی را از گرامرهای یافت شده بسازد.

ایجاد فایل گرامر

حالا بیایید گرامر خود را که در پست بالاتر تعریف کرده بودیم اضافه کنیم.

یک فایل ایجاد کنید Filter.g4 در main/antlr/com/your/path/antlr4 بسته بندی کنید و محتوا را کپی کنید.

ANTLR کلاس های Lexer و Parses را بر اساس گرامر تولید می کند. ما می خواهیم کلاس های تولید شده به بسته اضافه شوند که اینطور نیست root. برای این کار باید به ANTLR دستور دهیم تا تعریف بسته را به فایل های کلاس تولید شده اضافه کند. موارد زیر را اضافه کنید:

@header {
package com.your.path.antlr4;
}

به Filter.g4 فایل بعد از خط grammar Filter;.

اکنون ما آماده نوشتن کلاسی هستیم که ورودی را به Query Mongo تبدیل می کند. شما می توانید همین کار را برای DB دیگری انجام دهید، من Mongo را انتخاب کرده ام.

ایجاد شنونده

ایجاد کردن FilterListener.kt فایل کنید و یک کلاس ایجاد کنید:

class FilterListener : FilterBaseListener() {
  private var result: MutableList<String> = mutableListOf()
}

FilterBaseListener یک کلاس تولید شده توسط ANTLR است.

اسم کلاسمون رو گذاشتم FilterListener شنونده زیرا ما از شنونده برای این کار استفاده خواهیم کرد نه بازدیدکننده. چه تفاوتی بین این دو وجود دارد را می توان در آموزش مگا که در بالا ذکر شده است یا در یک خلاصه خوب در StackOverflow یافت..

فهرست کنید result جایی است که مقادیر در طول ساخت رشته پرس و جو فشار داده می شوند.

ما می خواهیم هنگام خروج از گره های بیانی چیزی بسازیم. برای این ما نیاز به نادیده گرفتن exitExpr روش.

class FilterListener : FilterBaseListener() {
  override fun exitExpr(ctx: FilterParser.ExprContext?) {}
}

ما علاقه مندیم زمانی که کد را ترک می کنیم، کد را اجرا کنیم and، or و عبارات مقایسه

class FilterListener : FilterBaseListener() {
  override fun exitExpr(ctx: FilterParser.ExprContext) {
    when {
      ctx.COMPARISON() != null -> buildComparison(
        property = ctx.PROPERTY().text,
        operator = ctx.COMPARISON().text,
        value = ctx.VALUE().text
      )
      ctx.AND() != null -> buildAnd()
      ctx.OR() != null -> buildOr()
    }
  }
}

روش buildComparison مسئول ساخت اپراتور پرس و جو مقایسه معتبر Mongo بر اساس داده های دریافتی است. به نظر می رسد این است:

private fun buildComparison(property: String, operator: String, value: String) {
  val parsed = when {
    value == "true" || value == "false" -> value
    value.toDoubleOrNull() != null -> "${value.toDouble()}"
    else ->
      ""${
      value
        .drop(1)
        .dropLast(1)
        .split("''")
        .joinToString("'")
      }""
  }

  val q = when (operator) {
    "gt", "lt", "eq", "ne" -> comparisonQueryOperator(property, "$$operator", parsed)
    "ge" -> comparisonQueryOperator(property, "$gte", parsed)
    "le" -> comparisonQueryOperator(property, "$lte", parsed)
    else -> throw Exception("operator $operator not implemented")
  }

  result.add(q)
}

private fun comparisonQueryOperator(property: String, operator: String, value: String): String {
  return "{ "$property": { $operator: $value } }"
}

چگونه کار می کند:

  • اگر این است true یا false سپس ما فقط این مقادیر را برمی گردانیم زیرا آنها بولی هستند.
  • اگر چیزی عددی است، سعی کنید آن را به دو برابر تبدیل کنید.
  • اگر هیچ کدام از موارد بالا نیست، باید یک رشته باشد.
  • در نهایت خود پرس و جو را بسازید.

زیرا پرس و جوها را می توان با کمک ترکیب کرد and یا or ما باید روش هایی را پیاده سازی کنیم که ایجاد کنند and و or پرس و جو از آنچه ذخیره شده است result.

private fun buildAnd() {
  val (left, right) = result
  result = mutableListOf()

  result.add("{ $and: [ $left, $right ] }")
}

private fun buildOr() {
  val (left, right) = result
  result = mutableListOf()

  result.add("{ $or: [ $left, $right ] }")
}

را result لیست همیشه فقط سمت چپ و راست درخت خواهد داشت (دو مقدار در لیست). AND و OR ترکیبی از قسمت های چپ و راست هستند.

عبور از درخت ساخته شده

اکنون زمان نوشتن متد public است که فیلتر خام را به عنوان رشته می گیرد و کوئری Mongo را برمی گرداند.

fun generateQueryString(filter: String): String {
  // lexer
  val chars = CharStreams.fromString(filter)
  val lexer = FilterLexer(chars)

  // parser
  val tokens = CommonTokenStream(lexer)
  val parser = FilterParser(tokens)

  // walk
  ParseTreeWalker().walk(this, parser.filter())
  return result.last()
}

در اینجا از کلاس های ANTLR برای ساخت lexer، تجزیه کننده، فیلتر، راه رفتن درخت و برگرداندن نتیجه استفاده می شود.

بررسی اینکه کار می کند

که در Main.kt اکنون می توانید موارد زیر را بنویسید:

fun main() {
  print("input filter: ")
  val filter = readln()
  val filterListener = FilterListener()
  println("output:")
  println(filterListener.generateQueryString(filter))
}

و آن را در ترمینال امتحان کنید:

input filter: name eq 'Milk'
output:
{ "name": { $eq: "Milk" } }

کلمات پایانی

بنابراین اینگونه می توانید شروع به ساخت فیلتر کنید. امیدوارم این کمک کند! وجود دارد مثال بزرگتر در GitHub که می توانید کشف کنید اما لطفا به این موضوع توجه داشته باشید این یک نمونه است و کد آماده تولید برای استفاده نیست. برای الهام استفاده کنید. روابط عمومی با بهبود استقبال می شود.

لینک منبع

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.