commit 45bc30b2f186272eb592a3af15339187b89fb4ba Author: ArtMares Date: Wed Jan 10 02:32:42 2024 +0300 feat: try to write orm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..967689f --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/d3/main.go b/d3/main.go new file mode 100644 index 0000000..2829d68 --- /dev/null +++ b/d3/main.go @@ -0,0 +1,10 @@ +package main + +import "flag" + +var debug = flag.Bool("debug", false, "dont delete temporary files") + +func main() { + flag.Parse() + +} diff --git a/d3/parser/mod.go b/d3/parser/mod.go new file mode 100644 index 0000000..cd23b07 --- /dev/null +++ b/d3/parser/mod.go @@ -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 "" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b64849b --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5bddba9 --- /dev/null +++ b/go.sum @@ -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= diff --git a/orm/entity/meta.go b/orm/entity/meta.go new file mode 100644 index 0000000..0999863 --- /dev/null +++ b/orm/entity/meta.go @@ -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 +} diff --git a/orm/entity/name.go b/orm/entity/name.go new file mode 100644 index 0000000..7e00c84 --- /dev/null +++ b/orm/entity/name.go @@ -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()), "/")) +} diff --git a/orm/entity/relation.go b/orm/entity/relation.go new file mode 100644 index 0000000..9d2b8ed --- /dev/null +++ b/orm/entity/relation.go @@ -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) +} diff --git a/orm/entity/tag.go b/orm/entity/tag.go new file mode 100644 index 0000000..7c7c511 --- /dev/null +++ b/orm/entity/tag.go @@ -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 +} diff --git a/orm/entity/tag_test.go b/orm/entity/tag_test.go new file mode 100644 index 0000000..696dc33 --- /dev/null +++ b/orm/entity/tag_test.go @@ -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:"`, + &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: ",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:,many_to_one:,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) + }) + } +}