feat: try to write orm

This commit is contained in:
ArtMares 2024-01-10 02:32:42 +03:00
commit 45bc30b2f1
10 changed files with 663 additions and 0 deletions

156
.gitignore vendored Normal file
View File

@ -0,0 +1,156 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Example user template template
### Example user template
# IntelliJ project files
.idea
*.iml
out
gen
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

10
d3/main.go Normal file
View File

@ -0,0 +1,10 @@
package main
import "flag"
var debug = flag.Bool("debug", false, "dont delete temporary files")
func main() {
flag.Parse()
}

46
d3/parser/mod.go Normal file
View File

@ -0,0 +1,46 @@
package parser
import (
"bytes"
"strconv"
)
var (
doubleSlash = []byte("//")
moduleStr = []byte("module")
)
func ModulePath(mod []byte) string {
for len(mod) > 0 {
line := mod
mod = nil
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, mod = line[:i], line[i+1:]
}
if i := bytes.Index(line, doubleSlash); i >= 0 {
line = line[:i]
}
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, moduleStr) {
continue
}
line = line[len(moduleStr):]
n := len(line)
line = bytes.TrimSpace(line)
if len(line) == n || len(line) == 0 {
continue
}
if line[0] == '"' || line[0] == '`' {
p, err := strconv.Unquote(string(line))
if err != nil {
return ""
}
return p
}
return string(line)
}
return ""
}

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module git.influ.su/artmares/d3orm
go 1.20
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

17
go.sum Normal file
View File

@ -0,0 +1,17 @@
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=

58
orm/entity/meta.go Normal file
View File

@ -0,0 +1,58 @@
package entity
import "reflect"
type PkStrategy int
const (
_ PkStrategy = iota
Auto
Manual
)
type (
Index struct {
Name string
Columns []string
Unique bool
}
InternalTools struct {
ExtractField FieldExtractor
SetFieldValue FieldSetter
NewInstance Instantiator
Copy Copier
CompareField FieldComparator
}
FieldExtractor func(e any, name string) (any, error)
FieldSetter func(e any, name string, value any) error
Instantiator func() any
Copier func(src any, dst any)
FieldComparator func(e1, e2 any, name string) bool
)
type D3Entity interface {
D3Token() MetaToken
}
type MetaToken struct {
Tools InternalTools
Tpl any
TableName string
Indexes []Index
}
type MetaInfo struct {
Tpl any
EntityName Name
TableName string
Indexes []Index
Relations map[string]Relation
}
type FieldInfo struct {
Name string
DBAlias string
FullDBAlias string
AssociatedType reflect.Type
}

48
orm/entity/name.go Normal file
View File

@ -0,0 +1,48 @@
package entity
import (
"fmt"
"reflect"
"strings"
)
type Name string
func NameFromEntity(e any) Name {
t := reflect.TypeOf(e)
switch t.Kind() {
case reflect.Ptr:
return Name(fmt.Sprintf("%s/%s", t.Elem().PkgPath(), t.Elem().Name()))
default:
return Name(fmt.Sprintf("%s/%s", t.PkgPath(), t.Name()))
}
}
func nameFromTag(tag string, parentName Name) Name {
defined := Name(tag)
if defined.IsShort() {
return parentName.Combine(defined)
}
return defined
}
func (n Name) Short() string {
path := strings.Split(string(n), "/")
return path[len(path)-1]
}
func (n Name) IsShort() bool {
return !strings.Contains(string(n), "/")
}
func (n Name) Equal(name Name) bool {
return n == name
}
func (n Name) Combine(entity Name) Name {
path := strings.Split(string(n), "/")
return Name(strings.Join(append(path[:len(path)-1], entity.Short()), "/"))
}

52
orm/entity/relation.go Normal file
View File

@ -0,0 +1,52 @@
package entity
type DeleteStrategy int
const (
_ DeleteStrategy = iota
None
Cascade
Nullable
)
func deleteStrategyFromAlias(alias string) DeleteStrategy {
switch alias {
case "cascade":
return Cascade
case "nullable":
return Nullable
default:
return None
}
}
type RelationType int
const (
_ RelationType = iota
Lazy
Eager
SmartLazy
)
func relationTypeFromAlias(alias string) RelationType {
switch alias {
case "lazy":
return Lazy
case "eager":
return Eager
default:
return Lazy
}
}
type Relation interface {
Type() RelationType
DeleteStrategy() DeleteStrategy
RelationWith() Name
Field() *FieldInfo
setField(f *FieldInfo)
fillFromTag(tag *parsedTag, parent *MetaInfo)
}

138
orm/entity/tag.go Normal file
View File

@ -0,0 +1,138 @@
package entity
import (
"reflect"
"strings"
)
type property struct {
name string
value string
subProperty map[string]property
}
func (p *property) getSubPropVal(name string) string {
prop, ok := p.subProperty[name]
if !ok {
return ""
}
return prop.value
}
type parsedTag struct {
properties map[string]property
}
func (p *parsedTag) hasProperty(name string) bool {
_, ok := p.properties[name]
return ok
}
func (p *parsedTag) getProperty(name string) (property, bool) {
prop, ok := p.properties[name]
return prop, ok
}
// one_to_one:"target_entity:Address, join_column:address_id, reference:id"
func parseTag(tag reflect.StructTag) *parsedTag {
result := &parsedTag{
properties: make(map[string]property),
}
tagVal, ok := tag.Lookup("d3")
if !ok {
return result
}
for name, value := range extractKVFromTag(tagVal) {
d3Property := property{
name: name,
value: value,
subProperty: make(map[string]property),
}
for subName, subValue := range extractKVFromValue(value) {
d3Property.subProperty[subName] = property{
name: subName,
value: subValue,
subProperty: make(map[string]property),
}
}
result.properties[name] = d3Property
}
return result
}
func extractKVFromTag(tag string) map[string]string {
result := make(map[string]string)
var i int
for tag != "" {
for i < len(tag) && tag[i] != ':' {
i++
}
if i >= len(tag) {
break
}
name := strings.Trim(tag[:i], " :,")
tag = tag[i+1:]
i = 0
for i < len(tag) && tag[i] != '<' {
i++
}
if i >= len(tag) {
result[name] = strings.Trim(tag[:i], "\" ,")
break
}
tag = tag[i+1:]
i = 0
for i < len(tag) && tag[i] != '>' {
i++
}
result[name] = strings.Trim(tag[:i], "\" ,")
if i >= len(tag) {
break
}
tag = tag[i+1:]
i = 0
}
return result
}
func extractKVFromValue(tag string) map[string]string {
result := make(map[string]string)
var i int
for tag != "" {
for i < len(tag) && tag[i] != ':' {
i++
}
if i >= len(tag) {
break
}
name := strings.Trim(tag[:i], " :")
tag = tag[i+1:]
i = 0
for i < len(tag) && tag[i] != ',' {
i++
}
result[name] = strings.Trim(tag[:i], " ")
if i >= len(tag) {
break
}
tag = tag[i+1:]
i = 0
}
return result
}

127
orm/entity/tag_test.go Normal file
View File

@ -0,0 +1,127 @@
package entity
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func TestTagParsing(t *testing.T) {
tests := []struct {
name string
arg string
expect *parsedTag
}{
{
"One Tag",
`d3:"one_to_one:<target_entity:Address, join_column:address_id, reference:id>"`,
&parsedTag{
properties: map[string]property{
"one_to_one": {
name: "one_to_one",
value: "target_entity:Address, join_column:address_id, reference:id",
subProperty: map[string]property{
"target_entity": {
name: "target_entity",
value: "Address",
subProperty: map[string]property{},
},
"join_column": {
name: "join_column",
value: "address_id",
subProperty: map[string]property{},
},
"reference": {
name: "reference",
value: "id",
subProperty: map[string]property{},
},
},
},
},
},
},
{
"Two Tags With Unknown",
`d3:"one_to_one: <target_entity:Address , join_column: address_id , reference: id> ",unknown_tag:"key1:val1,key2:val2"`,
&parsedTag{
properties: map[string]property{
"one_to_one": {
name: "one_to_one",
value: "target_entity:Address , join_column: address_id , reference: id",
subProperty: map[string]property{
"target_entity": {
name: "target_entity",
value: "Address",
subProperty: map[string]property{},
},
"join_column": {
name: "join_column",
value: "address_id",
subProperty: map[string]property{},
},
"reference": {
name: "reference",
value: "id",
subProperty: map[string]property{},
},
},
},
},
},
},
{
"Tree Tags",
`d3:"one_to_one:<target_entity:Address,join_column:address_id,reference:id>,many_to_one:<target_entity:User>,type:lazy"`,
&parsedTag{
properties: map[string]property{
"one_to_one": {
name: "one_to_one",
value: "target_entity:Address,join_column:address_id,reference:id",
subProperty: map[string]property{
"target_entity": {
name: "target_entity",
value: "Address",
subProperty: map[string]property{},
},
"join_column": {
name: "join_column",
value: "address_id",
subProperty: map[string]property{},
},
"reference": {
name: "reference",
value: "id",
subProperty: map[string]property{},
},
},
},
"many_to_one": {
name: "many_to_one",
value: "target_entity:User",
subProperty: map[string]property{
"target_entity": {
name: "target_entity",
value: "User",
subProperty: map[string]property{},
},
},
},
"type": {
name: "type",
value: "lazy",
subProperty: map[string]property{},
},
},
},
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
result := parseTag(reflect.StructTag(testCase.arg))
require.Equal(t, testCase.expect, result)
})
}
}