snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
855
.all-contributorsrc
Normal file
855
.all-contributorsrc
Normal file
@@ -0,0 +1,855 @@
|
||||
{
|
||||
"files": [
|
||||
"doc/md/contributors.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "a8m",
|
||||
"name": "Ariel Mashraki",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7413593?v=4",
|
||||
"profile": "https://github.com/a8m",
|
||||
"contributions": [
|
||||
"maintenance",
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alexsn",
|
||||
"name": "Alex Snast",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/987019?v=4",
|
||||
"profile": "https://github.com/alexsn",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rotemtam",
|
||||
"name": "Rotem Tamir",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1522681?v=4",
|
||||
"profile": "https://rotemtam.com/",
|
||||
"contributions": [
|
||||
"maintenance",
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cliedeman",
|
||||
"name": "Ciaran Liedeman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3578740?v=4",
|
||||
"profile": "https://github.com/cliedeman",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "marwan-at-work",
|
||||
"name": "Marwan Sulaiman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16294261?v=4",
|
||||
"profile": "https://www.marwan.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "napei",
|
||||
"name": "Nathaniel Peiffer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8946502?v=4",
|
||||
"profile": "https://nathaniel.peiffer.com.au/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tmc",
|
||||
"name": "Travis Cline",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3977?v=4",
|
||||
"profile": "https://github.com/tmc",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hantmac",
|
||||
"name": "Jeremy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7600925?v=4",
|
||||
"profile": "https://cloudsjhan.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aca",
|
||||
"name": "aca",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/50316549?v=4",
|
||||
"profile": "https://github.com/aca",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "BrentChesny",
|
||||
"name": "BrentChesny",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1449435?v=4",
|
||||
"profile": "https://github.com/BrentChesny",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "giautm",
|
||||
"name": "Giau. Tran Minh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12751435?v=4",
|
||||
"profile": "https://github.com/giautm",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "htdvisser",
|
||||
"name": "Hylke Visser",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/181308?v=4",
|
||||
"profile": "https://htdvisser.dev/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kerbelp",
|
||||
"name": "Pavel Kerbel",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3934990?v=4",
|
||||
"profile": "https://github.com/kerbelp",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "day-dreams",
|
||||
"name": "zhangnan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24593904?v=4",
|
||||
"profile": "https://github.com/day-dreams",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "uta-mori",
|
||||
"name": "mori yuta",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59682979?v=4",
|
||||
"profile": "https://github.com/uta-mori",
|
||||
"contributions": [
|
||||
"code",
|
||||
"translation",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chris-rock",
|
||||
"name": "Christoph Hartmann",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1178413?v=4",
|
||||
"profile": "http://lollyrock.com/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rubensayshi",
|
||||
"name": "Ruben de Vries",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/649160?v=4",
|
||||
"profile": "https://github.com/rubensayshi",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ernado",
|
||||
"name": "Aleksandr Razumov",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/866677?v=4",
|
||||
"profile": "https://keybase.io/ernado",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "apbuteau",
|
||||
"name": "apbuteau",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6796073?v=4",
|
||||
"profile": "https://github.com/apbuteau",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ichord",
|
||||
"name": "Harold.Luo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1324791?v=4",
|
||||
"profile": "https://github.com/ichord",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "idoshveki",
|
||||
"name": "ido shveki",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11615669?v=4",
|
||||
"profile": "https://github.com/idoshveki",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "masseelch",
|
||||
"name": "MasseElch",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12862103?v=4",
|
||||
"profile": "https://github.com/masseelch",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kidlj",
|
||||
"name": "Jian Li",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/300616?v=4",
|
||||
"profile": "https://github.com/kidlj",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nolotz",
|
||||
"name": "Noah-Jerome Lotzer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5778728?v=4",
|
||||
"profile": "https://noah.je/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "danf0rth",
|
||||
"name": "danforth",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/14220891?v=4",
|
||||
"profile": "https://github.com/danf0rth",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "maxiloEmmmm",
|
||||
"name": "maxilozoz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16779121?v=4",
|
||||
"profile": "https://github.com/maxiloEmmmm",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zzwx",
|
||||
"name": "zzwx",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8169082?v=4",
|
||||
"profile": "https://gist.github.com/zzwx",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ix64",
|
||||
"name": "MengYX",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13902388?v=4",
|
||||
"profile": "https://github.com/ix64",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mattn",
|
||||
"name": "mattn",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10111?v=4",
|
||||
"profile": "https://mattn.kaoriya.net/",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Bladrak",
|
||||
"name": "Hugo Briand",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1321977?v=4",
|
||||
"profile": "https://github.com/Bladrak",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "enmand",
|
||||
"name": "Dan Enman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/432487?v=4",
|
||||
"profile": "https://danielenman.com/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "UnAfraid",
|
||||
"name": "Rumen Nikiforov",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2185291?v=4",
|
||||
"profile": "http://www.l2junity.org/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wenerme",
|
||||
"name": "陈杨文",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1777211?v=4",
|
||||
"profile": "https://wener.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joesonw",
|
||||
"name": "Qiaosen (Joeson) Huang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1635441?v=4",
|
||||
"profile": "https://djwong.net",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "davebehr1",
|
||||
"name": "AlonDavidBehr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16716239?v=4",
|
||||
"profile": "https://github.com/davebehr1",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "DuGlaser",
|
||||
"name": "DuGlaser",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/50506482?v=4",
|
||||
"profile": "http://duglaser.dev",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "shanna",
|
||||
"name": "Shane Hanna",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28489?v=4",
|
||||
"profile": "https://github.com/shanna",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mahmud2011",
|
||||
"name": "Mahmudul Haque",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5278142?v=4",
|
||||
"profile": "https://www.linkedin.com/in/mahmud2011",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sywesk",
|
||||
"name": "Benjamin Bourgeais",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/862607?v=4",
|
||||
"profile": "http://blog.scaleprocess.net",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "8ayac",
|
||||
"name": "8ayac(Yoshinori Hayashi)",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/29266382?v=4",
|
||||
"profile": "https://about.8ay.ac/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "y-yagi",
|
||||
"name": "y-yagi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/987638?v=4",
|
||||
"profile": "https://github.com/y-yagi",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Sacro",
|
||||
"name": "Ben Woodward",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2659869?v=4",
|
||||
"profile": "https://github.com/Sacro",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wzyjerry",
|
||||
"name": "WzyJerry",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11435169?v=4",
|
||||
"profile": "https://github.com/wzyjerry",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tarrencev",
|
||||
"name": "Tarrence van As",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4740651?v=4",
|
||||
"profile": "https://github.com/tarrencev",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MONAKA0721",
|
||||
"name": "Yuya Sumie",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32859963?v=4",
|
||||
"profile": "https://mo7ka.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "akfaew",
|
||||
"name": "Michal Mazurek",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7853732?v=4",
|
||||
"profile": "http://jasminek.net",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nmemoto",
|
||||
"name": "Takafumi Umemoto",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1522332?v=4",
|
||||
"profile": "https://github.com/nmemoto",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "squarebat",
|
||||
"name": "Khadija Sidhpuri",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59063821?v=4",
|
||||
"profile": "http://www.linkedin.com/in/khadija-sidhpuri-87709316a",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "neel229",
|
||||
"name": "Neel Modi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53475167?v=4",
|
||||
"profile": "https://github.com/neel229",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "shomodj",
|
||||
"name": "Boris Shomodjvarac",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/304768?v=4",
|
||||
"profile": "https://ie.linkedin.com/in/boris-shomodjvarac-51970879",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sadmansakib",
|
||||
"name": "Sadman Sakib",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17023844?v=4",
|
||||
"profile": "https://github.com/sadmansakib",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dakimura",
|
||||
"name": "dakimura",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/34202807?v=4",
|
||||
"profile": "https://github.com/dakimura",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RiskyFeryansyahP",
|
||||
"name": "Risky Feryansyah",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/36788585?v=4",
|
||||
"profile": "https://github.com/RiskyFeryansyahP",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "seiichi1101",
|
||||
"name": "seiichi ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20941952?v=4",
|
||||
"profile": "https://github.com/seiichi1101",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "odeke-em",
|
||||
"name": "Emmanuel T Odeke",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4898263?v=4",
|
||||
"profile": "https://orijtech.com/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "isoppp",
|
||||
"name": "Hiroki Isogai",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16318727?v=4",
|
||||
"profile": "https://isoppp.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tsingsun",
|
||||
"name": "李清山",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5848549?v=4",
|
||||
"profile": "https://github.com/tsingsun",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "s-takehana",
|
||||
"name": "s-takehana",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3423547?v=4",
|
||||
"profile": "https://github.com/s-takehana",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EndlessIdea",
|
||||
"name": "Kuiba",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1527796?v=4",
|
||||
"profile": "https://github.com/EndlessIdea",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "storyicon",
|
||||
"name": "storyicon",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/29772821?v=4",
|
||||
"profile": "https://github.com/storyicon",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "evanlurvey",
|
||||
"name": "Evan Lurvey",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/54965655?v=4",
|
||||
"profile": "https://github.com/evanlurvey",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "attackordie",
|
||||
"name": "Brian",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20145334?v=4",
|
||||
"profile": "https://github.com/attackordie",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ThinkontrolSY",
|
||||
"name": "Shen Yang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11331554?v=4",
|
||||
"profile": "http://www.thinkontrol.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sivchari",
|
||||
"name": "sivchari",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55221074?v=4",
|
||||
"profile": "https://twitter.com/sivchari",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mookjp",
|
||||
"name": "mook",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1519309?v=4",
|
||||
"profile": "https://blog.mookjp.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "heliumbrain",
|
||||
"name": "heliumbrain",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1607668?v=4",
|
||||
"profile": "http://www.entiros.se",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JeremyV2014",
|
||||
"name": "Jeremy Maxey-Vesperman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9276415?v=4",
|
||||
"profile": "https://github.com/JeremyV2014",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tankbusta",
|
||||
"name": "Christopher Schmitt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/592749?v=4",
|
||||
"profile": "https://github.com/tankbusta",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "grevych",
|
||||
"name": "Gerardo Reyes",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3792003?v=4",
|
||||
"profile": "https://github.com/grevych",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "naormatania",
|
||||
"name": "Naor Matania",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6978437?v=4",
|
||||
"profile": "https://github.com/naormatania",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "idc77",
|
||||
"name": "idc77",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/87644834?v=4",
|
||||
"profile": "https://github.com/idc77",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "HurSungYun",
|
||||
"name": "Sungyun Hur",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8033896?v=4",
|
||||
"profile": "http://ethanhur.me",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "peanut-cc",
|
||||
"name": "peanut-pg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55480838?v=4",
|
||||
"profile": "https://github.com/peanut-cc",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "m3hm3t",
|
||||
"name": "Mehmet Yılmaz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/22320354?v=4",
|
||||
"profile": "https://github.com/m3hm3t",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Laconty",
|
||||
"name": "Roman Maklakov",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17760166?v=4",
|
||||
"profile": "https://github.com/Laconty",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "genevieve",
|
||||
"name": "Genevieve",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12158641?v=4",
|
||||
"profile": "https://github.com/genevieve",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cjraa",
|
||||
"name": "Clarence",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/62199269?v=4",
|
||||
"profile": "https://github.com/cjraa",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "iamnande",
|
||||
"name": "Nicholas Anderson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7806510?v=4",
|
||||
"profile": "https://www.linkedin.com/in/iamnande/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hezhizhen",
|
||||
"name": "Zhizhen He",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7611700?v=4",
|
||||
"profile": "https://github.com/hezhizhen",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "crossworth",
|
||||
"name": "Pedro Henrique",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1251151?v=4",
|
||||
"profile": "https://pedro.dev.br",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MrParano1d",
|
||||
"name": "MrParano1d",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7414374?v=4",
|
||||
"profile": "https://2jp.de",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tprebs",
|
||||
"name": "Thomas Prebble",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6523587?v=4",
|
||||
"profile": "https://github.com/tprebs",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "imhuytq",
|
||||
"name": "Huy TQ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5723282?v=4",
|
||||
"profile": "https://huytq.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "maorlipchuk",
|
||||
"name": "maorlipchuk",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7034637?v=4",
|
||||
"profile": "https://github.com/maorlipchuk",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "iwata",
|
||||
"name": "Motonori Iwata",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/121048?v=4",
|
||||
"profile": "https://mobcov.hatenadiary.org/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "CharlesGe129",
|
||||
"name": "Charles Ge",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20162173?v=4",
|
||||
"profile": "https://github.com/CharlesGe129",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "thmeitz",
|
||||
"name": "Thomas Meitz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/92851940?v=4",
|
||||
"profile": "https://github.com/thmeitz",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "booleangate",
|
||||
"name": "Justin Johnson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/181567?v=4",
|
||||
"profile": "http://justinjohnson.org",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hax10",
|
||||
"name": "hax10",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/85743468?v=4",
|
||||
"profile": "https://github.com/hax10",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "water-a",
|
||||
"name": "water-a",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/38114545?v=4",
|
||||
"profile": "https://github.com/water-a",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jhwz",
|
||||
"name": "jhwz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52683873?v=4",
|
||||
"profile": "https://github.com/jhwz",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kortschak",
|
||||
"name": "Dan Kortschak",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/275221?v=4",
|
||||
"profile": "https://kortschak.io/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "ent",
|
||||
"projectOwner": "ent",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
}
|
||||
19
.devcontainer/devcontainer-lock.json
Normal file
19
.devcontainer/devcontainer-lock.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/copilot-cli:1": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "ghcr.io/devcontainers/features/copilot-cli@sha256:757c6c2899dc902c44a9f6164c1f7392832ced13c6bb632d8d60a880f2e92456",
|
||||
"integrity": "sha256:757c6c2899dc902c44a9f6164c1f7392832ced13c6bb632d8d60a880f2e92456"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"version": "2.17.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c",
|
||||
"integrity": "sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
|
||||
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
|
||||
}
|
||||
}
|
||||
}
|
||||
37
.devcontainer/devcontainer.json
Normal file
37
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,37 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/go .
|
||||
{
|
||||
"name": "Go",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/go:2-1.26",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/copilot-cli:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"dockerDashComposeVersion": "latest",
|
||||
"installDockerBuildx": true,
|
||||
"moby": false,
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"installDirectlyFromGitHubRelease": true,
|
||||
"version": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "go version",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
58
.github/ISSUE_TEMPLATE/1.bug.md
vendored
Normal file
58
.github/ISSUE_TEMPLATE/1.bug.md
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: Bug report 🐛
|
||||
about: Create a bug report.
|
||||
labels: 'status: needs triage'
|
||||
---
|
||||
|
||||
<!-- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
<!--
|
||||
Thank you very much for contributing to Ent by creating an issue! ❤️
|
||||
To avoid duplicate issues we ask you to check off the following list.
|
||||
-->
|
||||
|
||||
<!-- Checked checkbox should look like this: [x] -->
|
||||
|
||||
- [ ] The issue is present in the latest release.
|
||||
- [ ] I have searched the [issues](https://github.com/ent/ent/issues) of this repository and believe that this is not a duplicate.
|
||||
|
||||
## Current Behavior 😯
|
||||
|
||||
<!-- Describe what happens instead of the expected behavior. -->
|
||||
|
||||
## Expected Behavior 🤔
|
||||
|
||||
<!-- Describe what should happen. -->
|
||||
|
||||
## Steps to Reproduce 🕹
|
||||
|
||||
<!--
|
||||
Are you able to reproduce this bug? If so, follow the steps in the ent/bug repository: https://github.com/ent/bug
|
||||
After you are done, paste the link to your new repository here (make sure it's public)!
|
||||
(In this case, you can disregard filling out the steps below)
|
||||
-->
|
||||
|
||||
|
||||
<!--
|
||||
TODO: reproduction repo
|
||||
-->
|
||||
|
||||
Steps:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Your Environment 🌎
|
||||
|
||||
<!--
|
||||
Include as many relevant details about the environment with which you experienced the bug.
|
||||
-->
|
||||
|
||||
| Tech | Version |
|
||||
| ----------- | ------- |
|
||||
| Go | 1.17.? |
|
||||
| Ent | 0.9.? |
|
||||
| Database | MySQL |
|
||||
| Driver | https://github.com/go-sql-driver/mysql |
|
||||
27
.github/ISSUE_TEMPLATE/2.feature.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/2.feature.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Feature request 🎉
|
||||
about: Suggest a new idea for the project.
|
||||
labels: 'status: needs triage'
|
||||
---
|
||||
|
||||
<!-- Provide a general summary of the feature in the Title above -->
|
||||
|
||||
<!--
|
||||
Thank you very much for contributing to Ent by creating an issue! ❤️
|
||||
To avoid duplicate issues we ask you to check off the following list.
|
||||
-->
|
||||
|
||||
<!-- Checked checkbox should look like this: [x] -->
|
||||
|
||||
- [ ] I have searched the [issues](https://github.com/ent/ent/issues) of this repository and believe that this is not a duplicate.
|
||||
|
||||
## Summary 💡
|
||||
|
||||
<!-- Describe how it should work. -->
|
||||
|
||||
## Motivation 🔦
|
||||
|
||||
<!--
|
||||
What are you trying to accomplish? How has the lack of this feature affected you?
|
||||
Providing context helps us come up with a solution that is most useful in the real world.
|
||||
-->
|
||||
5
.github/ISSUE_TEMPLATE/3.support.md
vendored
Normal file
5
.github/ISSUE_TEMPLATE/3.support.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: Question
|
||||
about: General support
|
||||
labels: 'status: needs triage'
|
||||
---
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false # force the usage of a template
|
||||
contact_links:
|
||||
- name: Something Else ❔
|
||||
url: https://gophers.slack.com/archives/C01FMSQDT53
|
||||
about: Come chat to us in the gophers slack
|
||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
57
.github/workflows/atlas-ci-public.yaml
vendored
Normal file
57
.github/workflows/atlas-ci-public.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Atlas CI Public
|
||||
on:
|
||||
# Run whenever code is changed in the master branch,
|
||||
# change this to your root branch.
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'examples/migration/ent/migrate/migrations/*'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'examples/migration/ent/migrate/migrations/*'
|
||||
jobs:
|
||||
sync:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
needs: lint
|
||||
if: github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ariga/atlas-sync-action@v0
|
||||
with:
|
||||
dir: 'examples/migration/ent/migrate/migrations'
|
||||
driver: mysql # or: postgres | sqlite
|
||||
cloud-public: true
|
||||
lint:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
services:
|
||||
# Spin up a mysql:8.0.29 container to be used as the dev-database for analysis.
|
||||
mysql:
|
||||
image: mysql:8.0.29
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
MYSQL_DATABASE: dev
|
||||
ports:
|
||||
- "3306:3306"
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Mandatory unless "latest" is set below.
|
||||
- uses: ariga/atlas-action@v0
|
||||
with:
|
||||
dir: 'examples/migration/ent/migrate/migrations'
|
||||
dev-url: mysql://root:pass@localhost:3306/dev
|
||||
cloud-public: true
|
||||
501
.github/workflows/ci.yml
vendored
Normal file
501
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,501 @@
|
||||
name: Continuous Integration
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
tags-ignore:
|
||||
- '*.*'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: './go.mod'
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
args: --verbose
|
||||
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ['1.23', '1.24']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run cmd tests
|
||||
run: go test -race ./...
|
||||
working-directory: cmd
|
||||
- name: Run dialect tests
|
||||
run: go test -race ./...
|
||||
working-directory: dialect
|
||||
- name: Run schema tests
|
||||
run: go test -race ./...
|
||||
working-directory: schema
|
||||
- name: Run loader tests
|
||||
run: go test -race ./...
|
||||
working-directory: entc/load
|
||||
- name: Run codegen tests
|
||||
run: go test -race ./...
|
||||
working-directory: entc/gen
|
||||
- name: Run example tests
|
||||
working-directory: examples
|
||||
run: go test -race ./...
|
||||
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: './go.mod'
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run go generate
|
||||
run: go generate ./... && go mod tidy
|
||||
- name: Run go generate on examples directory
|
||||
working-directory: examples
|
||||
run: go generate ./... && go mod tidy
|
||||
- name: Run go generate on integration directory
|
||||
working-directory: entc/integration
|
||||
run: go generate ./... && go mod tidy
|
||||
- name: Check generated files
|
||||
run: |
|
||||
status=$(git status --porcelain)
|
||||
if [ -n "$status" ]; then
|
||||
echo "you need to run 'go generate ./...' in root and 'examples' dirs and commit the changes"
|
||||
echo "$status"
|
||||
git --no-pager diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql56:
|
||||
image: mysql:5.6.35
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
mysql57:
|
||||
image: mysql:5.7.26
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 3307:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
mysql8:
|
||||
image: mysql:8
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 3308:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
maria:
|
||||
image: mariadb:10.4 # Temporary to unblock PRs from failing.
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 4306:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
maria102:
|
||||
image: mariadb:10.2.32
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 4307:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
maria103:
|
||||
image: mariadb:10.3.13
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 4308:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
postgres10:
|
||||
image: postgres:10
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5430:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres11:
|
||||
image: postgres:11
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5431:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres12:
|
||||
image: postgres:12.3
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres13:
|
||||
image: postgres:13.1
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5433:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres14:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5434:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres15:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5435:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres16:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5436:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres17:
|
||||
image: postgres:17
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5437:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
gremlin-server:
|
||||
image: entgo/gremlin-server
|
||||
ports:
|
||||
- 8182:8182
|
||||
options: >-
|
||||
--health-cmd "netstat -an | grep -q 8182"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: './go.mod'
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run integration tests
|
||||
working-directory: entc/integration
|
||||
run: go test -race -count=2 ./...
|
||||
|
||||
migration:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.ref != 'refs/heads/master' }}
|
||||
services:
|
||||
mysql56:
|
||||
image: mysql:5.6.35
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
mysql57:
|
||||
image: mysql:5.7.26
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 3307:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
mysql8:
|
||||
image: mysql:8
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 3308:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
maria:
|
||||
image: mariadb:10.4 # Temporary to unblock PRs from failing.
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 4306:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
maria102:
|
||||
image: mariadb:10.2.32
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 4307:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
maria103:
|
||||
image: mariadb:10.3.13
|
||||
env:
|
||||
MYSQL_DATABASE: test
|
||||
MYSQL_ROOT_PASSWORD: pass
|
||||
ports:
|
||||
- 4308:3306
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping -ppass"
|
||||
--health-interval 10s
|
||||
--health-start-period 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
postgres10:
|
||||
image: postgres:10
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5430:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres11:
|
||||
image: postgres:11
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5431:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres12:
|
||||
image: postgres:12.3
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres13:
|
||||
image: postgres:13.1
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5433:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres14:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5434:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres15:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5435:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres16:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5436:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
postgres17:
|
||||
image: postgres:17
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5437:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
gremlin-server:
|
||||
image: entgo/gremlin-server
|
||||
ports:
|
||||
- 8182:8182
|
||||
options: >-
|
||||
--health-cmd "netstat -an | grep -q 8182"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: './go.mod'
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Checkout origin/master
|
||||
run: git checkout origin/master
|
||||
- name: Run integration on origin/master
|
||||
working-directory: entc/integration
|
||||
run: go test -race -count=2 ./...
|
||||
- name: Checkout previous HEAD
|
||||
run: git checkout -
|
||||
- name: Run integration on HEAD
|
||||
working-directory: entc/integration
|
||||
run: go test -race -count=2 ./...
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
72
.golangci.yml
Normal file
72
.golangci.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
run:
|
||||
go: '1.19'
|
||||
timeout: 5m
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
ignore: fmt:.*,Read|Write|Close|Exec,io:Copy
|
||||
dupl:
|
||||
threshold: 100
|
||||
funlen:
|
||||
lines: 200
|
||||
statements: 200
|
||||
goheader:
|
||||
template: |-
|
||||
Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
This source code is licensed under the Apache 2.0 license found
|
||||
in the LICENSE file in the root directory of this source tree.
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- funlen
|
||||
- gocritic
|
||||
# - gofmt; Enable back when upgrading CI to Go 1.20.
|
||||
- goheader
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- gosec
|
||||
- gocritic
|
||||
- linters:
|
||||
- unused
|
||||
source: ent.Schema
|
||||
- path: dialect/sql/schema
|
||||
linters:
|
||||
- dupl
|
||||
- gosec
|
||||
- text: "Expect WriteFile permissions to be 0600 or less"
|
||||
linters:
|
||||
- gosec
|
||||
- path: privacy/privacy.go
|
||||
linters:
|
||||
- stylecheck
|
||||
- path: entc/load/schema.go
|
||||
linters:
|
||||
- staticcheck
|
||||
- path: entc/gen/graph.go
|
||||
linters:
|
||||
- gocritic
|
||||
- path: \.go
|
||||
linters:
|
||||
- staticcheck
|
||||
text: SA1019
|
||||
77
CODE_OF_CONDUCT.md
Normal file
77
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when
|
||||
an individual is representing the project or its community in public spaces.
|
||||
Examples of representing a project or community include using an official
|
||||
project e-mail address, posting via an official social media account, or acting
|
||||
as an appointed representative at an online or offline event. Representation of
|
||||
a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at <opensource-conduct@fb.com>. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
|
||||
57
CONTRIBUTING.md
Normal file
57
CONTRIBUTING.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Contributing to ent
|
||||
We want to make contributing to this project as easy and transparent as
|
||||
possible.
|
||||
|
||||
# Project structure
|
||||
|
||||
- `dialect` - Contains SQL and Gremlin code used by the generated code.
|
||||
- `dialect/sql/schema` - Auto migration logic resides there.
|
||||
- `dialect/sql/sqljson` - JSON extension for SQL.
|
||||
|
||||
- `schema` - User schema API.
|
||||
- `schema/{field, edge, index, mixin}` - provides schema builders API.
|
||||
- `schema/field/gen` - Templates and codegen for numeric builders.
|
||||
|
||||
- `entc` - Codegen of `ent`.
|
||||
- `entc/load` - `entc` loader API for loading user schemas into a Go objects at runtime.
|
||||
- `entc/gen` - The actual code generation logic resides in this package (and its `templates` package).
|
||||
- `integration` - Integration tests for `entc`.
|
||||
|
||||
- `privacy` - Runtime code for [privacy layer](https://entgo.io/docs/privacy/).
|
||||
|
||||
- `doc` - Documentation code for `entgo.io` (uses [Docusaurus](https://docusaurus.io)).
|
||||
- `doc/md` - Markdown files for documentation.
|
||||
- `doc/website` - Website code and assets.
|
||||
|
||||
In order to test your documentation changes, run `npm start` from the `doc/website` directory, and open [localhost:3000](http://localhost:3000/).
|
||||
|
||||
# Run integration tests
|
||||
If you touch any file in `entc`, run the following commands in `entc/integration` and 'examples' dirs:
|
||||
|
||||
```
|
||||
go generate ./...
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
Then, in `entc/integration` run `docker-compose` in order to spin-up all database containers:
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
Then, run `go test ./...` to run all integration tests.
|
||||
|
||||
|
||||
## Pull Requests
|
||||
We actively welcome your pull requests.
|
||||
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Ensure the test suite passes.
|
||||
5. Make sure your code lints.
|
||||
6. If you haven't already, complete the Contributor License Agreement ("CLA").
|
||||
|
||||
## License
|
||||
By contributing to ent, you agree that your contributions will be licensed
|
||||
under the LICENSE file in the root directory of this source tree.
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
56
README.md
Normal file
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## ent - An Entity Framework For Go
|
||||
|
||||
[](https://twitter.com/entgo_io)
|
||||
[](https://discord.gg/qZmPgTE6RX)
|
||||
|
||||
[English](README.md) | [中文](README_zh.md) | [日本語](README_jp.md) | [한국어](README_kr.md)
|
||||
|
||||
<img width="50%"
|
||||
align="right"
|
||||
style="display: block; margin:40px auto;"
|
||||
src="https://s3.eu-central-1.amazonaws.com/entgo.io/assets/gopher_graph.png"/>
|
||||
|
||||
Simple, yet powerful entity framework for Go, that makes it easy to build and maintain applications
|
||||
with large data-models.
|
||||
|
||||
- **Schema As Code** - model any database schema as Go objects.
|
||||
- **Easily Traverse Any Graph** - run queries, aggregations and traverse any graph structure easily.
|
||||
- **Statically Typed And Explicit API** - 100% statically typed and explicit API using code generation.
|
||||
- **Multi Storage Driver** - supports MySQL, MariaDB, TiDB, PostgreSQL, CockroachDB, SQLite and Gremlin.
|
||||
- **Extendable** - simple to extend and customize using Go templates.
|
||||
|
||||
## Quick Installation
|
||||
```console
|
||||
go install entgo.io/ent/cmd/ent@latest
|
||||
```
|
||||
|
||||
For proper installation using [Go modules], visit [entgo.io website][entgo install].
|
||||
|
||||
## Docs and Support
|
||||
The documentation for developing and using ent is available at: https://entgo.io
|
||||
|
||||
For discussion and support, [open an issue](https://github.com/ent/ent/issues/new/choose) or join our [Discord server](https://discord.gg/qZmPgTE6RX).
|
||||
|
||||
## Developed and Sponsored by the [Atlas](https://atlasgo.io) Team
|
||||
|
||||
[](https://atlasgo.io?utm_source=ent-readme)
|
||||
|
||||
## About the Project
|
||||
The `ent` project was inspired by Ent, an entity framework used internally at Meta (Facebook). It was created by [a8m](https://github.com/a8m) and [alexsn](https://github.com/alexsn) from the [Facebook Connectivity][fbc] team. These days, it is developed and maintained by the [Atlas](https://github.com/ariga/atlas) team, and the roadmap for its v1 release is described [here](https://github.com/ent/ent/issues/46).
|
||||
|
||||
## Join the ent Community
|
||||
Building `ent` would not have been possible without the collective work of our entire community. We maintain a [contributors page](doc/md/contributors.md)
|
||||
which lists the contributors to this `ent`.
|
||||
|
||||
In order to contribute to `ent`, see the [CONTRIBUTING](CONTRIBUTING.md) file for how to go get started.
|
||||
If your company or your product is using `ent`, please let us know by adding yourself to the [ent users page](https://github.com/ent/ent/wiki/ent-users).
|
||||
|
||||
For updates, follow us on Twitter at https://twitter.com/entgo_io
|
||||
|
||||
## License
|
||||
ent is licensed under Apache 2.0 as found in the [LICENSE file](LICENSE).
|
||||
|
||||
|
||||
[entgo install]: https://entgo.io/docs/code-gen/#version-compatibility-between-entc-and-ent
|
||||
[Go modules]: https://github.com/golang/go/wiki/Modules#quick-start
|
||||
[fbc]: https://connectivity.fb.com
|
||||
54
README_jp.md
Normal file
54
README_jp.md
Normal file
@@ -0,0 +1,54 @@
|
||||
## ent - Goのエンティティーフレームワーク
|
||||
|
||||
[](https://twitter.com/entgo_io)
|
||||
|
||||
[English](README.md) | [中文](README_zh.md) | [日本語](README_jp.md)
|
||||
|
||||
<img width="50%"
|
||||
align="right"
|
||||
style="display: block; margin:40px auto;"
|
||||
src="https://s3.eu-central-1.amazonaws.com/entgo.io/assets/gopher_graph.png"/>
|
||||
|
||||
シンプルながらもパワフルなGoのエンティティフレームワークであり、大規模なデータモデルを持つアプリケーションを容易に構築・保守できるようにします。
|
||||
|
||||
- **Schema As Code(コードとしてのスキーマ)** - あらゆるデータベーススキーマをGoオブジェクトとしてモデル化します。
|
||||
- **任意のグラフを簡単にトラバースできます** - クエリや集約の実行、任意のグラフ構造の走査を容易に実行できます。
|
||||
- **100%静的に型付けされた明示的なAPI** - コード生成により、100%静的に型付けされた曖昧さのないAPIを提供します。
|
||||
- **マルチストレージドライバ** - MySQL、MariaDB、 TiDB、PostgreSQL、CockroachDB、SQLite、Gremlinをサポートしています。
|
||||
- **拡張性** - Goテンプレートを使用して簡単に拡張、カスタマイズできます。
|
||||
|
||||
## クイックインストール
|
||||
```console
|
||||
go install entgo.io/ent/cmd/ent@latest
|
||||
```
|
||||
|
||||
[Go modules]を使ったインストールについては、[entgo.ioのWebサイト](https://entgo.io/ja/docs/code-gen/#entc-%E3%81%A8-ent-%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E4%B8%80%E8%87%B4%E3%81%95%E3%81%9B%E3%82%8B)をご覧ください。
|
||||
|
||||
## ドキュメントとサポート
|
||||
entを開発・使用するためのドキュメントは、こちら: https://entgo.io
|
||||
|
||||
議論やサポートについては、[Issueを開く](https://github.com/ent/ent/issues/new/choose)か、gophers Slackの[チャンネル](https://gophers.slack.com/archives/C01FMSQDT53)に参加してください。
|
||||
|
||||
## entコミュニティへの参加
|
||||
`ent`の構築は、コミュニティ全体の協力なしには実現できませんでした。 私たちは、この`ent`の貢献者をリストアップした[contributorsページ](doc/md/contributors.md)を管理しています。
|
||||
|
||||
`ent`に貢献するときは、まず[CONTRIBUTING](CONTRIBUTING.md)を参照してください。
|
||||
もし、あなたの会社や製品で`ent`を利用している場合は、[ent usersページ](https://github.com/ent/ent/wiki/ent-users)に追記する形で、そのことをぜひ教えて下さい。
|
||||
|
||||
最新情報については、Twitter(<https://twitter.com/entgo_io>)をフォローしてください。
|
||||
|
||||
|
||||
|
||||
## プロジェクトについて
|
||||
`ent`プロジェクトは、私たちが社内で使用しているエンティティフレームワークであるEntからインスピレーションを得ています。
|
||||
entは、[Facebook Connectivity][fbc]チームの[a8m](https://github.com/a8m)と[alexsn](https://github.com/alexsn)が開発・保守しています。
|
||||
本番環境では複数のチームやプロジェクトで使用されており、v1リリースまでのロードマップは[こちら](https://github.com/ent/ent/issues/46)に記載されています。
|
||||
このプロジェクトの動機については[こちら](https://entgo.io/blog/2019/10/03/introducing-ent)をご覧ください。
|
||||
|
||||
## ライセンス
|
||||
entは、[LICENSEファイル](LICENSE)にもある通り、Apache 2.0でライセンスされています。
|
||||
|
||||
|
||||
[entgo instal]: https://entgo.io/docs/code-gen/#version-compatibility-between-entc-and-ent
|
||||
[Go modules]: https://github.com/golang/go/wiki/Modules#quick-start
|
||||
[fbc]: https://connectivity.fb.com
|
||||
52
README_kr.md
Normal file
52
README_kr.md
Normal file
@@ -0,0 +1,52 @@
|
||||
## ent - An Entity Framework For Go
|
||||
|
||||
[English](README.md) | [中文](README_zh.md) | [日本語](README_jp.md) | [한국어](README_kr.md)
|
||||
|
||||
<img width="50%"
|
||||
align="right"
|
||||
style="display: block; margin:40px auto;"
|
||||
src="https://s3.eu-central-1.amazonaws.com/entgo.io/assets/gopher_graph.png"/>
|
||||
|
||||
간단하지만 강력한 Go용 엔터티 프레임워크로, 대규모 데이터 모델이 포함된 애플리케이션을 쉽게 만들고 유지할 수 있습니다.
|
||||
|
||||
- **스키마를 코드로 관리** - 모든 데이터베이스 스키마와 모델을 Go Object로 구현 가능.
|
||||
- **어떤 그래프든 쉽게 탐색가능** - 쿼리실행, 집계, 그래프구조를 쉽게 탐색 가능.
|
||||
- **정적 타입 그리고 명시적인 API** - 100% 생성된 코드로, 정적타입과 명시적인 API를 제공.
|
||||
- **다양한 스토리지 드라이버** - MySQL, MariaDB, TiDB, PostgreSQL, CockroachDB, SQLite and Gremlin 를 지원
|
||||
- **확장성** - Go 템플릿을 이용하여 간단하게 확장, 커스터마이징 가능.
|
||||
|
||||
## 빠른 설치
|
||||
|
||||
```console
|
||||
go install entgo.io/ent/cmd/ent@latest
|
||||
```
|
||||
|
||||
[Go modules]을 사용하여 바르게 설치하려면, [entgo.io 웹페이지][entgo install]를 방문해주시길 바랍니다.
|
||||
|
||||
## 문서 및 지원
|
||||
|
||||
Ent 개발 및 사용에 관한 문서는 여기서 확인할 수 있습니다. : https://entgo.io
|
||||
|
||||
토론, 지원을 위해서 [open an issue](https://github.com/ent/ent/issues/new/choose)깃허브 이슈 또는 gophers Slack [채널](https://gophers.slack.com/archives/C01FMSQDT53)에 가입해주세요.
|
||||
|
||||
## ent 커뮤니티 가입
|
||||
|
||||
ent 커뮤니티의 공동작업이 없었다면, ent를 만들 수 없었을 것입니다. 우리는 기여한 사람들을 [contributors 페이지](doc/md/contributors.md)에 올리고 유지합니다.
|
||||
|
||||
ent에 기여하려면 [CONTRIBUTING](CONTRIBUTING.md)에서 시작 방법을 확인해보세요.
|
||||
프로젝트나 회사에서 ent를 사용중이면, [ent 유저 페이지](https://github.com/ent/ent/wiki/ent-users)에 추가하여 알려주세요.
|
||||
|
||||
트위터계정을 팔로우하여 업데이트 소식을 확인하세요. https://twitter.com/entgo_io
|
||||
|
||||
## 프로젝트에 관하여
|
||||
|
||||
ent프로젝트는 내부적으로 사용하는 엔터티 프레임워크 "Ent"에서 영감을 받았습니다. 개발 및 유지보수는 [a8m](https://github.com/a8m) 및 [alexsn](https://github.com/alexsn)[Facebook Connectivity][fbc] 팀에서 담당합니다. 여러 팀이 프로덕션 환경에서 사용하고 있습니다. v1 릴리즈 로드맵에 대한 설명은 [여기](https://github.com/ent/ent/issues/46)를 클릭해주세요.
|
||||
프로젝트 동기에 대해 더 궁금하시다면 [여기](https://entgo.io/blog/2019/10/03/introducing-ent)를 클릭해주세요.
|
||||
|
||||
## 라이센스
|
||||
|
||||
ent 라이센스는 Apache 2.0입니다. [LICENSE file](LICENSE)파일에서도 확인 가능합니다.
|
||||
|
||||
[entgo install]: https://entgo.io/docs/code-gen/#version-compatibility-between-entc-and-ent
|
||||
[go modules]: https://github.com/golang/go/wiki/Modules#quick-start
|
||||
[fbc]: https://connectivity.fb.com
|
||||
44
README_zh.md
Normal file
44
README_zh.md
Normal file
@@ -0,0 +1,44 @@
|
||||
## ent - 一个强大的Go语言实体框架
|
||||
|
||||
[English](README.md) | [中文](README_zh.md) | [日本語](README_jp.md)
|
||||
|
||||
<img width="50%"
|
||||
align="right"
|
||||
style="display: block; margin:40px auto;"
|
||||
src="https://s3.eu-central-1.amazonaws.com/entgo.io/assets/gopher_graph.png"/>
|
||||
|
||||
ent是一个简单而又功能强大的Go语言实体框架,ent易于构建和维护应用程序与大数据模型。
|
||||
|
||||
- **图就是代码** - 将任何数据库表建模为Go对象。
|
||||
- **轻松地遍历任何图形** - 可以轻松地运行查询、聚合和遍历任何图形结构。
|
||||
- **静态类型和显式API** - 使用代码生成静态类型和显式API,查询数据更加便捷。
|
||||
- **多存储驱动程序** - 支持MySQL, PostgreSQL, SQLite 和 Gremlin。
|
||||
- **可扩展** - 简单地扩展和使用Go模板自定义。
|
||||
|
||||
## 快速安装
|
||||
```console
|
||||
go install entgo.io/ent/cmd/ent@latest
|
||||
```
|
||||
|
||||
请访问[entgo.io website][entgo instal]以使用[Go modules]进行正确安装。
|
||||
|
||||
## 文档和支持
|
||||
开发和使用ent的文档请参照: https://entgo.io
|
||||
|
||||
如要讨论问题和支持, [创建一个issue](https://github.com/ent/ent/issues/new/choose) 或者加入我们的Gopher Slack(Slack软件,类似于论坛)[讨论组](https://gophers.slack.com/archives/C01FMSQDT53)
|
||||
|
||||
## 加入 ent 社区
|
||||
如果你想为`ent`做出贡献, [贡献代码](CONTRIBUTING.md) 中写了如何做出自己的贡献
|
||||
如果你的公司或者产品在使用`ent`,请让我们知道你已经加入 [ent 用户](https://github.com/ent/ent/wiki/ent-users)
|
||||
|
||||
## 关于项目
|
||||
`ent` 项目灵感来自于Ent,Ent是一个facebook内部使用的一个实体框架项目。 它由 [Facebook Connectivity][fbc] 团队通过 [a8m](https://github.com/a8m) 和 [alexsn](https://github.com/alexsn) 开发和维护
|
||||
, 它被生产中的多个团队和项目使用。它的v1版本的路线图为 [版本的路线图](https://github.com/ent/ent/issues/46).
|
||||
关于项目更多的信息 [ent介绍](https://entgo.io/blog/2019/10/03/introducing-ent)。
|
||||
|
||||
## 声明
|
||||
ent使用Apache 2.0协议授权,可以在[LICENSE文件](LICENSE)中找到。
|
||||
|
||||
[entgo instal]: https://entgo.io/docs/code-gen/#version-compatibility-between-entc-and-ent
|
||||
[Go modules]: https://github.com/golang/go/wiki/Modules#quick-start
|
||||
[fbc]: https://connectivity.fb.com
|
||||
483
blob.go
Normal file
483
blob.go
Normal file
@@ -0,0 +1,483 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
// Blob defines the interface for blob storage operations.
|
||||
// Implementations should return [io/fs.ErrNotExist] (or an error wrapping it)
|
||||
// from NewReader when the requested key does not exist.
|
||||
//
|
||||
// Single-row SQL create builders write blob data to external storage before
|
||||
// inserting the database row. If the row insertion fails (for example, due to
|
||||
// a constraint violation), generated code attempts to delete the just-written
|
||||
// blobs — but only those no database row references. See [BlobRefChecker].
|
||||
type Blob interface {
|
||||
// NewReader opens a reader for the given key.
|
||||
NewReader(ctx context.Context, key string) (io.ReadCloser, error)
|
||||
// NewWriter opens a writer for the given key.
|
||||
NewWriter(ctx context.Context, key string) (io.WriteCloser, error)
|
||||
// Delete removes the blob at the given key.
|
||||
// Implementations should return nil (not an error) if the key does not exist.
|
||||
Delete(ctx context.Context, key string) error
|
||||
// Close releases any resources held by the bucket.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// BlobOpener is a function that opens a [Blob] bucket for the given field name.
|
||||
type BlobOpener func(context.Context, string) (Blob, error)
|
||||
|
||||
// BlobKey identifies a blob in storage by field name and key.
|
||||
type BlobKey struct {
|
||||
Field string
|
||||
Key string
|
||||
}
|
||||
|
||||
// BlobRefChecker reports which of the given blob keys rows in the database hold.
|
||||
//
|
||||
// Every path that deletes from storage consults it first, because a blob key is
|
||||
// not private to the row that wrote it. With content-addressed keys — the default
|
||||
// — identical content always yields an identical key, so the object written ahead
|
||||
// of a failed insert may be the very object an existing row points at, and two
|
||||
// rows with the same content share one object. A key is safe to remove only once
|
||||
// no row holds it.
|
||||
//
|
||||
// Keys are looked up per field, which takes each blob field to have its own
|
||||
// bucket: the model [BlobOpener] describes. Fields pointing at one shared bucket
|
||||
// also share objects, and cleanup cannot see across them.
|
||||
//
|
||||
// The lookup always runs while the mutation's statement (and transaction) is still
|
||||
// open, timed so that the table already reads the way the mutation will leave it:
|
||||
// before the INSERT for blobs written ahead of a row, and after the UPDATE or
|
||||
// DELETE for keys the mutation gives up ([BlobUpdateResult.Cleanup],
|
||||
// [BlobDeleteResult.Cleanup]). Nothing has to be excluded from the result that way.
|
||||
//
|
||||
// A row inserted concurrently between the lookup and the delete is not observed;
|
||||
// content-addressed storage needs reference counting or a sweeper to close that
|
||||
// window entirely.
|
||||
type BlobRefChecker interface {
|
||||
ReferencedBlobKeys(ctx context.Context, keys []BlobKey) ([]BlobKey, error)
|
||||
}
|
||||
|
||||
// BlobQuerier queries existing blob keys from the database.
|
||||
// [Blobs.Update] passes the mutated field names; [Blobs.Delete] passes nil
|
||||
// to indicate all fields should be queried.
|
||||
type BlobQuerier interface {
|
||||
BlobRefChecker
|
||||
QueryBlobKeys(ctx context.Context, fields []string) ([]BlobKey, error)
|
||||
}
|
||||
|
||||
// BlobUpdateResult holds post-update blob operations.
|
||||
type BlobUpdateResult struct {
|
||||
Rollback BlobOp // Deletes newly-written blobs. Call on SQL failure.
|
||||
|
||||
blobs *Blobs
|
||||
orphaned []BlobKey
|
||||
}
|
||||
|
||||
// Cleanup reports which of the blobs the update replaced no row holds any more, and
|
||||
// returns the [BlobOp] that removes them from storage.
|
||||
//
|
||||
// Call it once the UPDATE has run and before the transaction commits — the reference
|
||||
// check has to see the row on its new key, and it cannot run on a transaction that has
|
||||
// already settled. Invoke the returned op after the commit.
|
||||
func (r *BlobUpdateResult) Cleanup(ctx context.Context) (BlobOp, error) {
|
||||
if r.blobs == nil {
|
||||
return noOp, nil
|
||||
}
|
||||
return r.blobs.deleteOp(ctx, r.orphaned)
|
||||
}
|
||||
|
||||
// BlobDeleteResult holds the blob keys of the rows a delete mutation removes.
|
||||
type BlobDeleteResult struct {
|
||||
blobs *Blobs
|
||||
keys []BlobKey
|
||||
}
|
||||
|
||||
// Cleanup reports which of those blobs no row holds any more, and returns the [BlobOp]
|
||||
// that removes them from storage.
|
||||
//
|
||||
// Call it once the DELETE has run and before the transaction commits — the reference
|
||||
// check has to see the rows gone, and it cannot run on a transaction that has already
|
||||
// settled. Invoke the returned op after the commit.
|
||||
func (r *BlobDeleteResult) Cleanup(ctx context.Context) (BlobOp, error) {
|
||||
if r.blobs == nil {
|
||||
return noOp, nil
|
||||
}
|
||||
return r.blobs.deleteOp(ctx, r.keys)
|
||||
}
|
||||
|
||||
// BlobOp is a deferred blob storage operation (e.g. rollback or commit).
|
||||
type BlobOp func(context.Context) error
|
||||
|
||||
// BlobKeyFunc generates a storage key for a blob from its content.
|
||||
type BlobKeyFunc func(context.Context, []byte) (string, error)
|
||||
|
||||
// Blobs orchestrates blob storage operations for a single mutation.
|
||||
// Use [NewBlobs] to create, then call [Blobs.Set] or [Blobs.SetCleared]
|
||||
// for each blob field, then [Blobs.Create] or [Blobs.Update].
|
||||
type Blobs struct {
|
||||
opener BlobOpener
|
||||
inputs []blobInput
|
||||
refs BlobRefChecker
|
||||
}
|
||||
|
||||
type blobInput struct {
|
||||
field string
|
||||
data []byte
|
||||
newKey BlobKeyFunc
|
||||
apply func(string)
|
||||
cleared bool
|
||||
clear func()
|
||||
}
|
||||
|
||||
// NewBlobs creates a blob orchestrator for the given opener.
|
||||
func NewBlobs(opener BlobOpener) *Blobs {
|
||||
return &Blobs{opener: opener}
|
||||
}
|
||||
|
||||
// Set adds a blob field to be written. The apply callback is called with
|
||||
// the generated key to set it on the SQL spec and node.
|
||||
func (b *Blobs) Set(f string, data []byte, key BlobKeyFunc, apply func(string)) {
|
||||
b.inputs = append(b.inputs, blobInput{field: f, data: data, newKey: key, apply: apply})
|
||||
}
|
||||
|
||||
// SetCleared marks a blob field as cleared. The clear callback should
|
||||
// remove the key column from the SQL spec.
|
||||
func (b *Blobs) SetCleared(f string, clear func()) {
|
||||
b.inputs = append(b.inputs, blobInput{field: f, cleared: true, clear: clear})
|
||||
}
|
||||
|
||||
// Create prepares inputs, writes blobs, and returns a rollback [BlobOp].
|
||||
// refs keeps the rollback from deleting an object that rows in the database
|
||||
// already reference; a nil refs skips that check.
|
||||
func (b *Blobs) Create(ctx context.Context, refs BlobRefChecker) (BlobOp, error) {
|
||||
b.refs = refs
|
||||
writes, err := b.prepare(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.write(ctx, writes)
|
||||
}
|
||||
|
||||
// Update prepares inputs, queries old keys, writes new blobs, and returns
|
||||
// a [BlobUpdateResult] for post-SQL handling.
|
||||
func (b *Blobs) Update(ctx context.Context, q BlobQuerier) (*BlobUpdateResult, error) {
|
||||
if len(b.inputs) == 0 {
|
||||
return noopBlobResult, nil
|
||||
}
|
||||
b.refs = q
|
||||
writes, err := b.prepare(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mutated := make([]string, len(b.inputs))
|
||||
cleared := make(map[string]bool)
|
||||
for i := range b.inputs {
|
||||
mutated[i] = b.inputs[i].field
|
||||
if b.inputs[i].cleared {
|
||||
cleared[b.inputs[i].field] = true
|
||||
}
|
||||
}
|
||||
keys, err := q.QueryBlobKeys(ctx, mutated)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying old blob keys: %w", err)
|
||||
}
|
||||
// Build a set of old keys per field to detect unchanged blobs.
|
||||
oldKeys := make(map[string]string, len(keys))
|
||||
for _, k := range keys {
|
||||
oldKeys[k.Field] = k.Key
|
||||
}
|
||||
// Filter out writes where the key is unchanged (same content). Keep this in
|
||||
// its own slice — the orphan collection below reads the unfiltered writes.
|
||||
filtered := make([]blobWrite, 0, len(writes))
|
||||
for _, wr := range writes {
|
||||
if oldKeys[wr.Field] == wr.Key {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, wr)
|
||||
}
|
||||
rollback, err := b.write(ctx, filtered)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Collect orphaned blobs: old keys for fields that changed or were cleared.
|
||||
var orphaned []BlobKey
|
||||
for _, k := range keys {
|
||||
if cleared[k.Field] {
|
||||
orphaned = append(orphaned, k)
|
||||
continue
|
||||
}
|
||||
for _, wr := range writes {
|
||||
if wr.Field == k.Field && wr.Key != k.Key {
|
||||
orphaned = append(orphaned, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// The orphaned keys are resolved by BlobUpdateResult.Cleanup rather than here:
|
||||
// until the UPDATE runs, this row still holds them.
|
||||
return &BlobUpdateResult{
|
||||
Rollback: rollback,
|
||||
blobs: b,
|
||||
orphaned: orphaned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete collects the blob keys held by the rows the mutation matches. Call it
|
||||
// before the DELETE runs; [BlobDeleteResult.Cleanup] resolves what to remove from
|
||||
// storage once the rows are gone. Use for delete mutations.
|
||||
func (b *Blobs) Delete(ctx context.Context, q BlobQuerier) (*BlobDeleteResult, error) {
|
||||
b.refs = q
|
||||
keys, err := q.QueryBlobKeys(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BlobDeleteResult{blobs: b, keys: keys}, nil
|
||||
}
|
||||
|
||||
type blobWrite struct {
|
||||
BlobKey
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (b *Blobs) prepare(ctx context.Context) ([]blobWrite, error) {
|
||||
var writes []blobWrite
|
||||
for _, inp := range b.inputs {
|
||||
if inp.cleared {
|
||||
inp.clear()
|
||||
continue
|
||||
}
|
||||
k, err := inp.newKey(ctx, inp.data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating blob key for %s: %w", inp.field, err)
|
||||
}
|
||||
if inp.apply != nil {
|
||||
inp.apply(k)
|
||||
}
|
||||
writes = append(writes, blobWrite{
|
||||
BlobKey: BlobKey{Field: inp.field, Key: k},
|
||||
data: inp.data,
|
||||
})
|
||||
}
|
||||
return writes, nil
|
||||
}
|
||||
|
||||
func (b *Blobs) write(ctx context.Context, writes []blobWrite) (BlobOp, error) {
|
||||
if len(writes) == 0 {
|
||||
return noOp, nil
|
||||
}
|
||||
w := NewBlobStore(b.opener)
|
||||
var written []BlobKey
|
||||
for _, wr := range writes {
|
||||
if err := w.write(ctx, wr.Field, wr.Key, wr.data); err != nil {
|
||||
// A failed write may still have left an object behind, so clean it up
|
||||
// with the rest — under the same reference check.
|
||||
written = append(written, wr.BlobKey)
|
||||
errs := []error{fmt.Errorf("writing blob for %s: %w", wr.Field, err), w.Close()}
|
||||
switch rollback, rerr := b.deleteOp(ctx, written); {
|
||||
case rerr != nil:
|
||||
errs = append(errs, rerr)
|
||||
default:
|
||||
errs = append(errs, rollback(ctx))
|
||||
}
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
written = append(written, wr.BlobKey)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.deleteOp(ctx, written)
|
||||
}
|
||||
|
||||
// deleteOp returns a [BlobOp] that removes keys from storage, skipping any key
|
||||
// that rows outside this mutation still hold.
|
||||
//
|
||||
// The lookup happens here rather than inside the returned op: the op runs after
|
||||
// the statement — or the whole transaction — has settled, when the mutation's own
|
||||
// view of the table is no longer reachable.
|
||||
func (b *Blobs) deleteOp(ctx context.Context, keys []BlobKey) (BlobOp, error) {
|
||||
if len(keys) == 0 {
|
||||
return noOp, nil
|
||||
}
|
||||
keys, err := b.unreferenced(ctx, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return noOp, nil
|
||||
}
|
||||
return func(ctx context.Context) error {
|
||||
s := NewBlobStore(b.opener)
|
||||
var errs []error
|
||||
for _, k := range keys {
|
||||
if err := s.delete(ctx, k.Field, k.Key); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if err := s.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// unreferenced returns the distinct keys no surviving row holds, dropping the
|
||||
// ones still in use. See [BlobRefChecker] for why a key can be shared at all.
|
||||
func (b *Blobs) unreferenced(ctx context.Context, keys []BlobKey) ([]BlobKey, error) {
|
||||
if b.refs == nil {
|
||||
return dedupBlobKeys(keys), nil
|
||||
}
|
||||
referenced, err := b.refs.ReferencedBlobKeys(ctx, keys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking blob key references: %w", err)
|
||||
}
|
||||
if len(referenced) == 0 {
|
||||
return dedupBlobKeys(keys), nil
|
||||
}
|
||||
held := make(map[BlobKey]bool, len(referenced))
|
||||
for _, k := range referenced {
|
||||
held[k] = true
|
||||
}
|
||||
unreferenced := make([]BlobKey, 0, len(keys))
|
||||
for _, k := range dedupBlobKeys(keys) {
|
||||
if !held[k] {
|
||||
unreferenced = append(unreferenced, k)
|
||||
}
|
||||
}
|
||||
return unreferenced, nil
|
||||
}
|
||||
|
||||
func dedupBlobKeys(keys []BlobKey) []BlobKey {
|
||||
seen := make(map[BlobKey]bool, len(keys))
|
||||
distinct := make([]BlobKey, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if seen[k] {
|
||||
continue
|
||||
}
|
||||
seen[k] = true
|
||||
distinct = append(distinct, k)
|
||||
}
|
||||
return distinct
|
||||
}
|
||||
|
||||
var (
|
||||
noOp = func(context.Context) error { return nil }
|
||||
noopBlobResult = &BlobUpdateResult{Rollback: noOp}
|
||||
)
|
||||
|
||||
// BlobReader returns a reader for the given key from the blob bucket.
|
||||
// The returned reader closes both the underlying reader and the bucket.
|
||||
// Returns nil, nil if the blob does not exist (fs.ErrNotExist).
|
||||
func BlobReader(ctx context.Context, b Blob, key string) (io.ReadCloser, error) {
|
||||
switch r, err := b.NewReader(ctx, key); {
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return nil, b.Close()
|
||||
case err != nil:
|
||||
return nil, errors.Join(err, b.Close())
|
||||
default:
|
||||
return &blobReadCloser{ReadCloser: r, bucket: b}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type blobReadCloser struct {
|
||||
io.ReadCloser
|
||||
bucket Blob
|
||||
}
|
||||
|
||||
func (r *blobReadCloser) Close() error {
|
||||
return errors.Join(r.ReadCloser.Close(), r.bucket.Close())
|
||||
}
|
||||
|
||||
// BlobStore manages blob bucket lifecycles for read, write, and delete operations.
|
||||
// It lazily opens buckets per field and reuses them for subsequent operations.
|
||||
type BlobStore struct {
|
||||
opener BlobOpener
|
||||
buckets map[string]Blob
|
||||
}
|
||||
|
||||
// NewBlobStore creates a store that uses opener to lazily open buckets.
|
||||
func NewBlobStore(opener BlobOpener) *BlobStore {
|
||||
return &BlobStore{buckets: make(map[string]Blob), opener: opener}
|
||||
}
|
||||
|
||||
// Close closes all open buckets.
|
||||
func (s *BlobStore) Close() error {
|
||||
var errs []error
|
||||
for _, b := range s.buckets {
|
||||
errs = append(errs, b.Close())
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// write writes data to the blob at key for the given field.
|
||||
func (s *BlobStore) write(ctx context.Context, field, key string, data []byte) error {
|
||||
b, err := s.bucket(ctx, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wr, err := b.NewWriter(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := wr.Write(data); err != nil {
|
||||
// Leave the key alone — it may be an object other rows reference. The
|
||||
// caller cleans up through [Blobs.deleteOp], which checks for references.
|
||||
return errors.Join(err, wr.Close())
|
||||
}
|
||||
return wr.Close()
|
||||
}
|
||||
|
||||
// delete removes the blob at key for the given field.
|
||||
func (s *BlobStore) delete(ctx context.Context, field, key string) error {
|
||||
b, err := s.bucket(ctx, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// Read reads the blob at key for the given field.
|
||||
// Returns nil, nil if the blob does not exist (fs.ErrNotExist).
|
||||
func (s *BlobStore) Read(ctx context.Context, field, key string) ([]byte, error) {
|
||||
b, err := s.bucket(ctx, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc, err := b.NewReader(ctx, key)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := io.ReadAll(rc)
|
||||
if closeErr := rc.Close(); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (s *BlobStore) bucket(ctx context.Context, field string) (Blob, error) {
|
||||
if b, ok := s.buckets[field]; ok {
|
||||
return b, nil
|
||||
}
|
||||
if s.opener == nil {
|
||||
return nil, errors.New("ent: blob storage not configured (missing WithBlobOpeners)")
|
||||
}
|
||||
b, err := s.opener(ctx, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.buckets[field] = b
|
||||
return b, nil
|
||||
}
|
||||
26
cmd/ent/ent.go
Normal file
26
cmd/ent/ent.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"entgo.io/ent/cmd/internal/base"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
cmd := &cobra.Command{Use: "ent"}
|
||||
cmd.AddCommand(
|
||||
base.NewCmd(),
|
||||
base.DescribeCmd(),
|
||||
base.GenerateCmd(),
|
||||
base.InitCmd(),
|
||||
base.SchemaCmd(),
|
||||
)
|
||||
cobra.CheckErr(cmd.Execute())
|
||||
}
|
||||
39
cmd/ent/ent_test.go
Normal file
39
cmd/ent/ent_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
defer os.RemoveAll("ent")
|
||||
cmd := exec.Command("go", "run", "entgo.io/ent/cmd/ent", "new", "User")
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run())
|
||||
require.Zero(t, stderr.String())
|
||||
cmd = exec.Command("go", "run", "entgo.io/ent/cmd/ent", "new", "User")
|
||||
require.Error(t, cmd.Run())
|
||||
|
||||
_, err := os.Stat("ent/generate.go")
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat("ent/schema/user.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd = exec.Command("go", "run", "entgo.io/ent/cmd/ent", "generate", "./ent/schema")
|
||||
stderr = bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run())
|
||||
require.Zero(t, stderr.String())
|
||||
|
||||
_, err = os.Stat("ent/user.go")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
41
cmd/entc/entc.go
Normal file
41
cmd/entc/entc.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"entgo.io/ent/cmd/internal/base"
|
||||
"entgo.io/ent/entc/gen"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
cmd := &cobra.Command{Use: "entc"}
|
||||
cmd.AddCommand(
|
||||
base.NewCmd(),
|
||||
base.DescribeCmd(),
|
||||
base.GenerateCmd(migrate),
|
||||
base.InitCmd(),
|
||||
)
|
||||
_ = cmd.Execute()
|
||||
}
|
||||
|
||||
func migrate(c *gen.Config) {
|
||||
var (
|
||||
target = filepath.Join(c.Target, "generate.go")
|
||||
oldCmd = []byte("entgo.io/ent/cmd/entc")
|
||||
)
|
||||
buf, err := os.ReadFile(target)
|
||||
if err != nil || !bytes.Contains(buf, oldCmd) {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(target, bytes.ReplaceAll(buf, oldCmd, []byte("entgo.io/ent/cmd/ent")), 0644)
|
||||
}
|
||||
35
cmd/entc/entc_test.go
Normal file
35
cmd/entc/entc_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
defer os.RemoveAll("ent")
|
||||
cmd := exec.Command("go", "run", "entgo.io/ent/cmd/entc", "new", "User")
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run(), stderr.String())
|
||||
|
||||
_, err := os.Stat("ent/generate.go")
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat("ent/schema/user.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd = exec.Command("go", "run", "entgo.io/ent/cmd/entc", "generate", "./ent/schema")
|
||||
stderr = bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run(), stderr.String())
|
||||
|
||||
_, err = os.Stat("ent/user.go")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
107
cmd/entfix/entfix.go
Normal file
107
cmd/entfix/entfix.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/schema"
|
||||
"entgo.io/ent/entc/gen"
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
type (
|
||||
// App configures the entfix CLI.
|
||||
App struct {
|
||||
GlobalID GlobalID `cmd:"" name:"globalid" help:"Migrate unique global id ent_types to ent global feature"`
|
||||
}
|
||||
// GlobalID represents the 'entfix globalid' command.
|
||||
GlobalID struct {
|
||||
Dialect string `name:"dialect" help:"Database dialect" required:"" enum:"mysql,postgres,sqlite3"`
|
||||
DSN string `name:"dsn" help:"Data source name" required:""`
|
||||
Path string `name:"path" help:"Path to the generated ent code" required:""`
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Ensure to stop execution on Interrupt signal.
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
app := kong.Parse(
|
||||
new(App),
|
||||
kong.BindTo(ctx, (*context.Context)(nil)),
|
||||
kong.UsageOnError(),
|
||||
)
|
||||
app.FatalIfErrorf(func() error {
|
||||
err := app.Run()
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}())
|
||||
}
|
||||
|
||||
func (cmd *GlobalID) Run(ctx context.Context) error {
|
||||
fmt.Print(`IMPORTANT INFORMATION
|
||||
|
||||
'entfix globalid' will convert the allocated id ranges for your nodes from the
|
||||
database stored 'ent_types' table to the new static configuration on the ent
|
||||
schema itself.
|
||||
|
||||
Please note, that the 'ent_types' table might differ between different environments
|
||||
where your app is deployed. This is especially true if you are using
|
||||
auto-migration instead of versioned migrations.
|
||||
|
||||
Please check, that all 'ent_types' tables for all deployments are equal!
|
||||
|
||||
Only 'yes' will be accepted to approve.
|
||||
|
||||
Enter a value: `)
|
||||
switch c, err := bufio.NewReader(os.Stdin).ReadString('\n'); {
|
||||
case err != nil:
|
||||
return err
|
||||
case strings.TrimSpace(c) != "yes":
|
||||
fmt.Println("\nAborted.")
|
||||
return nil
|
||||
}
|
||||
db, err := sql.Open(cmd.Dialect, cmd.DSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := &sql.Rows{}
|
||||
query, args := sql.Dialect(cmd.Dialect).
|
||||
Select("type").
|
||||
From(sql.Table(schema.TypeTable)).
|
||||
OrderBy(sql.Asc("id")).
|
||||
Query()
|
||||
if err := db.Query(ctx, query, args, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var ts []string
|
||||
if err := sql.ScanSlice(rows, &ts); err != nil {
|
||||
return err
|
||||
}
|
||||
is := make(gen.IncrementStarts, len(ts))
|
||||
for i, t := range ts {
|
||||
is[t] = int64(i << 32)
|
||||
}
|
||||
if err := is.WriteToDisk(cmd.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("\nSuccess! Please run code generation to complete the process.")
|
||||
return nil
|
||||
}
|
||||
89
cmd/entfix/entfix_test.go
Normal file
89
cmd/entfix/entfix_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
entsql "entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/schema"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func TestEntfix(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
stdout, err := os.CreateTemp(t.TempDir(), "")
|
||||
require.NoError(t, err)
|
||||
oldStdout := os.Stdout
|
||||
os.Stdout = stdout
|
||||
t.Cleanup(func() {
|
||||
os.Stdout = oldStdout
|
||||
require.NoError(t, stdout.Close())
|
||||
})
|
||||
|
||||
cli := &GlobalID{
|
||||
Dialect: dialect.SQLite,
|
||||
DSN: fmt.Sprintf("file:%s?mode=memory&cache=shared&_fk=1", t.Name()),
|
||||
Path: t.TempDir(),
|
||||
}
|
||||
|
||||
// Prints details, requires confirmation.
|
||||
os.Stdin = mockStdin(t, "no\n")
|
||||
require.NoError(t, cli.Run(ctx))
|
||||
buf, err := os.ReadFile(stdout.Name())
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(string(buf), "IMPORTANT INFORMATION\n\n"))
|
||||
require.True(t, strings.HasSuffix(string(buf), "Aborted.\n"))
|
||||
f, err := os.Open(cli.Path)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||
_, err = f.Readdirnames(1)
|
||||
require.ErrorIs(t, err, io.EOF)
|
||||
|
||||
// If approved, converts the 'ent_types' table.
|
||||
db, err := sql.Open(cli.Dialect, cli.DSN)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, db.Close()) })
|
||||
drv := entsql.OpenDB(dialect.SQLite, db)
|
||||
m, err := schema.NewMigrate(drv, schema.WithGlobalUniqueID(true))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Create(ctx))
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (type) VALUES ('z'), ('y'), ('a'), ('b')", schema.TypeTable))
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Stdin = mockStdin(t, "yes\n")
|
||||
require.NoError(t, cli.Run(ctx))
|
||||
buf, err = os.ReadFile(stdout.Name())
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(string(buf), "Success! Please run code generation to complete the process.\n"))
|
||||
c, err := os.ReadFile(filepath.Join(cli.Path, "internal", "globalid.go"))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t,
|
||||
string(c),
|
||||
fmt.Sprintf(`const IncrementStarts = "{\"a\":%d,\"b\":%d,\"y\":%d,\"z\":%d}"`, 2<<32, 3<<32, 1<<32, 0),
|
||||
)
|
||||
}
|
||||
|
||||
func mockStdin(t *testing.T, content string) *os.File {
|
||||
t.Helper()
|
||||
stdin, err := os.CreateTemp(t.TempDir(), "")
|
||||
require.NoError(t, err)
|
||||
_, err = stdin.WriteString(content)
|
||||
require.NoError(t, err)
|
||||
_, err = stdin.Seek(0, 0)
|
||||
require.NoError(t, err)
|
||||
oldStdin := os.Stdin
|
||||
t.Cleanup(func() {
|
||||
os.Stdin = oldStdin
|
||||
require.NoError(t, stdin.Close())
|
||||
})
|
||||
return stdin
|
||||
}
|
||||
32
cmd/entfix/go.mod
Normal file
32
cmd/entfix/go.mod
Normal file
@@ -0,0 +1,32 @@
|
||||
module entgo.io/ent/cmd/entfix
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6
|
||||
github.com/alecthomas/kong v1.6.1
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/stretchr/testify v1.8.2
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.23.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
67
cmd/entfix/go.sum
Normal file
67
cmd/entfix/go.sum
Normal file
@@ -0,0 +1,67 @@
|
||||
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1 h1:tqUmxmE7r2qqWmgczyNZNOyhzXLqmdUpRhVSuvCUqTU=
|
||||
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1/go.mod h1:cpxrs2J9HTrjMrfNKFmUpONY9L0vzAHZ1pY++nTtVw0=
|
||||
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6 h1:7H+NesJeDdcNtZxp0s5Ip/IYLkuUfoRFTIAC0fGcezg=
|
||||
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6/go.mod h1:nPwpHTcyAjBMt+hT6q7Y/SdF41e6IJudQPbTj1P5nYY=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8=
|
||||
github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
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/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
|
||||
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
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/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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
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=
|
||||
360
cmd/internal/base/base.go
Normal file
360
cmd/internal/base/base.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
// Package base defines shared basic pieces of the ent command.
|
||||
package base
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"entgo.io/ent/cmd/internal/printer"
|
||||
"entgo.io/ent/dialect/sql/schema"
|
||||
"entgo.io/ent/entc"
|
||||
"entgo.io/ent/entc/gen"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// IDType is a custom ID implementation for pflag.
|
||||
type IDType field.Type
|
||||
|
||||
// Set implements the Set method of the flag.Value interface.
|
||||
func (t *IDType) Set(s string) error {
|
||||
switch s {
|
||||
case field.TypeInt.String():
|
||||
*t = IDType(field.TypeInt)
|
||||
case field.TypeInt64.String():
|
||||
*t = IDType(field.TypeInt64)
|
||||
case field.TypeUint.String():
|
||||
*t = IDType(field.TypeUint)
|
||||
case field.TypeUint64.String():
|
||||
*t = IDType(field.TypeUint64)
|
||||
case field.TypeString.String():
|
||||
*t = IDType(field.TypeString)
|
||||
default:
|
||||
return fmt.Errorf("invalid type %q", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type representation of the id option for help command.
|
||||
func (IDType) Type() string {
|
||||
return fmt.Sprintf("%v", []field.Type{
|
||||
field.TypeInt,
|
||||
field.TypeInt64,
|
||||
field.TypeUint,
|
||||
field.TypeUint64,
|
||||
field.TypeString,
|
||||
})
|
||||
}
|
||||
|
||||
// String returns the default value for the help command.
|
||||
func (IDType) String() string {
|
||||
return field.TypeInt.String()
|
||||
}
|
||||
|
||||
// InitCmd returns the init command for ent/c packages.
|
||||
func InitCmd() *cobra.Command {
|
||||
c := NewCmd()
|
||||
c.Use = "init [flags] [schemas]"
|
||||
c.Short = "initialize an environment with zero or more schemas"
|
||||
c.Example = examples(
|
||||
"ent init Example",
|
||||
"ent init --target entv1/schema User Group",
|
||||
"ent init --template ./path/to/file.tmpl User",
|
||||
)
|
||||
c.Deprecated = `use "ent new" instead`
|
||||
return c
|
||||
}
|
||||
|
||||
// NewCmd returns the new command for ent/c packages.
|
||||
func NewCmd() *cobra.Command {
|
||||
var target, tmplPath string
|
||||
cmd := &cobra.Command{
|
||||
Use: "new [flags] [schemas]",
|
||||
Short: "initialize a new environment with zero or more schemas",
|
||||
Example: examples(
|
||||
"ent new Example",
|
||||
"ent new --target entv1/schema User Group",
|
||||
"ent new --template ./path/to/file.tmpl User",
|
||||
),
|
||||
Args: func(_ *cobra.Command, names []string) error {
|
||||
for _, name := range names {
|
||||
if !unicode.IsUpper(rune(name[0])) {
|
||||
return errors.New("schema names must begin with uppercase")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, names []string) {
|
||||
var (
|
||||
err error
|
||||
tmpl *template.Template
|
||||
)
|
||||
if tmplPath != "" {
|
||||
tmpl = template.New(filepath.Base(tmplPath)).Funcs(gen.Funcs)
|
||||
tmpl, err = tmpl.ParseFiles(tmplPath)
|
||||
} else {
|
||||
tmpl = template.New("schema").Funcs(gen.Funcs)
|
||||
tmpl, err = tmpl.Parse(defaultTemplate)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalln(fmt.Errorf("ent/new: could not parse template %w", err))
|
||||
}
|
||||
if err := newEnv(target, names, tmpl); err != nil {
|
||||
log.Fatalln(fmt.Errorf("ent/new: %w", err))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&target, "target", defaultSchema, "target directory for schemas")
|
||||
cmd.Flags().StringVar(&tmplPath, "template", "", "template to use for new schemas")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// DescribeCmd returns the describe command for ent/c packages.
|
||||
func DescribeCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "describe [flags] path",
|
||||
Short: "print a description of the graph schema",
|
||||
Example: examples(
|
||||
"ent describe ./ent/schema",
|
||||
"ent describe github.com/a8m/x",
|
||||
),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, path []string) {
|
||||
graph, err := entc.LoadGraph(path[0], &gen.Config{})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
printer.Fprint(os.Stdout, graph)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateCmd returns the generate command for ent/c packages.
|
||||
func GenerateCmd(postRun ...func(*gen.Config)) *cobra.Command {
|
||||
var (
|
||||
cfg gen.Config
|
||||
storage string
|
||||
features []string
|
||||
templates []string
|
||||
idtype = IDType(field.TypeInt)
|
||||
cmd = &cobra.Command{
|
||||
Use: "generate [flags] path",
|
||||
Short: "generate go code for the schema directory",
|
||||
Example: examples(
|
||||
"ent generate ./ent/schema",
|
||||
"ent generate github.com/a8m/x",
|
||||
),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, path []string) {
|
||||
opts := []entc.Option{
|
||||
entc.Storage(storage),
|
||||
entc.FeatureNames(features...),
|
||||
}
|
||||
for _, tmpl := range templates {
|
||||
typ := "dir"
|
||||
if parts := strings.SplitN(tmpl, "=", 2); len(parts) > 1 {
|
||||
typ, tmpl = parts[0], parts[1]
|
||||
}
|
||||
switch typ {
|
||||
case "dir":
|
||||
opts = append(opts, entc.TemplateDir(tmpl))
|
||||
case "file":
|
||||
opts = append(opts, entc.TemplateFiles(tmpl))
|
||||
case "glob":
|
||||
opts = append(opts, entc.TemplateGlob(tmpl))
|
||||
default:
|
||||
log.Fatalln("unsupported template type", typ)
|
||||
}
|
||||
}
|
||||
// If the target directory is not inferred from
|
||||
// the schema path, resolve its package path.
|
||||
if cfg.Target != "" {
|
||||
pkgPath, err := PkgPath(DefaultConfig, cfg.Target)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
cfg.Package = pkgPath
|
||||
}
|
||||
cfg.IDType = &field.TypeInfo{Type: field.Type(idtype)}
|
||||
if err := entc.Generate(path[0], &cfg, opts...); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
for _, fn := range postRun {
|
||||
fn(&cfg)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
cmd.Flags().Var(&idtype, "idtype", "type of the id field")
|
||||
cmd.Flags().StringVar(&storage, "storage", "sql", "storage driver to support in codegen")
|
||||
cmd.Flags().StringVar(&cfg.Header, "header", "", "override codegen header")
|
||||
cmd.Flags().StringVar(&cfg.Target, "target", "", "target directory for codegen")
|
||||
cmd.Flags().StringSliceVarP(&features, "feature", "", nil, "extend codegen with additional features")
|
||||
cmd.Flags().StringSliceVarP(&templates, "template", "", nil, "external templates to execute")
|
||||
// The --idtype flag predates the field.<Type>("id") option.
|
||||
// See, https://entgo.io/docs/schema-fields#id-field.
|
||||
cobra.CheckErr(cmd.Flags().MarkHidden("idtype"))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SchemaCmd returns DDL to use Ent as an Atlas schema loader.
|
||||
func SchemaCmd() *cobra.Command {
|
||||
var (
|
||||
cfg gen.Config
|
||||
dlct, version string
|
||||
features, buildTags []string
|
||||
hashSymbols bool
|
||||
cmd = &cobra.Command{
|
||||
Use: "schema [flags] path",
|
||||
Short: "dump the DDL for the schema directory",
|
||||
Example: examples(
|
||||
"ent schema ./ent/schema --dialect mysql --version 5.6",
|
||||
"ent schema ./ent/schema --dialect sqlite3",
|
||||
"ent schema github.com/a8m/x --dialect postgres --version 15",
|
||||
),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, path []string) {
|
||||
for _, o := range []entc.Option{
|
||||
entc.FeatureNames(features...),
|
||||
entc.BuildTags(buildTags...),
|
||||
} {
|
||||
if err := o(&cfg); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
// If the target directory is not inferred from
|
||||
// the schema path, resolve its package path.
|
||||
if cfg.Target != "" {
|
||||
pkgPath, err := PkgPath(DefaultConfig, cfg.Target)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
cfg.Package = pkgPath
|
||||
}
|
||||
g, err := entc.LoadGraph(path[0], &cfg)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
t, err := g.Tables()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
v, err := g.Views()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
ddl, err := schema.DDL(cmd.Context(), schema.DDLArgs{
|
||||
Dialect: dlct,
|
||||
Version: version,
|
||||
HashSymbols: hashSymbols,
|
||||
Tables: append(t, v...),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Println(ddl)
|
||||
},
|
||||
}
|
||||
)
|
||||
cmd.Flags().StringVar(&dlct, "dialect", "", "database dialect to use")
|
||||
cmd.Flags().StringVar(&version, "version", "", "database version to assume")
|
||||
cmd.Flags().StringSliceVarP(&features, "feature", "", nil, "extend codegen with additional features")
|
||||
cmd.Flags().StringSliceVarP(&buildTags, "build-tags", "", nil, "go build tags to use when loading the schema graph")
|
||||
cmd.Flags().BoolVar(&hashSymbols, "hash-symbols", false, "whether to hash long symbols")
|
||||
cobra.CheckErr(cmd.MarkFlagRequired("dialect"))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newEnv create a new environment for ent codegen.
|
||||
func newEnv(target string, names []string, tmpl *template.Template) error {
|
||||
if err := createDir(target); err != nil {
|
||||
return fmt.Errorf("create dir %s: %w", target, err)
|
||||
}
|
||||
for _, name := range names {
|
||||
if err := gen.ValidSchemaName(name); err != nil {
|
||||
return fmt.Errorf("new schema %s: %w", name, err)
|
||||
}
|
||||
if fileExists(target, name) {
|
||||
return fmt.Errorf("new schema %s: already exists", name)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
if err := tmpl.Execute(b, name); err != nil {
|
||||
return fmt.Errorf("executing template %s: %w", name, err)
|
||||
}
|
||||
newFileTarget := filepath.Join(target, strings.ToLower(name+".go"))
|
||||
if err := os.WriteFile(newFileTarget, b.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("writing file %s: %w", newFileTarget, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDir(target string) error {
|
||||
_, err := os.Stat(target)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(target, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("creating schema directory: %w", err)
|
||||
}
|
||||
if target != defaultSchema {
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile("ent/generate.go", []byte(genFile), 0644); err != nil {
|
||||
return fmt.Errorf("creating generate.go file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(target, name string) bool {
|
||||
var _, err = os.Stat(filepath.Join(target, strings.ToLower(name+".go")))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
const (
|
||||
// default schema package path.
|
||||
defaultSchema = "ent/schema"
|
||||
// ent/generate.go file used for "go generate" command.
|
||||
genFile = "package ent\n\n//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema\n"
|
||||
// schema template for the "init" command.
|
||||
defaultTemplate = `package schema
|
||||
|
||||
import "entgo.io/ent"
|
||||
|
||||
// {{ . }} holds the schema definition for the {{ . }} entity.
|
||||
type {{ . }} struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Fields of the {{ . }}.
|
||||
func ({{ . }}) Fields() []ent.Field {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edges of the {{ . }}.
|
||||
func ({{ . }}) Edges() []ent.Edge {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
// examples formats the given examples to the cli.
|
||||
func examples(ex ...string) string {
|
||||
for i := range ex {
|
||||
ex[i] = " " + ex[i] // indent each row with 2 spaces.
|
||||
}
|
||||
return strings.Join(ex, "\n")
|
||||
}
|
||||
55
cmd/internal/base/packages.go
Normal file
55
cmd/internal/base/packages.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
// DefaultConfig for loading Go base.
|
||||
var DefaultConfig = &packages.Config{Mode: packages.NeedName}
|
||||
|
||||
// PkgPath returns the Go package name for given target path.
|
||||
// Even if the existing path is not exist yet in the filesystem.
|
||||
//
|
||||
// If base.Config is nil, DefaultConfig will be used to load base.
|
||||
func PkgPath(config *packages.Config, target string) (string, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig
|
||||
}
|
||||
pathCheck, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var parts []string
|
||||
if _, err := os.Stat(pathCheck); os.IsNotExist(err) {
|
||||
parts = append(parts, filepath.Base(pathCheck))
|
||||
pathCheck = filepath.Dir(pathCheck)
|
||||
}
|
||||
// Try maximum 2 directories above the given
|
||||
// target to find the root package or module.
|
||||
for i := 0; i < 2; i++ {
|
||||
pkgs, err := packages.Load(config, pathCheck)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("load package info: %w", err)
|
||||
}
|
||||
if len(pkgs) == 0 || len(pkgs[0].Errors) != 0 {
|
||||
parts = append(parts, filepath.Base(pathCheck))
|
||||
pathCheck = filepath.Dir(pathCheck)
|
||||
continue
|
||||
}
|
||||
pkgPath := pkgs[0].PkgPath
|
||||
for j := len(parts) - 1; j >= 0; j-- {
|
||||
pkgPath = path.Join(pkgPath, parts[j])
|
||||
}
|
||||
return pkgPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("root package or module was not found for: %s", target)
|
||||
}
|
||||
49
cmd/internal/base/packages_test.go
Normal file
49
cmd/internal/base/packages_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/tools/go/packages/packagestest"
|
||||
)
|
||||
|
||||
func TestPkgPath(t *testing.T) { packagestest.TestAll(t, testPkgPath) }
|
||||
func testPkgPath(t *testing.T, x packagestest.Exporter) {
|
||||
e := packagestest.Export(t, x, []packagestest.Module{
|
||||
{
|
||||
Name: "golang.org/x",
|
||||
Files: map[string]any{
|
||||
"x.go": "package x",
|
||||
"y/y.go": "package y",
|
||||
},
|
||||
},
|
||||
})
|
||||
defer e.Cleanup()
|
||||
|
||||
e.Config.Dir = filepath.Dir(e.File("golang.org/x", "x.go"))
|
||||
target := filepath.Join(e.Config.Dir, "ent")
|
||||
pkgPath, err := PkgPath(e.Config, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "golang.org/x/ent", pkgPath)
|
||||
|
||||
e.Config.Dir = filepath.Dir(e.File("golang.org/x", "y/y.go"))
|
||||
target = filepath.Join(e.Config.Dir, "ent")
|
||||
pkgPath, err = PkgPath(e.Config, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "golang.org/x/y/ent", pkgPath)
|
||||
|
||||
target = filepath.Join(e.Config.Dir, "z/ent")
|
||||
pkgPath, err = PkgPath(e.Config, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "golang.org/x/y/z/ent", pkgPath)
|
||||
|
||||
target = filepath.Join(e.Config.Dir, "z/e/n/t")
|
||||
pkgPath, err = PkgPath(e.Config, target)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, pkgPath)
|
||||
}
|
||||
141
cmd/internal/printer/printer.go
Normal file
141
cmd/internal/printer/printer.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"entgo.io/ent/entc/gen"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// A Config controls the output of Fprint.
|
||||
type Config struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// Print prints a table description of the graph to the given writer.
|
||||
func (p Config) Print(g *gen.Graph) {
|
||||
for _, n := range g.Nodes {
|
||||
p.node(n)
|
||||
}
|
||||
}
|
||||
|
||||
// Fprint executes "pretty-printer" on the given writer.
|
||||
func Fprint(w io.Writer, g *gen.Graph) {
|
||||
Config{Writer: w}.Print(g)
|
||||
}
|
||||
|
||||
// node returns description of a type. The format of the description is:
|
||||
//
|
||||
// Type:
|
||||
// <Fields Table>
|
||||
//
|
||||
// <Edges Table>
|
||||
func (p Config) node(t *gen.Type) {
|
||||
var (
|
||||
b strings.Builder
|
||||
id []*gen.Field
|
||||
table = tablewriter.NewWriter(&b)
|
||||
header = []string{"Field", "Type", "Unique", "Optional", "Nillable", "Default", "UpdateDefault", "Immutable", "StructTag", "Validators", "Comment"}
|
||||
)
|
||||
b.WriteString(t.Name + ":\n")
|
||||
table.Options(
|
||||
tablewriter.WithHeaderConfig(tw.CellConfig{
|
||||
Padding: tw.CellPadding{
|
||||
Global: tw.Padding{
|
||||
Left: tw.Space,
|
||||
Right: tw.Space,
|
||||
},
|
||||
},
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoFormat: tw.Off,
|
||||
},
|
||||
}),
|
||||
tablewriter.WithRendition(tw.Rendition{
|
||||
Symbols: tw.NewSymbols(tw.StyleASCII),
|
||||
}),
|
||||
)
|
||||
table.Header(header)
|
||||
var alignment = make([]tw.Align, 0)
|
||||
if t.ID != nil {
|
||||
id = append(id, t.ID)
|
||||
}
|
||||
for _, f := range append(id, t.Fields...) {
|
||||
v := reflect.ValueOf(*f)
|
||||
row := make([]string, len(header))
|
||||
for i := 0; i < len(row)-1; i++ {
|
||||
field := v.FieldByNameFunc(func(name string) bool {
|
||||
// The first field is mapped from "Name" to "Field".
|
||||
return name == "Name" && i == 0 || name == header[i]
|
||||
})
|
||||
row[i] = fmt.Sprint(field.Interface())
|
||||
_, err := strconv.Atoi(row[i])
|
||||
if err == nil {
|
||||
alignment = append(alignment, tw.AlignRight)
|
||||
} else {
|
||||
alignment = append(alignment, tw.AlignLeft)
|
||||
}
|
||||
}
|
||||
row[len(row)-1] = f.Comment()
|
||||
_ = table.Append(row)
|
||||
table.Options(
|
||||
tablewriter.WithRowAlignmentConfig(
|
||||
tw.CellAlignment{PerColumn: alignment},
|
||||
),
|
||||
)
|
||||
}
|
||||
err := table.Render()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Create new table for edges
|
||||
table = tablewriter.NewWriter(&b)
|
||||
table.Options(
|
||||
tablewriter.WithHeaderConfig(tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{AutoFormat: tw.Off},
|
||||
Padding: tw.CellPadding{
|
||||
Global: tw.Padding{
|
||||
Left: tw.Space,
|
||||
Right: tw.Space,
|
||||
},
|
||||
},
|
||||
}),
|
||||
tablewriter.WithRendition(tw.Rendition{
|
||||
Symbols: tw.NewSymbols(tw.StyleASCII),
|
||||
}),
|
||||
)
|
||||
table.Header([]string{"Edge", "Type", "Inverse", "BackRef", "Relation", "Unique", "Optional", "Comment"})
|
||||
hasEdges := false
|
||||
for _, e := range t.Edges {
|
||||
hasEdges = true
|
||||
err := table.Append([]string{
|
||||
e.Name,
|
||||
e.Type.Name,
|
||||
strconv.FormatBool(e.IsInverse()),
|
||||
e.Inverse,
|
||||
e.Rel.Type.String(),
|
||||
strconv.FormatBool(e.Unique),
|
||||
strconv.FormatBool(e.Optional),
|
||||
e.Comment(),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if hasEdges {
|
||||
err := table.Render()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
io.WriteString(p, strings.ReplaceAll(b.String(), "\n", "\n\t")+"\n")
|
||||
}
|
||||
177
cmd/internal/printer/printer_test.go
Normal file
177
cmd/internal/printer/printer_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package printer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/entc/gen"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrinter_Print(t *testing.T) {
|
||||
tests := []struct {
|
||||
input *gen.Graph
|
||||
out string
|
||||
}{
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
|
||||
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
|
||||
{Name: "created_at", Type: &field.TypeInfo{Type: field.TypeTime}, Nillable: true, Immutable: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 1 | |
|
||||
| age | int | false | false | true | false | false | false | | 0 | |
|
||||
| created_at | time.Time | false | false | true | false | false | true | | 0 | |
|
||||
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| groups | Group | false | | M2M | false | true | |
|
||||
| spouse | User | false | | O2O | true | false | |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
|
||||
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
|
||||
},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 1 | |
|
||||
| age | int | false | false | true | false | false | false | | 0 | |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| groups | Group | false | | M2M | false | true | |
|
||||
| spouse | User | false | | O2O | true | false | |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
|
||||
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
|
||||
},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Group",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}},
|
||||
},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "users", Type: &gen.Type{Name: "User"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 1 | |
|
||||
| age | int | false | false | true | false | false | false | | 0 | |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| groups | Group | false | | M2M | false | true | |
|
||||
| spouse | User | false | | O2O | true | false | |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
Group:
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 0 | |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+-------+------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+-------+------+---------+---------+----------+--------+----------+---------+
|
||||
| users | User | false | | M2M | false | true | |
|
||||
+-------+------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
b := &strings.Builder{}
|
||||
Fprint(b, tt.input)
|
||||
assert.Equal(t, tt.out, "\n"+b.String())
|
||||
}
|
||||
}
|
||||
208
dialect/dialect.go
Normal file
208
dialect/dialect.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package dialect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Dialect names for external usage.
|
||||
const (
|
||||
MySQL = "mysql"
|
||||
SQLite = "sqlite3"
|
||||
Postgres = "postgres"
|
||||
Gremlin = "gremlin"
|
||||
)
|
||||
|
||||
// ExecQuerier wraps the 2 database operations.
|
||||
type ExecQuerier interface {
|
||||
// Exec executes a query that does not return records. For example, in SQL, INSERT or UPDATE.
|
||||
// It scans the result into the pointer v. For SQL drivers, it is dialect/sql.Result.
|
||||
Exec(ctx context.Context, query string, args, v any) error
|
||||
// Query executes a query that returns rows, typically a SELECT in SQL.
|
||||
// It scans the result into the pointer v. For SQL drivers, it is *dialect/sql.Rows.
|
||||
Query(ctx context.Context, query string, args, v any) error
|
||||
}
|
||||
|
||||
// Driver is the interface that wraps all necessary operations for ent clients.
|
||||
type Driver interface {
|
||||
ExecQuerier
|
||||
// Tx starts and returns a new transaction.
|
||||
// The provided context is used until the transaction is committed or rolled back.
|
||||
Tx(context.Context) (Tx, error)
|
||||
// Close closes the underlying connection.
|
||||
Close() error
|
||||
// Dialect returns the dialect name of the driver.
|
||||
Dialect() string
|
||||
}
|
||||
|
||||
// Tx wraps the Exec and Query operations in transaction.
|
||||
type Tx interface {
|
||||
ExecQuerier
|
||||
driver.Tx
|
||||
}
|
||||
|
||||
type nopTx struct {
|
||||
Driver
|
||||
}
|
||||
|
||||
func (nopTx) Commit() error { return nil }
|
||||
func (nopTx) Rollback() error { return nil }
|
||||
|
||||
// NopTx returns a Tx with a no-op Commit / Rollback methods wrapping
|
||||
// the provided Driver d.
|
||||
func NopTx(d Driver) Tx {
|
||||
return nopTx{d}
|
||||
}
|
||||
|
||||
// DebugDriver is a driver that logs all driver operations.
|
||||
type DebugDriver struct {
|
||||
Driver // underlying driver.
|
||||
log func(context.Context, ...any) // log function. defaults to log.Println.
|
||||
}
|
||||
|
||||
// Debug gets a driver and an optional logging function, and returns
|
||||
// a new debugged-driver that prints all outgoing operations.
|
||||
func Debug(d Driver, logger ...func(...any)) Driver {
|
||||
logf := log.Println
|
||||
if len(logger) == 1 {
|
||||
logf = logger[0]
|
||||
}
|
||||
drv := &DebugDriver{d, func(_ context.Context, v ...any) { logf(v...) }}
|
||||
return drv
|
||||
}
|
||||
|
||||
// DebugWithContext gets a driver and a logging function, and returns
|
||||
// a new debugged-driver that prints all outgoing operations with context.
|
||||
func DebugWithContext(d Driver, logger func(context.Context, ...any)) Driver {
|
||||
drv := &DebugDriver{d, logger}
|
||||
return drv
|
||||
}
|
||||
|
||||
// Exec logs its params and calls the underlying driver Exec method.
|
||||
func (d *DebugDriver) Exec(ctx context.Context, query string, args, v any) error {
|
||||
d.log(ctx, fmt.Sprintf("driver.Exec: query=%v args=%v", query, args))
|
||||
return d.Driver.Exec(ctx, query, args, v)
|
||||
}
|
||||
|
||||
// ExecContext logs its params and calls the underlying driver ExecContext method if it is supported.
|
||||
func (d *DebugDriver) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
||||
drv, ok := d.Driver.(interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Driver.ExecContext is not supported")
|
||||
}
|
||||
d.log(ctx, fmt.Sprintf("driver.ExecContext: query=%v args=%v", query, args))
|
||||
return drv.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// Query logs its params and calls the underlying driver Query method.
|
||||
func (d *DebugDriver) Query(ctx context.Context, query string, args, v any) error {
|
||||
d.log(ctx, fmt.Sprintf("driver.Query: query=%v args=%v", query, args))
|
||||
return d.Driver.Query(ctx, query, args, v)
|
||||
}
|
||||
|
||||
// QueryContext logs its params and calls the underlying driver QueryContext method if it is supported.
|
||||
func (d *DebugDriver) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
|
||||
drv, ok := d.Driver.(interface {
|
||||
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Driver.QueryContext is not supported")
|
||||
}
|
||||
d.log(ctx, fmt.Sprintf("driver.QueryContext: query=%v args=%v", query, args))
|
||||
return drv.QueryContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// Tx adds an log-id for the transaction and calls the underlying driver Tx command.
|
||||
func (d *DebugDriver) Tx(ctx context.Context) (Tx, error) {
|
||||
tx, err := d.Driver.Tx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := uuid.New().String()
|
||||
d.log(ctx, fmt.Sprintf("driver.Tx(%s): started", id))
|
||||
return &DebugTx{tx, id, d.log, ctx}, nil
|
||||
}
|
||||
|
||||
// BeginTx adds an log-id for the transaction and calls the underlying driver BeginTx command if it is supported.
|
||||
func (d *DebugDriver) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) {
|
||||
drv, ok := d.Driver.(interface {
|
||||
BeginTx(context.Context, *sql.TxOptions) (Tx, error)
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Driver.BeginTx is not supported")
|
||||
}
|
||||
tx, err := drv.BeginTx(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := uuid.New().String()
|
||||
d.log(ctx, fmt.Sprintf("driver.BeginTx(%s): started", id))
|
||||
return &DebugTx{tx, id, d.log, ctx}, nil
|
||||
}
|
||||
|
||||
// DebugTx is a transaction implementation that logs all transaction operations.
|
||||
type DebugTx struct {
|
||||
Tx // underlying transaction.
|
||||
id string // transaction logging id.
|
||||
log func(context.Context, ...any) // log function. defaults to fmt.Println.
|
||||
ctx context.Context // underlying transaction context.
|
||||
}
|
||||
|
||||
// Exec logs its params and calls the underlying transaction Exec method.
|
||||
func (d *DebugTx) Exec(ctx context.Context, query string, args, v any) error {
|
||||
d.log(ctx, fmt.Sprintf("Tx(%s).Exec: query=%v args=%v", d.id, query, args))
|
||||
return d.Tx.Exec(ctx, query, args, v)
|
||||
}
|
||||
|
||||
// ExecContext logs its params and calls the underlying transaction ExecContext method if it is supported.
|
||||
func (d *DebugTx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
||||
drv, ok := d.Tx.(interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Tx.ExecContext is not supported")
|
||||
}
|
||||
d.log(ctx, fmt.Sprintf("Tx(%s).ExecContext: query=%v args=%v", d.id, query, args))
|
||||
return drv.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// Query logs its params and calls the underlying transaction Query method.
|
||||
func (d *DebugTx) Query(ctx context.Context, query string, args, v any) error {
|
||||
d.log(ctx, fmt.Sprintf("Tx(%s).Query: query=%v args=%v", d.id, query, args))
|
||||
return d.Tx.Query(ctx, query, args, v)
|
||||
}
|
||||
|
||||
// QueryContext logs its params and calls the underlying transaction QueryContext method if it is supported.
|
||||
func (d *DebugTx) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
|
||||
drv, ok := d.Tx.(interface {
|
||||
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Tx.QueryContext is not supported")
|
||||
}
|
||||
d.log(ctx, fmt.Sprintf("Tx(%s).QueryContext: query=%v args=%v", d.id, query, args))
|
||||
return drv.QueryContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// Commit logs this step and calls the underlying transaction Commit method.
|
||||
func (d *DebugTx) Commit() error {
|
||||
d.log(d.ctx, fmt.Sprintf("Tx(%s): committed", d.id))
|
||||
return d.Tx.Commit()
|
||||
}
|
||||
|
||||
// Rollback logs this step and calls the underlying transaction Rollback method.
|
||||
func (d *DebugTx) Rollback() error {
|
||||
d.log(d.ctx, fmt.Sprintf("Tx(%s): rollbacked", d.id))
|
||||
return d.Tx.Rollback()
|
||||
}
|
||||
857
dialect/entsql/annotation.go
Normal file
857
dialect/entsql/annotation.go
Normal file
@@ -0,0 +1,857 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package entsql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema"
|
||||
)
|
||||
|
||||
// Annotation is a builtin schema annotation for attaching
|
||||
// SQL metadata to schema objects for both codegen and runtime.
|
||||
type Annotation struct {
|
||||
// The Schema option allows setting the schema which the table belongs to.
|
||||
// Note, this option is no-op for Ent default migration engine. However, schema
|
||||
// extensions (like Atlas) can accept this option and implement it accordingly.
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Schema: "public",
|
||||
// }
|
||||
//
|
||||
Schema string `json:"schema,omitempty"`
|
||||
|
||||
// The Table option allows overriding the default table
|
||||
// name that is generated by ent. For example:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Table: "Users",
|
||||
// }
|
||||
//
|
||||
Table string `json:"table,omitempty"`
|
||||
|
||||
// Charset defines the character-set of the table. For example:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Charset: "utf8mb4",
|
||||
// }
|
||||
//
|
||||
Charset string `json:"charset,omitempty"`
|
||||
|
||||
// Collation defines the collation of the table (a set of rules for comparing
|
||||
// characters in a character set). For example:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Collation: "utf8mb4_bin",
|
||||
// }
|
||||
//
|
||||
Collation string `json:"collation,omitempty"`
|
||||
|
||||
// Default specifies a literal default value of a column. Note that using
|
||||
// this option overrides the default behavior of the code-generation.
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Default: `{"key":"value"}`,
|
||||
// }
|
||||
//
|
||||
Default string `json:"default,omitempty"`
|
||||
|
||||
// DefaultExpr specifies an expression default value of a column. Using this option,
|
||||
// users can define custom expressions to be set as database default values. Note that
|
||||
// using this option overrides the default behavior of the code-generation.
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// DefaultExpr: "CURRENT_TIMESTAMP",
|
||||
// }
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// DefaultExpr: "uuid_generate_v4()",
|
||||
// }
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// DefaultExpr: "(a + b)",
|
||||
// }
|
||||
//
|
||||
DefaultExpr string `json:"default_expr,omitempty"`
|
||||
|
||||
// DefaultExpr specifies an expression default value of a column per dialect.
|
||||
// See, DefaultExpr for full doc.
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// DefaultExprs: map[string]string{
|
||||
// dialect.MySQL: "uuid()",
|
||||
// dialect.Postgres: "uuid_generate_v4",
|
||||
// }
|
||||
//
|
||||
DefaultExprs map[string]string `json:"default_exprs,omitempty"`
|
||||
|
||||
// Options defines the additional table options. For example:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Options: "ENGINE = INNODB",
|
||||
// }
|
||||
//
|
||||
Options string `json:"options,omitempty"`
|
||||
|
||||
// Size defines the column size in the generated schema. For example:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Size: 128,
|
||||
// }
|
||||
//
|
||||
Size int64 `json:"size,omitempty"`
|
||||
|
||||
// WithComments specifies whether fields' comments should
|
||||
// be stored in the database schema as column comments.
|
||||
//
|
||||
// withCommentsEnabled := true
|
||||
// entsql.WithComments{
|
||||
// WithComments: &withCommentsEnabled,
|
||||
// }
|
||||
//
|
||||
WithComments *bool `json:"with_comments,omitempty"`
|
||||
|
||||
// Incremental defines the auto-incremental behavior of a column. For example:
|
||||
//
|
||||
// incrementalEnabled := true
|
||||
// entsql.Annotation{
|
||||
// Incremental: &incrementalEnabled,
|
||||
// }
|
||||
//
|
||||
// By default, this value is nil defaulting to whatever best fits each scenario.
|
||||
//
|
||||
Incremental *bool `json:"incremental,omitempty"`
|
||||
|
||||
// IncrementStart defines the auto-incremental start value of a column. For example:
|
||||
//
|
||||
// incrementStart := 100
|
||||
// entsql.Annotation{
|
||||
// IncrementStart: &incrementStart,
|
||||
// }
|
||||
//
|
||||
// By default, this value is nil defaulting to whatever the database settings are.
|
||||
//
|
||||
IncrementStart *int `json:"increment_start,omitempty"`
|
||||
|
||||
// OnDelete specifies a custom referential action for DELETE operations on parent
|
||||
// table that has matching rows in the child table.
|
||||
//
|
||||
// For example, in order to delete rows from the parent table and automatically delete
|
||||
// their matching rows in the child table, pass the following annotation:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// OnDelete: entsql.Cascade,
|
||||
// }
|
||||
//
|
||||
OnDelete ReferenceOption `json:"on_delete,omitempty"`
|
||||
|
||||
// Check allows injecting custom "DDL" for setting an unnamed "CHECK" clause in "CREATE TABLE".
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Check: "age < 10",
|
||||
// }
|
||||
//
|
||||
Check string `json:"check,omitempty"`
|
||||
|
||||
// Checks allows injecting custom "DDL" for setting named "CHECK" clauses in "CREATE TABLE".
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Checks: map[string]string{
|
||||
// "valid_discount": "price > discount_price",
|
||||
// },
|
||||
// }
|
||||
//
|
||||
Checks map[string]string `json:"checks,omitempty"`
|
||||
|
||||
// Skip indicates that the field or the schema is skipped/ignored during
|
||||
// migration (e.g., defined externally).
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Skip: true,
|
||||
// }
|
||||
//
|
||||
Skip bool `json:"skip,omitempty"`
|
||||
|
||||
// ViewAs allows defining a view for the schema. For example:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// View: "SELECT name FROM users",
|
||||
// }
|
||||
ViewAs string `json:"view_as,omitempty"`
|
||||
|
||||
// ViewFor allows defining a view for the schema per dialect. For example:
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// ViewFor: map[string]string{
|
||||
// dialect.MySQL: "...",
|
||||
// dialect.Postgres: "...",
|
||||
// },
|
||||
// }
|
||||
ViewFor map[string]string `json:"view_for,omitempty"`
|
||||
|
||||
// error occurs during annotation build. This field is not
|
||||
// serialized to JSON and used only by the codegen loader.
|
||||
err error
|
||||
}
|
||||
|
||||
// Name describes the annotation name.
|
||||
func (Annotation) Name() string {
|
||||
return "EntSQL"
|
||||
}
|
||||
|
||||
// The Schema option allows setting the schema which the table belongs to.
|
||||
// Note, this option is no-op for Ent default migration engine. However, schema
|
||||
// extensions (like Atlas) can accept this option and implement it accordingly.
|
||||
//
|
||||
// func (T) Annotations() []schema.Annotation {
|
||||
// return []schema.Annotation{
|
||||
// entsql.Schema("public"),
|
||||
// }
|
||||
// }
|
||||
func Schema(s string) *Annotation {
|
||||
return &Annotation{
|
||||
Schema: s,
|
||||
}
|
||||
}
|
||||
|
||||
// The Table option allows overriding the default table
|
||||
// name that is generated by ent. For example:
|
||||
//
|
||||
// func (T) Annotations() []schema.Annotation {
|
||||
// return []schema.Annotation{
|
||||
// entsql.Table("Users"),
|
||||
// }
|
||||
// }
|
||||
func Table(t string) *Annotation {
|
||||
return &Annotation{
|
||||
Table: t,
|
||||
}
|
||||
}
|
||||
|
||||
// SchemaTable allows setting both schema and table name in one annotation.
|
||||
func SchemaTable(s, t string) *Annotation {
|
||||
return &Annotation{
|
||||
Schema: s,
|
||||
Table: t,
|
||||
}
|
||||
}
|
||||
|
||||
// Check allows injecting custom "DDL" for setting an unnamed "CHECK" clause in "CREATE TABLE".
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Check: "(`age` < 10)",
|
||||
// }
|
||||
func Check(c string) *Annotation {
|
||||
return &Annotation{
|
||||
Check: c,
|
||||
}
|
||||
}
|
||||
|
||||
// Checks allows injecting custom "DDL" for setting named "CHECK" clauses in "CREATE TABLE".
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Checks: map[string]string{
|
||||
// "valid_discount": "price > discount_price",
|
||||
// },
|
||||
// }
|
||||
func Checks(c map[string]string) *Annotation {
|
||||
return &Annotation{
|
||||
Checks: c,
|
||||
}
|
||||
}
|
||||
|
||||
// Skip indicates that the field or the schema is skipped/ignored during
|
||||
// migration (e.g., defined externally).
|
||||
func Skip() *Annotation {
|
||||
return &Annotation{Skip: true}
|
||||
}
|
||||
|
||||
// View specifies the definition of a view.
|
||||
func View(as string) *Annotation {
|
||||
return &Annotation{ViewAs: as}
|
||||
}
|
||||
|
||||
// ViewFor specifies the definition of a view.
|
||||
func ViewFor(dialect string, as func(*sql.Selector)) *Annotation {
|
||||
b := sql.Dialect(dialect).Select()
|
||||
as(b)
|
||||
switch q, args := b.Query(); {
|
||||
case len(args) > 0:
|
||||
return &Annotation{
|
||||
err: fmt.Errorf("entsql: view query should not contain arguments. got: %d", len(args)),
|
||||
}
|
||||
case q == "":
|
||||
return &Annotation{
|
||||
err: errors.New("entsql: view query is empty"),
|
||||
}
|
||||
case b.Err() != nil:
|
||||
return &Annotation{
|
||||
err: b.Err(),
|
||||
}
|
||||
default:
|
||||
return &Annotation{
|
||||
ViewFor: map[string]string{dialect: q},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default specifies a literal default value of a column. Note that using
|
||||
// this option overrides the default behavior of the code-generation.
|
||||
//
|
||||
// entsql.Annotation{
|
||||
// Default: `{"key":"value"}`,
|
||||
// }
|
||||
func Default(literal string) *Annotation {
|
||||
return &Annotation{
|
||||
Default: literal,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultExpr specifies an expression default value for the annotated column.
|
||||
// Using this option, users can define custom expressions to be set as database
|
||||
// default values.Note that using this option overrides the default behavior of
|
||||
// the code-generation.
|
||||
//
|
||||
// field.UUID("id", uuid.Nil).
|
||||
// Default(uuid.New).
|
||||
// Annotations(
|
||||
// entsql.DefaultExpr("uuid_generate_v4()"),
|
||||
// )
|
||||
func DefaultExpr(expr string) *Annotation {
|
||||
return &Annotation{
|
||||
DefaultExpr: expr,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultExprs specifies an expression default value for the annotated
|
||||
// column per dialect. See, DefaultExpr for full doc.
|
||||
//
|
||||
// field.UUID("id", uuid.Nil).
|
||||
// Default(uuid.New).
|
||||
// Annotations(
|
||||
// entsql.DefaultExprs(map[string]string{
|
||||
// dialect.MySQL: "uuid()",
|
||||
// dialect.Postgres: "uuid_generate_v4()",
|
||||
// }),
|
||||
// )
|
||||
func DefaultExprs(exprs map[string]string) *Annotation {
|
||||
return &Annotation{
|
||||
DefaultExprs: exprs,
|
||||
}
|
||||
}
|
||||
|
||||
// WithComments specifies whether fields' comments should
|
||||
// be stored in the database schema as column comments.
|
||||
//
|
||||
// func (T) Annotations() []schema.Annotation {
|
||||
// return []schema.Annotation{
|
||||
// entsql.WithComments(true),
|
||||
// }
|
||||
// }
|
||||
func WithComments(b bool) *Annotation {
|
||||
return &Annotation{
|
||||
WithComments: &b,
|
||||
}
|
||||
}
|
||||
|
||||
// OnDelete specifies a custom referential action for DELETE operations on parent
|
||||
// table that has matching rows in the child table.
|
||||
//
|
||||
// For example, in order to delete rows from the parent table and automatically delete
|
||||
// their matching rows in the child table, pass the following annotation:
|
||||
//
|
||||
// func (T) Annotations() []schema.Annotation {
|
||||
// return []schema.Annotation{
|
||||
// entsql.OnDelete(entsql.Cascade),
|
||||
// }
|
||||
// }
|
||||
func OnDelete(opt ReferenceOption) *Annotation {
|
||||
return &Annotation{
|
||||
OnDelete: opt,
|
||||
}
|
||||
}
|
||||
|
||||
// IncrementStart specifies the starting value for auto-increment columns.
|
||||
//
|
||||
// For example, in order to define the starting value for auto-increment to be 100:
|
||||
//
|
||||
// func (T) Annotations() []schema.Annotation {
|
||||
// return []schema.Annotation{
|
||||
// entsql.IncrementStart(100),
|
||||
// }
|
||||
// }
|
||||
func IncrementStart(i int) *Annotation {
|
||||
return &Annotation{
|
||||
IncrementStart: &i,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge implements the schema.Merger interface.
|
||||
func (a Annotation) Merge(other schema.Annotation) schema.Annotation {
|
||||
var ant Annotation
|
||||
switch other := other.(type) {
|
||||
case Annotation:
|
||||
ant = other
|
||||
case *Annotation:
|
||||
if other != nil {
|
||||
ant = *other
|
||||
}
|
||||
default:
|
||||
return a
|
||||
}
|
||||
if s := ant.Schema; s != "" {
|
||||
a.Schema = s
|
||||
}
|
||||
if t := ant.Table; t != "" {
|
||||
a.Table = t
|
||||
}
|
||||
if c := ant.Charset; c != "" {
|
||||
a.Charset = c
|
||||
}
|
||||
if c := ant.Collation; c != "" {
|
||||
a.Collation = c
|
||||
}
|
||||
if d := ant.Default; d != "" {
|
||||
a.Default = d
|
||||
}
|
||||
if d := ant.DefaultExpr; d != "" {
|
||||
a.DefaultExpr = d
|
||||
}
|
||||
if d := ant.DefaultExprs; d != nil {
|
||||
if a.DefaultExprs == nil {
|
||||
a.DefaultExprs = make(map[string]string)
|
||||
}
|
||||
for dialect, x := range d {
|
||||
a.DefaultExprs[dialect] = x
|
||||
}
|
||||
}
|
||||
if o := ant.Options; o != "" {
|
||||
a.Options = o
|
||||
}
|
||||
if s := ant.Size; s != 0 {
|
||||
a.Size = s
|
||||
}
|
||||
if b := ant.WithComments; b != nil {
|
||||
a.WithComments = b
|
||||
}
|
||||
if i := ant.Incremental; i != nil {
|
||||
a.Incremental = i
|
||||
}
|
||||
if i := ant.IncrementStart; i != nil {
|
||||
a.IncrementStart = i
|
||||
}
|
||||
if od := ant.OnDelete; od != "" {
|
||||
a.OnDelete = od
|
||||
}
|
||||
if c := ant.Check; c != "" {
|
||||
a.Check = c
|
||||
}
|
||||
if checks := ant.Checks; len(checks) > 0 {
|
||||
if a.Checks == nil {
|
||||
a.Checks = make(map[string]string)
|
||||
}
|
||||
for name, check := range checks {
|
||||
a.Checks[name] = check
|
||||
}
|
||||
}
|
||||
if ant.Skip {
|
||||
a.Skip = true
|
||||
}
|
||||
if v := ant.ViewAs; v != "" {
|
||||
a.ViewAs = v
|
||||
}
|
||||
if vf := ant.ViewFor; len(vf) > 0 {
|
||||
if a.ViewFor == nil {
|
||||
a.ViewFor = make(map[string]string)
|
||||
}
|
||||
for dialect, view := range vf {
|
||||
a.ViewFor[dialect] = view
|
||||
}
|
||||
}
|
||||
if ant.err != nil {
|
||||
a.err = errors.Join(a.err, ant.err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Err returns the error that occurred during annotation build, if any.
|
||||
func (a Annotation) Err() error {
|
||||
return a.err
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
schema.Annotation
|
||||
schema.Merger
|
||||
} = (*Annotation)(nil)
|
||||
|
||||
// ReferenceOption for constraint actions.
|
||||
type ReferenceOption string
|
||||
|
||||
// Reference options (actions) specified by ON UPDATE and ON DELETE
|
||||
// subclauses of the FOREIGN KEY clause.
|
||||
const (
|
||||
NoAction ReferenceOption = "NO ACTION"
|
||||
Restrict ReferenceOption = "RESTRICT"
|
||||
Cascade ReferenceOption = "CASCADE"
|
||||
SetNull ReferenceOption = "SET NULL"
|
||||
SetDefault ReferenceOption = "SET DEFAULT"
|
||||
)
|
||||
|
||||
// IndexAnnotation is a builtin schema annotation for attaching
|
||||
// SQL metadata to schema indexes for both codegen and runtime.
|
||||
type IndexAnnotation struct {
|
||||
// Prefix defines a column prefix for a single string column index.
|
||||
// In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("column").
|
||||
// Annotation(entsql.Prefix(100))
|
||||
//
|
||||
// CREATE INDEX `table_column` ON `table`(`column`(100))
|
||||
//
|
||||
Prefix uint
|
||||
|
||||
// PrefixColumns defines column prefixes for a multi-column index.
|
||||
// In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1", "c2", "c3").
|
||||
// Annotation(
|
||||
// entsql.PrefixColumn("c1", 100),
|
||||
// entsql.PrefixColumn("c2", 200),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX `table_c1_c2_c3` ON `table`(`c1`(100), `c2`(200), `c3`)
|
||||
//
|
||||
PrefixColumns map[string]uint
|
||||
|
||||
// Desc defines the DESC clause for a single column index.
|
||||
// In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("column").
|
||||
// Annotation(entsql.Desc())
|
||||
//
|
||||
// CREATE INDEX `table_column` ON `table`(`column` DESC)
|
||||
//
|
||||
Desc bool
|
||||
|
||||
// DescColumns defines the DESC clause for columns in multi-column index.
|
||||
// In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1", "c2", "c3").
|
||||
// Annotation(
|
||||
// entsql.DescColumns("c1", "c2"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX `table_c1_c2_c3` ON `table`(`c1` DESC, `c2` DESC, `c3`)
|
||||
//
|
||||
DescColumns map[string]bool
|
||||
|
||||
// IncludeColumns defines the INCLUDE clause for the index.
|
||||
// Works only in Postgres and its definition is as follows:
|
||||
//
|
||||
// index.Fields("c1").
|
||||
// Annotation(
|
||||
// entsql.IncludeColumns("c2"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX "table_column" ON "table"("c1") INCLUDE ("c2")
|
||||
//
|
||||
IncludeColumns []string
|
||||
|
||||
// Type defines the type of the index.
|
||||
// In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1").
|
||||
// Annotation(
|
||||
// entsql.IndexType("FULLTEXT"),
|
||||
// )
|
||||
//
|
||||
// CREATE FULLTEXT INDEX `table_c1` ON `table`(`c1`)
|
||||
//
|
||||
Type string
|
||||
|
||||
// Types is like the Type option but allows mapping an index-type per dialect.
|
||||
//
|
||||
// index.Fields("c1").
|
||||
// Annotation(
|
||||
// entsql.IndexTypes(map[string]string{
|
||||
// dialect.MySQL: "FULLTEXT",
|
||||
// dialect.Postgres: "GIN",
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
Types map[string]string
|
||||
|
||||
// OpClass defines the operator class for a single string column index.
|
||||
// In PostgreSQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("column").
|
||||
// Annotation(
|
||||
// entsql.IndexType("BRIN"),
|
||||
// entsql.OpClass("int8_bloom_ops"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX "table_column" ON "table" USING BRIN ("column" int8_bloom_ops)
|
||||
//
|
||||
OpClass string
|
||||
|
||||
// OpClassColumns defines operator-classes for a multi-column index.
|
||||
// In PostgreSQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1", "c2", "c3").
|
||||
// Annotation(
|
||||
// entsql.IndexType("BRIN"),
|
||||
// entsql.OpClassColumn("c1", "int8_bloom_ops"),
|
||||
// entsql.OpClassColumn("c2", "int8_minmax_multi_ops(values_per_range=8)"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX "table_column" ON "table" USING BRIN ("c1" int8_bloom_ops, "c2" int8_minmax_multi_ops(values_per_range=8), "c3")
|
||||
//
|
||||
OpClassColumns map[string]string
|
||||
|
||||
// IndexWhere allows configuring partial indexes in SQLite and PostgreSQL.
|
||||
// Read more: https://postgresql.org/docs/current/indexes-partial.html.
|
||||
//
|
||||
// Note that the `WHERE` clause should be defined exactly like it is
|
||||
// stored in the database (i.e. normal form). Read more about this on
|
||||
// the Atlas website: https://atlasgo.io/concepts/dev-database#diffing.
|
||||
//
|
||||
// index.Fields("a").
|
||||
// Annotations(
|
||||
// entsql.IndexWhere("b AND c > 0"),
|
||||
// )
|
||||
// CREATE INDEX "table_a" ON "table"("a") WHERE (b AND c > 0)
|
||||
Where string
|
||||
}
|
||||
|
||||
// Prefix returns a new index annotation with a single string column index.
|
||||
// In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("column").
|
||||
// Annotation(entsql.Prefix(100))
|
||||
//
|
||||
// CREATE INDEX `table_column` ON `table`(`column`(100))
|
||||
func Prefix(prefix uint) *IndexAnnotation {
|
||||
return &IndexAnnotation{
|
||||
Prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
// PrefixColumn returns a new index annotation with column prefix for
|
||||
// multi-column indexes. In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1", "c2", "c3").
|
||||
// Annotation(
|
||||
// entsql.PrefixColumn("c1", 100),
|
||||
// entsql.PrefixColumn("c2", 200),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX `table_c1_c2_c3` ON `table`(`c1`(100), `c2`(200), `c3`)
|
||||
func PrefixColumn(name string, prefix uint) *IndexAnnotation {
|
||||
return &IndexAnnotation{
|
||||
PrefixColumns: map[string]uint{
|
||||
name: prefix,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OpClass defines the operator class for a single string column index.
|
||||
// In PostgreSQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("column").
|
||||
// Annotation(
|
||||
// entsql.IndexType("BRIN"),
|
||||
// entsql.OpClass("int8_bloom_ops"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX "table_column" ON "table" USING BRIN ("column" int8_bloom_ops)
|
||||
func OpClass(op string) *IndexAnnotation {
|
||||
return &IndexAnnotation{
|
||||
OpClass: op,
|
||||
}
|
||||
}
|
||||
|
||||
// OpClassColumn returns a new index annotation with column operator
|
||||
// class for multi-column indexes. In PostgreSQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1", "c2", "c3").
|
||||
// Annotation(
|
||||
// entsql.IndexType("BRIN"),
|
||||
// entsql.OpClassColumn("c1", "int8_bloom_ops"),
|
||||
// entsql.OpClassColumn("c2", "int8_minmax_multi_ops(values_per_range=8)"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX "table_column" ON "table" USING BRIN ("c1" int8_bloom_ops, "c2" int8_minmax_multi_ops(values_per_range=8), "c3")
|
||||
func OpClassColumn(name, op string) *IndexAnnotation {
|
||||
return &IndexAnnotation{
|
||||
OpClassColumns: map[string]string{
|
||||
name: op,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Desc returns a new index annotation with the DESC clause for a
|
||||
// single column index. In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("column").
|
||||
// Annotation(entsql.Desc())
|
||||
//
|
||||
// CREATE INDEX `table_column` ON `table`(`column` DESC)
|
||||
func Desc() *IndexAnnotation {
|
||||
return &IndexAnnotation{
|
||||
Desc: true,
|
||||
}
|
||||
}
|
||||
|
||||
// DescColumns returns a new index annotation with the DESC clause attached to
|
||||
// the columns in the index. In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1", "c2", "c3").
|
||||
// Annotation(
|
||||
// entsql.DescColumns("c1", "c2"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX `table_c1_c2_c3` ON `table`(`c1` DESC, `c2` DESC, `c3`)
|
||||
func DescColumns(names ...string) *IndexAnnotation {
|
||||
ant := &IndexAnnotation{
|
||||
DescColumns: make(map[string]bool, len(names)),
|
||||
}
|
||||
for i := range names {
|
||||
ant.DescColumns[names[i]] = true
|
||||
}
|
||||
return ant
|
||||
}
|
||||
|
||||
// IncludeColumns defines the INCLUDE clause for the index.
|
||||
// Works only in Postgres and its definition is as follows:
|
||||
//
|
||||
// index.Fields("c1").
|
||||
// Annotation(
|
||||
// entsql.IncludeColumns("c2"),
|
||||
// )
|
||||
//
|
||||
// CREATE INDEX "table_column" ON "table"("c1") INCLUDE ("c2")
|
||||
func IncludeColumns(names ...string) *IndexAnnotation {
|
||||
return &IndexAnnotation{IncludeColumns: names}
|
||||
}
|
||||
|
||||
// IndexType defines the type of the index.
|
||||
// In MySQL, the following annotation maps to:
|
||||
//
|
||||
// index.Fields("c1").
|
||||
// Annotation(
|
||||
// entsql.IndexType("FULLTEXT"),
|
||||
// )
|
||||
//
|
||||
// CREATE FULLTEXT INDEX `table_c1` ON `table`(`c1`)
|
||||
func IndexType(t string) *IndexAnnotation {
|
||||
return &IndexAnnotation{Type: t}
|
||||
}
|
||||
|
||||
// IndexTypes is like the Type option but allows mapping an index-type per dialect.
|
||||
//
|
||||
// index.Fields("c1").
|
||||
// Annotations(
|
||||
// entsql.IndexTypes(map[string]string{
|
||||
// dialect.MySQL: "FULLTEXT",
|
||||
// dialect.Postgres: "GIN",
|
||||
// }),
|
||||
// )
|
||||
func IndexTypes(types map[string]string) *IndexAnnotation {
|
||||
return &IndexAnnotation{Types: types}
|
||||
}
|
||||
|
||||
// IndexWhere allows configuring partial indexes in SQLite and PostgreSQL.
|
||||
// Read more: https://postgresql.org/docs/current/indexes-partial.html.
|
||||
//
|
||||
// Note that the `WHERE` clause should be defined exactly like it is
|
||||
// stored in the database (i.e. normal form). Read more about this on the
|
||||
// Atlas website: https://atlasgo.io/concepts/dev-database#diffing.
|
||||
//
|
||||
// index.Fields("a").
|
||||
// Annotations(
|
||||
// entsql.IndexWhere("b AND c > 0"),
|
||||
// )
|
||||
// CREATE INDEX "table_a" ON "table"("a") WHERE (b AND c > 0)
|
||||
func IndexWhere(pred string) *IndexAnnotation {
|
||||
return &IndexAnnotation{Where: pred}
|
||||
}
|
||||
|
||||
// Name describes the annotation name.
|
||||
func (IndexAnnotation) Name() string {
|
||||
return "EntSQLIndexes"
|
||||
}
|
||||
|
||||
// Merge implements the schema.Merger interface.
|
||||
func (a IndexAnnotation) Merge(other schema.Annotation) schema.Annotation {
|
||||
var ant IndexAnnotation
|
||||
switch other := other.(type) {
|
||||
case IndexAnnotation:
|
||||
ant = other
|
||||
case *IndexAnnotation:
|
||||
if other != nil {
|
||||
ant = *other
|
||||
}
|
||||
default:
|
||||
return a
|
||||
}
|
||||
if ant.Prefix != 0 {
|
||||
a.Prefix = ant.Prefix
|
||||
}
|
||||
if ant.PrefixColumns != nil {
|
||||
if a.PrefixColumns == nil {
|
||||
a.PrefixColumns = make(map[string]uint)
|
||||
}
|
||||
for column, prefix := range ant.PrefixColumns {
|
||||
a.PrefixColumns[column] = prefix
|
||||
}
|
||||
}
|
||||
if ant.OpClass != "" {
|
||||
a.OpClass = ant.OpClass
|
||||
}
|
||||
if ant.OpClassColumns != nil {
|
||||
if a.OpClassColumns == nil {
|
||||
a.OpClassColumns = make(map[string]string)
|
||||
}
|
||||
for column, op := range ant.OpClassColumns {
|
||||
a.OpClassColumns[column] = op
|
||||
}
|
||||
}
|
||||
if ant.Desc {
|
||||
a.Desc = ant.Desc
|
||||
}
|
||||
if ant.DescColumns != nil {
|
||||
if a.DescColumns == nil {
|
||||
a.DescColumns = make(map[string]bool)
|
||||
}
|
||||
for column, desc := range ant.DescColumns {
|
||||
a.DescColumns[column] = desc
|
||||
}
|
||||
}
|
||||
if ant.IncludeColumns != nil {
|
||||
a.IncludeColumns = append(a.IncludeColumns, ant.IncludeColumns...)
|
||||
}
|
||||
if ant.Type != "" {
|
||||
a.Type = ant.Type
|
||||
}
|
||||
if ant.Types != nil {
|
||||
if a.Types == nil {
|
||||
a.Types = make(map[string]string)
|
||||
}
|
||||
for dialect, t := range ant.Types {
|
||||
a.Types[dialect] = t
|
||||
}
|
||||
}
|
||||
if ant.Where != "" {
|
||||
a.Where = ant.Where
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
schema.Annotation
|
||||
schema.Merger
|
||||
} = (*IndexAnnotation)(nil)
|
||||
77
dialect/gremlin/client.go
Normal file
77
dialect/gremlin/client.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RoundTripper is an interface representing the ability to execute a
|
||||
// single gremlin transaction, obtaining the Response for a given Request.
|
||||
type RoundTripper interface {
|
||||
RoundTrip(context.Context, *Request) (*Response, error)
|
||||
}
|
||||
|
||||
// The RoundTripperFunc type is an adapter to allow the use of ordinary functions as Gremlin RoundTripper.
|
||||
type RoundTripperFunc func(context.Context, *Request) (*Response, error)
|
||||
|
||||
// RoundTrip calls f(ctx, r).
|
||||
func (f RoundTripperFunc) RoundTrip(ctx context.Context, r *Request) (*Response, error) {
|
||||
return f(ctx, r)
|
||||
}
|
||||
|
||||
// Interceptor provides a hook to intercept the execution of a Gremlin Request.
|
||||
type Interceptor func(RoundTripper) RoundTripper
|
||||
|
||||
// A Client is a gremlin client.
|
||||
type Client struct {
|
||||
// Transport specifies the mechanism by which individual
|
||||
// Gremlin requests are made.
|
||||
Transport RoundTripper
|
||||
}
|
||||
|
||||
// MaxResponseSize defines the maximum response size allowed.
|
||||
const MaxResponseSize = 2 << 20
|
||||
|
||||
// NewClient creates a gremlin client from config and options.
|
||||
func NewClient(cfg Config, opt ...Option) (*Client, error) {
|
||||
return cfg.Build(opt...)
|
||||
}
|
||||
|
||||
// NewHTTPClient creates an http based gremlin client.
|
||||
func NewHTTPClient(url string, client *http.Client) (*Client, error) {
|
||||
transport, err := NewHTTPTransport(url, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{transport}, nil
|
||||
}
|
||||
|
||||
// Do sends a gremlin request and returns a gremlin response.
|
||||
func (c Client) Do(ctx context.Context, req *Request) (*Response, error) {
|
||||
rsp, err := c.Transport.RoundTrip(ctx, req)
|
||||
if err == nil {
|
||||
err = rsp.Err()
|
||||
}
|
||||
|
||||
// If we got an error, and the context has been canceled,
|
||||
// the context's error is probably more useful.
|
||||
if err != nil && ctx.Err() != nil {
|
||||
err = ctx.Err()
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// Query issues an eval request via the Do function.
|
||||
func (c Client) Query(ctx context.Context, query string) (*Response, error) {
|
||||
return c.Do(ctx, NewEvalRequest(query))
|
||||
}
|
||||
|
||||
// Queryf formats a query string and invokes Query.
|
||||
func (c Client) Queryf(ctx context.Context, format string, args ...any) (*Response, error) {
|
||||
return c.Query(ctx, fmt.Sprintf(format, args...))
|
||||
}
|
||||
93
dialect/gremlin/client_test.go
Normal file
93
dialect/gremlin/client_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
var cfg Config
|
||||
cfg.Endpoint.URL, _ = url.Parse("http://gremlin-server/gremlin")
|
||||
c, err := NewClient(cfg)
|
||||
assert.NotNil(t, c)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
type mockRoundTripper struct{ mock.Mock }
|
||||
|
||||
func (m *mockRoundTripper) RoundTrip(ctx context.Context, req *Request) (*Response, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*Response), args.Error(1)
|
||||
}
|
||||
|
||||
func TestClientRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
req, rsp := &Request{}, &Response{}
|
||||
|
||||
var m mockRoundTripper
|
||||
m.On("RoundTrip", ctx, req).
|
||||
Run(func(mock.Arguments) { rsp.Status.Code = StatusSuccess }).
|
||||
Return(rsp, nil).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
response, err := Client{&m}.Do(context.Background(), req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rsp, response)
|
||||
}
|
||||
|
||||
func TestClientResponseError(t *testing.T) {
|
||||
rsp := &Response{}
|
||||
|
||||
var m mockRoundTripper
|
||||
m.On("RoundTrip", mock.Anything, mock.Anything).
|
||||
Run(func(mock.Arguments) { rsp.Status.Code = StatusServerError }).
|
||||
Return(rsp, nil).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
_, err := Client{&m}.Do(context.Background(), nil)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestClientCanceledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var m mockRoundTripper
|
||||
m.On("RoundTrip", ctx, mock.Anything).
|
||||
Run(func(mock.Arguments) { cancel() }).
|
||||
Return(&Response{}, io.ErrUnexpectedEOF).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
_, err := Client{&m}.Query(ctx, "g.E()")
|
||||
assert.EqualError(t, err, context.Canceled.Error())
|
||||
}
|
||||
|
||||
func TestClientQuery(t *testing.T) {
|
||||
rsp := &Response{}
|
||||
rsp.Status.Code = StatusNoContent
|
||||
|
||||
var m mockRoundTripper
|
||||
m.On("RoundTrip", mock.Anything, mock.Anything).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*Request)
|
||||
assert.Equal(t, "g.V(1)", req.Arguments[ArgsGremlin])
|
||||
}).
|
||||
Return(rsp, nil).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
rsp, err := Client{&m}.Queryf(context.Background(), "g.V(%d)", 1)
|
||||
assert.NotNil(t, rsp)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
82
dialect/gremlin/config.go
Normal file
82
dialect/gremlin/config.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type (
|
||||
// Config offers a declarative way to construct a client.
|
||||
Config struct {
|
||||
Endpoint Endpoint `env:"ENDPOINT" long:"endpoint" default:"" description:"gremlin endpoint to connect to"`
|
||||
DisableExpansion bool `env:"DISABLE_EXPANSION" long:"disable-expansion" description:"disable bindings expansion"`
|
||||
}
|
||||
|
||||
// An Option configured client.
|
||||
Option func(*options)
|
||||
|
||||
options struct {
|
||||
interceptors []Interceptor
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Endpoint wraps a url to add flag unmarshalling.
|
||||
Endpoint struct {
|
||||
*url.URL
|
||||
}
|
||||
)
|
||||
|
||||
// WithInterceptor adds interceptors to the client's transport.
|
||||
func WithInterceptor(interceptors ...Interceptor) Option {
|
||||
return func(opts *options) {
|
||||
opts.interceptors = append(opts.interceptors, interceptors...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithHTTPClient assigns underlying http client to be used by http transport.
|
||||
func WithHTTPClient(client *http.Client) Option {
|
||||
return func(opts *options) {
|
||||
opts.httpClient = client
|
||||
}
|
||||
}
|
||||
|
||||
// Build constructs a client from Config.
|
||||
func (cfg Config) Build(opt ...Option) (c *Client, err error) {
|
||||
opts := cfg.buildOptions(opt)
|
||||
switch cfg.Endpoint.Scheme {
|
||||
case "http", "https":
|
||||
c, err = NewHTTPClient(cfg.Endpoint.String(), opts.httpClient)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported endpoint scheme: %s", cfg.Endpoint.Scheme)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := len(opts.interceptors) - 1; i >= 0; i-- {
|
||||
c.Transport = opts.interceptors[i](c.Transport)
|
||||
}
|
||||
if !cfg.DisableExpansion {
|
||||
c.Transport = ExpandBindings(c.Transport)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (Config) buildOptions(opts []Option) options {
|
||||
var o options
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// UnmarshalFlag implements flag.Unmarshaler interface.
|
||||
func (ep *Endpoint) UnmarshalFlag(value string) (err error) {
|
||||
ep.URL, err = url.Parse(value)
|
||||
return
|
||||
}
|
||||
154
dialect/gremlin/config_test.go
Normal file
154
dialect/gremlin/config_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigParsing(t *testing.T) {
|
||||
var cfg Config
|
||||
_, err := flags.ParseArgs(&cfg, []string{
|
||||
"--disable-expansion",
|
||||
"--endpoint", "http://localhost:8182/gremlin",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, cfg.DisableExpansion)
|
||||
assert.Equal(t, "http", cfg.Endpoint.Scheme)
|
||||
assert.Equal(t, "http://localhost:8182/gremlin", cfg.Endpoint.String())
|
||||
|
||||
cfg = Config{}
|
||||
_, err = flags.ParseArgs(&cfg, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg.Endpoint.URL)
|
||||
}
|
||||
|
||||
func TestConfigBuild(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
opts []Option
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "HTTP",
|
||||
cfg: Config{
|
||||
Endpoint: Endpoint{
|
||||
URL: func() *url.URL {
|
||||
u, _ := url.Parse("http://gremlin-server/gremlin")
|
||||
return u
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NoScheme",
|
||||
cfg: Config{
|
||||
Endpoint: Endpoint{
|
||||
URL: &url.URL{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "BadScheme",
|
||||
cfg: Config{
|
||||
Endpoint: Endpoint{
|
||||
URL: &url.URL{
|
||||
Scheme: "bad",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "WithOptions",
|
||||
cfg: Config{
|
||||
Endpoint: Endpoint{
|
||||
URL: func() *url.URL {
|
||||
u, _ := url.Parse("http://gremlin-server/gremlin")
|
||||
return u
|
||||
}(),
|
||||
},
|
||||
DisableExpansion: true,
|
||||
},
|
||||
opts: []Option{WithHTTPClient(&http.Client{})},
|
||||
},
|
||||
{
|
||||
name: "NoExpansion",
|
||||
cfg: Config{
|
||||
Endpoint: Endpoint{
|
||||
URL: &url.URL{
|
||||
Scheme: "bad",
|
||||
},
|
||||
},
|
||||
DisableExpansion: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, err := tc.cfg.Build(tc.opts...)
|
||||
if !tc.wantErr {
|
||||
assert.NotNil(t, client)
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testRoundTripper struct{ mock.Mock }
|
||||
|
||||
func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
args := rt.Called(req)
|
||||
rsp, _ := args.Get(0).(*http.Response)
|
||||
return rsp, args.Error(1)
|
||||
}
|
||||
|
||||
func TestBuildWithHTTPClient(t *testing.T) {
|
||||
var transport testRoundTripper
|
||||
transport.On("RoundTrip", mock.Anything).
|
||||
Return(nil, errors.New("noop")).
|
||||
Once()
|
||||
defer transport.AssertExpectations(t)
|
||||
|
||||
u, err := url.Parse("http://gremlin-server:8182/gremlin")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := Config{Endpoint: Endpoint{u}}.
|
||||
Build(WithHTTPClient(&http.Client{Transport: &transport}))
|
||||
require.NoError(t, err)
|
||||
_, _ = client.Do(context.Background(), NewEvalRequest("g.V()"))
|
||||
}
|
||||
|
||||
func TestExpandOrdering(t *testing.T) {
|
||||
var cfg Config
|
||||
cfg.Endpoint.URL, _ = url.Parse("http://gremlin-server/gremlin")
|
||||
interceptor := func(RoundTripper) RoundTripper {
|
||||
return RoundTripperFunc(func(ctx context.Context, req *Request) (*Response, error) {
|
||||
assert.Equal(t, `g.V().hasLabel("user")`, req.Arguments[ArgsGremlin])
|
||||
assert.Nil(t, req.Arguments[ArgsBindings])
|
||||
return nil, errors.New("noop")
|
||||
})
|
||||
}
|
||||
c, err := cfg.Build(WithInterceptor(interceptor))
|
||||
require.NoError(t, err)
|
||||
req := NewEvalRequest("g.V().hasLabel($1)", WithBindings(map[string]any{"$1": "user"}))
|
||||
_, _ = c.Do(context.Background(), req)
|
||||
}
|
||||
58
dialect/gremlin/driver.go
Normal file
58
dialect/gremlin/driver.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/gremlin/graph/dsl"
|
||||
)
|
||||
|
||||
// Driver is a dialect.Driver implementation for TinkerPop gremlin.
|
||||
type Driver struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// NewDriver returns a new dialect.Driver implementation for gremlin.
|
||||
func NewDriver(c *Client) *Driver {
|
||||
c.Transport = ExpandBindings(c.Transport)
|
||||
return &Driver{c}
|
||||
}
|
||||
|
||||
// Dialect implements the dialect.Dialect method.
|
||||
func (Driver) Dialect() string { return dialect.Gremlin }
|
||||
|
||||
// Exec implements the dialect.Exec method.
|
||||
func (c *Driver) Exec(ctx context.Context, query string, args, v any) error {
|
||||
vr, ok := v.(*Response)
|
||||
if !ok {
|
||||
return fmt.Errorf("dialect/gremlin: invalid type %T. expect *gremlin.Response", v)
|
||||
}
|
||||
bindings, ok := args.(dsl.Bindings)
|
||||
if !ok {
|
||||
return fmt.Errorf("dialect/gremlin: invalid type %T. expect map[string]any for bindings", args)
|
||||
}
|
||||
res, err := c.Do(ctx, NewEvalRequest(query, WithBindings(bindings)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*vr = *res
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query implements the dialect.Query method.
|
||||
func (c *Driver) Query(ctx context.Context, query string, args, v any) error {
|
||||
return c.Exec(ctx, query, args, v)
|
||||
}
|
||||
|
||||
// Close is a nop close call. It should close the connection in case of WS client.
|
||||
func (Driver) Close() error { return nil }
|
||||
|
||||
// Tx returns a nop transaction.
|
||||
func (c *Driver) Tx(context.Context) (dialect.Tx, error) { return dialect.NopTx(c), nil }
|
||||
|
||||
var _ dialect.Driver = (*Driver)(nil)
|
||||
99
dialect/gremlin/encoding/graphson/bench_test.go
Normal file
99
dialect/gremlin/encoding/graphson/bench_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
type book struct {
|
||||
ID string `json:"id" graphson:"g:UUID"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Pages int `json:"num_pages"`
|
||||
Chapters []string `json:"chapters"`
|
||||
}
|
||||
|
||||
func generateObject() *book {
|
||||
return &book{
|
||||
ID: "21d5dcbf-1fd4-493e-9b74-d6c429f9e4a5",
|
||||
Title: "The Art of Computer Programming, Vol. 2",
|
||||
Author: "Donald E. Knuth",
|
||||
Pages: 784,
|
||||
Chapters: []string{"Random numbers", "Arithmetic"},
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalObject(b *testing.B) {
|
||||
obj := generateObject()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := Marshal(obj)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalObject(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
out, err := Marshal(generateObject())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
obj := &book{}
|
||||
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = Unmarshal(out, obj)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalInterface(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
data, err := jsoniter.Marshal(generateObject())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var obj any
|
||||
if err = jsoniter.Unmarshal(data, &obj); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err = Marshal(obj)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalInterface(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
data, err := Marshal(generateObject())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var obj any
|
||||
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
54
dialect/gremlin/encoding/graphson/common_test.go
Normal file
54
dialect/gremlin/encoding/graphson/common_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type mocker struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Encode belongs to jsoniter.ValEncoder interface.
|
||||
func (m *mocker) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
m.Called(ptr, stream)
|
||||
}
|
||||
|
||||
// IsEmpty belongs to jsoniter.ValEncoder interface.
|
||||
func (m *mocker) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
args := m.Called(ptr)
|
||||
return args.Bool(0)
|
||||
}
|
||||
|
||||
// Decode implements jsoniter.ValDecoder interface.
|
||||
func (m *mocker) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
m.Called(ptr, iter)
|
||||
}
|
||||
|
||||
// CheckType implements typeChecker interface.
|
||||
func (m *mocker) CheckType(typ Type) error {
|
||||
args := m.Called(typ)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// MarshalGraphson implements Marshaler interface.
|
||||
func (m *mocker) MarshalGraphson() ([]byte, error) {
|
||||
args := m.Called()
|
||||
data, err := args.Get(0), args.Error(1)
|
||||
if data == nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.([]byte), err
|
||||
}
|
||||
|
||||
// UnmarshalGraphson implements Unmarshaler interface.
|
||||
func (m *mocker) UnmarshalGraphson(data []byte) error {
|
||||
args := m.Called(data)
|
||||
return args.Error(0)
|
||||
}
|
||||
109
dialect/gremlin/encoding/graphson/decode.go
Normal file
109
dialect/gremlin/encoding/graphson/decode.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
type decodeExtension struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
// Unmarshal parses the graphson encoded data and stores the result
|
||||
// in the value pointed to by v.
|
||||
func Unmarshal(data []byte, v any) error {
|
||||
return config.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// UnmarshalFromString parses the graphson encoded str and stores the result
|
||||
// in the value pointed to by v.
|
||||
func UnmarshalFromString(str string, v any) error {
|
||||
return config.UnmarshalFromString(str, v)
|
||||
}
|
||||
|
||||
// Decoder defines a graphson decoder.
|
||||
type Decoder interface {
|
||||
Decode(any) error
|
||||
}
|
||||
|
||||
// NewDecoder create a graphson decoder.
|
||||
func NewDecoder(r io.Reader) Decoder {
|
||||
return config.NewDecoder(r)
|
||||
}
|
||||
|
||||
// Unmarshaler is the interface implemented by types
|
||||
// that can unmarshal a graphson description of themselves.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalGraphson([]byte) error
|
||||
}
|
||||
|
||||
// UpdateStructDescriptor decorates struct field encoders for graphson tags.
|
||||
func (ext decodeExtension) UpdateStructDescriptor(desc *jsoniter.StructDescriptor) {
|
||||
for _, binding := range desc.Fields {
|
||||
if tag, ok := binding.Field.Tag().Lookup("graphson"); ok && tag != "-" {
|
||||
if dec := ext.DecoratorOfStructField(binding.Decoder, tag); dec != nil {
|
||||
binding.Decoder = dec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDecoder returns a value decoder for type.
|
||||
func (ext decodeExtension) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
if dec := ext.DecoderOfRegistered(typ); dec != nil {
|
||||
return dec
|
||||
}
|
||||
if dec := ext.DecoderOfUnmarshaler(typ); dec != nil {
|
||||
return dec
|
||||
}
|
||||
if dec := ext.DecoderOfNative(typ); dec != nil {
|
||||
return dec
|
||||
}
|
||||
switch typ.Kind() {
|
||||
case reflect.Array:
|
||||
return ext.DecoderOfArray(typ)
|
||||
case reflect.Slice:
|
||||
return ext.DecoderOfSlice(typ)
|
||||
case reflect.Map:
|
||||
return ext.DecoderOfMap(typ)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecorateDecoder decorates an passed in value decoder for type.
|
||||
func (ext decodeExtension) DecorateDecoder(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
if dec := ext.DecoratorOfRegistered(dec); dec != nil {
|
||||
return dec
|
||||
}
|
||||
if dec := ext.DecoratorOfUnmarshaler(typ, dec); dec != nil {
|
||||
return dec
|
||||
}
|
||||
if dec := ext.DecoratorOfTyper(typ, dec); dec != nil {
|
||||
return dec
|
||||
}
|
||||
if dec := ext.DecoratorOfNative(typ, dec); dec != nil {
|
||||
return dec
|
||||
}
|
||||
switch typ.Kind() {
|
||||
case reflect.Ptr, reflect.Struct:
|
||||
return dec
|
||||
case reflect.Interface:
|
||||
return ext.DecoratorOfInterface(typ, dec)
|
||||
case reflect.Slice:
|
||||
return ext.DecoratorOfSlice(typ, dec)
|
||||
case reflect.Array:
|
||||
return ext.DecoratorOfArray(dec)
|
||||
case reflect.Map:
|
||||
return ext.DecoratorOfMap(dec)
|
||||
default:
|
||||
return ext.DecoderOfError("graphson: unsupported type: %s", typ.String())
|
||||
}
|
||||
}
|
||||
5
dialect/gremlin/encoding/graphson/decode_test.go
Normal file
5
dialect/gremlin/encoding/graphson/decode_test.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
98
dialect/gremlin/encoding/graphson/encode.go
Normal file
98
dialect/gremlin/encoding/graphson/encode.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
type encodeExtension struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
// Marshal returns the graphson encoding of v.
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
return config.Marshal(v)
|
||||
}
|
||||
|
||||
// MarshalToString returns the graphson encoding of v as string.
|
||||
func MarshalToString(v any) (string, error) {
|
||||
return config.MarshalToString(v)
|
||||
}
|
||||
|
||||
// Encoder defines a graphson encoder.
|
||||
type Encoder interface {
|
||||
Encode(any) error
|
||||
}
|
||||
|
||||
// NewEncoder create a graphson encoder.
|
||||
func NewEncoder(w io.Writer) Encoder {
|
||||
return config.NewEncoder(w)
|
||||
}
|
||||
|
||||
// Marshaler is the interface implemented by types that
|
||||
// can marshal themselves as graphson.
|
||||
type Marshaler interface {
|
||||
MarshalGraphson() ([]byte, error)
|
||||
}
|
||||
|
||||
// UpdateStructDescriptor decorates struct field encoders for graphson tags.
|
||||
func (ext encodeExtension) UpdateStructDescriptor(desc *jsoniter.StructDescriptor) {
|
||||
for _, binding := range desc.Fields {
|
||||
if tag, ok := binding.Field.Tag().Lookup("graphson"); ok && tag != "-" {
|
||||
if enc := ext.DecoratorOfStructField(binding.Encoder, tag); enc != nil {
|
||||
binding.Encoder = enc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateEncoder returns a value encoder for type.
|
||||
func (ext encodeExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
if enc := ext.EncoderOfRegistered(typ); enc != nil {
|
||||
return enc
|
||||
}
|
||||
if enc := ext.EncoderOfNative(typ); enc != nil {
|
||||
return enc
|
||||
}
|
||||
switch typ.Kind() {
|
||||
case reflect.Map:
|
||||
return ext.EncoderOfMap(typ)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecorateEncoder decorates an passed in value encoder for type.
|
||||
func (ext encodeExtension) DecorateEncoder(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
if enc := ext.DecoratorOfRegistered(enc); enc != nil {
|
||||
return enc
|
||||
}
|
||||
if enc := ext.DecoratorOfMarshaler(typ, enc); enc != nil {
|
||||
return enc
|
||||
}
|
||||
if enc := ext.DecoratorOfTyper(typ, enc); enc != nil {
|
||||
return enc
|
||||
}
|
||||
if enc := ext.DecoratorOfNative(typ, enc); enc != nil {
|
||||
return enc
|
||||
}
|
||||
switch typ.Kind() {
|
||||
case reflect.Ptr, reflect.Interface, reflect.Struct:
|
||||
return enc
|
||||
case reflect.Array:
|
||||
return ext.DecoratorOfArray(enc)
|
||||
case reflect.Slice:
|
||||
return ext.DecoratorOfSlice(typ, enc)
|
||||
case reflect.Map:
|
||||
return ext.DecoratorOfMap(enc)
|
||||
default:
|
||||
return ext.EncoderOfError("graphson: unsupported type: %s", typ.String())
|
||||
}
|
||||
}
|
||||
16
dialect/gremlin/encoding/graphson/encode_test.go
Normal file
16
dialect/gremlin/encoding/graphson/encode_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncodeUnsupportedType(t *testing.T) {
|
||||
_, err := Marshal(func() {})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
45
dialect/gremlin/encoding/graphson/error.go
Normal file
45
dialect/gremlin/encoding/graphson/error.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// EncoderOfError returns a value encoder which always fails to encode.
|
||||
func (encodeExtension) EncoderOfError(format string, args ...any) jsoniter.ValEncoder {
|
||||
return decoratorOfError(format, args...)
|
||||
}
|
||||
|
||||
// DecoderOfError returns a value decoder which always fails to decode.
|
||||
func (decodeExtension) DecoderOfError(format string, args ...any) jsoniter.ValDecoder {
|
||||
return decoratorOfError(format, args...)
|
||||
}
|
||||
|
||||
func decoratorOfError(format string, args ...any) errorCodec {
|
||||
err := fmt.Errorf(format, args...)
|
||||
return errorCodec{err}
|
||||
}
|
||||
|
||||
type errorCodec struct{ error }
|
||||
|
||||
func (ec errorCodec) Encode(_ unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
if stream.Error == nil {
|
||||
stream.Error = ec.error
|
||||
}
|
||||
}
|
||||
|
||||
func (errorCodec) IsEmpty(unsafe.Pointer) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ec errorCodec) Decode(_ unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
if iter.Error == nil {
|
||||
iter.Error = ec.error
|
||||
}
|
||||
}
|
||||
30
dialect/gremlin/encoding/graphson/error_test.go
Normal file
30
dialect/gremlin/encoding/graphson/error_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorCodec(t *testing.T) {
|
||||
codec := errorCodec{errors.New("codec error")}
|
||||
assert.False(t, codec.IsEmpty(nil))
|
||||
|
||||
var buf bytes.Buffer
|
||||
stream := config.BorrowStream(&buf)
|
||||
defer config.ReturnStream(stream)
|
||||
codec.Encode(nil, stream)
|
||||
assert.Empty(t, buf.Bytes())
|
||||
assert.EqualError(t, stream.Error, codec.Error())
|
||||
|
||||
iter := config.BorrowIterator([]byte{})
|
||||
defer config.ReturnIterator(iter)
|
||||
codec.Decode(nil, iter)
|
||||
assert.EqualError(t, iter.Error, codec.Error())
|
||||
}
|
||||
88
dialect/gremlin/encoding/graphson/extension.go
Normal file
88
dialect/gremlin/encoding/graphson/extension.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
var (
|
||||
typeEncoders = map[string]jsoniter.ValEncoder{}
|
||||
typeDecoders = map[string]jsoniter.ValDecoder{}
|
||||
)
|
||||
|
||||
// RegisterTypeEncoder register type encoder for typ.
|
||||
func RegisterTypeEncoder(typ string, enc jsoniter.ValEncoder) {
|
||||
typeEncoders[typ] = enc
|
||||
}
|
||||
|
||||
// RegisterTypeDecoder register type decoder for typ.
|
||||
func RegisterTypeDecoder(typ string, dec jsoniter.ValDecoder) {
|
||||
typeDecoders[typ] = dec
|
||||
}
|
||||
|
||||
type registeredEncoder struct{ jsoniter.ValEncoder }
|
||||
|
||||
// EncoderOfRegistered returns a value encoder of a registered type.
|
||||
func (encodeExtension) EncoderOfRegistered(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
enc := typeEncoders[typ.String()]
|
||||
if enc != nil {
|
||||
return registeredEncoder{enc}
|
||||
}
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
ptrType := typ.(reflect2.PtrType)
|
||||
enc := typeEncoders[ptrType.Elem().String()]
|
||||
if enc != nil {
|
||||
return registeredEncoder{
|
||||
ValEncoder: &jsoniter.OptionalEncoder{
|
||||
ValueEncoder: enc,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecoratorOfRegistered decorates a value encoder of a registered type.
|
||||
func (encodeExtension) DecoratorOfRegistered(enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
if _, ok := enc.(registeredEncoder); ok {
|
||||
return enc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type registeredDecoder struct{ jsoniter.ValDecoder }
|
||||
|
||||
// DecoderOfRegistered returns a value decoder of a registered type.
|
||||
func (decodeExtension) DecoderOfRegistered(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
dec := typeDecoders[typ.String()]
|
||||
if dec != nil {
|
||||
return registeredDecoder{dec}
|
||||
}
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
ptrType := typ.(reflect2.PtrType)
|
||||
dec := typeDecoders[ptrType.Elem().String()]
|
||||
if dec != nil {
|
||||
return registeredDecoder{
|
||||
ValDecoder: &jsoniter.OptionalDecoder{
|
||||
ValueType: ptrType.Elem(),
|
||||
ValueDecoder: dec,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecoratorOfRegistered decorates a value decoder of a registered type.
|
||||
func (decodeExtension) DecoratorOfRegistered(dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
if _, ok := dec.(registeredDecoder); ok {
|
||||
return dec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
dialect/gremlin/encoding/graphson/init.go
Normal file
16
dialect/gremlin/encoding/graphson/init.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var config = jsoniter.Config{}.Froze()
|
||||
|
||||
func init() {
|
||||
config.RegisterExtension(&encodeExtension{})
|
||||
config.RegisterExtension(&decodeExtension{})
|
||||
}
|
||||
153
dialect/gremlin/encoding/graphson/interface.go
Normal file
153
dialect/gremlin/encoding/graphson/interface.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
// DecoratorOfInterface decorates a value decoder of an interface type.
|
||||
func (decodeExtension) DecoratorOfInterface(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
if _, ok := typ.(*reflect2.UnsafeEFaceType); ok {
|
||||
return efaceDecoder{typ, dec}
|
||||
}
|
||||
return dec
|
||||
}
|
||||
|
||||
type efaceDecoder struct {
|
||||
typ reflect2.Type
|
||||
jsoniter.ValDecoder
|
||||
}
|
||||
|
||||
func (dec efaceDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
switch next := iter.WhatIsNext(); next {
|
||||
case jsoniter.StringValue, jsoniter.BoolValue, jsoniter.NilValue:
|
||||
dec.ValDecoder.Decode(ptr, iter)
|
||||
case jsoniter.ObjectValue:
|
||||
dec.decode(ptr, iter)
|
||||
default:
|
||||
iter.ReportError("decode empty interface", fmt.Sprintf("unexpected value type: %d", next))
|
||||
}
|
||||
}
|
||||
|
||||
func (dec efaceDecoder) decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
data := iter.SkipAndReturnBytes()
|
||||
if iter.Error != nil && iter.Error != io.EOF {
|
||||
return
|
||||
}
|
||||
|
||||
rtype, err := dec.reflectBytes(data)
|
||||
if err != nil {
|
||||
iter.ReportError("decode empty interface", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
it := config.BorrowIterator(data)
|
||||
defer config.ReturnIterator(it)
|
||||
|
||||
var val any
|
||||
if rtype != nil {
|
||||
val = rtype.New()
|
||||
it.ReadVal(val)
|
||||
val = rtype.Indirect(val)
|
||||
} else {
|
||||
if jsoniter.Get(data, TypeKey).LastError() == nil {
|
||||
vk := jsoniter.Get(data, ValueKey)
|
||||
if vk.LastError() == nil {
|
||||
val = vk.GetInterface()
|
||||
}
|
||||
}
|
||||
if val == nil {
|
||||
val = it.Read()
|
||||
}
|
||||
}
|
||||
|
||||
if it.Error != nil && it.Error != io.EOF {
|
||||
iter.ReportError("decode empty interface", it.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// nolint: gas
|
||||
dec.typ.UnsafeSet(ptr, unsafe.Pointer(&val))
|
||||
}
|
||||
|
||||
func (dec efaceDecoder) reflectBytes(data []byte) (reflect2.Type, error) {
|
||||
typ := Type(jsoniter.Get(data, TypeKey).ToString())
|
||||
rtype := dec.reflectType(typ)
|
||||
if rtype != nil {
|
||||
return rtype, nil
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case listType:
|
||||
return dec.reflectSlice(data)
|
||||
case mapType:
|
||||
return dec.reflectMap(data)
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (efaceDecoder) reflectType(typ Type) reflect2.Type {
|
||||
switch typ {
|
||||
case doubleType:
|
||||
return reflect2.TypeOf(float64(0))
|
||||
case floatType:
|
||||
return reflect2.TypeOf(float32(0))
|
||||
case byteType:
|
||||
return reflect2.TypeOf(uint8(0))
|
||||
case int16Type:
|
||||
return reflect2.TypeOf(int16(0))
|
||||
case int32Type:
|
||||
return reflect2.TypeOf(int32(0))
|
||||
case int64Type, bigIntegerType:
|
||||
return reflect2.TypeOf(int64(0))
|
||||
case byteBufferType:
|
||||
return reflect2.TypeOf([]byte{})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (efaceDecoder) reflectSlice(data []byte) (reflect2.Type, error) {
|
||||
var elem any
|
||||
if err := Unmarshal(data, &[...]*any{&elem}); err != nil {
|
||||
return nil, fmt.Errorf("cannot read first list element: %w", err)
|
||||
}
|
||||
|
||||
if elem == nil {
|
||||
return reflect2.TypeOf([]any{}), nil
|
||||
}
|
||||
|
||||
sliceType := reflect.SliceOf(reflect.TypeOf(elem))
|
||||
return reflect2.Type2(sliceType), nil
|
||||
}
|
||||
|
||||
func (efaceDecoder) reflectMap(data []byte) (reflect2.Type, error) {
|
||||
var key, elem any
|
||||
if err := Unmarshal(
|
||||
bytes.Replace(data, []byte(mapType), []byte(listType), 1),
|
||||
&[...]*any{&key, &elem},
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal first map item: %w", err)
|
||||
}
|
||||
|
||||
if key == nil {
|
||||
return reflect2.TypeOf(map[any]any{}), nil
|
||||
} else if elem == nil {
|
||||
return nil, errors.New("expect map element, but found only key")
|
||||
}
|
||||
|
||||
mapType := reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(elem))
|
||||
return reflect2.Type2(mapType), nil
|
||||
}
|
||||
352
dialect/gremlin/encoding/graphson/interface_test.go
Normal file
352
dialect/gremlin/encoding/graphson/interface_test.go
Normal file
@@ -0,0 +1,352 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecodeInterface(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Boolean",
|
||||
in: "false",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "String",
|
||||
in: `"str"`,
|
||||
want: "str",
|
||||
},
|
||||
{
|
||||
name: "Double",
|
||||
in: `{
|
||||
"@type": "g:Double",
|
||||
"@value": 3.14
|
||||
}`,
|
||||
want: float64(3.14),
|
||||
},
|
||||
{
|
||||
name: "Float",
|
||||
in: `{
|
||||
"@type": "g:Float",
|
||||
"@value": -22.567
|
||||
}`,
|
||||
want: float32(-22.567),
|
||||
},
|
||||
{
|
||||
name: "Int32",
|
||||
in: `{
|
||||
"@type": "g:Int32",
|
||||
"@value": 9000
|
||||
}`,
|
||||
want: int32(9000),
|
||||
},
|
||||
{
|
||||
name: "Int64",
|
||||
in: `{
|
||||
"@type": "g:Int64",
|
||||
"@value": 188786
|
||||
}`,
|
||||
want: int64(188786),
|
||||
},
|
||||
{
|
||||
name: "BigInteger",
|
||||
in: `{
|
||||
"@type": "gx:BigInteger",
|
||||
"@value": 352353463712
|
||||
}`,
|
||||
want: int64(352353463712),
|
||||
},
|
||||
{
|
||||
name: "Byte",
|
||||
in: `{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 100
|
||||
}`,
|
||||
want: uint8(100),
|
||||
},
|
||||
{
|
||||
name: "Int16",
|
||||
in: `{
|
||||
"@type": "gx:Int16",
|
||||
"@value": 2000
|
||||
}`,
|
||||
want: int16(2000),
|
||||
},
|
||||
{
|
||||
name: "UnknownType",
|
||||
in: `{
|
||||
"@type": "g:T",
|
||||
"@value": "label"
|
||||
}`,
|
||||
want: "label",
|
||||
},
|
||||
{
|
||||
name: "UntypedArray",
|
||||
in: "[]",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "NoType",
|
||||
in: `{
|
||||
"@typ": "g:Int32",
|
||||
"@value": 345
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "BadObject",
|
||||
in: `{
|
||||
"@type": "g:Int32",
|
||||
"@value": 345
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "BadList",
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@val": 123457990
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "BadMap",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@val": 123457990
|
||||
},
|
||||
"First"
|
||||
]
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "KeyOnlyMap",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": ["Key"]
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var got any
|
||||
err := UnmarshalFromString(tc.in, &got)
|
||||
if !tc.wantErr {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeInterfaceSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want any
|
||||
}{
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": []
|
||||
}`,
|
||||
want: []any{},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": ["x", "y", "z"]
|
||||
}`,
|
||||
want: []string{"x", "y", "z"},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 123457990
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 23456111
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": -687450
|
||||
}
|
||||
]
|
||||
}`,
|
||||
want: []int64{123457990, 23456111, -687450},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "gx:ByteBuffer",
|
||||
"@value": "AQIDBAU="
|
||||
}`,
|
||||
want: []byte{1, 2, 3, 4, 5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var got any
|
||||
err := UnmarshalFromString(tc.in, &got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeInterfaceMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want any
|
||||
}{
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": []
|
||||
}`,
|
||||
want: map[any]any{},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
"Sep",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 9
|
||||
},
|
||||
"Oct",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 10
|
||||
},
|
||||
"Nov",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 11
|
||||
}
|
||||
]
|
||||
}`,
|
||||
want: map[string]int32{
|
||||
"Sep": int32(9),
|
||||
"Oct": int32(10),
|
||||
"Nov": int32(11),
|
||||
},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
"One",
|
||||
{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"Two",
|
||||
{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"Three",
|
||||
{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
want: map[string][]int32{
|
||||
"One": {1},
|
||||
"Two": {2},
|
||||
"Three": {3},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var got any
|
||||
err := UnmarshalFromString(tc.in, &got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeInterfaceObject(t *testing.T) {
|
||||
book := struct {
|
||||
ID string `json:"id" graphson:"g:UUID"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Pages int `json:"num_pages"`
|
||||
Chapters []string `json:"chapters"`
|
||||
}{
|
||||
ID: "21d5dcbf-1fd4-493e-9b74-d6c429f9e4a5",
|
||||
Title: "The Art of Computer Programming, Vol. 2",
|
||||
Author: "Donald E. Knuth",
|
||||
Pages: 784,
|
||||
Chapters: []string{"Random numbers", "Arithmetic"},
|
||||
}
|
||||
data, err := Marshal(book)
|
||||
require.NoError(t, err)
|
||||
|
||||
var v any
|
||||
err = Unmarshal(data, &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
obj := v.(map[string]any)
|
||||
assert.Equal(t, book.ID, obj["id"])
|
||||
assert.Equal(t, book.Title, obj["title"])
|
||||
assert.Equal(t, book.Author, obj["author"])
|
||||
assert.EqualValues(t, book.Pages, obj["num_pages"])
|
||||
assert.ElementsMatch(t, book.Chapters, obj["chapters"])
|
||||
}
|
||||
74
dialect/gremlin/encoding/graphson/lazy.go
Normal file
74
dialect/gremlin/encoding/graphson/lazy.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
// LazyEncoderOf returns a lazy encoder for type.
|
||||
func (encodeExtension) LazyEncoderOf(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
return &lazyEncoder{resolve: func() jsoniter.ValEncoder {
|
||||
return config.EncoderOf(typ)
|
||||
}}
|
||||
}
|
||||
|
||||
// LazyDecoderOf returns a lazy unique decoder for type.
|
||||
func (decodeExtension) LazyDecoderOf(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
return &lazyDecoder{resolve: func() jsoniter.ValDecoder {
|
||||
dec := config.DecoderOf(reflect2.PtrTo(typ))
|
||||
if td, ok := dec.(typeDecoder); ok {
|
||||
td.typeChecker = &uniqueType{elemChecker: td.typeChecker}
|
||||
dec = td
|
||||
}
|
||||
return dec
|
||||
}}
|
||||
}
|
||||
|
||||
type lazyEncoder struct {
|
||||
jsoniter.ValEncoder
|
||||
resolve func() jsoniter.ValEncoder
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (enc *lazyEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
enc.once.Do(func() { enc.ValEncoder = enc.resolve() })
|
||||
enc.ValEncoder.Encode(ptr, stream)
|
||||
}
|
||||
|
||||
func (enc *lazyEncoder) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
enc.once.Do(func() { enc.ValEncoder = enc.resolve() })
|
||||
return enc.ValEncoder.IsEmpty(ptr)
|
||||
}
|
||||
|
||||
type lazyDecoder struct {
|
||||
jsoniter.ValDecoder
|
||||
resolve func() jsoniter.ValDecoder
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (dec *lazyDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
dec.once.Do(func() { dec.ValDecoder = dec.resolve() })
|
||||
dec.ValDecoder.Decode(ptr, iter)
|
||||
}
|
||||
|
||||
type uniqueType struct {
|
||||
typ Type
|
||||
once sync.Once
|
||||
elemChecker typeChecker
|
||||
}
|
||||
|
||||
func (u *uniqueType) CheckType(other Type) error {
|
||||
u.once.Do(func() { u.typ = other })
|
||||
if u.typ != other {
|
||||
return fmt.Errorf("expect type %s, but found %s", u.typ, other)
|
||||
}
|
||||
return u.elemChecker.CheckType(u.typ)
|
||||
}
|
||||
46
dialect/gremlin/encoding/graphson/lazy_test.go
Normal file
46
dialect/gremlin/encoding/graphson/lazy_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestLazyEncode(t *testing.T) {
|
||||
var m mocker
|
||||
m.On("IsEmpty", mock.Anything).Return(false).Once()
|
||||
m.On("Encode", mock.Anything, mock.Anything).Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
var cnt uint32
|
||||
var enc jsoniter.ValEncoder = &lazyEncoder{resolve: func() jsoniter.ValEncoder {
|
||||
assert.Equal(t, uint32(1), atomic.AddUint32(&cnt, 1))
|
||||
return &m
|
||||
}}
|
||||
|
||||
enc.IsEmpty(nil)
|
||||
enc.Encode(nil, nil)
|
||||
}
|
||||
|
||||
func TestLazyDecode(t *testing.T) {
|
||||
var m mocker
|
||||
m.On("Decode", mock.Anything, mock.Anything).Times(3)
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
var cnt uint32
|
||||
var dec jsoniter.ValDecoder = &lazyDecoder{resolve: func() jsoniter.ValDecoder {
|
||||
assert.Equal(t, uint32(1), atomic.AddUint32(&cnt, 1))
|
||||
return &m
|
||||
}}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
dec.Decode(nil, nil)
|
||||
}
|
||||
}
|
||||
116
dialect/gremlin/encoding/graphson/map.go
Normal file
116
dialect/gremlin/encoding/graphson/map.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
// EncoderOfMap returns a value encoder of a map type.
|
||||
func (ext encodeExtension) EncoderOfMap(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
mapType := typ.(reflect2.MapType)
|
||||
return &mapEncoder{
|
||||
mapType: mapType,
|
||||
keyEnc: ext.LazyEncoderOf(mapType.Key()),
|
||||
elemEnc: ext.LazyEncoderOf(mapType.Elem()),
|
||||
}
|
||||
}
|
||||
|
||||
// DecoratorOfMap decorates a value encoder of a map type.
|
||||
func (encodeExtension) DecoratorOfMap(enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
return typeEncoder{enc, mapType}
|
||||
}
|
||||
|
||||
type mapEncoder struct {
|
||||
mapType reflect2.MapType
|
||||
keyEnc jsoniter.ValEncoder
|
||||
elemEnc jsoniter.ValEncoder
|
||||
}
|
||||
|
||||
func (enc *mapEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
iter := enc.mapType.UnsafeIterate(ptr)
|
||||
if !iter.HasNext() {
|
||||
stream.WriteEmptyArray()
|
||||
return
|
||||
}
|
||||
|
||||
stream.WriteArrayStart()
|
||||
for {
|
||||
key, elem := iter.UnsafeNext()
|
||||
enc.keyEnc.Encode(key, stream)
|
||||
stream.WriteMore()
|
||||
enc.elemEnc.Encode(elem, stream)
|
||||
if !iter.HasNext() {
|
||||
break
|
||||
}
|
||||
stream.WriteMore()
|
||||
}
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
|
||||
func (enc *mapEncoder) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
return !enc.mapType.UnsafeIterate(ptr).HasNext()
|
||||
}
|
||||
|
||||
// DecoderOfMap returns a value decoder of a map type.
|
||||
func (ext decodeExtension) DecoderOfMap(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
mapType := typ.(reflect2.MapType)
|
||||
keyType, elemType := mapType.Key(), mapType.Elem()
|
||||
return &mapDecoder{
|
||||
mapType: mapType,
|
||||
keyType: keyType,
|
||||
elemType: elemType,
|
||||
keyDec: ext.LazyDecoderOf(keyType),
|
||||
elemDec: ext.LazyDecoderOf(elemType),
|
||||
}
|
||||
}
|
||||
|
||||
// DecoratorOfMap decorates a value decoder of a map type.
|
||||
func (decodeExtension) DecoratorOfMap(dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
return typeDecoder{dec, mapType}
|
||||
}
|
||||
|
||||
type mapDecoder struct {
|
||||
mapType reflect2.MapType
|
||||
keyType reflect2.Type
|
||||
elemType reflect2.Type
|
||||
keyDec jsoniter.ValDecoder
|
||||
elemDec jsoniter.ValDecoder
|
||||
}
|
||||
|
||||
func (dec *mapDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
mapType := dec.mapType
|
||||
if mapType.UnsafeIsNil(ptr) {
|
||||
mapType.UnsafeSet(ptr, mapType.UnsafeMakeMap(0))
|
||||
}
|
||||
|
||||
var key unsafe.Pointer
|
||||
if !iter.ReadArrayCB(func(iter *jsoniter.Iterator) bool {
|
||||
if key == nil {
|
||||
key = dec.keyType.UnsafeNew()
|
||||
dec.keyDec.Decode(key, iter)
|
||||
return iter.Error == nil
|
||||
}
|
||||
|
||||
elem := dec.elemType.UnsafeNew()
|
||||
dec.elemDec.Decode(elem, iter)
|
||||
if iter.Error != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
mapType.UnsafeSetIndex(ptr, key, elem)
|
||||
key = nil
|
||||
return true
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
iter.ReportError("decode map", "odd number of map items")
|
||||
}
|
||||
}
|
||||
355
dialect/gremlin/encoding/graphson/map_test.go
Normal file
355
dialect/gremlin/encoding/graphson/map_test.go
Normal file
@@ -0,0 +1,355 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncodeMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in any
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
in: map[int32]string{
|
||||
3: "Mar",
|
||||
1: "Jan",
|
||||
2: "Feb",
|
||||
},
|
||||
want: `[
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 1
|
||||
},
|
||||
"Jan",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 2
|
||||
},
|
||||
"Feb",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 3
|
||||
},
|
||||
"Mar"
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "mixed",
|
||||
in: map[string]any{
|
||||
"byte": byte('a'),
|
||||
"string": "str",
|
||||
"slice": []int{1, 2, 3},
|
||||
"map": map[string]int{},
|
||||
},
|
||||
want: `[
|
||||
"byte",
|
||||
{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 97
|
||||
},
|
||||
"string",
|
||||
"str",
|
||||
"slice",
|
||||
{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 1
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 2
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
"map",
|
||||
{
|
||||
"@type": "g:Map",
|
||||
"@value": []
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "struct-key",
|
||||
in: map[struct {
|
||||
K string `json:"key"`
|
||||
}]int32{
|
||||
|
||||
{"result"}: 42,
|
||||
},
|
||||
want: `[
|
||||
{
|
||||
"key": "result"
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 42
|
||||
}
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "nil",
|
||||
in: map[string]uint8(nil),
|
||||
want: "null",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data, err := Marshal(tc.in)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "g:Map", jsoniter.Get(data, "@type").ToString())
|
||||
var want []any
|
||||
err = jsoniter.UnmarshalFromString(tc.want, &want)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, ok := jsoniter.Get(data, "@value").GetInterface().([]any)
|
||||
require.True(t, ok)
|
||||
assert.ElementsMatch(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": []
|
||||
}`,
|
||||
want: map[int]int{},
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 6
|
||||
},
|
||||
"Jun",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 7
|
||||
},
|
||||
"Jul",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 8
|
||||
},
|
||||
"Aug"
|
||||
]
|
||||
}`,
|
||||
want: map[int]string{
|
||||
6: "Jun",
|
||||
7: "Jul",
|
||||
8: "Aug",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
"Sep",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 9
|
||||
},
|
||||
"Oct",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 65
|
||||
},
|
||||
"Oct",
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 10
|
||||
},
|
||||
"Nov",
|
||||
null
|
||||
]
|
||||
}`,
|
||||
want: map[string]*int{
|
||||
"Sep": func() *int { v := 9; return &v }(),
|
||||
"Oct": func() *int { v := 10; return &v }(),
|
||||
"Nov": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
typ := reflect2.TypeOf(tc.want).(reflect2.MapType)
|
||||
got := typ.MakeMap(0)
|
||||
err := UnmarshalFromString(tc.in, got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, typ.Indirect(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeMapIntoNil(t *testing.T) {
|
||||
var got map[int64]int32
|
||||
err := UnmarshalFromString(`{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": -9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 99
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": -99
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 999
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": -999
|
||||
}
|
||||
]
|
||||
}`, &got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[int64]int32{9: -9, 99: -99, 999: -999}, got)
|
||||
}
|
||||
|
||||
func TestDecodeBadMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
}{
|
||||
{
|
||||
name: "BadValue",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": "55"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "NoValue",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 42
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "AlterKeyType",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 42
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 42
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "AlterValType",
|
||||
in: `{
|
||||
"@type": "g:Map",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 9
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 42
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 42
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var v map[int]int
|
||||
err := NewDecoder(strings.NewReader(tc.in)).Decode(&v)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
133
dialect/gremlin/encoding/graphson/marshaler.go
Normal file
133
dialect/gremlin/encoding/graphson/marshaler.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
// DecoratorOfMarshaler decorates a value encoder of a Marshaler interface.
|
||||
func (ext encodeExtension) DecoratorOfMarshaler(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
if typ == marshalerType {
|
||||
enc := marshalerEncoder{enc, typ}
|
||||
return directMarshalerEncoder{enc}
|
||||
}
|
||||
if typ.Implements(marshalerType) {
|
||||
return marshalerEncoder{enc, typ}
|
||||
}
|
||||
ptrType := reflect2.PtrTo(typ)
|
||||
if ptrType.Implements(marshalerType) {
|
||||
ptrEnc := ext.LazyEncoderOf(ptrType)
|
||||
enc := marshalerEncoder{ptrEnc, ptrType}
|
||||
return referenceEncoder{enc}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecoderOfUnmarshaler returns a value decoder of an Unmarshaler interface.
|
||||
func (decodeExtension) DecoderOfUnmarshaler(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
ptrType := reflect2.PtrTo(typ)
|
||||
if ptrType.Implements(unmarshalerType) {
|
||||
return referenceDecoder{
|
||||
unmarshalerDecoder{ptrType},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecoratorOfUnmarshaler decorates a value encoder of an Unmarshaler interface.
|
||||
func (decodeExtension) DecoratorOfUnmarshaler(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
if reflect2.PtrTo(typ).Implements(unmarshalerType) {
|
||||
return dec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
marshalerType = reflect2.TypeOfPtr((*Marshaler)(nil)).Elem()
|
||||
unmarshalerType = reflect2.TypeOfPtr((*Unmarshaler)(nil)).Elem()
|
||||
)
|
||||
|
||||
type marshalerEncoder struct {
|
||||
jsoniter.ValEncoder
|
||||
reflect2.Type
|
||||
}
|
||||
|
||||
func (enc marshalerEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
marshaler := enc.Type.UnsafeIndirect(ptr).(Marshaler)
|
||||
enc.encode(marshaler, stream)
|
||||
}
|
||||
|
||||
func (enc marshalerEncoder) encode(marshaler Marshaler, stream *jsoniter.Stream) {
|
||||
data, err := marshaler.MarshalGraphson()
|
||||
if err != nil {
|
||||
stream.Error = fmt.Errorf("graphson: error calling MarshalGraphson for type %s: %w", enc.Type, err)
|
||||
return
|
||||
}
|
||||
if !config.Valid(data) {
|
||||
stream.Error = fmt.Errorf("graphson: syntax error when marshaling type %s", enc.Type)
|
||||
return
|
||||
}
|
||||
_, stream.Error = stream.Write(data)
|
||||
}
|
||||
|
||||
type directMarshalerEncoder struct {
|
||||
marshalerEncoder
|
||||
}
|
||||
|
||||
func (enc directMarshalerEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
marshaler := *(*Marshaler)(ptr)
|
||||
enc.encode(marshaler, stream)
|
||||
}
|
||||
|
||||
type referenceEncoder struct {
|
||||
jsoniter.ValEncoder
|
||||
}
|
||||
|
||||
func (enc referenceEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
// nolint: gas
|
||||
enc.ValEncoder.Encode(unsafe.Pointer(&ptr), stream)
|
||||
}
|
||||
|
||||
func (enc referenceEncoder) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
// nolint: gas
|
||||
return enc.ValEncoder.IsEmpty(unsafe.Pointer(&ptr))
|
||||
}
|
||||
|
||||
type unmarshalerDecoder struct {
|
||||
reflect2.Type
|
||||
}
|
||||
|
||||
func (dec unmarshalerDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
bytes := iter.SkipAndReturnBytes()
|
||||
if iter.Error != nil && iter.Error != io.EOF {
|
||||
return
|
||||
}
|
||||
|
||||
unmarshaler := dec.UnsafeIndirect(ptr).(Unmarshaler)
|
||||
if err := unmarshaler.UnmarshalGraphson(bytes); err != nil {
|
||||
iter.ReportError(
|
||||
"unmarshal graphson",
|
||||
fmt.Sprintf(
|
||||
"graphson: error calling UnmarshalGraphson for type %s: %s",
|
||||
dec.Type, err,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type referenceDecoder struct {
|
||||
jsoniter.ValDecoder
|
||||
}
|
||||
|
||||
func (dec referenceDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
// nolint: gas
|
||||
dec.ValDecoder.Decode(unsafe.Pointer(&ptr), iter)
|
||||
}
|
||||
101
dialect/gremlin/encoding/graphson/marshaler_test.go
Normal file
101
dialect/gremlin/encoding/graphson/marshaler_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMarshalerEncode(t *testing.T) {
|
||||
want := []byte(`{"@type": "g:Int32", "@value": 42}`)
|
||||
m := &mocker{}
|
||||
call := m.On("MarshalGraphson").Return(want, nil)
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
tests := []any{m, &m, func() *Marshaler { marshaler := Marshaler(m); return &marshaler }(), Marshaler(nil)}
|
||||
call.Times(len(tests) - 1)
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%T", tc), func(t *testing.T) {
|
||||
got, err := Marshal(tc)
|
||||
assert.NoError(t, err)
|
||||
if !reflect2.IsNil(tc) {
|
||||
assert.Equal(t, want, got)
|
||||
} else {
|
||||
assert.Equal(t, []byte("null"), got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalerError(t *testing.T) {
|
||||
errStr := "marshaler error"
|
||||
m := &mocker{}
|
||||
m.On("MarshalGraphson").Return(nil, errors.New(errStr)).Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
_, err := Marshal(m)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), errStr)
|
||||
}
|
||||
|
||||
func TestBadMarshaler(t *testing.T) {
|
||||
m := &mocker{}
|
||||
m.On("MarshalGraphson").Return([]byte(`{"@type": "g:Int32", "@value":`), nil).Once()
|
||||
defer m.AssertExpectations(t)
|
||||
_, err := Marshal(m)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnmarshalerDecode(t *testing.T) {
|
||||
data := `{"@type": "g:UUID", "@value": "cb682578-9d92-4499-9ebc-5c6aa73c5397"}`
|
||||
var value string
|
||||
|
||||
m := &mocker{}
|
||||
m.On("UnmarshalGraphson", mock.Anything).
|
||||
Run(func(args mock.Arguments) {
|
||||
data := args.Get(0).([]byte)
|
||||
value = jsoniter.Get(data, "@value").ToString()
|
||||
}).
|
||||
Return(nil).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
err := UnmarshalFromString(data, m)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "cb682578-9d92-4499-9ebc-5c6aa73c5397", value)
|
||||
}
|
||||
|
||||
func TestUnmarshalerError(t *testing.T) {
|
||||
errStr := "unmarshaler error"
|
||||
m := &mocker{}
|
||||
m.On("UnmarshalGraphson", mock.Anything).Return(errors.New(errStr)).Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
err := Unmarshal([]byte(`{}`), m)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(),
|
||||
fmt.Sprintf("graphson: error calling UnmarshalGraphson for type %s: %s",
|
||||
reflect.TypeOf(m), errStr,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func TestUnmarshalBadInput(t *testing.T) {
|
||||
m := &mocker{}
|
||||
defer m.AssertExpectations(t)
|
||||
err := UnmarshalFromString(`{"@type"}`, m)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
137
dialect/gremlin/encoding/graphson/native.go
Normal file
137
dialect/gremlin/encoding/graphson/native.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
// EncoderOfNative returns a value encoder of a native type.
|
||||
func (encodeExtension) EncoderOfNative(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
switch typ.Kind() {
|
||||
case reflect.Float64:
|
||||
return float64Encoder{typ}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecoratorOfNative decorates a value encoder of a native type.
|
||||
func (encodeExtension) DecoratorOfNative(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
switch typ.Kind() {
|
||||
case reflect.Bool, reflect.String:
|
||||
return enc
|
||||
case reflect.Int64, reflect.Int, reflect.Uint32:
|
||||
return typeEncoder{enc, int64Type}
|
||||
case reflect.Int32, reflect.Int8, reflect.Uint16:
|
||||
return typeEncoder{enc, int32Type}
|
||||
case reflect.Int16:
|
||||
return typeEncoder{enc, int16Type}
|
||||
case reflect.Uint64, reflect.Uint:
|
||||
return typeEncoder{enc, bigIntegerType}
|
||||
case reflect.Uint8:
|
||||
return typeEncoder{enc, byteType}
|
||||
case reflect.Float32:
|
||||
return typeEncoder{enc, floatType}
|
||||
case reflect.Float64:
|
||||
return typeEncoder{enc, doubleType}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecoderOfNative returns a value decoder of a native type.
|
||||
func (decodeExtension) DecoderOfNative(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
switch typ.Kind() {
|
||||
case reflect.Float64:
|
||||
return float64Decoder{typ}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecoratorOfNative decorates a value decoder of a native type.
|
||||
func (decodeExtension) DecoratorOfNative(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
switch typ.Kind() {
|
||||
case reflect.Bool:
|
||||
return dec
|
||||
case reflect.String:
|
||||
return typeDecoder{dec, typeCheckerFunc(func(Type) error { return nil })}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return typeDecoder{dec, integerTypes}
|
||||
case reflect.Float32:
|
||||
return typeDecoder{dec, floatTypes}
|
||||
case reflect.Float64:
|
||||
return typeDecoder{dec, doubleTypes}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type float64Encoder struct {
|
||||
reflect2.Type
|
||||
}
|
||||
|
||||
func (enc float64Encoder) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
return enc.UnsafeIndirect(ptr).(float64) == 0
|
||||
}
|
||||
|
||||
func (enc float64Encoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
f := enc.UnsafeIndirect(ptr).(float64)
|
||||
switch {
|
||||
case math.IsNaN(f):
|
||||
stream.WriteString("NaN")
|
||||
case math.IsInf(f, 1):
|
||||
stream.WriteString("Infinity")
|
||||
case math.IsInf(f, -1):
|
||||
stream.WriteString("-Infinity")
|
||||
default:
|
||||
stream.WriteFloat64(f)
|
||||
}
|
||||
}
|
||||
|
||||
type float64Decoder struct {
|
||||
reflect2.Type
|
||||
}
|
||||
|
||||
var (
|
||||
integerTypes = Types{byteType, int16Type, int32Type, int64Type, bigIntegerType}
|
||||
floatTypes = append(integerTypes, floatType, bigDecimal)
|
||||
doubleTypes = append(floatTypes, doubleType)
|
||||
)
|
||||
|
||||
func (dec float64Decoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
var val float64
|
||||
switch next := iter.WhatIsNext(); next {
|
||||
case jsoniter.NumberValue:
|
||||
val = iter.ReadFloat64()
|
||||
case jsoniter.StringValue:
|
||||
switch str := iter.ReadString(); str {
|
||||
case "NaN":
|
||||
val = math.NaN()
|
||||
case "Infinity":
|
||||
val = math.Inf(1)
|
||||
case "-Infinity":
|
||||
val = math.Inf(-1)
|
||||
default:
|
||||
iter.ReportError("decode float64", "invalid value "+str)
|
||||
}
|
||||
default:
|
||||
iter.ReportError("decode float64", fmt.Sprintf("unexpected value type: %d", next))
|
||||
}
|
||||
|
||||
if iter.Error == nil || iter.Error == io.EOF {
|
||||
// nolint: gas
|
||||
dec.UnsafeSet(ptr, unsafe.Pointer(&val))
|
||||
}
|
||||
}
|
||||
299
dialect/gremlin/encoding/graphson/native_test.go
Normal file
299
dialect/gremlin/encoding/graphson/native_test.go
Normal file
@@ -0,0 +1,299 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncodeNative(t *testing.T) {
|
||||
tests := []struct {
|
||||
in any
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
in: true,
|
||||
want: "true",
|
||||
},
|
||||
{
|
||||
in: "hello",
|
||||
want: `"hello"`,
|
||||
},
|
||||
{
|
||||
in: int8(120),
|
||||
want: `{
|
||||
"@type": "g:Int32",
|
||||
"@value": 120
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: int16(-16),
|
||||
want: `{
|
||||
"@type": "gx:Int16",
|
||||
"@value": -16
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: int32(3232),
|
||||
want: `{
|
||||
"@type": "g:Int32",
|
||||
"@value": 3232
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: int64(646464),
|
||||
want: `{
|
||||
"@type": "g:Int64",
|
||||
"@value": 646464
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: int(127001),
|
||||
want: `{
|
||||
"@type": "g:Int64",
|
||||
"@value": 127001
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: uint8(81),
|
||||
want: `{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 81
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: uint16(12345),
|
||||
want: `{
|
||||
"@type": "g:Int32",
|
||||
"@value": 12345
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: uint32(123454321),
|
||||
want: `{
|
||||
"@type": "g:Int64",
|
||||
"@value": 123454321
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: uint64(1234567890),
|
||||
want: `{
|
||||
"@type": "gx:BigInteger",
|
||||
"@value": 1234567890
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: uint(9876543210),
|
||||
want: `{
|
||||
"@type" :"gx:BigInteger",
|
||||
"@value": 9876543210
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: float32(math.Pi),
|
||||
want: `{
|
||||
"@type": "g:Float",
|
||||
"@value": 3.1415927
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: float64(math.E),
|
||||
want: `{
|
||||
"@type": "g:Double",
|
||||
"@value": 2.718281828459045
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: math.NaN(),
|
||||
want: `{
|
||||
"@type": "g:Double",
|
||||
"@value": "NaN"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: math.Inf(1),
|
||||
want: `{
|
||||
"@type": "g:Double",
|
||||
"@value": "Infinity"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: math.Inf(-1),
|
||||
want: `{
|
||||
"@type": "g:Double",
|
||||
"@value": "-Infinity"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: func() *int { v := 7142; return &v }(),
|
||||
want: `{
|
||||
"@type": "g:Int64",
|
||||
"@value": 7142
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: func() any { v := int16(6116); return &v }(),
|
||||
want: `{
|
||||
"@type": "gx:Int16",
|
||||
"@value": 6116
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: nil,
|
||||
want: "null",
|
||||
},
|
||||
{
|
||||
in: make(chan int),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%T", tc.in), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := MarshalToString(tc.in)
|
||||
if !tc.wantErr {
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, tc.want, got)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeNative(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want any
|
||||
}{
|
||||
{
|
||||
in: `{"@type": "g:Float", "@value": 3.14}`,
|
||||
want: float32(3.14),
|
||||
},
|
||||
{
|
||||
in: `{"@type": "g:Float", "@value": "Float"}`,
|
||||
},
|
||||
{
|
||||
in: `{"@type": "g:Double", "@value": 2.71}`,
|
||||
want: float64(2.71),
|
||||
},
|
||||
{
|
||||
in: `{"@type": "gx:BigDecimal", "@value": 3.142}`,
|
||||
want: float32(3.142),
|
||||
},
|
||||
{
|
||||
in: `{"@type": "gx:BigDecimal", "@value": 55512.5176}`,
|
||||
want: float64(55512.5176),
|
||||
},
|
||||
{
|
||||
in: `{"@type": "g:T", "@value": "world"}`,
|
||||
want: "world",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if tc.want != nil {
|
||||
typ := reflect2.TypeOf(tc.want)
|
||||
got := typ.New()
|
||||
err := UnmarshalFromString(tc.in, got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, typ.Indirect(got))
|
||||
} else {
|
||||
var msg jsoniter.RawMessage
|
||||
err := UnmarshalFromString(tc.in, &msg)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTypeMismatch(t *testing.T) {
|
||||
t.Run("FloatToInt", func(t *testing.T) {
|
||||
var v int
|
||||
err := UnmarshalFromString(`{"@type": "g:Float", "@value": 3.14}`, &v)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("DoubleToFloat", func(t *testing.T) {
|
||||
var v float32
|
||||
err := UnmarshalFromString(`{"@type": "g:Double", "@value": 5.51}`, &v)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("BigDecimalToUint64", func(t *testing.T) {
|
||||
var v uint64
|
||||
err := UnmarshalFromString(`{"@type": "gx:BigDecimal", "@value": 5645.51834}`, &v)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDecodeNaNInfinity(t *testing.T) {
|
||||
tests := []struct {
|
||||
data []byte
|
||||
expect func(*testing.T, float64, error)
|
||||
}{
|
||||
{
|
||||
data: []byte(`{"@type": "g:Double", "@value": "NaN"}`),
|
||||
expect: func(t *testing.T, f float64, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, math.IsNaN(f))
|
||||
},
|
||||
},
|
||||
{
|
||||
data: []byte(`{"@type": "g:Double", "@value": "Infinity"}`),
|
||||
expect: func(t *testing.T, f float64, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, math.IsInf(f, 1))
|
||||
},
|
||||
},
|
||||
{
|
||||
data: []byte(`{"@type": "g:Double", "@value": "-Infinity"}`),
|
||||
expect: func(t *testing.T, f float64, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, math.IsInf(f, -1))
|
||||
},
|
||||
},
|
||||
{
|
||||
data: []byte(`{"@type": "g:Double", "@value": "Junk"}`),
|
||||
expect: func(t *testing.T, _ float64, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
data: []byte(`{"@type": "g:Double", "@value": [42]}`),
|
||||
expect: func(t *testing.T, _ float64, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
var f float64
|
||||
err := Unmarshal(tc.data, &f)
|
||||
tc.expect(t, f, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTypeDefinition(t *testing.T) {
|
||||
type Status int
|
||||
const StatusOk Status = 42
|
||||
|
||||
var status Status
|
||||
err := UnmarshalFromString(`{"@type": "g:Int64", "@value": 42}`, &status)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, StatusOk, status)
|
||||
}
|
||||
35
dialect/gremlin/encoding/graphson/raw.go
Normal file
35
dialect/gremlin/encoding/graphson/raw.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// RawMessage is a raw encoded graphson value.
|
||||
type RawMessage []byte
|
||||
|
||||
// RawMessage must implement Marshaler/Unmarshaler interfaces.
|
||||
var (
|
||||
_ Marshaler = (*RawMessage)(nil)
|
||||
_ Unmarshaler = (*RawMessage)(nil)
|
||||
)
|
||||
|
||||
// MarshalGraphson returns m as the graphson encoding of m.
|
||||
func (m RawMessage) MarshalGraphson() ([]byte, error) {
|
||||
if m == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// UnmarshalGraphson sets *m to a copy of data.
|
||||
func (m *RawMessage) UnmarshalGraphson(data []byte) error {
|
||||
if m == nil {
|
||||
return errors.New("graphson.RawMessage: UnmarshalGraphson on nil pointer")
|
||||
}
|
||||
*m = append((*m)[0:0], data...)
|
||||
return nil
|
||||
}
|
||||
32
dialect/gremlin/encoding/graphson/raw_test.go
Normal file
32
dialect/gremlin/encoding/graphson/raw_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRawMessageEncoding(t *testing.T) {
|
||||
var s struct{ M RawMessage }
|
||||
got, err := MarshalToString(s)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"M":null}`, got)
|
||||
|
||||
s.M = []byte(`"155a"`)
|
||||
got, err = MarshalToString(s)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{"M": "155a"}`, got)
|
||||
|
||||
err = (*RawMessage)(nil).UnmarshalGraphson(s.M)
|
||||
assert.Error(t, err)
|
||||
|
||||
s.M = nil
|
||||
err = UnmarshalFromString(got, &s)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `"155a"`, string(s.M))
|
||||
}
|
||||
142
dialect/gremlin/encoding/graphson/slice.go
Normal file
142
dialect/gremlin/encoding/graphson/slice.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
// DecoratorOfSlice decorates a value encoder of a slice type.
|
||||
func (encodeExtension) DecoratorOfSlice(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
encoder := typeEncoder{ValEncoder: enc}
|
||||
sliceType := typ.(reflect2.SliceType)
|
||||
if sliceType.Elem().Kind() == reflect.Uint8 {
|
||||
encoder.Type = byteBufferType
|
||||
} else {
|
||||
encoder.Type = listType
|
||||
}
|
||||
return sliceEncoder{sliceType, encoder}
|
||||
}
|
||||
|
||||
// DecoratorOfArray decorates a value encoder of an array type.
|
||||
func (encodeExtension) DecoratorOfArray(enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
return typeEncoder{enc, listType}
|
||||
}
|
||||
|
||||
// DecoderOfSlice returns a value decoder of a slice type.
|
||||
func (ext decodeExtension) DecoderOfSlice(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
sliceType := typ.(reflect2.SliceType)
|
||||
elemType := sliceType.Elem()
|
||||
if elemType.Kind() == reflect.Uint8 {
|
||||
return nil
|
||||
}
|
||||
return sliceDecoder{
|
||||
sliceType: sliceType,
|
||||
elemDec: ext.LazyDecoderOf(elemType),
|
||||
}
|
||||
}
|
||||
|
||||
// DecoderOfArray returns a value decoder of an array type.
|
||||
func (ext decodeExtension) DecoderOfArray(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
arrayType := typ.(reflect2.ArrayType)
|
||||
return arrayDecoder{
|
||||
arrayType: arrayType,
|
||||
elemDec: ext.LazyDecoderOf(arrayType.Elem()),
|
||||
}
|
||||
}
|
||||
|
||||
// DecoratorOfSlice decorates a value decoder of a slice type.
|
||||
func (ext decodeExtension) DecoratorOfSlice(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
if typ.(reflect2.SliceType).Elem().Kind() == reflect.Uint8 {
|
||||
return typeDecoder{dec, byteBufferType}
|
||||
}
|
||||
return typeDecoder{dec, listType}
|
||||
}
|
||||
|
||||
// DecoratorOfArray decorates a value decoder of an array type.
|
||||
func (ext decodeExtension) DecoratorOfArray(dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
return typeDecoder{dec, listType}
|
||||
}
|
||||
|
||||
type sliceEncoder struct {
|
||||
sliceType reflect2.SliceType
|
||||
jsoniter.ValEncoder
|
||||
}
|
||||
|
||||
func (enc sliceEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
if enc.sliceType.UnsafeIsNil(ptr) {
|
||||
stream.WriteNil()
|
||||
} else {
|
||||
enc.ValEncoder.Encode(ptr, stream)
|
||||
}
|
||||
}
|
||||
|
||||
type sliceDecoder struct {
|
||||
sliceType reflect2.SliceType
|
||||
elemDec jsoniter.ValDecoder
|
||||
}
|
||||
|
||||
func (dec sliceDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
dec.decode(ptr, iter)
|
||||
if iter.Error != nil && iter.Error != io.EOF {
|
||||
iter.Error = fmt.Errorf("decoding slice %s: %w", dec.sliceType, iter.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func (dec sliceDecoder) decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
sliceType := dec.sliceType
|
||||
if iter.ReadNil() {
|
||||
sliceType.UnsafeSetNil(ptr)
|
||||
return
|
||||
}
|
||||
|
||||
sliceType.UnsafeSet(ptr, sliceType.UnsafeMakeSlice(0, 0))
|
||||
var length int
|
||||
|
||||
iter.ReadArrayCB(func(iter *jsoniter.Iterator) bool {
|
||||
idx := length
|
||||
length++
|
||||
sliceType.UnsafeGrow(ptr, length)
|
||||
elem := sliceType.UnsafeGetIndex(ptr, idx)
|
||||
dec.elemDec.Decode(elem, iter)
|
||||
return iter.Error == nil
|
||||
})
|
||||
}
|
||||
|
||||
type arrayDecoder struct {
|
||||
arrayType reflect2.ArrayType
|
||||
elemDec jsoniter.ValDecoder
|
||||
}
|
||||
|
||||
func (dec arrayDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
dec.decode(ptr, iter)
|
||||
if iter.Error != nil && iter.Error != io.EOF {
|
||||
iter.Error = fmt.Errorf("decoding array %s: %w", dec.arrayType, iter.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func (dec arrayDecoder) decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
var (
|
||||
arrayType = dec.arrayType
|
||||
length int
|
||||
)
|
||||
iter.ReadArrayCB(func(iter *jsoniter.Iterator) bool {
|
||||
if length < arrayType.Len() {
|
||||
idx := length
|
||||
length++
|
||||
elem := arrayType.UnsafeGetIndex(ptr, idx)
|
||||
dec.elemDec.Decode(elem, iter)
|
||||
} else {
|
||||
iter.Skip()
|
||||
}
|
||||
return iter.Error == nil
|
||||
})
|
||||
}
|
||||
219
dialect/gremlin/encoding/graphson/slice_test.go
Normal file
219
dialect/gremlin/encoding/graphson/slice_test.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncodeArray(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := MarshalToString([...]string{"a", "b", "c"})
|
||||
require.NoError(t, err)
|
||||
want := `{ "@type": "g:List", "@value": ["a", "b", "c"]}`
|
||||
assert.JSONEq(t, want, got)
|
||||
}
|
||||
|
||||
func TestEncodeSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
in any
|
||||
want string
|
||||
}{
|
||||
{
|
||||
in: []int32{5, 6, 7, 8},
|
||||
want: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 5
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 6
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 7
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 8
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: []byte{1, 2, 3, 4, 5},
|
||||
want: `{
|
||||
"@type": "gx:ByteBuffer",
|
||||
"@value": "AQIDBAU="
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: [...]byte{4, 5},
|
||||
want: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 4
|
||||
},
|
||||
{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 5
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
in: []uint64(nil),
|
||||
want: "null",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%T", tc.in), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var got bytes.Buffer
|
||||
err := NewEncoder(&got).Encode(tc.in)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, tc.want, got.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want any
|
||||
}{
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 3
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": -2
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 1
|
||||
}
|
||||
]
|
||||
}`,
|
||||
want: []int32{3, -2, 1},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": ["a", "b", "c"]
|
||||
}`,
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "gx:ByteBuffer",
|
||||
"@value": "AQIDBAU="
|
||||
}`,
|
||||
want: []byte{1, 2, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 42
|
||||
},
|
||||
{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 55
|
||||
},
|
||||
{
|
||||
"@type": "gx:Byte",
|
||||
"@value": 94
|
||||
}
|
||||
]
|
||||
}`,
|
||||
want: [...]byte{42, 55},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
typ := reflect2.TypeOf(tc.want)
|
||||
got := typ.New()
|
||||
err := NewDecoder(strings.NewReader(tc.in)).Decode(got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, typ.Indirect(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBadSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
new func() any
|
||||
}{
|
||||
{
|
||||
name: "TypeMismatch",
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 3
|
||||
},
|
||||
{
|
||||
"@type": "g:Int64",
|
||||
"@value": 2
|
||||
}
|
||||
]
|
||||
}`,
|
||||
new: func() any { return &[]int{} },
|
||||
},
|
||||
{
|
||||
name: "BadValue",
|
||||
in: `{
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": 3
|
||||
},
|
||||
{
|
||||
"@type": "g:Int32",
|
||||
"@value": "2"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
new: func() any { return &[2]int{} },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := NewDecoder(strings.NewReader(tc.in)).Decode(tc.new())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
39
dialect/gremlin/encoding/graphson/struct.go
Normal file
39
dialect/gremlin/encoding/graphson/struct.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import jsoniter "github.com/json-iterator/go"
|
||||
|
||||
// DecoratorOfStructField decorates a struct field value encoder.
|
||||
func (encodeExtension) DecoratorOfStructField(enc jsoniter.ValEncoder, tag string) jsoniter.ValEncoder {
|
||||
typ, _ := parseTag(tag)
|
||||
if typ == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
encoder, ok := enc.(typeEncoder)
|
||||
if !ok {
|
||||
encoder = typeEncoder{ValEncoder: enc}
|
||||
}
|
||||
encoder.Type = Type(typ)
|
||||
|
||||
return encoder
|
||||
}
|
||||
|
||||
// DecoratorOfStructField decorates a struct field value decoder.
|
||||
func (decodeExtension) DecoratorOfStructField(dec jsoniter.ValDecoder, tag string) jsoniter.ValDecoder {
|
||||
typ, _ := parseTag(tag)
|
||||
if typ == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
decoder, ok := dec.(typeDecoder)
|
||||
if !ok {
|
||||
decoder = typeDecoder{ValDecoder: dec}
|
||||
}
|
||||
decoder.typeChecker = Type(typ)
|
||||
|
||||
return decoder
|
||||
}
|
||||
205
dialect/gremlin/encoding/graphson/struct_test.go
Normal file
205
dialect/gremlin/encoding/graphson/struct_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncodeStruct(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in any
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Simple",
|
||||
in: struct {
|
||||
S string
|
||||
I int
|
||||
}{
|
||||
S: "string",
|
||||
I: 1000,
|
||||
},
|
||||
want: `{
|
||||
"S":"string",
|
||||
"I": {
|
||||
"@type": "g:Int64",
|
||||
"@value": 1000
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "Tagged",
|
||||
in: struct {
|
||||
ID string `json:"requestId" graphson:"g:UUID"`
|
||||
Seq int `json:"seq" graphson:"g:Int32"`
|
||||
Op string `json:"op" graphson:","`
|
||||
Args map[string]string `json:"args"`
|
||||
}{
|
||||
ID: "cb682578-9d92-4499-9ebc-5c6aa73c5397",
|
||||
Seq: 42,
|
||||
Op: "authentication",
|
||||
Args: map[string]string{
|
||||
"sasl": "AHN0ZXBocGhlbgBwYXNzd29yZA==",
|
||||
},
|
||||
},
|
||||
want: `{
|
||||
"requestId": {
|
||||
"@type": "g:UUID",
|
||||
"@value": "cb682578-9d92-4499-9ebc-5c6aa73c5397"
|
||||
},
|
||||
"seq": {
|
||||
"@type": "g:Int32",
|
||||
"@value": 42
|
||||
},
|
||||
"op": "authentication",
|
||||
"args": {
|
||||
"@type": "g:Map",
|
||||
"@value": ["sasl", "AHN0ZXBocGhlbgBwYXNzd29yZA=="]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data, err := MarshalToString(tc.in)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, tc.want, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeNestedStruct(t *testing.T) {
|
||||
type S struct {
|
||||
Parent *S `json:"parent,omitempty"`
|
||||
ID int `json:"id" graphson:"g:Int32"`
|
||||
}
|
||||
|
||||
v := S{Parent: &S{ID: 1}, ID: 2}
|
||||
want := `{
|
||||
"id": {
|
||||
"@type": "g:Int32",
|
||||
"@value": 2
|
||||
},
|
||||
"parent": {
|
||||
"id": {
|
||||
"@type": "g:Int32",
|
||||
"@value": 1
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
got, err := MarshalToString(&v)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, want, got)
|
||||
}
|
||||
|
||||
func TestDecodeStruct(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "Simple",
|
||||
in: `{
|
||||
"S":"str",
|
||||
"I": {
|
||||
"@type": "g:Int32",
|
||||
"@value": 9999
|
||||
}
|
||||
}`,
|
||||
want: struct {
|
||||
S string
|
||||
I int32
|
||||
}{
|
||||
S: "str",
|
||||
I: 9999,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Tagged",
|
||||
in: `{
|
||||
"requestId": {
|
||||
"@type": "g:UUID",
|
||||
"@value": "cb682578-9d92-4499-9ebc-5c6aa73c5397"
|
||||
},
|
||||
"seq": {
|
||||
"@type": "g:Int32",
|
||||
"@value": 42
|
||||
},
|
||||
"op": "authentication",
|
||||
"args": {
|
||||
"@type": "g:Map",
|
||||
"@value": ["sasl", "AHN0ZXBocGhlbgBwYXNzd29yZA=="]
|
||||
}
|
||||
}`,
|
||||
want: struct {
|
||||
ID string `json:"requestId" graphson:"g:UUID"`
|
||||
Seq int `json:"seq" graphson:"g:Int32"`
|
||||
Op string `json:"op" graphson:","`
|
||||
Args map[string]string `json:"args"`
|
||||
}{
|
||||
ID: "cb682578-9d92-4499-9ebc-5c6aa73c5397",
|
||||
Seq: 42,
|
||||
Op: "authentication",
|
||||
Args: map[string]string{
|
||||
"sasl": "AHN0ZXBocGhlbgBwYXNzd29yZA==",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
in: `{}`,
|
||||
want: struct{}{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
typ := reflect2.TypeOf(tc.want)
|
||||
got := typ.New()
|
||||
err := UnmarshalFromString(tc.in, got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, typ.Indirect(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeNestedStruct(t *testing.T) {
|
||||
type S struct {
|
||||
Parent *S `json:"parent,omitempty"`
|
||||
ID int `json:"id" graphson:"g:Int32"`
|
||||
}
|
||||
|
||||
in := `{
|
||||
"id": {
|
||||
"@type": "g:Int32",
|
||||
"@value": 37
|
||||
},
|
||||
"parent": {
|
||||
"id": {
|
||||
"@type": "g:Int32",
|
||||
"@value": 65
|
||||
}
|
||||
}
|
||||
}`
|
||||
var got S
|
||||
err := UnmarshalFromString(in, &got)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := S{Parent: &S{ID: 65}, ID: 37}
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
39
dialect/gremlin/encoding/graphson/tags.go
Normal file
39
dialect/gremlin/encoding/graphson/tags.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type tagOptions string
|
||||
|
||||
// parseTag splits a struct field's graphson tag into its type and
|
||||
// comma-separated options.
|
||||
func parseTag(tag string) (string, tagOptions) {
|
||||
if idx := strings.Index(tag, ","); idx != -1 {
|
||||
return tag[:idx], tagOptions(tag[idx+1:])
|
||||
}
|
||||
return tag, ""
|
||||
}
|
||||
|
||||
// Contains reports whether a comma-separated list of options
|
||||
// contains a particular substr flag. substr must be surrounded by a
|
||||
// string boundary or commas.
|
||||
func (opts tagOptions) Contains(opt string) bool {
|
||||
s := string(opts)
|
||||
for s != "" {
|
||||
var next string
|
||||
i := strings.Index(s, ",")
|
||||
if i >= 0 {
|
||||
s, next = s[:i], s[i+1:]
|
||||
}
|
||||
if s == opt {
|
||||
return true
|
||||
}
|
||||
s = next
|
||||
}
|
||||
return false
|
||||
}
|
||||
60
dialect/gremlin/encoding/graphson/tags_test.go
Normal file
60
dialect/gremlin/encoding/graphson/tags_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
typ string
|
||||
opts tagOptions
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
},
|
||||
{
|
||||
name: "TypeOnly",
|
||||
tag: "g:Int32",
|
||||
typ: "g:Int32",
|
||||
},
|
||||
{
|
||||
name: "OptsOnly",
|
||||
tag: ",opt1,opt2",
|
||||
opts: "opt1,opt2",
|
||||
},
|
||||
{
|
||||
name: "TypeAndOpts",
|
||||
tag: "g:UUID,opt3,opt4",
|
||||
typ: "g:UUID",
|
||||
opts: "opt3,opt4",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
typ, opts := parseTag(tc.tag)
|
||||
assert.Equal(t, tc.typ, typ)
|
||||
assert.Equal(t, tc.opts, opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagOptionsContains(t *testing.T) {
|
||||
_, opts := parseTag(",opt1,opt2,opt3")
|
||||
assert.True(t, opts.Contains("opt1"))
|
||||
assert.True(t, opts.Contains("opt2"))
|
||||
assert.True(t, opts.Contains("opt3"))
|
||||
assert.False(t, opts.Contains("opt4"))
|
||||
assert.False(t, opts.Contains("opt11"))
|
||||
assert.False(t, opts.Contains(""))
|
||||
}
|
||||
34
dialect/gremlin/encoding/graphson/time.go
Normal file
34
dialect/gremlin/encoding/graphson/time.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTypeEncoder("time.Time", typeEncoder{timeCodec{}, Timestamp})
|
||||
RegisterTypeDecoder("time.Time", typeDecoder{timeCodec{}, Types{Timestamp, Date}})
|
||||
}
|
||||
|
||||
type timeCodec struct{}
|
||||
|
||||
func (timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
ts := *((*time.Time)(ptr))
|
||||
return ts.IsZero()
|
||||
}
|
||||
|
||||
func (timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
ts := *((*time.Time)(ptr))
|
||||
stream.WriteInt64(ts.UnixNano() / time.Millisecond.Nanoseconds())
|
||||
}
|
||||
|
||||
func (timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
ns := iter.ReadInt64() * time.Millisecond.Nanoseconds()
|
||||
*((*time.Time)(ptr)) = time.Unix(0, ns)
|
||||
}
|
||||
36
dialect/gremlin/encoding/graphson/time_test.go
Normal file
36
dialect/gremlin/encoding/graphson/time_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTimeEncoding(t *testing.T) {
|
||||
const ms = 1481750076295
|
||||
ts := time.Unix(0, ms*time.Millisecond.Nanoseconds())
|
||||
|
||||
for _, v := range []any{ts, &ts} {
|
||||
got, err := MarshalToString(v)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{ "@type": "g:Timestamp", "@value": 1481750076295 }`, got)
|
||||
}
|
||||
|
||||
strs := []string{
|
||||
`{ "@type": "g:Timestamp", "@value": 1481750076295 }`,
|
||||
`{ "@type": "g:Date", "@value": 1481750076295 }`,
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var v time.Time
|
||||
err := UnmarshalFromString(str, &v)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ts.Equal(v))
|
||||
}
|
||||
}
|
||||
155
dialect/gremlin/encoding/graphson/type.go
Normal file
155
dialect/gremlin/encoding/graphson/type.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
// A Type is a graphson type.
|
||||
type Type string
|
||||
|
||||
// graphson typed value types.
|
||||
const (
|
||||
// core
|
||||
doubleType Type = "g:Double"
|
||||
floatType Type = "g:Float"
|
||||
int32Type Type = "g:Int32"
|
||||
int64Type Type = "g:Int64"
|
||||
listType Type = "g:List"
|
||||
mapType Type = "g:Map"
|
||||
Timestamp Type = "g:Timestamp"
|
||||
Date Type = "g:Date"
|
||||
|
||||
// extended
|
||||
bigIntegerType Type = "gx:BigInteger"
|
||||
bigDecimal Type = "gx:BigDecimal"
|
||||
byteType Type = "gx:Byte"
|
||||
byteBufferType Type = "gx:ByteBuffer"
|
||||
int16Type Type = "gx:Int16"
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (typ Type) String() string {
|
||||
return string(typ)
|
||||
}
|
||||
|
||||
// CheckType implements typeChecker interface.
|
||||
func (typ Type) CheckType(other Type) error {
|
||||
if typ != other {
|
||||
return fmt.Errorf("expect type %s, but found %s", typ, other)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Types is a slice of Type.
|
||||
type Types []Type
|
||||
|
||||
// Contains reports whether a slice of types contains a particular type.
|
||||
func (types Types) Contains(typ Type) bool {
|
||||
for i := range types {
|
||||
if types[i] == typ {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (types Types) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteByte('[')
|
||||
for i := range types {
|
||||
if i > 0 {
|
||||
builder.WriteByte(',')
|
||||
}
|
||||
builder.WriteString(types[i].String())
|
||||
}
|
||||
builder.WriteByte(']')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// CheckType implements typeChecker interface.
|
||||
func (types Types) CheckType(typ Type) error {
|
||||
if !types.Contains(typ) {
|
||||
return fmt.Errorf("expect any of %s, but found %s", types, typ)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Typer is the interface implemented by types that
|
||||
// define an underlying graphson type.
|
||||
type Typer interface {
|
||||
GraphsonType() Type
|
||||
}
|
||||
|
||||
var typerType = reflect2.TypeOfPtr((*Typer)(nil)).Elem()
|
||||
|
||||
// DecoratorOfTyper decorates a value encoder of a Typer interface.
|
||||
func (ext encodeExtension) DecoratorOfTyper(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
if typ.Implements(typerType) {
|
||||
return typerEncoder{
|
||||
typeEncoder: typeEncoder{ValEncoder: enc},
|
||||
typerOf: func(ptr unsafe.Pointer) Typer {
|
||||
return typ.UnsafeIndirect(ptr).(Typer)
|
||||
},
|
||||
}
|
||||
}
|
||||
ptrType := reflect2.PtrTo(typ)
|
||||
if ptrType.Implements(typerType) {
|
||||
return typerEncoder{
|
||||
typeEncoder: typeEncoder{ValEncoder: enc},
|
||||
typerOf: func(ptr unsafe.Pointer) Typer {
|
||||
// nolint: gas
|
||||
return ptrType.UnsafeIndirect(unsafe.Pointer(&ptr)).(Typer)
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecoratorOfTyper decorates a value decoder of a Typer interface.
|
||||
func (ext decodeExtension) DecoratorOfTyper(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
|
||||
ptrType := reflect2.PtrTo(typ)
|
||||
if ptrType.Implements(typerType) {
|
||||
return typerDecoder{
|
||||
typeDecoder: typeDecoder{ValDecoder: dec},
|
||||
typerOf: func(ptr unsafe.Pointer) Typer {
|
||||
// nolint: gas
|
||||
return ptrType.UnsafeIndirect(unsafe.Pointer(&ptr)).(Typer)
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type typerEncoder struct {
|
||||
typeEncoder
|
||||
typerOf func(unsafe.Pointer) Typer
|
||||
}
|
||||
|
||||
func (enc typerEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
enc.typeEncoder.Type = enc.typerOf(ptr).GraphsonType()
|
||||
enc.typeEncoder.Encode(ptr, stream)
|
||||
}
|
||||
|
||||
type typerDecoder struct {
|
||||
typeDecoder
|
||||
typerOf func(unsafe.Pointer) Typer
|
||||
}
|
||||
|
||||
func (dec typerDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
dec.typeDecoder.typeChecker = dec.typerOf(ptr).GraphsonType()
|
||||
dec.typeDecoder.Decode(ptr, iter)
|
||||
}
|
||||
95
dialect/gremlin/encoding/graphson/type_test.go
Normal file
95
dialect/gremlin/encoding/graphson/type_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestTypeCheckType(t *testing.T) {
|
||||
assert.NoError(t, int32Type.CheckType(int32Type))
|
||||
assert.Error(t, int32Type.CheckType(int64Type))
|
||||
}
|
||||
|
||||
func TestTypesCheckType(t *testing.T) {
|
||||
assert.NoError(t, Types{int16Type, int32Type, int64Type}.CheckType(int32Type))
|
||||
assert.Error(t, Types{floatType, doubleType}.CheckType(bigIntegerType))
|
||||
}
|
||||
|
||||
func TestTypesString(t *testing.T) {
|
||||
assert.Equal(t, "[]", Types{}.String())
|
||||
assert.Equal(t, "[gx:Byte]", Types{byteType}.String())
|
||||
assert.Equal(t, "[gx:Int16,g:Int32,g:Int64]", Types{int16Type, int32Type, int64Type}.String())
|
||||
}
|
||||
|
||||
type vertex struct {
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
func (vertex) GraphsonType() Type {
|
||||
return Type("g:Vertex")
|
||||
}
|
||||
|
||||
type mockVertex struct {
|
||||
mock.Mock `json:"-"`
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
func (m *mockVertex) GraphsonType() Type {
|
||||
return m.Called().Get(0).(Type)
|
||||
}
|
||||
|
||||
func TestEncodeTyper(t *testing.T) {
|
||||
m := &mockVertex{ID: 42, Label: "person"}
|
||||
m.On("GraphsonType").Return(Type("g:Vertex")).Twice()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
v := vertex{ID: m.ID, Label: m.Label}
|
||||
var vv Typer = v
|
||||
|
||||
want := `{
|
||||
"@type": "g:Vertex",
|
||||
"@value": {
|
||||
"id": {
|
||||
"@type": "g:Int64",
|
||||
"@value": 42
|
||||
},
|
||||
"label": "person"
|
||||
}
|
||||
}`
|
||||
|
||||
for _, tc := range []any{m, &m, v, vv, &vv} {
|
||||
got, err := MarshalToString(tc)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTyper(t *testing.T) {
|
||||
var m mockVertex
|
||||
m.On("GraphsonType").Return(Type("g:Vertex")).Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
in := `{
|
||||
"@type": "g:Vertex",
|
||||
"@value": {
|
||||
"id": {
|
||||
"@type": "g:Int64",
|
||||
"@value": 55
|
||||
},
|
||||
"label": "user"
|
||||
}
|
||||
}`
|
||||
|
||||
err := UnmarshalFromString(in, &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 55, m.ID)
|
||||
assert.Equal(t, "user", m.Label)
|
||||
}
|
||||
114
dialect/gremlin/encoding/graphson/util.go
Normal file
114
dialect/gremlin/encoding/graphson/util.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// graphson encoding type / value keys
|
||||
const (
|
||||
TypeKey = "@type"
|
||||
ValueKey = "@value"
|
||||
)
|
||||
|
||||
// typeEncoder adds graphson type information to a value encoder.
|
||||
type typeEncoder struct {
|
||||
jsoniter.ValEncoder
|
||||
Type Type
|
||||
}
|
||||
|
||||
// Encode belongs to jsoniter.ValEncoder interface.
|
||||
func (enc typeEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
stream.WriteObjectStart()
|
||||
stream.WriteObjectField(TypeKey)
|
||||
stream.WriteString(enc.Type.String())
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField(ValueKey)
|
||||
enc.ValEncoder.Encode(ptr, stream)
|
||||
stream.WriteObjectEnd()
|
||||
}
|
||||
|
||||
type (
|
||||
// typeDecoder decorates a value decoder and adds graphson type verification.
|
||||
typeDecoder struct {
|
||||
jsoniter.ValDecoder
|
||||
typeChecker
|
||||
}
|
||||
|
||||
// typeChecker defines an interface for graphson type verification.
|
||||
typeChecker interface {
|
||||
CheckType(Type) error
|
||||
}
|
||||
|
||||
// typeCheckerFunc allows the use of functions as type checkers.
|
||||
typeCheckerFunc func(Type) error
|
||||
|
||||
// typeValue defines a graphson type / value pair.
|
||||
typeValue struct {
|
||||
Type Type
|
||||
Value jsoniter.RawMessage
|
||||
}
|
||||
)
|
||||
|
||||
// Decode belongs to jsoniter.ValDecoder interface.
|
||||
func (dec typeDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
if iter.WhatIsNext() != jsoniter.ObjectValue {
|
||||
dec.ValDecoder.Decode(ptr, iter)
|
||||
return
|
||||
}
|
||||
|
||||
data := iter.SkipAndReturnBytes()
|
||||
if iter.Error != nil && iter.Error != io.EOF {
|
||||
return
|
||||
}
|
||||
|
||||
var tv typeValue
|
||||
if err := jsoniter.Unmarshal(data, &tv); err != nil {
|
||||
iter.ReportError("unmarshal type value", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := dec.CheckType(tv.Type); err != nil {
|
||||
iter.ReportError("check type", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
it := config.BorrowIterator(tv.Value)
|
||||
defer config.ReturnIterator(it)
|
||||
|
||||
dec.ValDecoder.Decode(ptr, it)
|
||||
if it.Error != nil && it.Error != io.EOF {
|
||||
iter.ReportError("decode value", it.Error.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler interface.
|
||||
func (tv *typeValue) UnmarshalJSON(data []byte) error {
|
||||
var v struct {
|
||||
Type *Type `json:"@type"`
|
||||
Value jsoniter.RawMessage `json:"@value"`
|
||||
}
|
||||
|
||||
if err := jsoniter.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
if v.Type == nil || v.Value == nil {
|
||||
return errors.New("missing type or value")
|
||||
}
|
||||
|
||||
tv.Type = *v.Type
|
||||
tv.Value = v.Value
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckType implements typeChecker interface.
|
||||
func (f typeCheckerFunc) CheckType(typ Type) error {
|
||||
return f(typ)
|
||||
}
|
||||
129
dialect/gremlin/encoding/graphson/util_test.go
Normal file
129
dialect/gremlin/encoding/graphson/util_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graphson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTypeEncode(t *testing.T) {
|
||||
var got bytes.Buffer
|
||||
stream := config.BorrowStream(&got)
|
||||
defer config.ReturnStream(stream)
|
||||
|
||||
typ, val := int32Type, 42
|
||||
ptr := unsafe.Pointer(&val)
|
||||
|
||||
var m mocker
|
||||
m.On("Encode", ptr, stream).
|
||||
Run(func(args mock.Arguments) {
|
||||
stream := args.Get(1).(*jsoniter.Stream)
|
||||
stream.WriteInt(val)
|
||||
}).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
typeEncoder{&m, typ}.Encode(ptr, stream)
|
||||
require.NoError(t, stream.Flush())
|
||||
want := fmt.Sprintf(`{"@type": "%s", "@value": %d}`, typ, val)
|
||||
assert.JSONEq(t, want, got.String())
|
||||
}
|
||||
|
||||
func TestTypeDecode(t *testing.T) {
|
||||
typ, val := int64Type, 84
|
||||
ptr := unsafe.Pointer(&val)
|
||||
|
||||
data := fmt.Sprintf(`{"@value": %d, "@type": "%s"}`, val, typ)
|
||||
iter := config.BorrowIterator([]byte(data))
|
||||
defer config.ReturnIterator(iter)
|
||||
|
||||
m := &mocker{}
|
||||
m.On("CheckType", typ).
|
||||
Return(nil).
|
||||
Once()
|
||||
m.On("Decode", ptr, mock.Anything).
|
||||
Run(func(args mock.Arguments) {
|
||||
iter := args.Get(1).(*jsoniter.Iterator)
|
||||
assert.Equal(t, val, iter.ReadInt())
|
||||
}).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
typeDecoder{m, m}.Decode(ptr, iter)
|
||||
assert.NoError(t, iter.Error)
|
||||
}
|
||||
|
||||
func TestTypeDecodeBadType(t *testing.T) {
|
||||
typ, val := int64Type, 55
|
||||
ptr := unsafe.Pointer(&val)
|
||||
|
||||
m := &mocker{}
|
||||
m.On("CheckType", typ).Return(errors.New("bad type")).Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
data := fmt.Sprintf(`{"@type": "%s", "@value": %d}`, typ, val)
|
||||
iter := config.BorrowIterator([]byte(data))
|
||||
defer config.ReturnIterator(iter)
|
||||
|
||||
typeDecoder{m, m}.Decode(ptr, iter)
|
||||
require.Error(t, iter.Error)
|
||||
assert.Contains(t, iter.Error.Error(), "bad type")
|
||||
}
|
||||
|
||||
func TestTypeDecodeDuplicateField(t *testing.T) {
|
||||
data := `{"@type": "gx:Byte", "@value": 33, "@type": "g:Int32"}`
|
||||
iter := config.BorrowIterator([]byte(data))
|
||||
defer config.ReturnIterator(iter)
|
||||
var ptr unsafe.Pointer
|
||||
|
||||
m := &mocker{}
|
||||
m.On("CheckType", mock.MatchedBy(func(typ Type) bool { return typ == int32Type })).
|
||||
Return(nil).
|
||||
Once()
|
||||
m.On("Decode", ptr, mock.Anything).
|
||||
Run(func(args mock.Arguments) {
|
||||
args.Get(1).(*jsoniter.Iterator).Skip()
|
||||
require.NoError(t, iter.Error)
|
||||
}).
|
||||
Once()
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
typeDecoder{m, m}.Decode(ptr, iter)
|
||||
assert.NoError(t, iter.Error)
|
||||
}
|
||||
|
||||
func TestTypeDecodeMissingField(t *testing.T) {
|
||||
data := `{"@type": "g:Int32"}`
|
||||
iter := config.BorrowIterator([]byte(data))
|
||||
defer config.ReturnIterator(iter)
|
||||
|
||||
m := &mocker{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
typeDecoder{m, m}.Decode(nil, iter)
|
||||
require.Error(t, iter.Error)
|
||||
assert.Contains(t, iter.Error.Error(), "missing type or value")
|
||||
}
|
||||
|
||||
func TestTypeDecodeSyntaxError(t *testing.T) {
|
||||
data := `{"@type": "gx:Int16", "@value", 65000}`
|
||||
iter := config.BorrowIterator([]byte(data))
|
||||
defer config.ReturnIterator(iter)
|
||||
|
||||
m := &mocker{}
|
||||
defer m.AssertExpectations(t)
|
||||
|
||||
typeDecoder{m, m}.Decode(nil, iter)
|
||||
assert.Error(t, iter.Error)
|
||||
}
|
||||
30
dialect/gremlin/encoding/mime.go
Normal file
30
dialect/gremlin/encoding/mime.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// Mime defines a gremlin mime type.
|
||||
type Mime []byte
|
||||
|
||||
// GraphSON3Mime mime headers.
|
||||
var (
|
||||
GraphSON3Mime = NewMime("application/vnd.gremlin-v3.0+json")
|
||||
)
|
||||
|
||||
// NewMime creates a wire format mime header.
|
||||
func NewMime(s string) Mime {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte(byte(len(s)))
|
||||
buf.WriteString(s)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (m Mime) String() string {
|
||||
return string(m[1:])
|
||||
}
|
||||
21
dialect/gremlin/encoding/mime_test.go
Normal file
21
dialect/gremlin/encoding/mime_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewMime(t *testing.T) {
|
||||
str := "application/vnd.gremlin-v2.0+json"
|
||||
mime := NewMime(str)
|
||||
require.Len(t, mime, len(str)+1)
|
||||
assert.EqualValues(t, len(str), mime[0])
|
||||
assert.EqualValues(t, str, mime[1:])
|
||||
assert.Equal(t, str, mime.String())
|
||||
}
|
||||
46
dialect/gremlin/example_test.go
Normal file
46
dialect/gremlin/example_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExampleClient_Query() {
|
||||
addr := flag.String("gremlin-server", os.Getenv("GREMLIN_SERVER"), "gremlin server address")
|
||||
flag.Parse()
|
||||
|
||||
if *addr == "" {
|
||||
log.Fatal("missing gremlin server address")
|
||||
}
|
||||
|
||||
client, err := NewHTTPClient(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("creating client: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
||||
rsp, err := client.Query(ctx, "g.E()")
|
||||
if err != nil {
|
||||
log.Fatalf("executing query: %v", err)
|
||||
}
|
||||
|
||||
edges, err := rsp.ReadEdges()
|
||||
if err != nil {
|
||||
log.Fatalf("unmashal edges")
|
||||
}
|
||||
|
||||
defer cancel()
|
||||
|
||||
for _, e := range edges {
|
||||
log.Println(e.String())
|
||||
}
|
||||
// - Output:
|
||||
}
|
||||
47
dialect/gremlin/expand.go
Normal file
47
dialect/gremlin/expand.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// ExpandBindings expands the given RoundTripper and expands the request bindings into the Gremlin traversal.
|
||||
func ExpandBindings(rt RoundTripper) RoundTripper {
|
||||
return RoundTripperFunc(func(ctx context.Context, r *Request) (*Response, error) {
|
||||
bindings, ok := r.Arguments[ArgsBindings]
|
||||
if !ok {
|
||||
return rt.RoundTrip(ctx, r)
|
||||
}
|
||||
query, ok := r.Arguments[ArgsGremlin]
|
||||
if !ok {
|
||||
return rt.RoundTrip(ctx, r)
|
||||
}
|
||||
{
|
||||
query, bindings := query.(string), bindings.(map[string]any)
|
||||
keys := make(sort.StringSlice, 0, len(bindings))
|
||||
for k := range bindings {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Sort(sort.Reverse(keys))
|
||||
kv := make([]string, 0, len(bindings)*2)
|
||||
for _, k := range keys {
|
||||
s, err := jsoniter.MarshalToString(bindings[k])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal bindings value for key %s: %w", k, err)
|
||||
}
|
||||
kv = append(kv, k, s)
|
||||
}
|
||||
delete(r.Arguments, ArgsBindings)
|
||||
r.Arguments[ArgsGremlin] = strings.NewReplacer(kv...).Replace(query)
|
||||
}
|
||||
return rt.RoundTrip(ctx, r)
|
||||
})
|
||||
}
|
||||
71
dialect/gremlin/expand_test.go
Normal file
71
dialect/gremlin/expand_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExpandBindings(t *testing.T) {
|
||||
tests := []struct {
|
||||
req *Request
|
||||
wantErr bool
|
||||
wantQuery string
|
||||
}{
|
||||
{
|
||||
req: NewEvalRequest("no bindings"),
|
||||
wantQuery: "no bindings",
|
||||
},
|
||||
{
|
||||
req: NewEvalRequest("g.V($0)", WithBindings(map[string]any{"$0": 1})),
|
||||
wantQuery: "g.V(1)",
|
||||
},
|
||||
{
|
||||
req: NewEvalRequest("g.V().has($1, $2)", WithBindings(map[string]any{"$1": "name", "$2": "a8m"})),
|
||||
wantQuery: "g.V().has(\"name\", \"a8m\")",
|
||||
},
|
||||
{
|
||||
req: NewEvalRequest("g.V().limit(n)", WithBindings(map[string]any{"n": 10})),
|
||||
wantQuery: "g.V().limit(10)",
|
||||
},
|
||||
{
|
||||
req: NewEvalRequest("g.V()", WithBindings(map[string]any{"$0": func() {}})),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
req: NewEvalRequest("g.V().has($0, $1)", WithBindings(map[string]any{"$0": "active", "$1": true})),
|
||||
wantQuery: "g.V().has(\"active\", true)",
|
||||
},
|
||||
{
|
||||
req: NewEvalRequest("g.V().has($1, $11)", WithBindings(map[string]any{"$1": "active", "$11": true})),
|
||||
wantQuery: "g.V().has(\"active\", true)",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
rt := ExpandBindings(RoundTripperFunc(func(ctx context.Context, r *Request) (*Response, error) {
|
||||
assert.Equal(t, tt.wantQuery, r.Arguments[ArgsGremlin])
|
||||
return nil, nil
|
||||
}))
|
||||
_, err := rt.RoundTrip(context.Background(), tt.req)
|
||||
assert.Equal(t, tt.wantErr, err != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandBindingsNoQuery(t *testing.T) {
|
||||
rt := ExpandBindings(RoundTripperFunc(func(ctx context.Context, r *Request) (*Response, error) {
|
||||
return nil, nil
|
||||
}))
|
||||
_, err := rt.RoundTrip(context.Background(), &Request{Arguments: map[string]any{
|
||||
ArgsBindings: map[string]any{},
|
||||
}})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
75
dialect/gremlin/graph/dsl/__/dsl.go
Normal file
75
dialect/gremlin/graph/dsl/__/dsl.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package __
|
||||
|
||||
import "entgo.io/ent/dialect/gremlin/graph/dsl"
|
||||
|
||||
// As is the api for calling __.As().
|
||||
func As(args ...any) *dsl.Traversal { return New().As(args...) }
|
||||
|
||||
// Is is the api for calling __.Is().
|
||||
func Is(args ...any) *dsl.Traversal { return New().Is(args...) }
|
||||
|
||||
// Not is the api for calling __.Not().
|
||||
func Not(args ...any) *dsl.Traversal { return New().Not(args...) }
|
||||
|
||||
// Has is the api for calling __.Has().
|
||||
func Has(args ...any) *dsl.Traversal { return New().Has(args...) }
|
||||
|
||||
// HasNot is the api for calling __.HasNot().
|
||||
func HasNot(args ...any) *dsl.Traversal { return New().HasNot(args...) }
|
||||
|
||||
// Or is the api for calling __.Or().
|
||||
func Or(args ...any) *dsl.Traversal { return New().Or(args...) }
|
||||
|
||||
// And is the api for calling __.And().
|
||||
func And(args ...any) *dsl.Traversal { return New().And(args...) }
|
||||
|
||||
// In is the api for calling __.In().
|
||||
func In(args ...any) *dsl.Traversal { return New().In(args...) }
|
||||
|
||||
// Out is the api for calling __.Out().
|
||||
func Out(args ...any) *dsl.Traversal { return New().Out(args...) }
|
||||
|
||||
// OutE is the api for calling __.OutE().
|
||||
func OutE(args ...any) *dsl.Traversal { return New().OutE(args...) }
|
||||
|
||||
// InE is the api for calling __.InE().
|
||||
func InE(args ...any) *dsl.Traversal { return New().InE(args...) }
|
||||
|
||||
// InV is the api for calling __.InV().
|
||||
func InV(args ...any) *dsl.Traversal { return New().InV(args...) }
|
||||
|
||||
// V is the api for calling __.V().
|
||||
func V(args ...any) *dsl.Traversal { return New().V(args...) }
|
||||
|
||||
// OutV is the api for calling __.OutV().
|
||||
func OutV(args ...any) *dsl.Traversal { return New().OutV(args...) }
|
||||
|
||||
// Values is the api for calling __.Values().
|
||||
func Values(args ...string) *dsl.Traversal { return New().Values(args...) }
|
||||
|
||||
// Union is the api for calling __.Union().
|
||||
func Union(args ...any) *dsl.Traversal { return New().Union(args...) }
|
||||
|
||||
// Constant is the api for calling __.Constant().
|
||||
func Constant(args ...any) *dsl.Traversal { return New().Constant(args...) }
|
||||
|
||||
// Properties is the api for calling __.Properties().
|
||||
func Properties(args ...any) *dsl.Traversal { return New().Properties(args...) }
|
||||
|
||||
// OtherV is the api for calling __.OtherV().
|
||||
func OtherV() *dsl.Traversal { return New().OtherV() }
|
||||
|
||||
// Count is the api for calling __.Count().
|
||||
func Count() *dsl.Traversal { return New().Count() }
|
||||
|
||||
// Drop is the api for calling __.Drop().
|
||||
func Drop() *dsl.Traversal { return New().Drop() }
|
||||
|
||||
// Fold is the api for calling __.Fold().
|
||||
func Fold() *dsl.Traversal { return New().Fold() }
|
||||
|
||||
func New() *dsl.Traversal { return new(dsl.Traversal).Add(dsl.Token("__")) }
|
||||
214
dialect/gremlin/graph/dsl/dsl.go
Normal file
214
dialect/gremlin/graph/dsl/dsl.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
// Package dsl provide an API for writing gremlin dsl queries almost as-is
|
||||
// in Go without using strings in the code.
|
||||
//
|
||||
// Note that, the API is not type-safe and assume the provided query and
|
||||
// its arguments are valid.
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Node represents a DSL step in the traversal.
|
||||
type Node interface {
|
||||
// Code returns the code representation of the element and its bindings (if any).
|
||||
Code() (string, []any)
|
||||
}
|
||||
|
||||
type (
|
||||
// Token holds a simple token, like assignment.
|
||||
Token string
|
||||
// List represents a list of elements.
|
||||
List struct {
|
||||
Elements []any
|
||||
}
|
||||
// Func represents a function call.
|
||||
Func struct {
|
||||
Name string
|
||||
Args []any
|
||||
}
|
||||
// Block represents a block/group of nodes.
|
||||
Block struct {
|
||||
Nodes []any
|
||||
}
|
||||
// Var represents a variable assignment and usage.
|
||||
Var struct {
|
||||
Name string
|
||||
Elem any
|
||||
}
|
||||
)
|
||||
|
||||
// Code stringified the token.
|
||||
func (t Token) Code() (string, []any) { return string(t), nil }
|
||||
|
||||
// Code returns the code representation of a list.
|
||||
func (l List) Code() (string, []any) {
|
||||
c, args := codeList(", ", l.Elements...)
|
||||
return fmt.Sprintf("[%s]", c), args
|
||||
}
|
||||
|
||||
// Code returns the code representation of a function call.
|
||||
func (f Func) Code() (string, []any) {
|
||||
c, args := codeList(", ", f.Args...)
|
||||
return fmt.Sprintf("%s(%s)", f.Name, c), args
|
||||
}
|
||||
|
||||
// Code returns the code representation of group/block of nodes.
|
||||
func (b Block) Code() (string, []any) {
|
||||
return codeList("; ", b.Nodes...)
|
||||
}
|
||||
|
||||
// Code returns the code representation of variable declaration or its identifier.
|
||||
func (v Var) Code() (string, []any) {
|
||||
c, args := code(v.Elem)
|
||||
if v.Name == "" {
|
||||
return c, args
|
||||
}
|
||||
return fmt.Sprintf("%s = %s", v.Name, c), args
|
||||
}
|
||||
|
||||
// predefined nodes.
|
||||
var (
|
||||
G = Token("g")
|
||||
Dot = Token(".")
|
||||
)
|
||||
|
||||
// NewFunc returns a new function node.
|
||||
func NewFunc(name string, args ...any) *Func {
|
||||
return &Func{Name: name, Args: args}
|
||||
}
|
||||
|
||||
// NewList returns a new list node.
|
||||
func NewList(args ...any) *List {
|
||||
return &List{Elements: args}
|
||||
}
|
||||
|
||||
// Querier is the interface that wraps the Query method.
|
||||
type Querier interface {
|
||||
// Query returns the query-string (similar to the Gremlin byte-code) and its bindings.
|
||||
Query() (string, Bindings)
|
||||
}
|
||||
|
||||
// Bindings are used to associate a variable with a value.
|
||||
type Bindings map[string]any
|
||||
|
||||
// Add adds new value to the bindings map, formats it if needed, and returns its generated name.
|
||||
func (b Bindings) Add(v any) string {
|
||||
k := fmt.Sprintf("$%x", len(b))
|
||||
switch v := v.(type) {
|
||||
case time.Time:
|
||||
b[k] = v.UnixNano()
|
||||
default:
|
||||
b[k] = v
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// Cardinality of vertex properties.
|
||||
type Cardinality string
|
||||
|
||||
// Cardinality options.
|
||||
const (
|
||||
Set Cardinality = "set"
|
||||
Single Cardinality = "single"
|
||||
)
|
||||
|
||||
// Code implements the Node interface.
|
||||
func (c Cardinality) Code() (string, []any) { return string(c), nil }
|
||||
|
||||
// Keyword defines a Gremlin keyword.
|
||||
type Keyword string
|
||||
|
||||
// Keyword options.
|
||||
const (
|
||||
ID Keyword = "id"
|
||||
)
|
||||
|
||||
// Code implements the Node interface.
|
||||
func (k Keyword) Code() (string, []any) { return string(k), nil }
|
||||
|
||||
// Order of vertex properties.
|
||||
type Order string
|
||||
|
||||
// Order options.
|
||||
const (
|
||||
Incr Order = "incr"
|
||||
Decr Order = "decr"
|
||||
Shuffle Order = "shuffle"
|
||||
)
|
||||
|
||||
// Code implements the Node interface.
|
||||
func (o Order) Code() (string, []any) { return string(o), nil }
|
||||
|
||||
// Column references a particular type of column in a complex data structure such as a Map, a Map.Entry, or a Path.
|
||||
type Column string
|
||||
|
||||
// Column options.
|
||||
const (
|
||||
Keys Column = "keys"
|
||||
Values Column = "values"
|
||||
)
|
||||
|
||||
// Code implements the Node interface.
|
||||
func (o Column) Code() (string, []any) { return string(o), nil }
|
||||
|
||||
// Scope used for steps that have a variable scope which alter the manner in which the step will behave in relation to how the traverses are processed.
|
||||
type Scope string
|
||||
|
||||
// Scope options.
|
||||
const (
|
||||
Local Scope = "local"
|
||||
Global Scope = "global"
|
||||
)
|
||||
|
||||
// Code implements the Node interface.
|
||||
func (s Scope) Code() (string, []any) { return string(s), nil }
|
||||
|
||||
func codeList(sep string, vs ...any) (string, []any) {
|
||||
var (
|
||||
br strings.Builder
|
||||
args []any
|
||||
)
|
||||
for i, node := range vs {
|
||||
if i > 0 {
|
||||
br.WriteString(sep)
|
||||
}
|
||||
c, nargs := code(node)
|
||||
br.WriteString(c)
|
||||
args = append(args, nargs...)
|
||||
}
|
||||
return br.String(), args
|
||||
}
|
||||
|
||||
func code(v any) (string, []any) {
|
||||
switch n := v.(type) {
|
||||
case Node:
|
||||
return n.Code()
|
||||
case *Traversal:
|
||||
var (
|
||||
b strings.Builder
|
||||
args []any
|
||||
)
|
||||
for i := range n.nodes {
|
||||
code, nargs := n.nodes[i].Code()
|
||||
b.WriteString(code)
|
||||
args = append(args, nargs...)
|
||||
}
|
||||
return b.String(), args
|
||||
default:
|
||||
return "%s", []any{v}
|
||||
}
|
||||
}
|
||||
|
||||
func sface(args []string) (v []any) {
|
||||
for _, s := range args {
|
||||
v = append(v, s)
|
||||
}
|
||||
return
|
||||
}
|
||||
238
dialect/gremlin/graph/dsl/dsl_test.go
Normal file
238
dialect/gremlin/graph/dsl/dsl_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package dsl_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin/graph/dsl"
|
||||
"entgo.io/ent/dialect/gremlin/graph/dsl/__"
|
||||
"entgo.io/ent/dialect/gremlin/graph/dsl/g"
|
||||
"entgo.io/ent/dialect/gremlin/graph/dsl/p"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTraverse(t *testing.T) {
|
||||
tests := []struct {
|
||||
input dsl.Querier
|
||||
wantQuery string
|
||||
wantBinds dsl.Bindings
|
||||
}{
|
||||
{
|
||||
input: g.V(5),
|
||||
wantQuery: "g.V($0)",
|
||||
wantBinds: dsl.Bindings{"$0": 5},
|
||||
},
|
||||
{
|
||||
input: g.V(2).Both("knows"),
|
||||
wantQuery: "g.V($0).both($1)",
|
||||
wantBinds: dsl.Bindings{"$0": 2, "$1": "knows"},
|
||||
},
|
||||
{
|
||||
input: g.V(49).BothE("knows").OtherV().ValueMap(),
|
||||
wantQuery: "g.V($0).bothE($1).otherV().valueMap()",
|
||||
wantBinds: dsl.Bindings{"$0": 49, "$1": "knows"},
|
||||
},
|
||||
{
|
||||
input: g.AddV("person").Property("name", "a8m").Next(),
|
||||
wantQuery: "g.addV($0).property($1, $2).next()",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": "name", "$2": "a8m"},
|
||||
},
|
||||
{
|
||||
input: dsl.Each([]any{1, 2, 3}, func(it *dsl.Traversal) *dsl.Traversal {
|
||||
return g.V(it)
|
||||
}),
|
||||
wantQuery: "[$0, $1, $2].each { g.V(it) }",
|
||||
wantBinds: dsl.Bindings{"$0": 1, "$1": 2, "$2": 3},
|
||||
},
|
||||
{
|
||||
input: dsl.Each([]any{g.V(1).Next()}, func(it *dsl.Traversal) *dsl.Traversal {
|
||||
return it.ID()
|
||||
}),
|
||||
wantQuery: "[g.V($0).next()].each { it.id() }",
|
||||
wantBinds: dsl.Bindings{"$0": 1},
|
||||
},
|
||||
{
|
||||
input: g.AddV("person").AddE("knows").To(g.V(2)),
|
||||
wantQuery: "g.addV($0).addE($1).to(g.V($2))",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": "knows", "$2": 2},
|
||||
},
|
||||
{
|
||||
input: func() *dsl.Traversal {
|
||||
v1 := g.V(2).Next()
|
||||
v2 := g.AddV("person").Property("name", "a8m")
|
||||
e1 := g.V(v1).AddE("knows").To(v2)
|
||||
return dsl.Group(v1, v2, e1)
|
||||
}(),
|
||||
wantQuery: "t0 = g.V($0).next(); t1 = g.addV($1).property($2, $3); t2 = g.V(t0).addE($4).to(t1); t2",
|
||||
wantBinds: dsl.Bindings{"$0": 2, "$1": "person", "$2": "name", "$3": "a8m", "$4": "knows"},
|
||||
},
|
||||
{
|
||||
input: func() *dsl.Traversal {
|
||||
v1 := g.AddV("person")
|
||||
each := dsl.Each([]any{1, 2, 3}, func(it *dsl.Traversal) *dsl.Traversal {
|
||||
return g.V(v1).AddE("knows").To(g.V(it)).Next()
|
||||
})
|
||||
return dsl.Group(v1, each)
|
||||
}(),
|
||||
wantQuery: "t0 = g.addV($0); t1 = [$1, $2, $3].each { g.V(t0).addE($4).to(g.V(it)).next() }; t1",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": 1, "$2": 2, "$3": 3, "$4": "knows"},
|
||||
},
|
||||
{
|
||||
input: g.V().HasLabel("person").
|
||||
Choose(__.Values("age").Is(p.LTE(20))),
|
||||
wantQuery: "g.V().hasLabel($0).choose(__.values($1).is(lte($2)))",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": "age", "$2": 20},
|
||||
},
|
||||
{
|
||||
input: g.AddV("person").Property("name", "a8m").Properties(),
|
||||
wantQuery: "g.addV($0).property($1, $2).properties()",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": "name", "$2": "a8m"},
|
||||
},
|
||||
{
|
||||
input: func() *dsl.Traversal {
|
||||
v1 := g.AddV("person").Next()
|
||||
e1 := g.V(v1).AddE("knows").To(g.V(2).Next())
|
||||
return dsl.Group(v1, e1, g.V(v1).ValueMap(true))
|
||||
}(),
|
||||
wantQuery: "t0 = g.addV($0).next(); t1 = g.V(t0).addE($1).to(g.V($2).next()); t2 = g.V(t0).valueMap($3); t2",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": "knows", "$2": 2, "$3": true},
|
||||
},
|
||||
{
|
||||
input: func() *dsl.Traversal {
|
||||
vs := g.V().HasLabel("person").ToList()
|
||||
edge := g.V(vs).AddE("assoc").To(g.V(1)).Iterate()
|
||||
each := dsl.Each(vs, func(it *dsl.Traversal) *dsl.Traversal {
|
||||
return g.V(1).AddE("inverse").To(it).Next()
|
||||
})
|
||||
return dsl.Group(vs, edge, each)
|
||||
}(),
|
||||
wantQuery: "t0 = g.V().hasLabel($0).toList(); t1 = g.V(t0).addE($1).to(g.V($2)).iterate(); t2 = t0.each { g.V($3).addE($4).to(it).next() }; t2",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": "assoc", "$2": 1, "$3": 1, "$4": "inverse"},
|
||||
},
|
||||
{
|
||||
input: g.V().Where(__.Or(__.Has("age", 29), __.Has("age", 30))),
|
||||
wantQuery: "g.V().where(__.or(__.has($0, $1), __.has($2, $3)))",
|
||||
wantBinds: dsl.Bindings{"$0": "age", "$1": 29, "$2": "age", "$3": 30},
|
||||
},
|
||||
{
|
||||
input: g.V().Has("name", p.Containing("le")).Has("name", p.StartingWith("A")),
|
||||
wantQuery: `g.V().has($0, containing($1)).has($2, startingWith($3))`,
|
||||
wantBinds: dsl.Bindings{"$0": "name", "$1": "le", "$2": "name", "$3": "A"},
|
||||
},
|
||||
{
|
||||
input: g.AddV().Property(dsl.Single, "age", 32).ValueMap(),
|
||||
wantQuery: "g.addV().property(single, $0, $1).valueMap()",
|
||||
wantBinds: dsl.Bindings{"$0": "age", "$1": 32},
|
||||
},
|
||||
{
|
||||
input: g.V().Count(),
|
||||
wantQuery: "g.V().count()",
|
||||
wantBinds: dsl.Bindings{},
|
||||
},
|
||||
{
|
||||
input: g.V().HasNot("age"),
|
||||
wantQuery: "g.V().hasNot($0)",
|
||||
wantBinds: dsl.Bindings{"$0": "age"},
|
||||
},
|
||||
{
|
||||
input: func() *dsl.Traversal {
|
||||
v := g.V().HasID(1)
|
||||
u := v.Clone().InE().Drop()
|
||||
return dsl.Join(v, u)
|
||||
}(),
|
||||
wantQuery: "g.V().hasId($0); g.V().hasId($1).inE().drop()",
|
||||
wantBinds: dsl.Bindings{"$0": 1, "$1": 1},
|
||||
},
|
||||
{
|
||||
input: func() *dsl.Traversal {
|
||||
v := g.V().HasID(1)
|
||||
u := v.Clone().InE().Drop()
|
||||
w := u.Clone()
|
||||
return dsl.Join(v, u, w)
|
||||
}(),
|
||||
wantQuery: "g.V().hasId($0); g.V().hasId($1).inE().drop(); g.V().hasId($2).inE().drop()",
|
||||
wantBinds: dsl.Bindings{"$0": 1, "$1": 1, "$2": 1},
|
||||
},
|
||||
{
|
||||
input: g.V().OutE("knows").Where(__.InV().Has("name", "a8m")).OutV(),
|
||||
wantQuery: "g.V().outE($0).where(__.inV().has($1, $2)).outV()",
|
||||
wantBinds: dsl.Bindings{"$0": "knows", "$1": "name", "$2": "a8m"},
|
||||
},
|
||||
{
|
||||
input: g.V().Has("name", p.Within("a8m", "alex")),
|
||||
wantQuery: "g.V().has($0, within($1, $2))",
|
||||
wantBinds: dsl.Bindings{"$0": "name", "$1": "a8m", "$2": "alex"},
|
||||
},
|
||||
{
|
||||
input: g.V().HasID(p.Within(1, 2)),
|
||||
wantQuery: "g.V().hasId(within($0, $1))",
|
||||
wantBinds: dsl.Bindings{"$0": 1, "$1": 2},
|
||||
},
|
||||
{
|
||||
input: g.V().HasID(p.Without(1, 2)),
|
||||
wantQuery: "g.V().hasId(without($0, $1))",
|
||||
wantBinds: dsl.Bindings{"$0": 1, "$1": 2},
|
||||
},
|
||||
{
|
||||
input: g.V().Order().By("name"),
|
||||
wantQuery: "g.V().order().by($0)",
|
||||
wantBinds: dsl.Bindings{"$0": "name"},
|
||||
},
|
||||
{
|
||||
input: g.V().Order().By("name", dsl.Incr),
|
||||
wantQuery: "g.V().order().by($0, incr)",
|
||||
wantBinds: dsl.Bindings{"$0": "name"},
|
||||
},
|
||||
{
|
||||
input: g.V().Order().By("name", dsl.Incr).Undo(),
|
||||
wantQuery: "g.V().order()",
|
||||
wantBinds: dsl.Bindings{},
|
||||
},
|
||||
{
|
||||
input: g.V().OutE("knows").Where(__.InV().Has("name", "a8m")).Undo(),
|
||||
wantQuery: "g.V().outE($0)",
|
||||
wantBinds: dsl.Bindings{"$0": "knows"},
|
||||
},
|
||||
{
|
||||
input: g.V().Has("name").Group().By("name").By("age").Select(dsl.Values),
|
||||
wantQuery: "g.V().has($0).group().by($1).by($2).select(values)",
|
||||
wantBinds: dsl.Bindings{"$0": "name", "$1": "name", "$2": "age"},
|
||||
},
|
||||
{
|
||||
input: g.V().Fold().Unfold(),
|
||||
wantQuery: "g.V().fold().unfold()",
|
||||
wantBinds: dsl.Bindings{},
|
||||
},
|
||||
{
|
||||
input: g.V().Has("person", "name", "a8m").Count().Coalesce(
|
||||
__.Is(p.NEQ(0)).Constant("unique constraint failed"),
|
||||
g.AddV("person").Property("name", "a8m").ValueMap(true),
|
||||
),
|
||||
wantQuery: "g.V().has($0, $1, $2).count().coalesce(__.is(neq($3)).constant($4), g.addV($5).property($6, $7).valueMap($8))",
|
||||
wantBinds: dsl.Bindings{"$0": "person", "$1": "name", "$2": "a8m", "$3": 0, "$4": "unique constraint failed", "$5": "person", "$6": "name", "$7": "a8m", "$8": true},
|
||||
},
|
||||
{
|
||||
input: g.V().Has("age").Property("age", __.Union(__.Values("age"), __.Constant(10)).Sum()).ValueMap(),
|
||||
wantQuery: "g.V().has($0).property($1, __.union(__.values($2), __.constant($3)).sum()).valueMap()",
|
||||
wantBinds: dsl.Bindings{"$0": "age", "$1": "age", "$2": "age", "$3": 10},
|
||||
},
|
||||
{
|
||||
input: g.V().Has("age").SideEffect(__.Properties("name").Drop()).ValueMap(),
|
||||
wantQuery: "g.V().has($0).sideEffect(__.properties($1).drop()).valueMap()",
|
||||
wantBinds: dsl.Bindings{"$0": "age", "$1": "name"},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
query, bindings := tt.input.Query()
|
||||
require.Equal(t, tt.wantQuery, query)
|
||||
require.Equal(t, tt.wantBinds, bindings)
|
||||
})
|
||||
}
|
||||
}
|
||||
19
dialect/gremlin/graph/dsl/g/g.go
Normal file
19
dialect/gremlin/graph/dsl/g/g.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package g
|
||||
|
||||
import "entgo.io/ent/dialect/gremlin/graph/dsl"
|
||||
|
||||
// V is the api for calling g.V().
|
||||
func V(args ...any) *dsl.Traversal { return dsl.NewTraversal().V(args...) }
|
||||
|
||||
// E is the api for calling g.E().
|
||||
func E(args ...any) *dsl.Traversal { return dsl.NewTraversal().E(args...) }
|
||||
|
||||
// AddV is the api for calling g.AddV().
|
||||
func AddV(args ...any) *dsl.Traversal { return dsl.NewTraversal().AddV(args...) }
|
||||
|
||||
// AddE is the api for calling g.AddE().
|
||||
func AddE(args ...any) *dsl.Traversal { return dsl.NewTraversal().AddE(args...) }
|
||||
93
dialect/gremlin/graph/dsl/p/p.go
Normal file
93
dialect/gremlin/graph/dsl/p/p.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package p
|
||||
|
||||
import (
|
||||
"entgo.io/ent/dialect/gremlin/graph/dsl"
|
||||
)
|
||||
|
||||
// EQ is the equal predicate.
|
||||
func EQ(v any) *dsl.Traversal {
|
||||
return op("eq", v)
|
||||
}
|
||||
|
||||
// NEQ is the not-equal predicate.
|
||||
func NEQ(v any) *dsl.Traversal {
|
||||
return op("neq", v)
|
||||
}
|
||||
|
||||
// GT is the greater than predicate.
|
||||
func GT(v any) *dsl.Traversal {
|
||||
return op("gt", v)
|
||||
}
|
||||
|
||||
// GTE is the greater than or equal predicate.
|
||||
func GTE(v any) *dsl.Traversal {
|
||||
return op("gte", v)
|
||||
}
|
||||
|
||||
// LT is the less than predicate.
|
||||
func LT(v any) *dsl.Traversal {
|
||||
return op("lt", v)
|
||||
}
|
||||
|
||||
// LTE is the less than or equal predicate.
|
||||
func LTE(v any) *dsl.Traversal {
|
||||
return op("lte", v)
|
||||
}
|
||||
|
||||
// Between is the between/contains predicate.
|
||||
func Between(v, u any) *dsl.Traversal {
|
||||
return op("between", v, u)
|
||||
}
|
||||
|
||||
// StartingWith is the prefix test predicate.
|
||||
func StartingWith(prefix string) *dsl.Traversal {
|
||||
return op("startingWith", prefix)
|
||||
}
|
||||
|
||||
// EndingWith is the suffix test predicate.
|
||||
func EndingWith(suffix string) *dsl.Traversal {
|
||||
return op("endingWith", suffix)
|
||||
}
|
||||
|
||||
// Containing is the sub string test predicate.
|
||||
func Containing(substr string) *dsl.Traversal {
|
||||
return op("containing", substr)
|
||||
}
|
||||
|
||||
// NotStartingWith is the negation of StartingWith.
|
||||
func NotStartingWith(prefix string) *dsl.Traversal {
|
||||
return op("notStartingWith", prefix)
|
||||
}
|
||||
|
||||
// NotEndingWith is the negation of EndingWith.
|
||||
func NotEndingWith(suffix string) *dsl.Traversal {
|
||||
return op("notEndingWith", suffix)
|
||||
}
|
||||
|
||||
// NotContaining is the negation of Containing.
|
||||
func NotContaining(substr string) *dsl.Traversal {
|
||||
return op("notContaining", substr)
|
||||
}
|
||||
|
||||
// Within Determines if a value is within the specified list of values.
|
||||
func Within[T any](args ...T) *dsl.Traversal {
|
||||
return op("within", args...)
|
||||
}
|
||||
|
||||
// Without determines if a value is not within the specified list of values.
|
||||
func Without[T any](args ...T) *dsl.Traversal {
|
||||
return op("without", args...)
|
||||
}
|
||||
|
||||
func op[T any](name string, args ...T) *dsl.Traversal {
|
||||
t := &dsl.Traversal{}
|
||||
vs := make([]any, len(args))
|
||||
for i, arg := range args {
|
||||
vs[i] = arg
|
||||
}
|
||||
return t.Add(dsl.NewFunc(name, vs...))
|
||||
}
|
||||
426
dialect/gremlin/graph/dsl/traversal.go
Normal file
426
dialect/gremlin/graph/dsl/traversal.go
Normal file
@@ -0,0 +1,426 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Traversal mimics the TinkerPop graph traversal.
|
||||
type Traversal struct {
|
||||
// nodes holds the dsl nodes. first element is the reference name
|
||||
// of the TinkerGraph. defaults to "g".
|
||||
nodes []Node
|
||||
errs []error
|
||||
}
|
||||
|
||||
// NewTraversal returns a new default traversal with "g" as a reference name to the Graph.
|
||||
func NewTraversal() *Traversal {
|
||||
return &Traversal{nodes: []Node{G}}
|
||||
}
|
||||
|
||||
// Group groups a list of traversals into one. all traversals are assigned into a temporary
|
||||
// variables named by their index. The last variable functions as a return value of the query.
|
||||
// Note that, this "temporary hack" is not perfect and may not work in some cases because of
|
||||
// the limitation of evaluation order.
|
||||
func Group(trs ...*Traversal) *Traversal {
|
||||
var (
|
||||
b = Block{}
|
||||
names = make(map[*Traversal]Token)
|
||||
)
|
||||
for i, tr := range trs {
|
||||
if _, ok := names[tr]; ok {
|
||||
continue
|
||||
}
|
||||
v := &Var{Name: fmt.Sprintf("t%d", i), Elem: &Traversal{nodes: tr.nodes}}
|
||||
b.Nodes = append(b.Nodes, v)
|
||||
names[tr] = Token(v.Name)
|
||||
}
|
||||
for _, tr := range trs {
|
||||
tr.nodes = []Node{names[tr]}
|
||||
}
|
||||
b.Nodes = append(b.Nodes, names[trs[len(trs)-1]])
|
||||
return &Traversal{nodes: []Node{b}}
|
||||
}
|
||||
|
||||
// Join joins a list of traversals with a semicolon separator.
|
||||
func Join(trs ...*Traversal) *Traversal {
|
||||
b := Block{}
|
||||
for _, tr := range trs {
|
||||
b.Nodes = append(b.Nodes, &Traversal{nodes: tr.nodes})
|
||||
}
|
||||
return &Traversal{nodes: []Node{b}}
|
||||
}
|
||||
|
||||
// AddError adds an error to the traversal.
|
||||
func (t *Traversal) AddError(err error) *Traversal {
|
||||
t.errs = append(t.errs, err)
|
||||
return t
|
||||
}
|
||||
|
||||
// Err returns a concatenated error of all errors encountered during
|
||||
// the query-building, or were added manually by calling AddError.
|
||||
func (t *Traversal) Err() error {
|
||||
if len(t.errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
br := strings.Builder{}
|
||||
for i := range t.errs {
|
||||
if i > 0 {
|
||||
br.WriteString("; ")
|
||||
}
|
||||
br.WriteString(t.errs[i].Error())
|
||||
}
|
||||
return errors.New(br.String())
|
||||
}
|
||||
|
||||
// V step is usually used to start a traversal but it may also be used mid-traversal.
|
||||
func (t *Traversal) V(args ...any) *Traversal {
|
||||
t.Add(Dot, NewFunc("V", args...))
|
||||
return t
|
||||
}
|
||||
|
||||
// OtherV maps the Edge to the incident vertex that was not just traversed from in the path history.
|
||||
func (t *Traversal) OtherV() *Traversal {
|
||||
t.Add(Dot, NewFunc("otherV"))
|
||||
return t
|
||||
}
|
||||
|
||||
// E step is usually used to start a traversal but it may also be used mid-traversal.
|
||||
func (t *Traversal) E(args ...any) *Traversal {
|
||||
t.Add(Dot, NewFunc("E", args...))
|
||||
return t
|
||||
}
|
||||
|
||||
// AddV adds a vertex.
|
||||
func (t *Traversal) AddV(args ...any) *Traversal {
|
||||
t.Add(Dot, NewFunc("addV", args...))
|
||||
return t
|
||||
}
|
||||
|
||||
// AddE adds an edge.
|
||||
func (t *Traversal) AddE(args ...any) *Traversal {
|
||||
t.Add(Dot, NewFunc("addE", args...))
|
||||
return t
|
||||
}
|
||||
|
||||
// Next gets the next n-number of results from the traversal.
|
||||
func (t *Traversal) Next() *Traversal {
|
||||
return t.Add(Dot, NewFunc("next"))
|
||||
}
|
||||
|
||||
// Drop removes elements and properties from the graph.
|
||||
func (t *Traversal) Drop() *Traversal {
|
||||
return t.Add(Dot, NewFunc("drop"))
|
||||
}
|
||||
|
||||
// Property sets a Property value and related meta properties if supplied,
|
||||
// if supported by the Graph and if the Element is a VertexProperty.
|
||||
func (t *Traversal) Property(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("property", args...))
|
||||
}
|
||||
|
||||
// Both maps the Vertex to its adjacent vertices given the edge labels.
|
||||
func (t *Traversal) Both(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("both", args...))
|
||||
}
|
||||
|
||||
// BothE maps the Vertex to its incident edges given the edge labels.
|
||||
func (t *Traversal) BothE(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("bothE", args...))
|
||||
}
|
||||
|
||||
// Has filters vertices, edges and vertex properties based on their properties.
|
||||
// See: http://tinkerpop.apache.org/docs/current/reference/#has-step.
|
||||
func (t *Traversal) Has(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("has", args...))
|
||||
}
|
||||
|
||||
// HasNot filters vertices, edges and vertex properties based on the non-existence of properties.
|
||||
// See: http://tinkerpop.apache.org/docs/current/reference/#has-step.
|
||||
func (t *Traversal) HasNot(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("hasNot", args...))
|
||||
}
|
||||
|
||||
// HasID filters vertices, edges and vertex properties based on their identifier.
|
||||
func (t *Traversal) HasID(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("hasId", args...))
|
||||
}
|
||||
|
||||
// HasLabel filters vertices, edges and vertex properties based on their label.
|
||||
func (t *Traversal) HasLabel(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("hasLabel", args...))
|
||||
}
|
||||
|
||||
// HasNext returns true if the iteration has more elements.
|
||||
func (t *Traversal) HasNext() *Traversal {
|
||||
return t.Add(Dot, NewFunc("hasNext"))
|
||||
}
|
||||
|
||||
// Match maps the Traverser to a Map of bindings as specified by the provided match traversals.
|
||||
func (t *Traversal) Match(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("match", args...))
|
||||
}
|
||||
|
||||
// Choose routes the current traverser to a particular traversal branch option which allows the creation of if-then-else like semantics within a traversal.
|
||||
func (t *Traversal) Choose(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("choose", args...))
|
||||
}
|
||||
|
||||
// Select arbitrary values from the traversal.
|
||||
func (t *Traversal) Select(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("select", args...))
|
||||
}
|
||||
|
||||
// Group organizes objects in the stream into a Map.Calls to group() are typically accompanied with by() modulators which help specify how the grouping should occur.
|
||||
func (t *Traversal) Group() *Traversal {
|
||||
return t.Add(Dot, NewFunc("group"))
|
||||
}
|
||||
|
||||
// Values maps the Element to the values of the associated properties given the provide property keys.
|
||||
func (t *Traversal) Values(args ...string) *Traversal {
|
||||
return t.Add(Dot, NewFunc("values", sface(args)...))
|
||||
}
|
||||
|
||||
// ValueMap maps the Element to a Map of the property values key'd according to their Property.key().
|
||||
func (t *Traversal) ValueMap(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("valueMap", args...))
|
||||
}
|
||||
|
||||
// Properties maps the Element to its associated properties given the provide property keys.
|
||||
func (t *Traversal) Properties(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("properties", args...))
|
||||
}
|
||||
|
||||
// Range filters the objects in the traversal by the number of them to pass through the stream.
|
||||
func (t *Traversal) Range(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("range", args...))
|
||||
}
|
||||
|
||||
// Limit filters the objects in the traversal by the number of them to pass through the stream, where only the first n objects are allowed as defined by the limit argument.
|
||||
func (t *Traversal) Limit(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("limit", args...))
|
||||
}
|
||||
|
||||
// ID maps the Element to its Element.id().
|
||||
func (t *Traversal) ID() *Traversal {
|
||||
return t.Add(Dot, NewFunc("id"))
|
||||
}
|
||||
|
||||
// Label maps the Element to its Element.label().
|
||||
func (t *Traversal) Label() *Traversal {
|
||||
return t.Add(Dot, NewFunc("label"))
|
||||
}
|
||||
|
||||
// From provides from()-modulation to respective steps.
|
||||
func (t *Traversal) From(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("from", args...))
|
||||
}
|
||||
|
||||
// To used as a modifier to addE(String) this method specifies the traversal to use for selecting the incoming vertex of the newly added Edge.
|
||||
func (t *Traversal) To(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("to", args...))
|
||||
}
|
||||
|
||||
// As provides a label to the step that can be accessed later in the traversal by other steps.
|
||||
func (t *Traversal) As(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("as", args...))
|
||||
}
|
||||
|
||||
// Or ensures that at least one of the provided traversals yield a result.
|
||||
func (t *Traversal) Or(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("or", args...))
|
||||
}
|
||||
|
||||
// And ensures that all of the provided traversals yield a result.
|
||||
func (t *Traversal) And(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("and", args...))
|
||||
}
|
||||
|
||||
// Is filters the E object if it is not P.eq(V) to the provided value.
|
||||
func (t *Traversal) Is(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("is", args...))
|
||||
}
|
||||
|
||||
// Not removes objects from the traversal stream when the traversal provided as an argument does not return any objects.
|
||||
func (t *Traversal) Not(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("not", args...))
|
||||
}
|
||||
|
||||
// In maps the Vertex to its incoming adjacent vertices given the edge labels.
|
||||
func (t *Traversal) In(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("in", args...))
|
||||
}
|
||||
|
||||
// Where filters the current object based on the object itself or the path history.
|
||||
func (t *Traversal) Where(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("where", args...))
|
||||
}
|
||||
|
||||
// Out maps the Vertex to its outgoing adjacent vertices given the edge labels.
|
||||
func (t *Traversal) Out(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("out", args...))
|
||||
}
|
||||
|
||||
// OutE maps the Vertex to its outgoing incident edges given the edge labels.
|
||||
func (t *Traversal) OutE(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("outE", args...))
|
||||
}
|
||||
|
||||
// InE maps the Vertex to its incoming incident edges given the edge labels.
|
||||
func (t *Traversal) InE(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("inE", args...))
|
||||
}
|
||||
|
||||
// OutV maps the Edge to its outgoing/tail incident Vertex.
|
||||
func (t *Traversal) OutV(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("outV", args...))
|
||||
}
|
||||
|
||||
// InV maps the Edge to its incoming/head incident Vertex.
|
||||
func (t *Traversal) InV(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("inV", args...))
|
||||
}
|
||||
|
||||
// ToList puts all the results into a Groovy list.
|
||||
func (t *Traversal) ToList() *Traversal {
|
||||
return t.Add(Dot, NewFunc("toList"))
|
||||
}
|
||||
|
||||
// Iterate iterates the traversal presumably for the generation of side-effects.
|
||||
func (t *Traversal) Iterate() *Traversal {
|
||||
return t.Add(Dot, NewFunc("iterate"))
|
||||
}
|
||||
|
||||
// Count maps the traversal stream to its reduction as a sum of the Traverser.bulk() values
|
||||
// (i.e. count the number of traversers up to this point).
|
||||
func (t *Traversal) Count(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("count", args...))
|
||||
}
|
||||
|
||||
// Order all the objects in the traversal up to this point and then emit them one-by-one in their ordered sequence.
|
||||
func (t *Traversal) Order(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("order", args...))
|
||||
}
|
||||
|
||||
// By can be applied to a number of different step to alter their behaviors.
|
||||
// This form is essentially an identity() modulation.
|
||||
func (t *Traversal) By(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("by", args...))
|
||||
}
|
||||
|
||||
// Fold rolls up objects in the stream into an aggregate list..
|
||||
func (t *Traversal) Fold() *Traversal {
|
||||
return t.Add(Dot, NewFunc("fold"))
|
||||
}
|
||||
|
||||
// Unfold unrolls a Iterator, Iterable or Map into a linear form or simply emits the object if it is not one of those types.
|
||||
func (t *Traversal) Unfold() *Traversal {
|
||||
return t.Add(Dot, NewFunc("unfold"))
|
||||
}
|
||||
|
||||
// Sum maps the traversal stream to its reduction as a sum of the Traverser.get() values multiplied by their Traverser.bulk().
|
||||
func (t *Traversal) Sum(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("sum", args...))
|
||||
}
|
||||
|
||||
// Mean determines the mean value in the stream.
|
||||
func (t *Traversal) Mean(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("mean", args...))
|
||||
}
|
||||
|
||||
// Min determines the smallest value in the stream.
|
||||
func (t *Traversal) Min(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("min", args...))
|
||||
}
|
||||
|
||||
// Max determines the greatest value in the stream.
|
||||
func (t *Traversal) Max(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("max", args...))
|
||||
}
|
||||
|
||||
// Coalesce evaluates the provided traversals and returns the result of the first traversal to emit at least one object.
|
||||
func (t *Traversal) Coalesce(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("coalesce", args...))
|
||||
}
|
||||
|
||||
// Dedup removes all duplicates in the traversal stream up to this point.
|
||||
func (t *Traversal) Dedup(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("dedup", args...))
|
||||
}
|
||||
|
||||
// Constant maps any object to a fixed E value.
|
||||
func (t *Traversal) Constant(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("constant", args...))
|
||||
}
|
||||
|
||||
// Union merges the results of an arbitrary number of traversals.
|
||||
func (t *Traversal) Union(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("union", args...))
|
||||
}
|
||||
|
||||
// SideEffect allows the traverser to proceed unchanged, but yield some computational
|
||||
// sideEffect in the process.
|
||||
func (t *Traversal) SideEffect(args ...any) *Traversal {
|
||||
return t.Add(Dot, NewFunc("sideEffect", args...))
|
||||
}
|
||||
|
||||
// Each is a Groovy each-loop function.
|
||||
func Each(v any, cb func(it *Traversal) *Traversal) *Traversal {
|
||||
t := &Traversal{}
|
||||
switch v := v.(type) {
|
||||
case *Traversal:
|
||||
t.Add(&Var{Elem: v})
|
||||
case []any:
|
||||
t.Add(NewList(v...))
|
||||
default:
|
||||
t.Add(Token("undefined"))
|
||||
}
|
||||
t.Add(Dot, Token("each"), Token(" { "))
|
||||
t.Add(cb(&Traversal{nodes: []Node{Token("it")}}).nodes...)
|
||||
t.Add(Token(" }"))
|
||||
return t
|
||||
}
|
||||
|
||||
// Add is the public API for adding new nodes to the traversal by its sub packages.
|
||||
func (t *Traversal) Add(n ...Node) *Traversal {
|
||||
t.nodes = append(t.nodes, n...)
|
||||
return t
|
||||
}
|
||||
|
||||
// Query returns the query-representation and its binding of this traversal object.
|
||||
func (t *Traversal) Query() (string, Bindings) {
|
||||
var (
|
||||
names []any
|
||||
query strings.Builder
|
||||
bindings = Bindings{}
|
||||
)
|
||||
for _, n := range t.nodes {
|
||||
code, args := n.Code()
|
||||
query.WriteString(code)
|
||||
for _, arg := range args {
|
||||
names = append(names, bindings.Add(arg))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(query.String(), names...), bindings
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of an existing traversal.
|
||||
func (t *Traversal) Clone() *Traversal {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return &Traversal{nodes: append(make([]Node, 0, len(t.nodes)), t.nodes...)}
|
||||
}
|
||||
|
||||
// Undo reverts the last-step of the traversal.
|
||||
func (t *Traversal) Undo() *Traversal {
|
||||
if n := len(t.nodes); n > 2 {
|
||||
t.nodes = t.nodes[:n-2]
|
||||
}
|
||||
return t
|
||||
}
|
||||
94
dialect/gremlin/graph/edge.go
Normal file
94
dialect/gremlin/graph/edge.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
)
|
||||
|
||||
type (
|
||||
// An Edge between two vertices.
|
||||
Edge struct {
|
||||
Element
|
||||
OutV, InV Vertex
|
||||
}
|
||||
|
||||
// graphson edge repr.
|
||||
edge struct {
|
||||
Element
|
||||
OutV any `json:"outV"`
|
||||
OutVLabel string `json:"outVLabel"`
|
||||
InV any `json:"inV"`
|
||||
InVLabel string `json:"inVLabel"`
|
||||
}
|
||||
)
|
||||
|
||||
// NewEdge create a new graph edge.
|
||||
func NewEdge(id any, label string, outV, inV Vertex) Edge {
|
||||
return Edge{
|
||||
Element: NewElement(id, label),
|
||||
OutV: outV,
|
||||
InV: inV,
|
||||
}
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (e Edge) String() string {
|
||||
return fmt.Sprintf("e[%v][%v-%s->%v]", e.ID, e.OutV.ID, e.Label, e.InV.ID)
|
||||
}
|
||||
|
||||
// MarshalGraphson implements graphson.Marshaler interface.
|
||||
func (e Edge) MarshalGraphson() ([]byte, error) {
|
||||
return graphson.Marshal(edge{
|
||||
Element: e.Element,
|
||||
OutV: e.OutV.ID,
|
||||
OutVLabel: e.OutV.Label,
|
||||
InV: e.InV.ID,
|
||||
InVLabel: e.InV.Label,
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalGraphson implements graphson.Unmarshaler interface.
|
||||
func (e *Edge) UnmarshalGraphson(data []byte) error {
|
||||
var edge edge
|
||||
if err := graphson.Unmarshal(data, &edge); err != nil {
|
||||
return fmt.Errorf("unmarshalling edge: %w", err)
|
||||
}
|
||||
|
||||
*e = NewEdge(
|
||||
edge.ID, edge.Label,
|
||||
NewVertex(edge.OutV, edge.OutVLabel),
|
||||
NewVertex(edge.InV, edge.InVLabel),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GraphsonType implements graphson.Typer interface.
|
||||
func (edge) GraphsonType() graphson.Type {
|
||||
return "g:Edge"
|
||||
}
|
||||
|
||||
// Property denotes a key/value pair associated with an edge.
|
||||
type Property struct {
|
||||
Key string `json:"key"`
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
// NewProperty create a new graph edge property.
|
||||
func NewProperty(key string, value any) Property {
|
||||
return Property{key, value}
|
||||
}
|
||||
|
||||
// GraphsonType implements graphson.Typer interface.
|
||||
func (Property) GraphsonType() graphson.Type {
|
||||
return "g:Property"
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (p Property) String() string {
|
||||
return fmt.Sprintf("p[%s->%v]", p.Key, p.Value)
|
||||
}
|
||||
108
dialect/gremlin/graph/edge_test.go
Normal file
108
dialect/gremlin/graph/edge_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEdgeString(t *testing.T) {
|
||||
e := NewEdge(
|
||||
13, "develops",
|
||||
NewVertex(1, ""),
|
||||
NewVertex(10, ""),
|
||||
)
|
||||
assert.Equal(t, "e[13][1-develops->10]", fmt.Sprint(e))
|
||||
}
|
||||
|
||||
func TestEdgeEncoding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := NewEdge(13, "develops",
|
||||
NewVertex(1, "person"),
|
||||
NewVertex(10, "software"),
|
||||
)
|
||||
got, err := graphson.MarshalToString(e)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := `{
|
||||
"@type" : "g:Edge",
|
||||
"@value" : {
|
||||
"id" : {
|
||||
"@type" : "g:Int64",
|
||||
"@value" : 13
|
||||
},
|
||||
"label" : "develops",
|
||||
"inVLabel" : "software",
|
||||
"outVLabel" : "person",
|
||||
"inV" : {
|
||||
"@type" : "g:Int64",
|
||||
"@value" : 10
|
||||
},
|
||||
"outV" : {
|
||||
"@type" : "g:Int64",
|
||||
"@value" : 1
|
||||
}
|
||||
}
|
||||
}`
|
||||
assert.JSONEq(t, want, got)
|
||||
|
||||
e = Edge{}
|
||||
err = graphson.UnmarshalFromString(got, &e)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, NewElement(int64(13), "develops"), e.Element)
|
||||
assert.Equal(t, NewVertex(int64(1), "person"), e.OutV)
|
||||
assert.Equal(t, NewVertex(int64(10), "software"), e.InV)
|
||||
}
|
||||
|
||||
func TestPropertyEncoding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
props := []Property{
|
||||
NewProperty("from", int32(2017)),
|
||||
NewProperty("to", int32(2019)),
|
||||
}
|
||||
got, err := graphson.MarshalToString(props)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := `{
|
||||
"@type" : "g:List",
|
||||
"@value" : [
|
||||
{
|
||||
"@type" : "g:Property",
|
||||
"@value" : {
|
||||
"key" : "from",
|
||||
"value" : {
|
||||
"@type" : "g:Int32",
|
||||
"@value" : 2017
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type" : "g:Property",
|
||||
"@value" : {
|
||||
"key" : "to",
|
||||
"value" : {
|
||||
"@type" : "g:Int32",
|
||||
"@value" : 2019
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
assert.JSONEq(t, want, got)
|
||||
}
|
||||
|
||||
func TestPropertyString(t *testing.T) {
|
||||
p := NewProperty("since", 2019)
|
||||
assert.Equal(t, "p[since->2019]", fmt.Sprint(p))
|
||||
}
|
||||
16
dialect/gremlin/graph/element.go
Normal file
16
dialect/gremlin/graph/element.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graph
|
||||
|
||||
// Element defines a base struct for graph elements.
|
||||
type Element struct {
|
||||
ID any `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// NewElement create a new graph element.
|
||||
func NewElement(id any, label string) Element {
|
||||
return Element{id, label}
|
||||
}
|
||||
57
dialect/gremlin/graph/valuemap.go
Normal file
57
dialect/gremlin/graph/valuemap.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// ValueMap models a .valueMap() gremlin response.
|
||||
type ValueMap []map[string]any
|
||||
|
||||
// Decode decodes a value map into v.
|
||||
func (m ValueMap) Decode(v any) error {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
return errors.New("cannot unmarshal into a non pointer")
|
||||
}
|
||||
if rv.IsNil() {
|
||||
return errors.New("cannot unmarshal into a nil pointer")
|
||||
}
|
||||
|
||||
if rv.Elem().Kind() != reflect.Slice {
|
||||
v = &[]any{v}
|
||||
}
|
||||
return m.decode(v)
|
||||
}
|
||||
|
||||
func (m ValueMap) decode(v any) error {
|
||||
cfg := mapstructure.DecoderConfig{
|
||||
DecodeHook: func(f, t reflect.Kind, data any) (any, error) {
|
||||
if f == reflect.Slice && t != reflect.Slice {
|
||||
rv := reflect.ValueOf(data)
|
||||
if rv.Len() == 1 {
|
||||
data = rv.Index(0).Interface()
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
},
|
||||
Result: v,
|
||||
TagName: "json",
|
||||
}
|
||||
|
||||
dec, err := mapstructure.NewDecoder(&cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating structure decoder: %w", err)
|
||||
}
|
||||
if err := dec.Decode(m); err != nil {
|
||||
return fmt.Errorf("decoding value map: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
74
dialect/gremlin/graph/valuemap_test.go
Normal file
74
dialect/gremlin/graph/valuemap_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValueMapDecodeOne(t *testing.T) {
|
||||
vm := ValueMap{map[string]any{
|
||||
"id": int64(1),
|
||||
"label": "person",
|
||||
"name": []any{"marko"},
|
||||
"age": []any{int32(29)},
|
||||
}}
|
||||
|
||||
var ent struct {
|
||||
ID uint64 `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
Age uint8 `json:"age"`
|
||||
}
|
||||
err := vm.Decode(&ent)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, uint64(1), ent.ID)
|
||||
assert.Equal(t, "person", ent.Label)
|
||||
assert.Equal(t, "marko", ent.Name)
|
||||
assert.Equal(t, uint8(29), ent.Age)
|
||||
}
|
||||
|
||||
func TestValueMapDecodeMany(t *testing.T) {
|
||||
vm := ValueMap{
|
||||
map[string]any{
|
||||
"id": int64(1),
|
||||
"label": "person",
|
||||
"name": []any{"chico"},
|
||||
},
|
||||
map[string]any{
|
||||
"id": int64(2),
|
||||
"label": "person",
|
||||
"name": []any{"dico"},
|
||||
},
|
||||
}
|
||||
|
||||
ents := []struct {
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
err := vm.Decode(&ents)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ents, 2)
|
||||
assert.Equal(t, 1, ents[0].ID)
|
||||
assert.Equal(t, "person", ents[0].Label)
|
||||
assert.Equal(t, "chico", ents[0].Name)
|
||||
assert.Equal(t, 2, ents[1].ID)
|
||||
assert.Equal(t, "person", ents[1].Label)
|
||||
assert.Equal(t, "dico", ents[1].Name)
|
||||
}
|
||||
|
||||
func TestValueMapDecodeBadInput(t *testing.T) {
|
||||
type s struct{ Name string }
|
||||
err := ValueMap{}.Decode(s{})
|
||||
assert.Error(t, err)
|
||||
err = ValueMap{}.Decode((*s)(nil))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
62
dialect/gremlin/graph/vertex.go
Normal file
62
dialect/gremlin/graph/vertex.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
)
|
||||
|
||||
// Vertex represents a graph vertex.
|
||||
type Vertex struct {
|
||||
Element
|
||||
}
|
||||
|
||||
// NewVertex create a new graph vertex.
|
||||
func NewVertex(id any, label string) Vertex {
|
||||
if label == "" {
|
||||
label = "vertex"
|
||||
}
|
||||
return Vertex{
|
||||
Element: NewElement(id, label),
|
||||
}
|
||||
}
|
||||
|
||||
// GraphsonType implements graphson.Typer interface.
|
||||
func (Vertex) GraphsonType() graphson.Type {
|
||||
return "g:Vertex"
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (v Vertex) String() string {
|
||||
return fmt.Sprintf("v[%v]", v.ID)
|
||||
}
|
||||
|
||||
// VertexProperty denotes a key/value pair associated with a vertex.
|
||||
type VertexProperty struct {
|
||||
ID any `json:"id"`
|
||||
Key string `json:"label"`
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
// NewVertexProperty create a new graph vertex property.
|
||||
func NewVertexProperty(id any, key string, value any) VertexProperty {
|
||||
return VertexProperty{
|
||||
ID: id,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// GraphsonType implements graphson.Typer interface.
|
||||
func (VertexProperty) GraphsonType() graphson.Type {
|
||||
return "g:VertexProperty"
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (vp VertexProperty) String() string {
|
||||
return fmt.Sprintf("vp[%s->%v]", vp.Key, vp.Value)
|
||||
}
|
||||
86
dialect/gremlin/graph/vertex_test.go
Normal file
86
dialect/gremlin/graph/vertex_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVertexCreation(t *testing.T) {
|
||||
v := NewVertex(45, "person")
|
||||
assert.Equal(t, 45, v.ID)
|
||||
assert.Equal(t, "person", v.Label)
|
||||
v = NewVertex(46, "")
|
||||
assert.Equal(t, "vertex", v.Label)
|
||||
}
|
||||
|
||||
func TestVertexString(t *testing.T) {
|
||||
v := NewVertex(42, "")
|
||||
assert.Equal(t, "v[42]", fmt.Sprint(v))
|
||||
}
|
||||
|
||||
func TestVertexEncoding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := NewVertex(1, "user")
|
||||
got, err := graphson.MarshalToString(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := `{
|
||||
"@type" : "g:Vertex",
|
||||
"@value" : {
|
||||
"id" : {
|
||||
"@type" : "g:Int64",
|
||||
"@value" : 1
|
||||
},
|
||||
"label" : "user"
|
||||
}
|
||||
}`
|
||||
assert.JSONEq(t, want, got)
|
||||
|
||||
v = Vertex{}
|
||||
err = graphson.UnmarshalFromString(got, &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(1), v.ID)
|
||||
assert.Equal(t, "user", v.Label)
|
||||
}
|
||||
|
||||
func TestVertexPropertyEncoding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vp := NewVertexProperty("46ab60c2-918c-4cc4-a13b-350510e8908a", "name", "alex")
|
||||
got, err := graphson.MarshalToString(vp)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := `{
|
||||
"@type" : "g:VertexProperty",
|
||||
"@value" : {
|
||||
"id" : "46ab60c2-918c-4cc4-a13b-350510e8908a",
|
||||
"label": "name",
|
||||
"value": "alex"
|
||||
}
|
||||
}`
|
||||
assert.JSONEq(t, want, got)
|
||||
|
||||
vp = VertexProperty{}
|
||||
err = graphson.UnmarshalFromString(got, &vp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "46ab60c2-918c-4cc4-a13b-350510e8908a", vp.ID)
|
||||
assert.Equal(t, "name", vp.Key)
|
||||
assert.Equal(t, "alex", vp.Value)
|
||||
}
|
||||
|
||||
func TestVertexPropertyString(t *testing.T) {
|
||||
vp := NewVertexProperty(55, "country", "israel")
|
||||
assert.Equal(t, "vp[country->israel]", fmt.Sprint(vp))
|
||||
}
|
||||
85
dialect/gremlin/http.go
Normal file
85
dialect/gremlin/http.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
type httpTransport struct {
|
||||
client *http.Client
|
||||
url string
|
||||
}
|
||||
|
||||
// NewHTTPTransport returns a new http transport.
|
||||
func NewHTTPTransport(urlStr string, client *http.Client) (RoundTripper, error) {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gremlin/http: parsing url: %w", err)
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return &httpTransport{client, u.String()}, nil
|
||||
}
|
||||
|
||||
// RoundTrip implements RouterTripper interface.
|
||||
func (t *httpTransport) RoundTrip(ctx context.Context, req *Request) (*Response, error) {
|
||||
if req.Operation != OpsEval {
|
||||
return nil, fmt.Errorf("gremlin/http: unsupported operation: %q", req.Operation)
|
||||
}
|
||||
if _, ok := req.Arguments[ArgsGremlin]; !ok {
|
||||
return nil, errors.New("gremlin/http: missing query expression")
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
go func() {
|
||||
err := jsoniter.NewEncoder(pw).Encode(req.Arguments)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("gremlin/http: encoding request: %w", err)
|
||||
}
|
||||
_ = pw.CloseWithError(err)
|
||||
}()
|
||||
|
||||
var br io.Reader
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPost, t.url, pr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gremlin/http: creating http request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rsp, err := t.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gremlin/http: posting http request: %w", err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
if rsp.StatusCode < http.StatusOK || rsp.StatusCode > http.StatusPartialContent {
|
||||
body, _ := io.ReadAll(rsp.Body)
|
||||
return nil, fmt.Errorf("gremlin/http: status=%q, body=%q", rsp.Status, body)
|
||||
}
|
||||
if rsp.ContentLength > MaxResponseSize {
|
||||
return nil, errors.New("gremlin/http: context length exceeds limit")
|
||||
}
|
||||
br = rsp.Body
|
||||
}
|
||||
|
||||
var rsp Response
|
||||
if err := graphson.NewDecoder(io.LimitReader(br, MaxResponseSize)).Decode(&rsp); err != nil {
|
||||
return nil, fmt.Errorf("gremlin/http: decoding response: %w", err)
|
||||
}
|
||||
return &rsp, nil
|
||||
}
|
||||
130
dialect/gremlin/http_test.go
Normal file
130
dialect/gremlin/http_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package gremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHTTPTransportRoundTripper(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, r.Header.Get("Content-Type"), "application/json")
|
||||
got, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{"gremlin": "g.V(1)", "language": "gremlin-groovy"}`, string(got))
|
||||
|
||||
_, err = io.WriteString(w, `{
|
||||
"requestId": "f679127f-8701-425c-af55-049a44720db6",
|
||||
"result": {
|
||||
"data": {
|
||||
"@type": "g:List",
|
||||
"@value": [
|
||||
{
|
||||
"@type": "g:Vertex",
|
||||
"@value": {
|
||||
"id": {
|
||||
"@type": "g:Int64",
|
||||
"@value": 1
|
||||
},
|
||||
"label": "person"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"@type": "g:Map",
|
||||
"@value": []
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"attributes": {
|
||||
"@type": "g:Map",
|
||||
"@value": []
|
||||
},
|
||||
"code": 200,
|
||||
"message": ""
|
||||
}
|
||||
}`)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
transport, err := NewHTTPTransport(srv.URL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
rsp, err := transport.RoundTrip(context.Background(), NewEvalRequest("g.V(1)"))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "f679127f-8701-425c-af55-049a44720db6", rsp.RequestID)
|
||||
assert.Equal(t, 200, rsp.Status.Code)
|
||||
assert.Empty(t, rsp.Status.Message)
|
||||
|
||||
v := jsoniter.Get(rsp.Result.Data, graphson.ValueKey, 0, graphson.ValueKey)
|
||||
require.NoError(t, v.LastError())
|
||||
assert.Equal(t, 1, v.Get("id", graphson.ValueKey).ToInt())
|
||||
assert.Equal(t, "person", v.Get("label").ToString())
|
||||
}
|
||||
|
||||
func TestNewHTTPTransportBadURL(t *testing.T) {
|
||||
transport, err := NewHTTPTransport(":", nil)
|
||||
assert.Nil(t, transport)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPTransportBadRequest(t *testing.T) {
|
||||
transport, err := NewHTTPTransport("example.com", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := NewEvalRequest("g.V()")
|
||||
req.Operation = ""
|
||||
rsp, err := transport.RoundTrip(context.Background(), req)
|
||||
assert.EqualError(t, err, `gremlin/http: unsupported operation: ""`)
|
||||
assert.Nil(t, rsp)
|
||||
|
||||
req = NewEvalRequest("g.V()")
|
||||
delete(req.Arguments, ArgsGremlin)
|
||||
rsp, err = transport.RoundTrip(context.Background(), req)
|
||||
assert.EqualError(t, err, "gremlin/http: missing query expression")
|
||||
assert.Nil(t, rsp)
|
||||
}
|
||||
|
||||
func TestHTTPTransportBadResponseStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
transport, err := NewHTTPTransport(srv.URL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = transport.RoundTrip(context.Background(), NewEvalRequest("g.E()."))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
func TestHTTPTransportBadResponseBody(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, err := io.WriteString(w, "{{{")
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
transport, err := NewHTTPTransport(srv.URL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = transport.RoundTrip(context.Background(), NewEvalRequest("g.E()."))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "decoding response")
|
||||
}
|
||||
315
dialect/gremlin/internal/ws/conn.go
Normal file
315
dialect/gremlin/internal/ws/conn.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin"
|
||||
"entgo.io/ent/dialect/gremlin/encoding"
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 5 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 10 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
)
|
||||
|
||||
type (
|
||||
// A Dialer contains options for connecting to Gremlin server.
|
||||
Dialer struct {
|
||||
// Underlying websocket dialer.
|
||||
websocket.Dialer
|
||||
|
||||
// Gremlin server basic auth credentials.
|
||||
user, pass string
|
||||
}
|
||||
|
||||
// Conn performs operations on a gremlin server.
|
||||
Conn struct {
|
||||
// Underlying websocket connection.
|
||||
conn *websocket.Conn
|
||||
|
||||
// Credentials for basic authentication.
|
||||
user, pass string
|
||||
|
||||
// Goroutine tracking.
|
||||
ctx context.Context
|
||||
grp *errgroup.Group
|
||||
|
||||
// Channel of outbound requests.
|
||||
send chan io.Reader
|
||||
|
||||
// Map of in flight requests.
|
||||
inflight sync.Map
|
||||
}
|
||||
|
||||
// inflight tracks request state.
|
||||
inflight struct {
|
||||
// partially received data
|
||||
frags []graphson.RawMessage
|
||||
|
||||
// response channel
|
||||
result chan<- result
|
||||
}
|
||||
|
||||
// represents an execution result.
|
||||
result struct {
|
||||
rsp *gremlin.Response
|
||||
err error
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultDialer is a dialer with all fields set to the default values.
|
||||
DefaultDialer = &Dialer{
|
||||
Dialer: websocket.Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
HandshakeTimeout: 5 * time.Second,
|
||||
WriteBufferSize: 8192,
|
||||
ReadBufferSize: 8192,
|
||||
},
|
||||
}
|
||||
|
||||
// ErrConnClosed is returned by the Conn's Execute method when
|
||||
// the underlying gremlin server connection is closed.
|
||||
ErrConnClosed = errors.New("gremlin: server connection closed")
|
||||
|
||||
// ErrDuplicateRequest is returned by the Conns Execute method on
|
||||
// request identifier key collision.
|
||||
ErrDuplicateRequest = errors.New("gremlin: duplicate request")
|
||||
)
|
||||
|
||||
// Dial creates a new connection by calling DialContext with a background context.
|
||||
func (d *Dialer) Dial(uri string) (*Conn, error) {
|
||||
return d.DialContext(context.Background(), uri)
|
||||
}
|
||||
|
||||
// DialContext creates a new Gremlin connection.
|
||||
func (d *Dialer) DialContext(ctx context.Context, uri string) (*Conn, error) {
|
||||
c, rsp, err := d.Dialer.DialContext(ctx, uri, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gremlin: dialing uri %s: %w", uri, err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
conn := &Conn{
|
||||
conn: c,
|
||||
user: d.user,
|
||||
pass: d.pass,
|
||||
send: make(chan io.Reader),
|
||||
}
|
||||
conn.grp, conn.ctx = errgroup.WithContext(context.Background())
|
||||
|
||||
conn.grp.Go(conn.sender)
|
||||
conn.grp.Go(conn.receiver)
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Execute executes a request against a Gremlin server.
|
||||
func (c *Conn) Execute(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
|
||||
// buffered result channel prevents receiver block on context cancellation
|
||||
result := make(chan result, 1)
|
||||
|
||||
// request id must be unique across inflight request
|
||||
if _, loaded := c.inflight.LoadOrStore(req.RequestID, &inflight{result: result}); loaded {
|
||||
return nil, ErrDuplicateRequest
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
|
||||
// stream graphson encoding into request
|
||||
c.grp.Go(func() error {
|
||||
err := graphson.NewEncoder(pw).Encode(req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("encoding request: %w", err)
|
||||
}
|
||||
pw.CloseWithError(err)
|
||||
return err
|
||||
})
|
||||
|
||||
// local copy for single write
|
||||
send := c.send
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
c.inflight.Delete(req.RequestID)
|
||||
return nil, ErrConnClosed
|
||||
case <-ctx.Done():
|
||||
c.inflight.Delete(req.RequestID)
|
||||
return nil, ctx.Err()
|
||||
case send <- pr:
|
||||
send = nil
|
||||
case result := <-result:
|
||||
return result.rsp, result.err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close connection with a Gremlin server.
|
||||
func (c *Conn) Close() error {
|
||||
c.grp.Go(func() error { return ErrConnClosed })
|
||||
_ = c.grp.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) sender() error {
|
||||
pinger := time.NewTicker(pingPeriod)
|
||||
defer pinger.Stop()
|
||||
|
||||
// closing connection terminates receiver
|
||||
defer c.conn.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case r := <-c.send:
|
||||
// ensure write completes within a window
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
|
||||
// fetch next message writer
|
||||
w, err := c.conn.NextWriter(websocket.BinaryMessage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting message writer: %w", err)
|
||||
}
|
||||
|
||||
// write mime header
|
||||
if _, err := w.Write(encoding.GraphSON3Mime); err != nil {
|
||||
return fmt.Errorf("writing mime header: %w", err)
|
||||
}
|
||||
|
||||
// write request body
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return fmt.Errorf("writing request: %w", err)
|
||||
}
|
||||
|
||||
// finish message write
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("closing message writer: %w", err)
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
// connection closing
|
||||
return c.conn.WriteControl(
|
||||
websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
|
||||
time.Time{},
|
||||
)
|
||||
case <-pinger.C:
|
||||
// periodic connection keepalive
|
||||
if err := c.conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeWait)); err != nil {
|
||||
return fmt.Errorf("writing ping message: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) receiver() error {
|
||||
// handle keepalive responses
|
||||
c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.conn.SetPongHandler(func(string) error {
|
||||
return c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
})
|
||||
|
||||
// complete all in flight requests on termination
|
||||
defer c.inflight.Range(func(id, ifr any) bool {
|
||||
ifr.(*inflight).result <- result{err: ErrConnClosed}
|
||||
c.inflight.Delete(id)
|
||||
return true
|
||||
})
|
||||
|
||||
for {
|
||||
// rely on sender connection close during termination
|
||||
_, r, err := c.conn.NextReader()
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing ping message: %w", err)
|
||||
}
|
||||
|
||||
// decode received response
|
||||
var rsp gremlin.Response
|
||||
if err := graphson.NewDecoder(r).Decode(&rsp); err != nil {
|
||||
return fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
ifr, ok := c.inflight.Load(rsp.RequestID)
|
||||
if !ok {
|
||||
// context cancellation aborts inflight requests
|
||||
continue
|
||||
}
|
||||
|
||||
// handle incoming response
|
||||
if done := c.receive(ifr.(*inflight), &rsp); done {
|
||||
// stop tracking finished requests
|
||||
c.inflight.Delete(rsp.RequestID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) receive(ifr *inflight, rsp *gremlin.Response) bool {
|
||||
result := result{rsp: rsp}
|
||||
switch rsp.Status.Code {
|
||||
case gremlin.StatusSuccess:
|
||||
// quickly handle non fragmented responses
|
||||
if ifr.frags == nil {
|
||||
break
|
||||
}
|
||||
// handle fragment
|
||||
fallthrough
|
||||
case gremlin.StatusPartialContent:
|
||||
// append received fragment
|
||||
var frag []graphson.RawMessage
|
||||
if err := graphson.Unmarshal(rsp.Result.Data, &frag); err != nil {
|
||||
result.err = fmt.Errorf("decoding response fragment: %w", err)
|
||||
break
|
||||
}
|
||||
ifr.frags = append(ifr.frags, frag...)
|
||||
|
||||
// partial response requires additional fragments
|
||||
if rsp.Status.Code == gremlin.StatusPartialContent {
|
||||
return false
|
||||
}
|
||||
|
||||
// reassemble fragmented response
|
||||
if rsp.Result.Data, result.err = graphson.Marshal(ifr.frags); result.err != nil {
|
||||
result.err = fmt.Errorf("assembling fragmented response: %w", result.err)
|
||||
}
|
||||
case gremlin.StatusAuthenticate:
|
||||
// receiver should never block
|
||||
c.grp.Go(func() error {
|
||||
var buf bytes.Buffer
|
||||
if err := graphson.NewEncoder(&buf).Encode(
|
||||
gremlin.NewAuthRequest(rsp.RequestID, c.user, c.pass),
|
||||
); err != nil {
|
||||
return fmt.Errorf("encoding auth request: %w", err)
|
||||
}
|
||||
select {
|
||||
case c.send <- &buf:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
return c.ctx.Err()
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
ifr.result <- result
|
||||
return true
|
||||
}
|
||||
390
dialect/gremlin/internal/ws/conn_test.go
Normal file
390
dialect/gremlin/internal/ws/conn_test.go
Normal file
@@ -0,0 +1,390 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin"
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type conn struct{ *websocket.Conn }
|
||||
|
||||
func (c conn) ReadRequest() (*gremlin.Request, error) {
|
||||
_, data, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var req gremlin.Request
|
||||
if err := graphson.Unmarshal(data[data[0]+1:], &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func (c conn) WriteResponse(rsp *gremlin.Response) error {
|
||||
data, err := graphson.Marshal(rsp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.WriteMessage(websocket.BinaryMessage, data)
|
||||
}
|
||||
|
||||
func serve(handler func(conn)) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
|
||||
c, _ := upgrader.Upgrade(w, r, nil)
|
||||
defer c.Close()
|
||||
handler(conn{c})
|
||||
for {
|
||||
_, _, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TestConnectClosure(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
defer wg.Wait()
|
||||
|
||||
srv := serve(func(conn conn) {
|
||||
defer wg.Done()
|
||||
_, _, err := conn.ReadMessage()
|
||||
assert.True(t, websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = conn.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = conn.Execute(context.Background(), gremlin.NewEvalRequest("g.V()"))
|
||||
assert.EqualError(t, err, ErrConnClosed.Error())
|
||||
}
|
||||
|
||||
func TestSimpleQuery(t *testing.T) {
|
||||
srv := serve(func(conn conn) {
|
||||
typ, data, err := conn.ReadMessage()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, websocket.BinaryMessage, typ)
|
||||
|
||||
var req gremlin.Request
|
||||
err = graphson.Unmarshal(data[data[0]+1:], &req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "g.V()", req.Arguments["gremlin"])
|
||||
|
||||
rsp := gremlin.Response{RequestID: req.RequestID}
|
||||
rsp.Status.Code = gremlin.StatusNoContent
|
||||
err = conn.WriteResponse(&rsp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer assert.Condition(t, func() bool { return assert.NoError(t, conn.Close()) })
|
||||
|
||||
rsp, err := conn.Execute(context.Background(), gremlin.NewEvalRequest("g.V()"))
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, rsp)
|
||||
assert.Equal(t, gremlin.StatusNoContent, rsp.Status.Code)
|
||||
}
|
||||
|
||||
func TestDuplicateRequest(t *testing.T) {
|
||||
// skip until flakiness will be fixed.
|
||||
t.SkipNow()
|
||||
srv := serve(func(conn conn) {
|
||||
req, err := conn.ReadRequest()
|
||||
require.NoError(t, err)
|
||||
|
||||
rsp := gremlin.Response{RequestID: req.RequestID}
|
||||
rsp.Status.Code = gremlin.StatusNoContent
|
||||
err = conn.WriteResponse(&rsp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
var errors [2]error
|
||||
req := gremlin.NewEvalRequest("g.V()")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(errors))
|
||||
|
||||
for i := range errors {
|
||||
go func(i int) {
|
||||
_, errors[i] = conn.Execute(context.Background(), req)
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
err = errors[0]
|
||||
if err == nil {
|
||||
err = errors[1]
|
||||
}
|
||||
assert.EqualError(t, err, ErrDuplicateRequest.Error())
|
||||
}
|
||||
|
||||
func TestConnectCancellation(t *testing.T) {
|
||||
srv := serve(func(conn) {})
|
||||
defer srv.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
conn, err := DefaultDialer.DialContext(ctx, "ws://"+srv.Listener.Addr().String())
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, conn)
|
||||
}
|
||||
|
||||
func TestQueryCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
srv := serve(func(conn conn) {
|
||||
if _, _, err := conn.ReadMessage(); err == nil {
|
||||
cancel()
|
||||
}
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Execute(ctx, gremlin.NewEvalRequest("g.E()"))
|
||||
assert.EqualError(t, err, context.Canceled.Error())
|
||||
}
|
||||
|
||||
func TestBadResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mangle func(*gremlin.Response) *gremlin.Response
|
||||
}{
|
||||
{
|
||||
name: "NoStatus",
|
||||
mangle: func(rsp *gremlin.Response) *gremlin.Response {
|
||||
return rsp
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Malformed",
|
||||
mangle: func(rsp *gremlin.Response) *gremlin.Response {
|
||||
rsp.Status.Code = gremlin.StatusMalformedRequest
|
||||
rsp.Status.Message = "bad request"
|
||||
return rsp
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
mangle: func(rsp *gremlin.Response) *gremlin.Response {
|
||||
rsp.Status.Code = 424242
|
||||
return rsp
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
srv := serve(func(conn conn) {
|
||||
for {
|
||||
req, err := conn.ReadRequest()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
idx, err := strconv.ParseInt(req.Arguments["gremlin"].(string), 10, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = conn.WriteResponse(tests[idx].mangle(&gremlin.Response{RequestID: req.RequestID}))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(tests))
|
||||
|
||||
ctx := context.Background()
|
||||
for i, tc := range tests {
|
||||
i, tc := i, tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer wg.Done()
|
||||
rsp, err := conn.Execute(ctx, gremlin.NewEvalRequest(strconv.FormatInt(int64(i), 10)))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, rsp.IsErr())
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestServerHangup(t *testing.T) {
|
||||
// skip until flakiness will be fixed.
|
||||
t.SkipNow()
|
||||
srv := serve(func(conn conn) { _ = conn.Close() })
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Execute(context.Background(), gremlin.NewEvalRequest("g.V()"))
|
||||
assert.EqualError(t, err, ErrConnClosed.Error())
|
||||
assert.Error(t, conn.ctx.Err())
|
||||
}
|
||||
|
||||
func TestCanceledLongRequest(t *testing.T) {
|
||||
// skip until flakiness will be fixed.
|
||||
t.SkipNow()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
srv := serve(func(conn conn) {
|
||||
var responses [3]*gremlin.Response
|
||||
for i := 0; i < len(responses); i++ {
|
||||
req, err := conn.ReadRequest()
|
||||
require.NoError(t, err)
|
||||
|
||||
rsp := gremlin.Response{RequestID: req.RequestID}
|
||||
rsp.Status.Code = gremlin.StatusSuccess
|
||||
rsp.Result.Data = graphson.RawMessage(`"ok"`)
|
||||
responses[i] = &rsp
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
responses[0], responses[2] = responses[2], responses[0]
|
||||
for i := 0; i < len(responses); i++ {
|
||||
err := conn.WriteResponse(responses[i])
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
defer wg.Wait()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
go func(ctx context.Context, idx int) {
|
||||
defer wg.Done()
|
||||
rsp, err := conn.Execute(ctx, gremlin.NewEvalRequest("g.V()"))
|
||||
if idx > 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []byte(`"ok"`), rsp.Result.Data)
|
||||
} else {
|
||||
assert.EqualError(t, err, context.Canceled.Error())
|
||||
}
|
||||
}(ctx, i)
|
||||
ctx = context.Background()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialResponse(t *testing.T) {
|
||||
type kv struct {
|
||||
Key string
|
||||
Value int
|
||||
}
|
||||
kvs := []kv{
|
||||
{"one", 1},
|
||||
{"two", 2},
|
||||
{"three", 3},
|
||||
}
|
||||
srv := serve(func(conn conn) {
|
||||
req, err := conn.ReadRequest()
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := range kvs {
|
||||
data, err := graphson.Marshal([]kv{kvs[i]})
|
||||
require.NoError(t, err)
|
||||
|
||||
rsp := gremlin.Response{RequestID: req.RequestID}
|
||||
rsp.Result.Data = graphson.RawMessage(data)
|
||||
|
||||
if i != len(kvs)-1 {
|
||||
rsp.Status.Code = gremlin.StatusPartialContent
|
||||
} else {
|
||||
rsp.Status.Code = gremlin.StatusSuccess
|
||||
}
|
||||
|
||||
err = conn.WriteResponse(&rsp)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
rsp, err := conn.Execute(context.Background(), gremlin.NewEvalRequest("g.E()"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result []kv
|
||||
err = graphson.Unmarshal(rsp.Result.Data, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, kvs, result)
|
||||
}
|
||||
|
||||
func TestAuthentication(t *testing.T) {
|
||||
user, pass := "username", "password"
|
||||
srv := serve(func(conn conn) {
|
||||
req, err := conn.ReadRequest()
|
||||
require.NoError(t, err)
|
||||
|
||||
rsp := gremlin.Response{RequestID: req.RequestID}
|
||||
rsp.Status.Code = gremlin.StatusAuthenticate
|
||||
err = conn.WriteResponse(&rsp)
|
||||
require.NoError(t, err)
|
||||
|
||||
areq, err := conn.ReadRequest()
|
||||
require.NoError(t, err)
|
||||
|
||||
var acreds gremlin.Credentials
|
||||
err = acreds.UnmarshalText([]byte(areq.Arguments["sasl"].(string)))
|
||||
assert.NoError(t, err)
|
||||
areq.Arguments["sasl"] = acreds
|
||||
assert.Equal(t, gremlin.NewAuthRequest(req.RequestID, user, pass), areq)
|
||||
|
||||
rsp = gremlin.Response{RequestID: req.RequestID}
|
||||
rsp.Status.Code = gremlin.StatusNoContent
|
||||
err = conn.WriteResponse(&rsp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
dialer := *DefaultDialer
|
||||
dialer.user = user
|
||||
dialer.pass = pass
|
||||
|
||||
client, err := dialer.Dial("ws://" + srv.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.Execute(context.Background(), gremlin.NewEvalRequest("g.E().drop()"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
64
dialect/gremlin/ocgremlin/client.go
Normal file
64
dialect/gremlin/ocgremlin/client.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package ocgremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin"
|
||||
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// Transport is an gremlin.RoundTripper that instruments all outgoing requests with
|
||||
// OpenCensus stats and tracing.
|
||||
type Transport struct {
|
||||
// Base is a wrapped gremlin.RoundTripper that does the actual requests.
|
||||
Base gremlin.RoundTripper
|
||||
|
||||
// StartOptions are applied to the span started by this Transport around each
|
||||
// request.
|
||||
//
|
||||
// StartOptions.SpanKind will always be set to trace.SpanKindClient
|
||||
// for spans started by this transport.
|
||||
StartOptions trace.StartOptions
|
||||
|
||||
// GetStartOptions allows to set start options per request. If set,
|
||||
// StartOptions is going to be ignored.
|
||||
GetStartOptions func(context.Context, *gremlin.Request) trace.StartOptions
|
||||
|
||||
// NameFromRequest holds the function to use for generating the span name
|
||||
// from the information found in the outgoing Gremlin Request. By default the
|
||||
// name equals the URL Path.
|
||||
FormatSpanName func(context.Context, *gremlin.Request) string
|
||||
|
||||
// WithQuery, if set to true, will enable recording of gremlin queries in spans.
|
||||
// Only allow this if it is safe to have queries recorded with respect to
|
||||
// security.
|
||||
WithQuery bool
|
||||
}
|
||||
|
||||
// RoundTrip implements gremlin.RoundTripper, delegating to Base and recording stats and traces for the request.
|
||||
func (t *Transport) RoundTrip(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
|
||||
spanNameFormatter := t.FormatSpanName
|
||||
if spanNameFormatter == nil {
|
||||
spanNameFormatter = func(context.Context, *gremlin.Request) string {
|
||||
return "gremlin:traversal"
|
||||
}
|
||||
}
|
||||
startOpts := t.StartOptions
|
||||
if t.GetStartOptions != nil {
|
||||
startOpts = t.GetStartOptions(ctx, req)
|
||||
}
|
||||
|
||||
var rt gremlin.RoundTripper = &traceTransport{
|
||||
base: t.Base,
|
||||
formatSpanName: spanNameFormatter,
|
||||
startOptions: startOpts,
|
||||
withQuery: t.WithQuery,
|
||||
}
|
||||
rt = statsTransport{rt}
|
||||
return rt.RoundTrip(ctx, req)
|
||||
}
|
||||
74
dialect/gremlin/ocgremlin/client_test.go
Normal file
74
dialect/gremlin/ocgremlin/client_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package ocgremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
type mockExporter struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (e *mockExporter) ExportSpan(s *trace.SpanData) {
|
||||
e.Called(s)
|
||||
}
|
||||
|
||||
func TestTransportOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
spanName string
|
||||
wantName string
|
||||
}{
|
||||
{
|
||||
name: "Default formatter",
|
||||
wantName: "gremlin:traversal",
|
||||
},
|
||||
{
|
||||
name: "Custom formatter",
|
||||
spanName: "tester",
|
||||
wantName: "tester",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var exporter mockExporter
|
||||
exporter.On(
|
||||
"ExportSpan",
|
||||
mock.MatchedBy(func(s *trace.SpanData) bool { return s.Name == tt.wantName })).
|
||||
Once()
|
||||
defer exporter.AssertExpectations(t)
|
||||
trace.RegisterExporter(&exporter)
|
||||
defer trace.UnregisterExporter(&exporter)
|
||||
|
||||
transport := &mockTransport{}
|
||||
transport.On("RoundTrip", mock.Anything, mock.Anything).
|
||||
Return(nil, errors.New("noop")).
|
||||
Once()
|
||||
defer transport.AssertExpectations(t)
|
||||
|
||||
rt := &Transport{
|
||||
Base: transport,
|
||||
GetStartOptions: func(context.Context, *gremlin.Request) trace.StartOptions {
|
||||
return trace.StartOptions{Sampler: trace.AlwaysSample()}
|
||||
},
|
||||
}
|
||||
if tt.spanName != "" {
|
||||
rt.FormatSpanName = func(context.Context, *gremlin.Request) string {
|
||||
return tt.spanName
|
||||
}
|
||||
}
|
||||
_, _ = rt.RoundTrip(context.Background(), gremlin.NewEvalRequest("g.E()"))
|
||||
})
|
||||
}
|
||||
}
|
||||
117
dialect/gremlin/ocgremlin/stats.go
Normal file
117
dialect/gremlin/ocgremlin/stats.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package ocgremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin"
|
||||
|
||||
"go.opencensus.io/stats"
|
||||
"go.opencensus.io/stats/view"
|
||||
"go.opencensus.io/tag"
|
||||
)
|
||||
|
||||
// The following measures are supported for use in custom views.
|
||||
var (
|
||||
RequestCount = stats.Int64(
|
||||
"gremlin/request_count",
|
||||
"Number of Gremlin requests started",
|
||||
stats.UnitDimensionless,
|
||||
)
|
||||
ResponseBytes = stats.Int64(
|
||||
"gremlin/response_bytes",
|
||||
"Total number of bytes in response data",
|
||||
stats.UnitBytes,
|
||||
)
|
||||
RoundTripLatency = stats.Float64(
|
||||
"gremlin/roundtrip_latency",
|
||||
"End-to-end latency",
|
||||
stats.UnitMilliseconds,
|
||||
)
|
||||
)
|
||||
|
||||
// The following tags are applied to stats recorded by this package.
|
||||
var (
|
||||
// StatusCode is the numeric Gremlin response status code,
|
||||
// or "error" if a transport error occurred and no status code was read.
|
||||
StatusCode, _ = tag.NewKey("gremlin_status_code")
|
||||
)
|
||||
|
||||
// Default distributions used by views in this package.
|
||||
var (
|
||||
DefaultSizeDistribution = view.Distribution(32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576)
|
||||
DefaultLatencyDistribution = view.Distribution(1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130, 160, 200, 250, 300, 400, 500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000)
|
||||
)
|
||||
|
||||
// Package ocgremlin provides some convenience views for measures.
|
||||
// You still need to register these views for data to actually be collected.
|
||||
var (
|
||||
RequestCountView = &view.View{
|
||||
Name: "gremlin/request_count",
|
||||
Measure: RequestCount,
|
||||
Aggregation: view.Count(),
|
||||
Description: "Count of Gremlin requests started",
|
||||
}
|
||||
|
||||
ResponseCountView = &view.View{
|
||||
Name: "gremlin/response_count",
|
||||
Measure: RoundTripLatency,
|
||||
Aggregation: view.Count(),
|
||||
Description: "Count of responses received, by response status",
|
||||
TagKeys: []tag.Key{StatusCode},
|
||||
}
|
||||
|
||||
ResponseBytesView = &view.View{
|
||||
Name: "gremlin/response_bytes",
|
||||
Measure: ResponseBytes,
|
||||
Aggregation: DefaultSizeDistribution,
|
||||
Description: "Total number of bytes in response data",
|
||||
}
|
||||
|
||||
RoundTripLatencyView = &view.View{
|
||||
Name: "gremlin/roundtrip_latency",
|
||||
Measure: RoundTripLatency,
|
||||
Aggregation: DefaultLatencyDistribution,
|
||||
Description: "End-to-end latency, by response code",
|
||||
TagKeys: []tag.Key{StatusCode},
|
||||
}
|
||||
)
|
||||
|
||||
// Views are the default views provided by this package.
|
||||
func Views() []*view.View {
|
||||
return []*view.View{
|
||||
RequestCountView,
|
||||
ResponseCountView,
|
||||
ResponseBytesView,
|
||||
RoundTripLatencyView,
|
||||
}
|
||||
}
|
||||
|
||||
// statsTransport is an gremlin.RoundTripper that collects stats for the outgoing requests.
|
||||
type statsTransport struct {
|
||||
base gremlin.RoundTripper
|
||||
}
|
||||
|
||||
func (t statsTransport) RoundTrip(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
|
||||
stats.Record(ctx, RequestCount.M(1))
|
||||
start := time.Now()
|
||||
rsp, err := t.base.RoundTrip(ctx, req)
|
||||
latency := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
var (
|
||||
tags = make([]tag.Mutator, 1)
|
||||
ms = []stats.Measurement{RoundTripLatency.M(latency)}
|
||||
)
|
||||
if err == nil {
|
||||
tags[0] = tag.Upsert(StatusCode, strconv.Itoa(rsp.Status.Code))
|
||||
ms = append(ms, ResponseBytes.M(int64(len(rsp.Result.Data))))
|
||||
} else {
|
||||
tags[0] = tag.Upsert(StatusCode, "error")
|
||||
}
|
||||
_ = stats.RecordWithTags(ctx, tags, ms...)
|
||||
return rsp, err
|
||||
}
|
||||
94
dialect/gremlin/ocgremlin/stats_test.go
Normal file
94
dialect/gremlin/ocgremlin/stats_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package ocgremlin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect/gremlin"
|
||||
"entgo.io/ent/dialect/gremlin/encoding/graphson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opencensus.io/stats/view"
|
||||
)
|
||||
|
||||
func TestStatsCollection(t *testing.T) {
|
||||
err := view.Register(
|
||||
RequestCountView,
|
||||
ResponseCountView,
|
||||
ResponseBytesView,
|
||||
RoundTripLatencyView,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := gremlin.NewEvalRequest("g.E()")
|
||||
rsp := &gremlin.Response{RequestID: req.RequestID}
|
||||
rsp.Status.Code = gremlin.StatusSuccess
|
||||
rsp.Result.Data = graphson.RawMessage(
|
||||
`{"@type": "g:List", "@value": [{"@type": "g:Int32", "@value": 42}]}`,
|
||||
)
|
||||
|
||||
transport := &mockTransport{}
|
||||
transport.On("RoundTrip", mock.Anything, mock.Anything).
|
||||
Return(rsp, nil).
|
||||
Once()
|
||||
defer transport.AssertExpectations(t)
|
||||
|
||||
rt := &statsTransport{transport}
|
||||
_, _ = rt.RoundTrip(context.Background(), req)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expect func(*testing.T, *view.Row)
|
||||
}{
|
||||
{
|
||||
name: "gremlin/request_count",
|
||||
expect: func(t *testing.T, row *view.Row) {
|
||||
count, ok := row.Data.(*view.CountData)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, int64(1), count.Value)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gremlin/response_count",
|
||||
expect: func(t *testing.T, row *view.Row) {
|
||||
count, ok := row.Data.(*view.CountData)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, int64(1), count.Value)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gremlin/response_bytes",
|
||||
expect: func(t *testing.T, row *view.Row) {
|
||||
data, ok := row.Data.(*view.DistributionData)
|
||||
require.True(t, ok)
|
||||
assert.EqualValues(t, len(rsp.Result.Data), data.Sum())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gremlin/roundtrip_latency",
|
||||
expect: func(t *testing.T, row *view.Row) {
|
||||
data, ok := row.Data.(*view.DistributionData)
|
||||
require.True(t, ok)
|
||||
assert.NotZero(t, data.Sum())
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name[strings.Index(tt.name, "/")+1:], func(t *testing.T) {
|
||||
v := view.Find(tt.name)
|
||||
assert.NotNil(t, v)
|
||||
rows, err := view.RetrieveData(tt.name)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 1)
|
||||
tt.expect(t, rows[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user