Feakin - 软件开发工业化方法

Feakin 是一个软件开发工业化(软件架构设计与开发标准化)方法,基于 DDD (领域驱动设计)与 TypeFlow 编程思想。

Design Principles

核心设计理念:

  1. 架构孪生:双态绑定。提供架构设计态与实现态的双向绑定,保证架构设计与实现的一致性。
  2. 显性化设计意图。将软件设计的意图化,借助于 DSL 语言的特性,将意图转换化代码。
  3. 类型与事件驱动。通过事件驱动的方式,将数据类型与领域事件进行绑定。

详细见:《Design Principles 一节》

Feakin 主要组成部分:

  • Fklang 是一个基于软件开发工业化思想,设计的架构设计 DSL。以确保软件系统描述与实现的一致性。通过显式化的软件架构设计,用于支持 AI 代码生成系统的嵌入。
  • Intellij Plugin 是 Feakin 的一个 IntelliJ 插件,用于将 Feakin/Fklang 集成到项目中。
  • (Todo) Vscode Plugin 是 Feakin 的一个 Vscode 插件,用于将 Feakin/Fklang 集成到项目中。
  • Feakin Web 提供了一个架构设计与可视化协作工具,让架构师能够更加高效地进行架构设计与可视化协作。

Feakin IntelliJ Plugin

安装:Version

Feakin Impl Sample

Fklang 示例:

// DDD 上下文映射图
ContextMap TicketBooking {
    Reservation -> Cinema;
    Reservation -> Movie;
    Reservation -> User;
}

Context Reservation {
  Aggregate Reservation;
}

Context Cinema {
  Aggregate Cinema;
}

Aggregate Cinema {
  Entity Cinema, ScreeningRoom, Seat;
}

// DDD 领域事件
impl CinemaCreated {
    endpoint {
        // API 声明与验证
        GET "/book/{id}";
        authorization: Basic admin admin;
        response: Cinema;
    }
}

Feakin Web

在线地址:https://online.feakin.com/

点击 TEAMPLATES -> Feakin Sample 查看示例

Feakin Web

Fklang

下载地址:https://github.com/feakin/fklang/releases

Usage: fkl <COMMAND>

Commands:
  gen    Generate code from a fkl file, current support Java
  dot    Generate dot file from a fkl file
  parse  Parse a fkl file and print the AST
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help information

CLI 示例:

fkl gen --main /Users/phodal/IdeaProjects/untitled/simple.fkl --impl CinemaCreated
@GetMapping("/book/{id}")
public Cinema creatCinema() { }

Quick Start

  1. 安装 Feakin Intellij 插件: Version
  2. 下载 FKL Cli(SDK):https://github.com/feakin/fklang/releases
    • 重命名为 fkl(macOS 下为 fkl-macos, Windows 下为 fkl-windows.exe, ...),并将其放置到 PATH 中
    • 添加可执行权限:chmod +x fkl
    • 检查 CLI 是否安装成功:fkl --help
  3. 创建一个 FKL 文件,比如:cinema.fkl,添加如下代码:
impl CinemaCreated {
    endpoint {
        GET "/book/{id}";
        authorization: Basic admin admin;
        response: Cinema;
    }
}

impl CinemaUpdated {
   endpoint {
      POST "/book/{id}";
      request: CinemaUpdatedRequest;
      authorization: Basic admin admin;
      response: Cinema;
   }

   flow {
      via UserRepository::getUserById receive user: User
      via UserRepository::save(user: User) receive user: User;
      via MessageQueue send CinemaCreated to "CinemaCreated"
   }
}
  1. 在 Intellij IDE 中打开 FKL 文件,点击左侧的 Run 按钮,查看是否有如下输出:

Feakin Impl Sample

设计理念:软件开发工业化

Feakin 的核心三个设计思念是:

  1. 架构孪生:双态绑定。提供架构设计态与实现态的双向绑定,保证架构设计与实现的一致性。
  2. 显性化设计意图。将软件设计的意图化,借助于 DSL 语言的特性,将意图转换化代码。
  3. 类型与事件驱动。通过事件驱动的方式,将数据类型与领域事件进行绑定。

Feakin 作为架构设计态与实现代码的中间语言,如下图所示:

Design Principles

在实现上便是,通过声明式 DSL 来绑定代码实现与架构设计,保证架构设计与实现的一致性。

架构孪生:双态绑定

在治理架构时,我们(ArchGuard 开发团队)推荐采用三态治理的方式,即设计态、实现态和运行态。 而在实现 Feakin 时,则是关注于如何实现设计态与实现态的绑定,我们将这个理念称为 "架构孪生"。

架构孪生是一种旨在精确反映架构设计的虚拟模型,它以数字化的形式对软件的架构、代码模型、分层、实现技术等的进行动态的呈现。

在架构生命周期的早期实施数字孪生允许在每个阶段模拟新代码和设计变化,以优化系统的架构。架构模型是可持续建设和运营中使用的架构孪生策略的关键组成部分。

为了实现这样的技术,我们需要面对几个挑战:

  • 架构建模:架构的功能、特性和行为。
  • 模型实现:
  • 生命周期:

除此,还有一个非常有意思的问题:

  • 如何针对于新的需求,动态模拟软件的架构演进,以发现潜在的架构瓶颈?

PS:既然如此,那么模拟态也应该成为 Feakin 设计的一个要点。

双态绑定:设计态 + 实现态

  • 设计态。以 DDD 战略、战术作为基本的设计框架
  • 实现态。以架构扫描作为基本点。

生命周期模型

关注于:

  • 设计态模型。
  • 实现态模型。
  • 模板态/未来态模型。

架构因子

另外一个核心问题便是,如何定义这里的 "架构模型",以及设计对应的工具,诸如于:TwinDesign、Arch Twin?所以,应该由 CodeDB/ArchDB 来实现。

在构建数字孪生时,我们会给研究对象(例如,风力涡轮机)配备与重要功能方面相关的各种传感器。同样的在构建架构孪生时,我们也需要一系列的 "传感器" 来测量研究对象的各种属性。

也因此,我们也可以定义三种形态的 "传感器" :

  • 设计时 - 架构描述性传感器:描述性传感器用于描述架构的功能、特性和行为。
  • 开发时 - 静态量化传感器:静态量化传感器用于描述架构的实现技术。
  • 运行时 - 探针(probe):探针用于描述架构的运行时行为。

类型与事件驱动

在设计理念中,我们将代码逻辑分为两个部分:状态行为。状态是指数据,行为是指数据的操作。在我们的设计中,状态和行为是分离的,状态是不可变的,行为是可变的。这种设计的好处是,我们可以在不同的状态之间进行切换,而不需要重新创建行为。这种设计的缺点是,我们需要在状态和行为之间进行绑定,这样才能保证状态和行为的一致性。 对应实现上,状态既是类型,行为视为事件。

在数学里,一个函数就是一组输入与一组容许的输出之间的关系,并且每个输入都与唯一一个输出相对应。 —— 维基百科

意识着,函数是应该是无状态的 —— 输入确定,输出就是确定的。

如何内置记忆(memoization)属性是,我们在设计的时候,要考虑的因素。它可以帮助我们提升性能,但是也会带来一些副作用。

纯函数

《函数响应式领域建模》中的 Scala 示例:

trait Account {
  def name: String
  def balance: BigDecimal
}

case class OpenedAccount(..) extends Account

trait InterestBearingAccount extends Account {
  def interestRate: BigDecimal
}

case class SavingAccount(..) extends InterestBearingAccount
case class MoenyMarketAccount(..) extends InterestBearingAccount

trait AccountService {
  def calculateInterest(..): BigDecimal
}

事件驱动

领域事件是不可变的,它是我们在实现的过程中,对领域的抽象。

显性化设计意图

声明式编程

声明式编程(英语:Declarative programming)是一种编程范式,与命令式编程相对立。 它描述目标的性质,让计算机明白目标,而非流程。

显性化子任务

impl CinemaUpdated {
   ...
   flow {
      via UserRepository::getUserById receive user: User
      via UserRepository::save(user: User) receive user: User;
   }
}

AI 意图分析

生成 API

// create spring api POST "/user/{id}" with response: User
@RestController
@RequestMapping("/user")
class UserController {
    @PostMapping("/{id}")
    fun getUserById(@PathVariable id: String): User {
        return User()
    }
}

如下的代码是,我们借助于 AI 生成的 Java 代码示例:

// via UserRepository::getUserById receive user: User
User user = userRepository.getUserById();
// via UserRepository::save(user: User) receive user: User;
User user = userRepository.save(user);

DDD in Feakin

Feakin 的 DDD 语法参考了:https://contextmapper.org/docs/context-map/

Feakin 语法可以实现三个阶段:

  • 战略设计。语法:ContextMapContext
  • 战术设计。语法:AggregateEntityValueObject
  • 实现。语法:impl

Quick Start

  1. 在线编辑器:https://online.feakin.com/
  2. Template -> DDD Booking 使用 Feakin 语法设计
ContextMap TicketBooking {
    Reservation -> Cinema;
    Reservation -> Movie;
    Reservation -> User;
}

Context Reservation {
  Aggregate Reservation;
}

Context Cinema {
  Aggregate Cinema;
}

Aggregate Cinema {
  Entity Cinema, ScreeningRoom, Seat;
}

生成如下图所示的结果:

Basic Demo

详细过程:

  1. Fklang 编译器会转义 fkl 为 Dot 语法
  2. Feakin Render 渲染 Dot 语法

生成的 DOT 代码如下所示:

digraph TicketBooking2 {
  component=true;layout=fdp;
  node [shape=box style=filled];
  cluster_reservation -> cluster_cinema;
  cluster_reservation -> cluster_movie;
  cluster_reservation -> cluster_user;

  subgraph cluster_cinema {
    label="Cinema(Context)";

    subgraph cluster_aggregate_cinema {
      label="Cinema(Aggregate)";
      entity_Cinema [label="Cinema"];
      entity_ScreeningRoom [label="ScreeningRoom"];
      entity_Seat [label="Seat"];
    }
  }

  subgraph cluster_movie {
    label="Movie(Context)";
  }

  subgraph cluster_reservation {
    label="Reservation(Context)";

    subgraph cluster_aggregate_reservation {
      label="Reservation(Aggregate)";
    }
  }

  subgraph cluster_user {
    label="User(Context)";
  }
}

DDD Strategy

Step 1. Design Context Map

表示领域上下文图,用于描述系统的边界。

ContextMap TicketBooking {
  Reservation -> Cinema;
  Reservation -> Movie;
  Reservation -> User;
}

关系:

  • - - Undirected,
  • -> - PositiveDirected,
  • <- - NegativeDirected,
  • <-> - BiDirected,

Step 2. Context

表示限界上下文,以及对应的聚合间的关系。

Context Reservation {
  Aggregate Reservation;
}

DDD tactics

Step 1. DomainObject

表示领域对象,包括:聚合、实体、值对象。

Aggregate Reservation {
  Entity Ticket, Reservation;
}
Entity Reservation  {
  
}

附录:在线示例代码

ContextMap TicketBooking {
  Reservation -> Cinema;
  Reservation -> Movie;
  Reservation -> User;
}

Context Reservation {
  Aggregate Reservation;
}

Aggregate Reservation {
  Entity Ticket, Reservation;
}

Entity Reservation  {}

Entity Ticket  {}

Context Cinema {
  Aggregate Cinema;
}

Aggregate Cinema {
  Entity Cinema, ScreeningRoom, Seat;
}

Entity Cinema { }
Entity ScreeningRoom { }
Entity Seat { }

Context Movie {
  Aggregate Movie;
}

Aggregate Movie {
  Entity Movie, Actor, Publisher;
}

Entity Movie { }
Entity Actor { }
Entity Publisher { }

Context User {
  Aggregate User;
}

Aggregate User {
  Entity User;
}

Entity User {
}

Entity Payment {
}

ValueObject Price { }
ValueObject Notifications { }

Chapter 2 - Binding Implementation

本节需要结合 IDEA 插件使用:Version

编写后 Binding 之后,在左侧点击 Run 'Gen' 即可生成代码。

Implementation DomainEvent

创建 API 时,需要绑定领域事件到实现。

impl UserCreated {
    endpoint {
        POST "/user/{id}";
        authorization: Basic admin admin;
        response: User;
    }

    flow {
        via UserRepository::getUserById receive user: User
        via UserRepository::save(user: User) receive user: User;
        via Kafak send User to "user.create";
    }
}

Layered Implementation

在配置了如下的分层之后,将直接添加到 Controller 中:

layered DDD {
    dependency {
        "interface" -> "application"
        "interface" -> "domain"
        "domain" -> "application"
        "application" -> "infrastructure"
        "interface" -> "infrastructure"
    }
    layer interface {
        package: "com.feakin.demo.rest";
    }
    layer domain {
        package: "com.feakin.demo.domain";
    }
    layer application {
        package: "com.feakin.demo.application";
    }
    layer infrastructure {
        package: "com.feakin.demo.infrastructure";
    }
}

Code Generator

code generator for fklang

Download from: https://github.com/feakin/fklang/releases

Feakin is a architecture design and visual collaboration tool. This is the parser for Feakin.

Usage: fkl <COMMAND>

Commands:
  dot   generate Graphviz/Dot from fkl file
  ast   generate ast from fkl file
  gen   generate code from fkl file
  run   run function from fkl file
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help information
  -V, --version  Print version information

Basic: generate method

sample code:

impl HelloGot {
    endpoint {
        GET "/hello";
        response: String;
    }
}

output:

@GetMapping("/hello")
public String gotHello() {

}

Auto insert: Aggregate and Layered

declaration with aggregate and layered:

impl HelloGot {
    aggregate: Hello; // here
    endpoint {
        GET "/hello";
        response: String;
    }
}

layered DDD {
    layer interface {
        package: "com.feakin.demo.rest";
    }
    layer domain {
        package: "com.feakin.demo.domain";
    }
    layer application {
        package: "com.feakin.demo.application";
    }
    layer infrastructure {
        package: "com.feakin.demo.infrastructure";
    }
}

with insert code to: src/main/java/com/feakin/demo/rest/Controller.java

@GetMapping("/hello")
public String gotHello() {

}

在生成代码时,会先检查已有的 Event 是否存在于 Controller 中,如果存在则报错,如果不存在则自动插入。

过程:

  1. 使用 TreeSitter 解析目标源码,生成 AST
  2. 查找对应生成的方法是否已经存在

Test with Http Request

编写用例:

impl GitHubOpened {
    endpoint {
        GET "https://book.feakin.com/";
        response: String;
    }
}

impl FeakinJson {
    endpoint {
        GET "https://raw.githubusercontent.com/feakin/vscode-feakin/master/package.json";
        response: String;
    }
}

测试:

点击 endpoint 左侧的运行按钮,即可运行用例。

Run by Cli

$ fkl run --main ./test_data/cli/impl.fkl --impl GitHubOpened --func request

Architecture Guarding

分层架构守护

layered DDD {
    dependency {
        "interface" -> "application"
        "interface" -> "domain"
        "domain" -> "application"
        "application" -> "infrastructure"
        "interface" -> "infrastructure"
    }
    layer interface {
        package: "com.feakin.demo.rest";
    }
    layer domain {
        package: "com.feakin.demo.domain";
    }
    layer application {
        package: "com.feakin.demo.application";
    }
    layer infrastructure {
        package: "com.feakin.demo.infrastructure";
    }
}

Run by IDE:

Guarding

点击 Guard 按钮,会自动检查依赖是否符合预期。

Run by CLI:

$ fkl run --main ./test_data/cli/impl.fkl --func guarding

更多守护规则(TBD)

refs: Guarding

class(implementation "BaseParser")::name should endsWith "Parser";

class("java.util.Map") only accessed(["com.phodal.pepper.refactor.staticclass"]);
class(implementation "BaseParser")::name should not contains "Lexer";

Chapter 5 - HTTP API verify (TBD)

契约驱动的 HTTP API (TBD)

  • HTTP API 测试集成
  • 先验条件
  • 后验条件
impl GitHubOpened {
    endpoint {
        GET "https://book.feakin.com/";
        response: String;
    }
}

impl FeakinJson {
    endpoint {
        GET "https://raw.githubusercontent.com/feakin/vscode-feakin/master/package.json";
        response: String;
    }
}

Run by Cli

$ fkl run --main ./test_data/cli/impl.fkl --impl GitHubOpened --func http-request

校验机制

Chapter 6 - Mock Server

Run by CLI:

$ fkl run --main docs/samples/impl.fkl  --func mock-server --env Local

Run by IDE:

Mock Server

API Mock Server

Feakin 通过 Mock Server 来模拟 API 的返回结果,以便于前端开发人员在没有后端 API 的情况下进行开发。

处理逻辑:

  1. 从 Feakin 项目中读取 Struct 的定义:context -> aggregate -> entity
  2. 解析 Struct 的定义,生成对应的 Builtin Type
  3. 转换 Builtin Type 为 Mock Type
  4. 根据 Mock Type 生成 Fake Values
  5. 返回 Response

Builtin Type


#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum BuiltinType {
  Any,
  String,
  Integer,
  Float,
  Boolean,
  Date,
  DateTime,
  Timestamp,
  Array(Vec<BuiltinType>),
  Map(HashMap<String, BuiltinType>),
  Special(String),
}
}

Mock Type


#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub enum MockType {
  Null,
  // Optional(Box<MockType>),
  /// basic type
  Unknown(String),
  String(String),
  Integer(i64),
  Float(f64),
  Boolean(bool),
  /// structural type
  Array(Vec<MockType>),
  Map(IndexMap<String, MockType>),
  // additional type
  Date(String),
  DateTime(String),
  Timestamp(i64),
  Uuid(String),
}
}

Chapter 7 - Environment

Env

测试数据库连接

$ fkl run --main docs/samples/impl.fkl  --func test-connection --env Local
env Local {
    datasource {
        driver: postgresql
        host: "localhost"
        port: 5432
        database: "test"
    }
    server {
        port: 9090;
    }
}

env Staging {
    // URL 模式
    datasource {
        url: "mysql://localhost:3306/test"
    }
}

自定义环境变量


env Local {
    kafka {
        host: "localhost"
        port: 9092
    }
}

环境检查

verify-env

检查当前环境是否配置正确 ?


Fklang Specification

Fklang provide a two-way binding between design-implementation for architecture.

Basic Works:

  • DDD syntax. DDD strategy and tactic description.
  • DomainEvent Implementation. for generate implementation of DomainEvent.
  • Binding. mapping DSL to SourceCode
  • Layered syntax. layered structured syntax.

In dev:

  • Env for Database.
  • SourceSet Plugin. third-part integration, like PlantUml, Swagger.

Bootstrapping:

  • Builtin Types. like Context, Container, or else.
  • Description syntax. description design in fake code.
  • Typedef (TBD). for DDD syntax type bootstrapping.

Basic Syntax

关键词基本命名规则:

  • DDD 关键词以大写开头,驼峰式命名
  • 其他以小写开头,避免驼峰
  • 特殊场景,遵循该领域的命名规则(但是,应该支持全小写)
    • 例外场景 1:HTTP 请求方法,使用全大写

assign

attr_type: attr_value;

example:

request: CinemaUpdatedRequest;

declare

Keyword IDENTIFIER (COMMA  IDENTIFIER)*;

example

Aggregate Ticket {
  Entity Ticket, Seat;
}

DDD

DDD Syntax:

declusage
context_map_decl:[ 'ContextMap' ] [ ID ] '{' (context_node_decl | context_node_rel ) '}'
|att_list
context_node_decl:['context'] [ID]
context_node_rel:[ ID ] rel_symbol [ ID ]
rel_symbol:('->' | '<-' | '<->')
context_decl:[ 'Context' ] [ ID ] '{' aggregate_list? '}'
|att_list
att_list:attr_item+
attr_item:([ ID ] '=' [ value ] ','?)* ';'?
|([ ID ] ':' [ value ] ','?)* ';'?
|[ ID ] ([ value, ',' ])* ';'?
aggregate_decl:[ 'Aggregate' ] [ ID ] '{' entity_list '}'
|att_list
entity_decl:[ 'Entity' ] [ ID ] '{' value_object_list '}'
|att_list
value_object__decl:[ 'ValueObject' ] [ ID ] '{' value_list '}'
|att_list

Sample

ContextMap Ticket {}

Context ShoppingCarContext {}

// render wtih UML styled?
Aggregate Cart {
  """ inline doc sample
  just-demo for test
  """
  DomainEvent CartCreated, CartItemAdded, CartItemRemoved, CartItemQuantityChanged, CartCheckedOut;
  DomainEvent CartItemQuantityChanged;

  Entity Cart;
}

Entity Cart {
  // it's to many, can change in different way.
  ValueObject CartId
  ValueObject CartStatus
  ValueObject CartItem
  ValueObject CartItemQuantity
  ValueObject CartItemPrice
  ValueObject CartItemTotal
  ValueObject CartTotal
}

DomainEvent Implementation

Subscribe / Publish / Event / Flow

API

  • input -> request
    • pre-validate
  • output -> response
    • post-validate
  • process -> flow
    • tasking

compare to given-when-then.

impl CinemaCreated {
  endpoint {
    POST "${uri}/post";
    request: Request;
    authorization: Basic "{{username}}" "{{password}}";
  }
  
  // created in ApplicationService
  flow {
    via UserRepository::getUserById() receive user: User
    // send "book.created" to Kafka
    via UserRepository::saveUser(user: User) receive void
    // or
    via UserRepository::save(user: User) receive user: User;
    // message queue
    via MessageQueue send CinemaCreated to "CinemaCreated"
    // http request
    via HTTP::post() send Message to "${uri}/post"
    // grpc Greeter
    via GRPC::Greeter send CinemaCreated to "CinemaCreated"
    // map filter
    when(isUserValid) {
      is true => {
        // do something
      }
      is false => {
        // do something
      }
    } 
  }
}

expect generate code will be:

// get_user_by_user_id from JPA
public User getUserByUserId(String userId) {
  return userRepository.findByUserId(userId);
}

// get_user_by_user_id from MyBatis
public User getUserByUserId(String userId) {
  return userMapper.getUserByUserId(userId);
}

Binding

Binding provide a way to binding source code to Context

Aggregate Ticket {
  DomainEvent TicketCreated, TicketUpdated, TicketDeleted;
}

// or to service ?
impl TicketBinding {
  aggregate: Ticket; 
  endpoint {
    GET "/ticket/{id}";
    request: GetTicketRequest;
    authorization: Basic admin admin;
    response: Ticket;
  }
}

//define config: ExtraConfig {
//  baseUrl: "/ticket";
//  language: "Java";
//  package: "com.phodal.coco";
//}

If no config, will use default config by scanner?

SourceSet (TBD)

SourceSet is design for support 3rd-party dsl, like PlantUML, Swagger.yaml

declusage
source_set_decl:simple_source_set_decl
|space_source_set_decl
space_source_set_decl:[ 'SourceSet' ] [ ID ] '{' att_list '}'
simple_source_set_decl:[ 'SourceSet' ] [ ID ] '(' att_list ')'
implementation_decl:[ 'impl' ] [ID] '{' (inline_doc) '}'

PlantUML for Structure

file_type: uml, puml

SourceSet sourceSet {
  feakin {
    srcDir: ["src/main/resources/uml"]
  }
  puml {
    parser: "PlantUML"
    srcDir: ["src/main/resources/uml"]
  }
}

Swagger API (TBD)

file_type: Yaml, JSON

with: XPath

refs:

//

SourceSet petSwagger {
  swagger {
    parser: "Swagger"
    srcDir: ["src/main/resources/swagger"]
    xpath: "/definitions/Pet"
  }
}

// with XPath

UniqueLanguage model ? (TBD)

file_type: CSV, JSON, Markdown ?

SourceSet TicketLang {
  UniqueLanguage {
    srcDir: "ticket.csv";
    type: UniqueLanguage;
    prefix: "Ticket";
  }
}

Layered

Layered is design for decl

declusage
layered_decl:'layered' ([ ID ] | 'default' ) '{' layered_body? '}'
layered_body:layer_dependency
|layer_item_decl
layer_item_decl:'layer' [ ID ] '{' layer_item_entry* '}'
layer_item_entry:package_decl
package_decl:'package' ':' [ string ]

can be guarding for model

layered DDD {
  dependency {
    "interface" -> "application"
    "interface" -> "domain"
    "domain" -> "application"
    "application" -> "infrastructure"
    "interface" -> "infrastructure"
  }
  layer interface {
     package: "com.example.book"
  }
  layer domain {
     package: "com.example.domain"
  }
  layer application {
    package: "com.example.application"
  }
  layer infrastructure {
    package: "com.example.infrastructure"
  }
}

Env

For database and mock server

env Local {
  datasource {
    driver: postgresql
    host: "localhost"
    port: 5432
    database: "test"
  }
  server {
    port: 9090;
  }
}

with API testing (Todo)

with Help utils function

  • builtin-functions: mock server
  • builtin-functions: verify server for testing contract
impl CinemaCreated {
  endpoint {
    GET "/book/{id}";
    request: CreateBookRequest;
    authorization: Basic admin admin;
    response: Cinema;

    // a mock server for testing
    mock {
       port: 8080;
    };
    verify {
       env: Local;
       expect {
        "status": 200
        "data": {
          // build in APIs ?
          "id": {{$uuid}};
          "price": {{$randomInt}};
          "ts": {{$timestamp}};
          "value": "content"
        }
    }
  }

  // full processing (TBD)
  request CreateBookRequest {
//    schema => data,
//    schema {
//      "title" : "string",
//      "author" : "string",
//      "price" : "number"
//    }
    data {
      "title" : "The Lord of the Rings",
      "author" : "J.R.R. Tolkien",
      "price" : 29.99
    }
    validate {
      // title.length > 10 ? 
      title  {
        required { min: 3, max: 10 }
        pattern { regex: "^[a-zA-Z0-9]+$" }
        range { min: 1, max: 100 }
      }
    } 
  } 
  
  middle {
    via User get/update/delete/post userId 
    via Kafka send "book.created"
  }

  response CreateBookResponse {
     struct {
        "id" : "number"
     }
     validate  { }
  } 
  
  // with source side (TBD)
  output CreateBookResponse(xpath="");
  input CreateBookResponse(sourceSet="PetSwagger" location="");
}


env Local {
  host: "http://localhost:8080";
}

Default impl config (TBD)

var config: Config {
  language: "feakin"
  framework: "Spring"
  message: "Kafka" 
  dao: "JPA"
  cache: "Redis"
  search: "ElasticSearch"
}

Bootstrapping

Typedef (TBD)

Typedef provide custom syntax like container or others, can support for bootstrapping DDD syntax.

BuildIn Types

Basic Types

NameDescription
identifierunique identifier
binaryAny binary data
bitsA set of bits or flags
boolean"true" or "false"
enumerationEnumerated strings
stringstring
numberAny number, can be float or int
optional ?Optional type ?

Data Types ?

NameDescription
TableTable data
ListList data
MapMap data
SetSet data
TupleTuple data
ObjectObject data
ArrayArray data
DateDate data
TimeTime data
DateTimeDateTime data
DurationDuration data
IntervalInterval data

Variable

var source: JavaSource {
  language: "Java";
  package: "com.phodal.coco";
}

Container

def ContextMap {
    // todo: parser generator    
}
declusage
typedef_decl:[ 'typedef'] '(' metaType ')' ID '{' (decl_list) '}';
decl_list:decl_item*
decl_item:[ID] ':' decl_name

Expression

Description Syntax:

declusage
description_decl:[ ID ] '{' expr* '}'
expr:if_expr
|choose_expr
|behavior_expr
if_expr:[ 'if' ] '(' [ expression ] ')'
when_expr:[ 'when' ] '(' [ expression ] ')'
behavior_expr:['via'] [ ID ] action [ID]
action:[ 'get' | 'update' | 'delete' | 'create' | 'send']
var sample: FakeCode {
  // if (and ?) then {} else {}
  when(condition) {
    is condition1 {

    }
    is condition2 {

    }
    else {

    }
  }

  done
  operator: <, >, >=, <=, ==, +, -, *, %, /, ?
  // call
  via Entity send/ receive Event;
}

Function (Bootstrapping)

  • mock
  • verify
  • validate
mock {

}

func mock(container: MockContainer) {
  // steps
}

fun verify(input: Input, output: Output) {
  // steps
}

fun validate(input: Input, output: Output) {
  // steps
}

Development Guide

Setup

Feakin

repo: https://github.com/feakin/feakin

Web:

  1. Install Node.js
  2. clone repo
  3. install
npm install --legacy-peer-deps
  1. start server by nx
nx run render:start

Rust:

  1. Install Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. cd crates

Intellij IDEA

repo: https://github.com/feakin/intellij-feakin

  1. Install Intellij IDEA
  2. clone repo
  3. run ./gradlew buildPlugin

Collaboration

CRDT

服务端:Actix + Diamond Types + CRDT

对于服务端来说,它本身其实也是个客户端,只需要接受客户端生成的 patch 即可,在合并了 patch 之后,将它广播出去即可:


#![allow(unused)]
fn main() {
let before_version = live.lock().unwrap().version();
after_version = self.ops_by_patches(agent_name, patches).await;
// or let after_version = self.insert(content, pos).await;
// or after_version = self.delete(range).await;
let patch = coding.patch_since(&before_version);
}

Feakin,当前版本在这里,除了支持 patch,还可以同时支持 ins、del 这样的操。核心的代码就这么几行,剩下的代码都是 CRUD,没啥好玩的。

客户端:编辑生成 patches

从结果代码来说,这部分相当的简单::

let localVersion = doc.getLocalVersion();
event.changes.sort((change1, change2) => change2.rangeOffset - change1.rangeOffset).forEach(change => {
  doc.ins(change.rangeOffset, change.text);
  doc.del(change.rangeOffset, change.rangeLength);
})
let patch = doc.getPatchSince(localVersion);

由于在前端中 Feakin 采用的是 monaco 的实现,需要在发生变更时,执行 insdel 等,以生成 patch。

客户端:编辑器应用 patches

对于客户端来说,接受 patch 并应用也不复杂,然而我被坑了一晚上(被坑在了如何动态更新 Monaco 的模型上):

let merge_version = doc.mergeBytes(bytes)
doc.mergeVersions(doc.getLocalVersion(), merge_version);

let xfSinces: DTOperation[] = doc.xfSince(patchInfo.before);
xfSinces.forEach((op) => {
   ...
});        

Graph

Graph GIM

see in: graph.ts

export interface Graph {
  nodes: Node[];
  edges: Edge[];
  props?: GraphProperty;
  subgraphs?: Graph[];
}

export interface Node {
  id: string;
  label: string;
  x?: number;
  y?: number;
  width?: number;
  height?: number;
  subgraph?: boolean;

  data?: NodeData;
  props?: ElementProperty;
}

export interface Edge {
  id: string;
  label?: string;
  points: Point[];

  width?: number;
  height?: number;

  // like beziere curve need a cp
  controlPoints?: Point[];

  data?: EdgeData;

  props?: EdgeProperty;
}

Graph Exporter

Graphviz WASM

@hpcc-js/wasm 提供了一个 Graphviz 的 WASM 封装。

Render

Konva Canvas

Fklang

Fklang 是一个架构设计 DSL,通过显性化软件架构设计,以确保软件系统描述与实现的一致性。并在工作流中,内嵌对于 AI 代码生成软件的支持,以构筑完整的开发者体验。

inspired by: ContextMap and π-ADL

FKL Parser

解析器生成器:https://pest.rs/

Pest 在线编辑器:https://pest.rs/#editor

语法

declarations = _{ SOI ~ declaration* ~ EOI }

declaration = {
  include_decl
  | context_map_decl
  | context_decl
  | ext_module_decl
  | aggregate_decl
  | entity_decl
  | value_object_decl
  | struct_decl
  // ddd
  | component_decl
  | implementation_decl
  | layered_decl
  // extension
  | source_sets_decl
}

Fklang Code Generator

Java

AWS Lambda (TBD)

Fklang code binding

Whole process:

  1. parse Fklang binding syntax, identify the binding target.
  2. parse the target file, identify the binding target. (use Scanner)
  3. generate the binding code, and insert it into the target file.
  4. format the target file?

mods:

  • code_meta. metadata of the code, such as the package name, the class name, the function name, etc.
  • construct. parse source code to code_meta with TreeSitter.
  • inserter. insert code to source code.
  • exec. execute the binding process.

TreeSitter

Online playground: https://tree-sitter.github.io/tree-sitter/playground

TreeSitter is support Pattern Matching with Queries can use S-expression to query the AST.

Query examples

Java Code:

class DateTimeImpl {
    public Date getDate() {
        return new Date();
    }
}

Query Language:

(package_declaration
	(scoped_identifier) @package-name)

(import_declaration
	(scoped_identifier) @import-name)

(program
    (class_declaration
	    name: (identifier) @class-name
        interfaces: (super_interfaces (interface_type_list (type_identifier)  @impl-name))?
        body: (class_body (method_declaration
            (modifiers
                (annotation
                  name: (identifier) @annotation.name
                      arguments: (annotation_argument_list)? @annotation.key_values
                )
            )?
            type: (type_identifier) @return-type
            name: (identifier) @function-name
            parameters: (formal_parameters (formal_parameter
              type: (type_identifier) @param-type
                name: (identifier) @param-name
            ))?
          ))?
    )
)

Output:

program [0, 0] - [6, 0]
  class_declaration [0, 0] - [4, 1]
    name: identifier [0, 6] - [0, 18]
    body: class_body [0, 19] - [4, 1]
      method_declaration [1, 4] - [3, 5]
        modifiers [1, 4] - [1, 10]
        type: type_identifier [1, 11] - [1, 15]
        name: identifier [1, 16] - [1, 23]
        parameters: formal_parameters [1, 23] - [1, 25]
        body: block [1, 26] - [3, 5]
          return_statement [2, 8] - [2, 26]
            object_creation_expression [2, 15] - [2, 25]
              type: type_identifier [2, 19] - [2, 23]
              arguments: argument_list [2, 23] - [2, 25]

Fklang Build System

Intellij Plugins

Main Resources:

Define by plugin.xml

IDEA

<extensions defaultExtensionNs="com.intellij">
    <!-- File-type Factory -->
    <fileType name="Feakin File"
              language="Feakin"
              implementationClass="com.feakin.intellij.FkFileType"
              fieldName="INSTANCE"
              extensions="fkl"/>
    <internalFileTemplate name="Feakin File"/>

    <!-- Parser -->
    <lang.parserDefinition language="Feakin"
                           implementationClass="com.feakin.intellij.parser.FkParserDefinition"/>

    <lang.syntaxHighlighter language="Feakin"
                            implementationClass="com.feakin.intellij.highlight.FkSyntaxHighlighter"/>

    <lang.psiStructureViewFactory language="Feakin"
                                  implementationClass="com.feakin.intellij.structure.FkStructureViewFactory"/>

    <!-- Editor -->
    <extendWordSelectionHandler implementation="com.feakin.intellij.ide.editor.FkBlockSelectionHandler"/>
    <lang.foldingBuilder language="Feakin"
                         implementationClass="com.feakin.intellij.edit.FkFoldingBuilder"/>

    <lang.commenter language="Feakin" implementationClass="com.feakin.intellij.completion.FkCommenter"/>
    <lang.braceMatcher language="Feakin" implementationClass="com.feakin.intellij.ide.FkBraceMatcher"/>


    <!-- Navigate between useDomainObject and DomainObjectDecl -->
    <indexedRootsProvider implementation="com.feakin.intellij.indexing.FkIndexableSetContributor"/>

    <stubElementTypeHolder class="com.feakin.intellij.lexer.FkElementTypes"/>

    <stubIndex implementation="com.feakin.intellij.resolve.indexes.FkNamedElementIndex"/>
    <stubIndex implementation="com.feakin.intellij.resolve.indexes.FkGotoClassIndex"/>

    <gotoSymbolContributor implementation="com.feakin.intellij.ide.navigate.FkGotoSymbolContributor"/>

    <!-- Completion -->
    <completion.contributor language="Feakin"
                            implementationClass="com.feakin.intellij.completion.FkKeywordCompletionContributor"
                            id="FkKeywordCompletionContributor"
                            order="first"/>


    <!-- Line Marker Providers -->
    <codeInsight.lineMarkerProvider language="Feakin"
                                    implementationClass="com.feakin.intellij.linemarkers.FkImplMessageProvider"/>
    <codeInsight.lineMarkerProvider language="Feakin"
                                    implementationClass="com.feakin.intellij.linemarkers.FkImplMethodProvider"/>

    <runLineMarkerContributor language="Feakin"
                              implementationClass="com.feakin.intellij.linemarkers.FkImplLineMarkerContributor"/>

    <!-- Run Configurations -->
    <configurationType implementation="com.feakin.intellij.runconfig.FkCommandConfigurationType"/>

    <programRunner implementation="com.feakin.intellij.runconfig.FkCommandRunner"/>

    <runConfigurationProducer
            implementation="com.feakin.intellij.runconfig.command.FkRunConfigurationProducer"/>

    <!-- Formatter -->
    <lang.formatter language="Feakin" implementationClass="com.feakin.intellij.formatter.FkFormattingModelBuilder"/>

    <!-- Usages Provider -->
    <lang.findUsagesProvider language="Feakin" implementationClass="com.feakin.intellij.ide.search.FkFindUsagesProvider"/>
    <findUsagesHandlerFactory implementation="com.feakin.intellij.ide.search.FkFindUsagesHandlerFactory"/>
    <usageTypeProvider implementation="com.feakin.intellij.ide.search.FkUsageTypeProvider"/>
</extensions>

Indexes with Stub

sample:

contextMapDeclaration ::= CONTEXT_MAP_KEYWORD IDENTIFIER contextMapBody
{
  implements = [
    "com.feakin.intellij.psi.FkNamedElement"
    "com.feakin.intellij.psi.FkNameIdentifierOwner"
  ]
  mixin = "com.feakin.intellij.stubs.ext.FkContextMapImplMixin"
  stubClass = "com.feakin.intellij.stubs.FkContextMapDeclStub"
  elementTypeFactory = "com.feakin.intellij.stubs.StubImplementationsKt.factory"
}

Reference

添加 Ctrl/Command + B,需要配置双向 Reference。如:

  • FkContextNameReferenceImpl 用于寻找对应的 FkContextDeclaration
  • FkContextDeclReferenceImpl 用于寻找对应的 FkContextName

配置缓存支持两种方式:

  • 通过 stubIndex 与 BNF 中的 stubClass 配置 Stub。
  • 通过 CachedValuesManager.getCachedValue 配置。

配置 stubIndex 与 BNF 中的 stubClass 配置 Stub

  1. plugin.xml 中配置 stubIndex 配置缓存元素:
<stubIndex implementation="com.feakin.intellij.resolve.indexes.FkNamedElementIndex"/>
  1. 从 BNF 中配置 stubClass
contextDeclaration ::= CONTEXT_KEYWORD IDENTIFIER contextBody
{
  implements = [
    "com.feakin.intellij.psi.FkNamedElement"
    "com.feakin.intellij.psi.FkNameIdentifierOwner"
    "com.feakin.intellij.psi.ext.FkMandatoryReferenceElement"
  ]
  mixin = "com.feakin.intellij.stubs.ext.FkContextDeclarationImplMixin"
  stubClass = "com.feakin.intellij.stubs.FkContextDeclarationStub"
  elementTypeFactory = "com.feakin.intellij.stubs.StubImplementationsKt.factory"
}
  1. 实现对应的配置

Custom LineMarker

自定义 LineMarker 的方式有两种:

  • 通过 LineMarkerProvider 实现
  • 通过 RunLineMarkerContributor 实现
<!-- line marker -->
<codeInsight.lineMarkerProvider language="Feakin"
                                implementationClass="com.feakin.intellij.linemarkers.FkImplMessageProvider"/>

<!--  producer -->
<runConfigurationProducer implementation="com.feakin.intellij.runconfig.command.FkEndpointConfigurationProducer"/>

在 Intellij Feakin 中,先创建 RunLineMarkerContributor 后,再创建 RunConfigurationProducer,如 GencodeImplConfigurationProducer

class FkCodegenImplLineMarkerContributor : RunLineMarkerContributor() {
    override fun getInfo(element: PsiElement): Info? {
        if (element !is FkImplDeclaration) return null
        val state = GencodeImplConfigurationProducer().findConfig(listOf(element)) ?: return null

        val actions = ExecutorAction.getActions(0)
        return Info(
            AllIcons.RunConfigurations.TestState.Run,
            { state.configurationName },
            *actions
        )
    }
}

示例:

class GencodeImplConfigurationProducer : BaseLazyRunConfigurationProducer<GencodeConfig, FkImplDeclaration>() {
    override val commandName: String = "gen"

    init {
        registerConfigProvider { elements -> createConfigFor<FkImplDeclaration>(elements) }
    }

    private inline fun <reified T : FkImplDeclaration> createConfigFor(
        elements: List<PsiElement>
    ): GencodeConfig? {
        val path = elements.firstOrNull()?.containingFile?.virtualFile?.path ?: return null
        val sourceElement = elements.firstOrNull { it is T } ?: return null
        return GencodeConfig(commandName, path, sourceElement as FkImplDeclaration)
    }

    private fun registerConfigProvider(provider: (List<PsiElement>) -> GencodeConfig?) {
        runConfigProviders.add(provider)
    }
}

FkCommandLine

fkl_cli/src/main.rs 中的 Cli::Commands 保持一致:

class FkCommandLine(
    var path: String,
    var impl: String,
    private val subcommand: String,
    private val funcName: String = "",
)

Syntax BNF

语法解析,基于 Grammar Kit 来进行解析: https://github.com/JetBrains/Grammar-Kit

官方示例如下:

root_rule ::= rule_A rule_B rule_C rule_D                // sequence expression
rule_A ::= token | 'or_text' | "another_one"             // choice expression
rule_B ::= [ optional_token ] and_another_one?           // optional expression
rule_C ::= &required !forbidden                          // predicate expression
rule_D ::= { can_use_braces + (and_parens) * }           // grouping and repetition

// Grammar-Kit BNF syntax

{ generate=[psi="no"] }                                  // top-level global attributes
private left rule_with_modifier ::= '+'                  // rule modifiers
left rule_with_attributes ::= '?' {elementType=rule_D}   // rule attributes

private meta list ::= <<p>> (',' <<p>>) *                // meta rule with parameters
private list_usage ::= <<list rule_D>>                   // meta rule application

LSP (todo)

Monaco Editor

Custom Language

FAQ

cannot be opened because the developer cannot be verified.

refs:

Contributors

Here is a list of the contributors who have helped to improve fkbook. Big shout-out to them!

If you feel you're missing from this list, feel free to add yourself in a PR.