Initial commit:
Added framework to generate DOT and png files of character map chart from data folder. Added all figures in the 1-2 chapters. Added font file SimSum for render chart
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
# Go parameters
|
||||||
|
GOCMD=go
|
||||||
|
GOBUILD=$(GOCMD) build
|
||||||
|
GOTEST=$(GOCMD) test
|
||||||
|
GOCLEAN=$(GOCMD) clean
|
||||||
|
|
||||||
|
# Binary names
|
||||||
|
BINARY_NAME=xyj-figures
|
||||||
|
OUTPUT_DIR=output
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
$(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME) ./cmd/main.go
|
||||||
|
|
||||||
|
run: build
|
||||||
|
$(OUTPUT_DIR)/$(BINARY_NAME)
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(GOTEST) -v ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(GOCLEAN)
|
||||||
|
rm -rf $(OUTPUT_DIR)
|
||||||
BIN
Binary file not shown.
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"xyj-figures/pkg/batch"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "xyj-figures",
|
||||||
|
Short: "A binary to generate map chart for figures in 西游记",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
es, rs, errs := batch.LoadFolder("data")
|
||||||
|
if errs != nil {
|
||||||
|
log.Errorf("unable to load data on folder %s: %v", "data", errs)
|
||||||
|
}
|
||||||
|
log.Infof("loaded %d entities and %d relationships", len(es), len(rs))
|
||||||
|
|
||||||
|
ns, rs, errs := batch.DistillData(es, rs)
|
||||||
|
if errs != nil {
|
||||||
|
log.Errorf("unable to distill data: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs = batch.Save("output/output.dot", ns, rs)
|
||||||
|
if errs != nil {
|
||||||
|
log.Errorf("unable to save dot: %v", errs)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute executes the root command.
|
||||||
|
func Execute() error {
|
||||||
|
return rootCmd.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFormatter(&log.JSONFormatter{})
|
||||||
|
|
||||||
|
// Output to stdout instead of the default stderr
|
||||||
|
// Can be any io.Writer, see below for File example
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
// Only log the warning severity or above.
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Fatalf("failed to exec cmd: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 丘弘济真人
|
||||||
|
other_names:
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 丘弘济真人
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 唐僧
|
||||||
|
other_names:
|
||||||
|
- 唐玄奘
|
||||||
|
- 唐三藏
|
||||||
|
- 玄奘
|
||||||
|
relationships:
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 地藏王菩萨
|
||||||
|
other_names:
|
||||||
|
- 幽冥教主
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 地藏王菩萨
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 太白金星
|
||||||
|
other_names:
|
||||||
|
- 太白长庚星
|
||||||
|
relationships:
|
||||||
|
- source: 太白金星
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 招抚
|
||||||
|
bidirection: true
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 太白金星
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 孙悟空
|
||||||
|
other_names:
|
||||||
|
- 孙行者
|
||||||
|
- 者行孙
|
||||||
|
relationships:
|
||||||
|
- source: 孙悟空
|
||||||
|
destination: 唐僧
|
||||||
|
description: 师徒
|
||||||
|
bidirection: true
|
||||||
|
description: 孙悟空是明朝中后期畅销小说《西游记》当中个第一主角,是从石头里向蹦出来个猢狲。
|
||||||
|
links:
|
||||||
|
- https://zh.wikipedia.org/wiki/%E5%AD%99%E6%82%9F%E7%A9%BA
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 敖广
|
||||||
|
other_names:
|
||||||
|
- 东海龙王
|
||||||
|
- 水元下界东胜神洲东海小龙
|
||||||
|
relationships:
|
||||||
|
- source: 敖广
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 邻居
|
||||||
|
bidirection: true
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 敖广
|
||||||
|
description: 首领
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 敖钦
|
||||||
|
other_names:
|
||||||
|
- 南海龙王
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 敖钦
|
||||||
|
description: 首领
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 敖闰
|
||||||
|
other_names:
|
||||||
|
- 西海龙王
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 敖闰
|
||||||
|
description: 首领
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 敖顺
|
||||||
|
other_names:
|
||||||
|
- 北海龙王
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 敖顺
|
||||||
|
description: 首领
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 文曲星官
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 文曲星官
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 沙僧
|
||||||
|
other_names:
|
||||||
|
- 沙悟净
|
||||||
|
- 沙和尚
|
||||||
|
relationships:
|
||||||
|
- source: 沙僧
|
||||||
|
destination: 唐僧
|
||||||
|
description: 师徒
|
||||||
|
bidirection: true
|
||||||
|
description:
|
||||||
|
links:
|
||||||
|
- https://zh.wikipedia.org/wiki/%E6%B2%99%E5%83%A7
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 混世魔王
|
||||||
|
relationships:
|
||||||
|
- source: 孙悟空
|
||||||
|
destination: 混世魔王
|
||||||
|
description: 打死
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 灵台方寸山樵夫
|
||||||
|
relationships:
|
||||||
|
- source: 菩提祖师
|
||||||
|
destination: 灵台方寸山樵夫
|
||||||
|
description: 邻居
|
||||||
|
bidirection: true
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 牛魔王
|
||||||
|
relationships:
|
||||||
|
- source: 牛魔王
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 兄弟
|
||||||
|
bidirection: true
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 猪八戒
|
||||||
|
other_names:
|
||||||
|
- 猪悟能
|
||||||
|
relationships:
|
||||||
|
- source: 猪八戒
|
||||||
|
destination: 唐僧
|
||||||
|
description: 师徒
|
||||||
|
bidirection: true
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 玉皇大帝
|
||||||
|
other_names:
|
||||||
|
- 高天上圣大慈仁者玉皇大天尊玄穹高上帝
|
||||||
|
- 大天尊
|
||||||
|
relationships:
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 白龙马
|
||||||
|
other_names:
|
||||||
|
- 小白龙
|
||||||
|
relationships:
|
||||||
|
- source: 白龙马
|
||||||
|
destination: 唐僧
|
||||||
|
description: 师徒
|
||||||
|
bidirection: true
|
||||||
|
- source: 敖闰
|
||||||
|
destination: 白龙马
|
||||||
|
description: 三子
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 菩提祖师
|
||||||
|
relationships:
|
||||||
|
- source: 菩提祖师
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 师徒
|
||||||
|
bidirection: true
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 葛仙翁天师
|
||||||
|
other_names:
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 葛仙翁天师
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
schema: v1
|
||||||
|
type: group
|
||||||
|
details:
|
||||||
|
name: 十代冥王
|
||||||
|
other_names:
|
||||||
|
- 冥司
|
||||||
|
members:
|
||||||
|
- 秦广王
|
||||||
|
- 初江王
|
||||||
|
- 宋帝王
|
||||||
|
- 忤官王
|
||||||
|
- 阎罗王
|
||||||
|
- 平等王
|
||||||
|
- 泰山王
|
||||||
|
- 都市王
|
||||||
|
- 卞城王
|
||||||
|
- 转轮王
|
||||||
|
relationships:
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 秦广王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 初江王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 宋帝王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 忤官王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 阎罗王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 平等王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 泰山王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 都市王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 卞城王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 地藏王菩萨
|
||||||
|
destination: 转轮王
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
schema: v1
|
||||||
|
type: group
|
||||||
|
details:
|
||||||
|
name: 天庭
|
||||||
|
members:
|
||||||
|
- 千里眼
|
||||||
|
- 顺风耳
|
||||||
|
- 太白金星
|
||||||
|
- 文曲星官
|
||||||
|
relationships:
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 千里眼
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
- source: 玉皇大帝
|
||||||
|
destination: 顺风耳
|
||||||
|
description: 下级
|
||||||
|
bidirection: false
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
schema: v1
|
||||||
|
type: group
|
||||||
|
details:
|
||||||
|
name: 花果山群妖
|
||||||
|
members:
|
||||||
|
- 牛魔王
|
||||||
|
- 蛟魔王
|
||||||
|
- 鹏魔王
|
||||||
|
- 狮狔王
|
||||||
|
- 猕猴王
|
||||||
|
- 狨王
|
||||||
|
relationships:
|
||||||
|
- source: 孙悟空
|
||||||
|
destination: 牛魔王
|
||||||
|
description: 兄弟
|
||||||
|
bidirection: false
|
||||||
|
- source: 蛟魔王
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 兄弟
|
||||||
|
bidirection: true
|
||||||
|
- source: 鹏魔王
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 兄弟
|
||||||
|
bidirection: true
|
||||||
|
- source: 狮狔王
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 兄弟
|
||||||
|
bidirection: true
|
||||||
|
- source: 猕猴王
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 兄弟
|
||||||
|
bidirection: true
|
||||||
|
- source: 狨王
|
||||||
|
destination: 孙悟空
|
||||||
|
description: 兄弟
|
||||||
|
bidirection: true
|
||||||
|
description:
|
||||||
|
links:
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
module xyj-figures
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/flopp/go-findfont v0.1.0
|
||||||
|
github.com/go-test/deep v1.1.1
|
||||||
|
github.com/goccy/go-graphviz v0.1.3
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/spf13/cobra v1.8.1
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
|
golang.org/x/image v0.14.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/fogleman/gg v1.3.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA=
|
||||||
|
github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU=
|
||||||
|
github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
|
||||||
|
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||||
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
github.com/goccy/go-graphviz v0.1.3 h1:Pkt8y4FBnBNI9tfSobpoN5qy1qMNqRXPQYvLhaSUasY=
|
||||||
|
github.com/goccy/go-graphviz v0.1.3/go.mod h1:pMYpbAqJT10V8dzV1JN/g/wUlG/0imKPzn3ZsrchGCI=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY=
|
||||||
|
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||||
|
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package batch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"xyj-figures/pkg/entity"
|
||||||
|
"xyj-figures/pkg/relationship"
|
||||||
|
|
||||||
|
findfont "github.com/flopp/go-findfont"
|
||||||
|
graphviz "github.com/goccy/go-graphviz"
|
||||||
|
"github.com/goccy/go-graphviz/cgraph"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CLUSTER_PREFIX = "cluster_"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadFolder(path string) ([]entity.Entity, []relationship.Relationship, error) {
|
||||||
|
es := []entity.Entity{}
|
||||||
|
rs := []relationship.Relationship{}
|
||||||
|
errs := []string{}
|
||||||
|
|
||||||
|
// Triverse all files in path and load all entities and relationships
|
||||||
|
err := filepath.Walk(path, func(subpath string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
e, err := entity.Load(subpath)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
es = append(es, *e)
|
||||||
|
rs = append(rs, e.Details.(entity.EntityDetails).GetRelationships()...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return es, rs, fmt.Errorf("multiple errors: %v", strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
|
return es, rs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistillData takes loaded data and do all needed optimizations and extract only needed info for maps
|
||||||
|
func DistillData(es []entity.Entity, rs []relationship.Relationship) ([]entity.Entity, []relationship.Relationship, error) {
|
||||||
|
// Create a set of all entities, including characters and every members
|
||||||
|
// TODO: Duplicated name
|
||||||
|
nodes := map[string]entity.Entity{}
|
||||||
|
edges := map[string]relationship.Relationship{}
|
||||||
|
for _, e := range es {
|
||||||
|
switch e.Type {
|
||||||
|
case entity.EntityTypeCharacter:
|
||||||
|
if val, ok := nodes[e.Details.(entity.Character).Name]; ok {
|
||||||
|
if err := val.Merge(e); err != nil {
|
||||||
|
log.Warnf("failed to merge: %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nodes[e.Details.(entity.Character).Name] = e
|
||||||
|
case entity.EntityTypeGroup:
|
||||||
|
nodes[e.Details.(entity.Group).Name] = e
|
||||||
|
for _, m := range e.Details.(entity.Group).Members {
|
||||||
|
nodes[m] = entity.Entity{
|
||||||
|
Schema: "v1",
|
||||||
|
Type: entity.EntityTypeCharacter,
|
||||||
|
Details: entity.Character{
|
||||||
|
Name: m,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rs {
|
||||||
|
key1, key2 := r.GetKey()
|
||||||
|
val1, ok1 := edges[key1]
|
||||||
|
val2, ok2 := edges[key2]
|
||||||
|
// Both keys don't exist
|
||||||
|
if !ok1 && !ok2 {
|
||||||
|
edges[key1] = r
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Logs all unexpected edges
|
||||||
|
if ok1 && !r.IsSame(val1) {
|
||||||
|
log.Warningf("Found ambigious edges, existing: %+v, new: %+v", val1, r)
|
||||||
|
}
|
||||||
|
if ok2 && !r.IsSame(val2) {
|
||||||
|
log.Warningf("Found ambigious edges, existing: %+v, new: %+v", val2, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retns := make([]entity.Entity, 0, len(nodes))
|
||||||
|
for _, n := range nodes {
|
||||||
|
retns = append(retns, n)
|
||||||
|
}
|
||||||
|
retes := make([]relationship.Relationship, 0, len(edges))
|
||||||
|
for _, v := range edges {
|
||||||
|
retes = append(retes, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return retns, retes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save distilled info into DOT type files
|
||||||
|
func Save(path string, ents []entity.Entity, rels []relationship.Relationship) error {
|
||||||
|
g := graphviz.New()
|
||||||
|
g.SetLayout(graphviz.TWOPI)
|
||||||
|
useSystemFont(g, "SimSum.ttc")
|
||||||
|
rootgraph, err := g.Graph()
|
||||||
|
rootgraph.SafeSet("fontname", "NSimSun", "Times-Roman")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create graph: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := rootgraph.Close(); err != nil {
|
||||||
|
log.Errorf("failed to close graph: %v", err)
|
||||||
|
}
|
||||||
|
g.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
nodesPtr := map[string]*cgraph.Node{}
|
||||||
|
subGraphPtr := map[string]*cgraph.Graph{}
|
||||||
|
nodeGraphMap := map[string]string{}
|
||||||
|
for _, e := range ents {
|
||||||
|
if e.Type == entity.EntityTypeGroup {
|
||||||
|
subGraph := rootgraph.SubGraph(CLUSTER_PREFIX+e.Details.(entity.Group).Name, 1)
|
||||||
|
subGraph.SetLabel(e.Details.(entity.Group).Name)
|
||||||
|
// subGraph.SafeSet("fontname", "NSimSun", "Times-Roman")
|
||||||
|
subGraphPtr[e.Details.(entity.Group).Name] = subGraph
|
||||||
|
// Record all members
|
||||||
|
for _, m := range e.Details.(entity.Group).Members {
|
||||||
|
nodeGraphMap[m] = e.Details.(entity.Group).Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(subGraphPtr)
|
||||||
|
fmt.Println(nodeGraphMap)
|
||||||
|
|
||||||
|
for _, e := range ents {
|
||||||
|
if e.Type == entity.EntityTypeCharacter {
|
||||||
|
log.Debugf("entity: %+v", e)
|
||||||
|
if group, ok := nodeGraphMap[e.Details.(entity.Character).Name]; ok {
|
||||||
|
ptr, ok := subGraphPtr[group]
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("can't find sub graph %s", group)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ptr == nil {
|
||||||
|
log.Debugf("sub graph %s is nil", group)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n, err := ptr.CreateNode(e.Details.(entity.Character).Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create node in sub graph %s: %v", group, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nodesPtr[e.Details.(entity.Character).Name] = n
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node, err := rootgraph.CreateNode(e.Details.(entity.Character).Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create node: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nodesPtr[e.Details.(entity.Character).Name] = node
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(nodesPtr)
|
||||||
|
|
||||||
|
for _, r := range rels {
|
||||||
|
key, _ := r.GetKey()
|
||||||
|
srcPtr := nodesPtr[r.Src]
|
||||||
|
dstPtr := nodesPtr[r.Dst]
|
||||||
|
fmt.Printf("src: %s, dst: %s\n", r.Src, r.Dst)
|
||||||
|
e, err := rootgraph.CreateEdge(key, srcPtr, dstPtr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create edge: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.SetLabel(r.Description)
|
||||||
|
e.SafeSet("fontname", "NSimSun", "Times-Roman")
|
||||||
|
e.SafeSet("fontsize", "12", "10")
|
||||||
|
if r.Bidirection {
|
||||||
|
e.SetDir(cgraph.NoneDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := g.RenderFilename(rootgraph, "dot", path); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.RenderFilename(rootgraph, graphviz.PNG, path+".png"); err != nil {
|
||||||
|
return fmt.Errorf("failed to save graph to disk: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func useSystemFont(g *graphviz.Graphviz, name string) {
|
||||||
|
fontPath, err := findfont.Find(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fontData, err := os.ReadFile(fontPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
ft, err := truetype.Parse(fontData)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
g.SetFontFace(func(size float64) (font.Face, error) {
|
||||||
|
opt := &truetype.Options{
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
return truetype.NewFace(ft, opt), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package batch_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"xyj-figures/pkg/batch"
|
||||||
|
"xyj-figures/pkg/entity"
|
||||||
|
"xyj-figures/pkg/relationship"
|
||||||
|
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DataFolder = "../../data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadFolder(t *testing.T) {
|
||||||
|
tcs := []struct {
|
||||||
|
name string
|
||||||
|
filelist []string
|
||||||
|
want struct {
|
||||||
|
entities []entity.Entity
|
||||||
|
relationships []relationship.Relationship
|
||||||
|
errors error
|
||||||
|
}
|
||||||
|
wanterr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test1",
|
||||||
|
filelist: []string{
|
||||||
|
"character/孙悟空.yaml",
|
||||||
|
},
|
||||||
|
want: struct {
|
||||||
|
entities []entity.Entity
|
||||||
|
relationships []relationship.Relationship
|
||||||
|
errors error
|
||||||
|
}{
|
||||||
|
entities: []entity.Entity{
|
||||||
|
{
|
||||||
|
Schema: "v1",
|
||||||
|
Type: entity.EntityTypeCharacter,
|
||||||
|
Details: entity.Character{
|
||||||
|
Name: "孙悟空",
|
||||||
|
OtherNames: []string{"孙行者", "者行孙"},
|
||||||
|
Relationships: []relationship.Relationship{
|
||||||
|
{
|
||||||
|
Src: "孙悟空",
|
||||||
|
Dst: "唐僧",
|
||||||
|
Description: "师徒",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Description: "孙悟空是明朝中后期畅销小说《西游记》当中个第一主角,是从石头里向蹦出来个猢狲。",
|
||||||
|
Links: []string{
|
||||||
|
"https://zh.wikipedia.org/wiki/%E5%AD%99%E6%82%9F%E7%A9%BA",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relationships: []relationship.Relationship{
|
||||||
|
{
|
||||||
|
Src: "孙悟空",
|
||||||
|
Dst: "唐僧",
|
||||||
|
Description: "师徒",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wanterr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
for _, fn := range tc.filelist {
|
||||||
|
f, err := os.ReadFile(filepath.Join(DataFolder, fn))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to copy file %s: %v", fn, err)
|
||||||
|
}
|
||||||
|
dstPath := filepath.Join(tmpDir, fn)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dstPath), os.ModePerm); err != nil {
|
||||||
|
t.Fatalf("failed to copy file %s: %v", fn, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dstPath, f, 0644); err != nil {
|
||||||
|
t.Fatalf("failed to copy file %s: %v", fn, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
egot, rgot, errgot := batch.LoadFolder(tmpDir)
|
||||||
|
if (!tc.wanterr && errgot != nil) || (tc.wanterr && errgot == nil) {
|
||||||
|
t.Errorf("error not expected, wanterr: %v got: %+v", tc.wanterr, errgot)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(egot, tc.want.entities) {
|
||||||
|
t.Errorf("entities diff: %+v", deep.Equal(egot, tc.want.entities))
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(rgot, tc.want.relationships) {
|
||||||
|
t.Errorf("relationship diff: %+v", deep.Equal(rgot, tc.want.relationships))
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
r "xyj-figures/pkg/relationship"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Character struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
OtherNames []string `yaml:"other_names,flow"`
|
||||||
|
Relationships []r.Relationship `yaml:"relationships,flow"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Links []string `yaml:"links,flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Character) GetRelationships() []r.Relationship {
|
||||||
|
return c.Relationships
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
r "xyj-figures/pkg/relationship"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entity struct {
|
||||||
|
Schema string `yaml:"schema,flow"`
|
||||||
|
Type string `yaml:"type,flow"`
|
||||||
|
Details interface{} `yaml:"details,flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntityType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EntityTypeCharacter = "character"
|
||||||
|
EntityTypeGroup = "group"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntityDetails interface {
|
||||||
|
GetRelationships() []r.Relationship
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntity() (*Entity, error) {
|
||||||
|
return &Entity{Schema: "v1"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(path string) (*Entity, error) {
|
||||||
|
f, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open file: %v", err)
|
||||||
|
}
|
||||||
|
e, _ := NewEntity()
|
||||||
|
if err = yaml.Unmarshal(f, &e); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal file: %v", err)
|
||||||
|
}
|
||||||
|
switch e.Type {
|
||||||
|
case EntityTypeCharacter:
|
||||||
|
tmp, err := yaml.Marshal(e.Details)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal filed %s: %v", EntityTypeCharacter, err)
|
||||||
|
}
|
||||||
|
c := Character{}
|
||||||
|
if err = yaml.Unmarshal(tmp, &c); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal filed %s: %v", EntityTypeCharacter, err)
|
||||||
|
}
|
||||||
|
e.Details = c
|
||||||
|
case EntityTypeGroup:
|
||||||
|
tmp, err := yaml.Marshal(e.Details)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal filed %s: %v", EntityTypeGroup, err)
|
||||||
|
}
|
||||||
|
g := Group{}
|
||||||
|
if err = yaml.Unmarshal(tmp, &g); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal filed %s: %v", EntityTypeGroup, err)
|
||||||
|
}
|
||||||
|
e.Details = g
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entity) Merge(other Entity) error {
|
||||||
|
if (e.Schema != other.Schema) || (e.Type != other.Type) {
|
||||||
|
return fmt.Errorf("unmatch merge between %v and %v", e, other)
|
||||||
|
}
|
||||||
|
switch e.Type {
|
||||||
|
case EntityTypeCharacter:
|
||||||
|
if e.Details.(Character).Name != other.Details.(Character).Name {
|
||||||
|
return fmt.Errorf("unmatch merge between %v and %v: names unmatch", e, other)
|
||||||
|
}
|
||||||
|
e.Details = Character{
|
||||||
|
Name: e.Details.(Character).Name,
|
||||||
|
OtherNames: append(e.Details.(Character).OtherNames, other.Details.(Character).OtherNames...),
|
||||||
|
Relationships: append(e.Details.(Character).Relationships, other.Details.(Character).Relationships...),
|
||||||
|
Description: fmt.Sprintf("%s%s", e.Details.(Character).Description, other.Details.(Character).Description),
|
||||||
|
Links: append(e.Details.(Character).Links, other.Details.(Character).Links...),
|
||||||
|
}
|
||||||
|
case EntityTypeGroup:
|
||||||
|
e.Details = Group{
|
||||||
|
Name: e.Details.(Group).Name,
|
||||||
|
OtherNames: append(e.Details.(Group).OtherNames, other.Details.(Group).OtherNames...),
|
||||||
|
Members: append(e.Details.(Group).Members, other.Details.(Group).Members...),
|
||||||
|
Relationships: append(e.Details.(Group).Relationships, other.Details.(Group).Relationships...),
|
||||||
|
Description: fmt.Sprintf("%s%s", e.Details.(Group).Description, other.Details.(Group).Description),
|
||||||
|
Links: append(e.Details.(Group).Links, other.Details.(Group).Links...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package entity_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"xyj-figures/pkg/entity"
|
||||||
|
"xyj-figures/pkg/relationship"
|
||||||
|
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
tcs := []struct {
|
||||||
|
name string
|
||||||
|
payload string
|
||||||
|
want entity.Entity
|
||||||
|
wanterr bool
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Character",
|
||||||
|
payload: `schema: v1
|
||||||
|
type: character
|
||||||
|
details:
|
||||||
|
name: 孙悟空
|
||||||
|
other_names:
|
||||||
|
- 孙行者
|
||||||
|
- 者行孙
|
||||||
|
relationships:
|
||||||
|
- source: 孙悟空
|
||||||
|
destination: 唐三藏
|
||||||
|
description: 师徒
|
||||||
|
bidirection: true
|
||||||
|
description: 孙悟空是明朝中后期畅销小说《西游记》当中个第一主角,是从石头里向蹦出来个猢狲。
|
||||||
|
links:
|
||||||
|
- https://zh.wikipedia.org/wiki/%E5%AD%99%E6%82%9F%E7%A9%BA
|
||||||
|
`,
|
||||||
|
want: entity.Entity{
|
||||||
|
Schema: "v1",
|
||||||
|
Type: entity.EntityTypeCharacter,
|
||||||
|
Details: entity.Character{
|
||||||
|
Name: "孙悟空",
|
||||||
|
OtherNames: []string{"孙行者", "者行孙"},
|
||||||
|
Relationships: []relationship.Relationship{
|
||||||
|
{
|
||||||
|
Src: "孙悟空",
|
||||||
|
Dst: "唐三藏",
|
||||||
|
Description: "师徒",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Description: "孙悟空是明朝中后期畅销小说《西游记》当中个第一主角,是从石头里向蹦出来个猢狲。",
|
||||||
|
Links: []string{
|
||||||
|
"https://zh.wikipedia.org/wiki/%E5%AD%99%E6%82%9F%E7%A9%BA",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wanterr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid Group",
|
||||||
|
payload: `schema: v1
|
||||||
|
type: group
|
||||||
|
details:
|
||||||
|
name: 唐僧师徒
|
||||||
|
other_names:
|
||||||
|
- 取经团队
|
||||||
|
members:
|
||||||
|
- 孙悟空
|
||||||
|
- 猪八戒
|
||||||
|
- 沙僧
|
||||||
|
- 唐僧
|
||||||
|
- 白龙马
|
||||||
|
links:
|
||||||
|
- https://zh.wikipedia.org/wiki/%E8%A5%BF%E6%B8%B8%E8%AE%B0%E8%A7%92%E8%89%B2%E5%88%97%E8%A1%A8#%E5%94%90%E5%83%A7%E5%B8%AB%E5%BE%92
|
||||||
|
`,
|
||||||
|
want: entity.Entity{
|
||||||
|
Schema: "v1",
|
||||||
|
Type: entity.EntityTypeGroup,
|
||||||
|
Details: entity.Group{
|
||||||
|
Name: "唐僧师徒",
|
||||||
|
OtherNames: []string{"取经团队"},
|
||||||
|
Members: []string{"孙悟空", "猪八戒", "沙僧", "唐僧", "白龙马"},
|
||||||
|
Links: []string{
|
||||||
|
"https://zh.wikipedia.org/wiki/%E8%A5%BF%E6%B8%B8%E8%AE%B0%E8%A7%92%E8%89%B2%E5%88%97%E8%A1%A8#%E5%94%90%E5%83%A7%E5%B8%AB%E5%BE%92",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wanterr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Prepare data
|
||||||
|
tmpFile, err := os.Create(filepath.Join(tmpDir, tc.name))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temporary file: %v", err)
|
||||||
|
}
|
||||||
|
defer tmpFile.Close()
|
||||||
|
|
||||||
|
if _, err = tmpFile.Write([]byte(tc.payload)); err != nil {
|
||||||
|
t.Fatalf("Failed to write to temporary file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the function and assert the results
|
||||||
|
e, err := entity.Load(tmpFile.Name())
|
||||||
|
if err != nil && !tc.wanterr {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if err == nil && tc.wanterr {
|
||||||
|
t.Errorf("expected error but not get")
|
||||||
|
}
|
||||||
|
// Sanity Check
|
||||||
|
if !reflect.DeepEqual(*e, tc.want) {
|
||||||
|
log.Fatalf("Load got %+v want %+v, diff %+v", *e, tc.want, deep.Equal(*e, tc.want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import r "xyj-figures/pkg/relationship"
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
Name string
|
||||||
|
OtherNames []string `yaml:"other_names,flow"`
|
||||||
|
Members []string `yaml:"members,flow"`
|
||||||
|
Relationships []r.Relationship `yaml:"relationships,flow"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Links []string `yaml:"links,flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Group) GetRelationships() []r.Relationship {
|
||||||
|
return g.Relationships
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package relationship
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Relationship struct {
|
||||||
|
Src string `yaml:"source"`
|
||||||
|
Dst string `yaml:"destination"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Bidirection bool `yaml:"bidirection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Relationship) IsSame(other Relationship) bool {
|
||||||
|
if r.Bidirection {
|
||||||
|
if r.Description != other.Description {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (r.Src == other.Src && r.Dst == other.Dst) || (r.Src == other.Dst && r.Dst == other.Src)
|
||||||
|
}
|
||||||
|
return r.Src == other.Src && r.Dst == other.Dst && r.Description == other.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Relationship) GetKey() (string, string) {
|
||||||
|
return fmt.Sprintf("%s-%s", r.Src, r.Dst), fmt.Sprintf("%s-%s", r.Dst, r.Src)
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package relationship_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
r "xyj-figures/pkg/relationship"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRelationship_IsSame(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
r r.Relationship
|
||||||
|
other r.Relationship
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Bidirectional relationships with same source and destination",
|
||||||
|
r: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
other: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bidirectional relationships with swapped source and destination",
|
||||||
|
r: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
other: r.Relationship{
|
||||||
|
Src: "B",
|
||||||
|
Dst: "A",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bidirectional relationships with different descriptions",
|
||||||
|
r: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
other: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "enemy",
|
||||||
|
Bidirection: true,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unidirectional relationships with same source and destination",
|
||||||
|
r: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: false,
|
||||||
|
},
|
||||||
|
other: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: false,
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unidirectional relationships with swapped source and destination",
|
||||||
|
r: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: false,
|
||||||
|
},
|
||||||
|
other: r.Relationship{
|
||||||
|
Src: "B",
|
||||||
|
Dst: "A",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: false,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unidirectional relationships with different descriptions",
|
||||||
|
r: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "friend",
|
||||||
|
Bidirection: false,
|
||||||
|
},
|
||||||
|
other: r.Relationship{
|
||||||
|
Src: "A",
|
||||||
|
Dst: "B",
|
||||||
|
Description: "enemy",
|
||||||
|
Bidirection: false,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.expected, tt.r.IsSame(tt.other))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user