This document presents the design of Espresso++, a natural query language that abstracts the native query language of any data management system, and the intended audience is anybody involved in its realization.
This design document applies to any Skeeter API that supports data filtering.
- Lexer
-
The part of an interpreter that attaches meaning by classifying lexemes (string of symbols from the input) as particular tokens. For example, the lexemes
or
,and
, andnot
are classified as logical operators by the Espresso++ lexer. - Parser
-
The part of an interpreter that attaches meaning by classifying strings of tokens from the input (sentences) as particular non-terminals and by building the parse tree. For example, token strings like
[number][operator][number]
or[id][operator][id]
are classified as non-terminal expressions by the Espresso++ parser. - Interpreter
-
A computer program that converts each high-level statement into code or instructions that can be understood by the underlying system.
- Visitor Pattern
-
A way of separating an algorithm from the structure on which it operates. The result of this separation is the ability to add new operations to an existing structure without modifying the structure itself.
-
[1] Giuseppe Greco. The Espresso++ Language Specification. Skeeter Health. 2020.
Skeeter APIs let clients specify a filter to limit or control the data returned by an endpoint. A filter consists of one or more statements written in Espresso++ [1] that abstract away the native query language of the underlying data management system.
Espresso++ comes as a module together with a command-line utility that converts input Espresso++ expressions into native expressions that can be understood by the underlying data management system. The next sections describe what functionality Espresso++ shall provide and how that functionality shall be implemented.
Figure Actors, Use Cases, and their Interactions shows what functionality Espresso++ provides and what external systems (actors) interact with it.
Client << Aplication >> (Interpret Espresso++ Script) as (uc1) (Generate Native Code) as (uc2) Client -> (uc1) (uc1) ..> (uc2) : use
Use case Interpret Espresso++ Script is started by the Client when an Espresso++ expression is executed to filter the data returned by an API endpoint. Interpret Espresso++ Script parses the Espresso++ expression and passes the resulting parse tree to use case Generate Native Code, which in turn generates the native query to be executed by the underlying data management system.
This section describes the logical structure of Espresso++. The Espresso++ interpreter provides functionality for translating Espresso++ expressions into native queries. Figure Key Structural and Behavioral Elements shows the classes that make up Espresso++ and how they depend on each other.
abstract class Interpreter <<interface>> { +Accept(CodeGenerator, Reader, Writer) +Parse(Reader): Grammar } class EspressoppInterpreter { +Accept(CodeGenerator, Reader, Writer) +Parse(Reader): Grammar } abstract class CodeGenerator <<interface>> { +Visit(Interpreter, Reader, Writer) } class SqlCodeGenerator { +RenderingOptions: RenderingOptions +Visit(Interpreter, Reader, Writer) } class FieldProps { +Filterable: Bool +NativeName: String } class RenderingOptions { +AddFieldProps(String, FieldProps) +GetFieldProps(String): FieldProps +DeleteFieldProps(String): FieldProps } class Parser{ +Parse(Reader): Grammar } class Grammar{ +Query: {} } Client ..> Interpreter Client ..> CodeGenerator Interpreter <|-- EspressoppInterpreter : extends note left: Call CodeGenerator.Visit(Interpreter, ...) CodeGenerator <|-- SqlCodeGenerator : extends SqlCodeGenerator o-- RenderingOptions RenderingOptions ||--|{ FieldProps EspressoppInterpreter o-- Parser Grammar --* Parser
The design of Espresso++ is based on the visitor pattern so that new CodeGenerator
implementations
can be added anytime without the need to modify EspressoppInterpreter
. SqlCodeGenerator
is the
default CodeGenerator
implementation shipped with the first release of Espresso++.
This section describes how the use cases are implemented and examines how the various design structures contribute to the functionality of the system. It also describes the collaborations that realize Espresso++ and contribute to define the dynamic view of the system.
This section describes the relationship between use case Interpret Espresso++ Script and the collaborations that actually realize it.
The sequence diagram depicted in figure Scenario Interpret Espresso++ Script describes how an Espresso++ script is interpreted into a native query.
actor Client create EspressoppInterpreter Client --> EspressoppInterpreter : new create Parser EspressoppInterpreter --> Parser : new create Reader Client --> Reader : new create Writer Client --> Writer : new create SqlCodeGenerator Client --> SqlCodeGenerator : new Client -> EspressoppInterpreter : Accept(codeGenerator, reader, writer) activate EspressoppInterpreter EspressoppInterpreter -> SqlCodeGenerator : Visit(interpreter, reader, writer) activate SqlCodeGenerator SqlCodeGenerator -> EspressoppInterpreter : Parse(reader) EspressoppInterpreter -> Parser : Parse(reader) activate Parser Parser -> Reader : Read() activate Reader return script return grammar EspressoppInterpreter --> SqlCodeGenerator : grammar deactivate EspressoppInterpreter SqlCodeGenerator -> SqlCodeGenerator : generateSql activate SqlCodeGenerator return sql SqlCodeGenerator -> Writer : Write(sql) activate Writer deactivate SqlCodeGenerator deactivate Writer Client -> Writer : String() activate Writer return sql
The Interpreter
is initialized by the Client and provides functionality for parsing Espresso++
scripts to be converted into native queries by the CodeGenerator
. The CodeGenerator
is also
initialized by the Client and gets accepted together with the Reader
and Writer
by the
Interpreter
— this construct allows the CodeGenerator
to access the Parser
instantiated by the
Interpreter
and get back the Espresso++ grammar.
The Reader
is where the Espresso++ script is read from by the Parser
, whereas the Writer
is where the CodeGenerator
writes the resulting native query.
By default field names in the input Espresso++ expression remain unchanged in the output native
query. Should not the fields in the Espresso++ expression match the name of the fields in the
underlying database, a mapping needs to be provided by means of the RenderingOptions
.
The RenderingOptions
is used by CodeGenerator
implementations to control the way output queries
are generated, and it might be associated with one or more FieldProps
instances. A FieldProps
specifies the native name of the field and whether it can be queried.
The process view describes the concurrent aspects of the system, namely the tasks (or processes) that make the system run and the interactions between them. Espresso++ is a module to be included into other applications. However, Espresso++ ships with a command-line utility that takes an Espresso++ espression as an input and returns the resulting native query.
The diagram depicted in figure Process Composition describes the process composition of the Espresso++ command-line utility and the mapping of resources on it.
class espressopp <<process>> { interpreter: EspressoppInterpreter codeGenerator: SqlCodeGenerator reader: io.Reader writer: io.Writer }
The Espresso++ command-line utility uses the Interpreter
and CodeGenerator
exactly the same way
client applications do. It is just meant to help developers debug filters written in the Espresso++
language.
Copyright © 2020 Skeeter Health