feat: Add API
feat: Add DB-backed time provider
This commit is contained in:
@@ -1 +1,2 @@
|
||||
/.idea
|
||||
*.sqlite3*
|
||||
@@ -12,6 +12,8 @@ require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/doug-martin/goqu/v9 v9.18.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.42.0 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
@@ -19,6 +21,7 @@ require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.16.0 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
@@ -31,9 +34,11 @@ require (
|
||||
github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
|
||||
github.com/quic-go/quic-go v0.33.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/rs/zerolog v1.29.0 // indirect
|
||||
github.com/samber/lo v1.37.0 // indirect
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
@@ -49,4 +54,14 @@ require (
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.3 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sqlite v1.21.0 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
@@ -10,12 +11,19 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
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/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY=
|
||||
github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8=
|
||||
github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
@@ -31,6 +39,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/imroc/req/v3 v3.32.3 h1:FX3xCN0iWn1wJKHN0EB8l7EjXDTE4j/C8m7zeAb2UH0=
|
||||
github.com/imroc/req/v3 v3.32.3/go.mod h1:cZ+7C3L/AYOr4tLGG16hZF90F1WzAdAdzt1xFSlizXY=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
@@ -41,6 +51,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -50,6 +61,7 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
|
||||
github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
|
||||
github.com/onsi/gomega v1.27.1 h1:rfztXRbg6nv/5f+Raen9RcGoSecHIFgBBLQK3Wdj754=
|
||||
@@ -67,6 +79,9 @@ github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3w
|
||||
github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
||||
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
|
||||
github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -76,6 +91,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
|
||||
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||
@@ -85,6 +102,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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=
|
||||
@@ -102,6 +120,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -184,3 +204,23 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
|
||||
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
+8
-3
@@ -2,13 +2,14 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
recover "github.com/gofiber/fiber/v2/middleware/recover"
|
||||
|
||||
"prayertimes/pkg/prayer"
|
||||
@@ -40,14 +41,16 @@ func New(services Services) *fiber.App {
|
||||
})
|
||||
|
||||
app.Use(
|
||||
favicon.New(),
|
||||
recover.New(recover.Config{
|
||||
EnableStackTrace: true,
|
||||
}),
|
||||
favicon.New(),
|
||||
logger.New(),
|
||||
cors.New(),
|
||||
)
|
||||
|
||||
app.Get("/diyanet/prayertimes", cache.New(), func(ctx *fiber.Ctx) error {
|
||||
cacheHeader := fmt.Sprintf("max-age=%0f", (time.Hour * 24).Seconds())
|
||||
app.Get("/diyanet/prayertimes", func(ctx *fiber.Ctx) error {
|
||||
locationID := ctx.Query("location_id")
|
||||
|
||||
times, err := services.PrayerTimesProvider.Get(ctx.Context(), locationID)
|
||||
@@ -55,6 +58,8 @@ func New(services Services) *fiber.App {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Response().Header.Add(fiber.HeaderCacheControl, cacheHeader)
|
||||
|
||||
return ctx.JSON(times)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
goqusqlite3 "github.com/doug-martin/goqu/v9/dialect/sqlite3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goqu.RegisterDialect("sqlite3", func() *goqu.SQLDialectOptions {
|
||||
do := goqusqlite3.DialectOptions()
|
||||
do.SupportsReturn = true
|
||||
return do
|
||||
}())
|
||||
}
|
||||
|
||||
func NewSqliteDB(filename string) (*goqu.Database, error) {
|
||||
pragmas := url.Values{
|
||||
"_pragma": {
|
||||
"journal_mode(WAL)",
|
||||
"foreign_keys(1)",
|
||||
"synchronous(NORMAL)",
|
||||
"busy_timeout(5000)",
|
||||
},
|
||||
}.Encode()
|
||||
dsn := fmt.Sprintf("%s?%s", filename, pragmas)
|
||||
|
||||
conn, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
return goqu.New("sqlite3", conn), nil
|
||||
}
|
||||
@@ -1,33 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"prayertimes/internal/api"
|
||||
"prayertimes/internal/database"
|
||||
"prayertimes/internal/scrapeutils"
|
||||
"prayertimes/pkg/dbtimesprovider"
|
||||
"prayertimes/pkg/diyanet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port, ok := os.LookupEnv("PORT")
|
||||
if !ok {
|
||||
port = "8000"
|
||||
port := getDefaultEnv("PORT", "8000")
|
||||
|
||||
services, shutdown, err := newServices()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to init services")
|
||||
}
|
||||
services := newServices()
|
||||
defer shutdown()
|
||||
app := api.New(services)
|
||||
err := app.Listen(":" + port)
|
||||
|
||||
err = app.Listen(":" + port)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("exit with an error")
|
||||
}
|
||||
}
|
||||
|
||||
func newServices() api.Services {
|
||||
d := diyanet.Diyanet{
|
||||
func newServices() (api.Services, func(), error) {
|
||||
diyanetProvider := diyanet.Diyanet{
|
||||
FetcherFunc: scrapeutils.GetParsed,
|
||||
}
|
||||
return api.Services{
|
||||
PrayerTimesProvider: d,
|
||||
|
||||
db, err := database.NewSqliteDB(getDefaultEnv("DATABASE_URL", "app.sqlite3"))
|
||||
if err != nil {
|
||||
return api.Services{}, nil, fmt.Errorf("failed to init db: %w", err)
|
||||
}
|
||||
|
||||
dbProvider := dbtimesprovider.New(db, diyanetProvider)
|
||||
if err := dbtimesprovider.Migrate(db.Db.(*sql.DB)); err != nil {
|
||||
return api.Services{}, nil, fmt.Errorf("failed to migrate database: %w", err)
|
||||
}
|
||||
|
||||
return api.Services{
|
||||
PrayerTimesProvider: dbProvider,
|
||||
}, func() {
|
||||
defer db.Db.(*sql.DB).Close()
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getDefaultEnv(name string, defaultValue string) string {
|
||||
v, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
package dbtimesprovider
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"prayertimes/pkg/prayer"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var schema string
|
||||
|
||||
//go:embed locations.jsonl
|
||||
var locationsJSON string
|
||||
|
||||
type Provider struct {
|
||||
db *goqu.Database
|
||||
provider prayer.TimesProvider
|
||||
clockFunc func() time.Time
|
||||
}
|
||||
|
||||
func New(db *goqu.Database, provider prayer.TimesProvider) Provider {
|
||||
return Provider{
|
||||
provider: provider,
|
||||
clockFunc: time.Now,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Provider) Name() string {
|
||||
return "db:" + p.provider.Name()
|
||||
}
|
||||
|
||||
func (p Provider) Get(ctx context.Context, location string) ([]prayer.Times, error) {
|
||||
times, err := p.loadTimes(ctx, location)
|
||||
if err == nil && len(times) != 0 {
|
||||
return times, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to load prayer times from db: %w", err)
|
||||
}
|
||||
|
||||
times, err = p.provider.Get(ctx, location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prayer times: %w", err)
|
||||
}
|
||||
|
||||
if len(times) > 1 {
|
||||
if err := p.saveTimes(ctx, location, times); err != nil {
|
||||
return nil, fmt.Errorf("failed to save times to db: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return times, nil
|
||||
}
|
||||
|
||||
func Migrate(con *sql.DB) error {
|
||||
db := goqu.New("sqlite3", con)
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
return fmt.Errorf("failed to migrate: %w", err)
|
||||
}
|
||||
|
||||
count, _ := db.From("locations").Count()
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Country string `json:"country" db:"country"`
|
||||
Region string `json:"region" db:"region"`
|
||||
City string `json:"city" db:"city"`
|
||||
}
|
||||
s := bufio.NewScanner(strings.NewReader(locationsJSON))
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin tx: %w", err)
|
||||
}
|
||||
if err := tx.Wrap(func() error {
|
||||
for s.Scan() {
|
||||
var e entry
|
||||
if err := json.Unmarshal(s.Bytes(), &e); err != nil {
|
||||
return fmt.Errorf("failed to parse as json: %w", err)
|
||||
}
|
||||
|
||||
q := tx.Insert("locations").
|
||||
OnConflict(goqu.DoNothing()).
|
||||
Rows(e)
|
||||
|
||||
if _, err := q.Executor().Exec(); err != nil {
|
||||
return fmt.Errorf("failed to insert location: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type prayerTimesRow struct {
|
||||
ProviderID int64 `db:"provider_id"`
|
||||
LocationID string `db:"location_id"`
|
||||
Date time.Time `db:"date"`
|
||||
Fajr string `db:"fajr"`
|
||||
Sunrise string `db:"sunrise"`
|
||||
Dhuhr string `db:"dhuhr"`
|
||||
Asr string `db:"asr"`
|
||||
Maghrib string `db:"maghrib"`
|
||||
Isha string `db:"isha"`
|
||||
}
|
||||
|
||||
func (r prayerTimesRow) toDomain() prayer.Times {
|
||||
return prayer.Times{
|
||||
Date: r.Date,
|
||||
Fajr: r.Fajr,
|
||||
Sunrise: r.Sunrise,
|
||||
Dhuhr: r.Dhuhr,
|
||||
Asr: r.Asr,
|
||||
Maghrib: r.Maghrib,
|
||||
Isha: r.Isha,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Provider) saveTimes(ctx context.Context, locationID string, times []prayer.Times) error {
|
||||
providerID, err := p.saveProvider(ctx, p.provider.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows := lo.Map(times, func(item prayer.Times, _ int) prayerTimesRow {
|
||||
return prayerTimesRow{
|
||||
ProviderID: providerID,
|
||||
LocationID: locationID,
|
||||
Date: item.Date,
|
||||
Fajr: item.Fajr,
|
||||
Sunrise: item.Sunrise,
|
||||
Dhuhr: item.Dhuhr,
|
||||
Asr: item.Asr,
|
||||
Maghrib: item.Maghrib,
|
||||
Isha: item.Isha,
|
||||
}
|
||||
})
|
||||
q := p.db.
|
||||
Insert("prayer_times").
|
||||
OnConflict(goqu.DoNothing()).
|
||||
Rows(rows)
|
||||
|
||||
if _, err := q.Executor().ExecContext(ctx); err != nil {
|
||||
return fmt.Errorf("failed to save times: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Provider) loadTimes(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
||||
q := p.db.
|
||||
From(goqu.T("prayer_times").As("pt")).
|
||||
Join(goqu.T("providers").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("pt.provider_id")))).
|
||||
Where(
|
||||
goqu.I("p.name").Eq(p.provider.Name()),
|
||||
goqu.I("pt.location_id").Eq(locationID),
|
||||
goqu.I("pt.date").Gte(p.clockFunc()),
|
||||
).
|
||||
Limit(100)
|
||||
|
||||
var rows []prayerTimesRow
|
||||
if err := q.ScanStructsContext(ctx, &rows); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan times: %w", err)
|
||||
}
|
||||
|
||||
return lo.Map(rows, func(row prayerTimesRow, _ int) prayer.Times {
|
||||
return row.toDomain()
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p Provider) saveProvider(ctx context.Context, name string) (int64, error) {
|
||||
q := p.db.Insert("providers").
|
||||
OnConflict(goqu.DoUpdate("name", goqu.Record{"name": name})).
|
||||
Rows(goqu.Record{"name": name}).
|
||||
Returning("id")
|
||||
var id int64
|
||||
_, err := q.Executor().ScanValContext(ctx, &id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to insert provider: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package dbtimesprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"prayertimes/internal/database"
|
||||
"prayertimes/pkg/prayer"
|
||||
)
|
||||
|
||||
func testDB(t *testing.T) *goqu.Database {
|
||||
t.Helper()
|
||||
|
||||
db, err := database.NewSqliteDB(":memory:")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Migrate(db.Db.(*sql.DB))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Insert("locations").Rows(goqu.Record{"id": 1}).Executor().Exec()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
db.Db.(*sql.DB).Close()
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
type mockProvider func() ([]prayer.Times, error)
|
||||
|
||||
func (m mockProvider) Get(ctx context.Context, location string) ([]prayer.Times, error) { return m() }
|
||||
|
||||
func (m mockProvider) Name() string { return "mock" }
|
||||
|
||||
func TestProvider_Get(t *testing.T) {
|
||||
then := time.Date(2023, 3, 5, 0, 0, 0, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
setupDB func(t *testing.T, db *goqu.Database)
|
||||
provider prayer.TimesProvider
|
||||
clock time.Time
|
||||
assertRes func(t *testing.T, db *goqu.Database, times []prayer.Times, err error)
|
||||
}{
|
||||
{
|
||||
name: "provider succeeds, empty db",
|
||||
provider: mockProvider(func() ([]prayer.Times, error) {
|
||||
return []prayer.Times{
|
||||
{Date: time.Date(2023, 3, 4, 0, 0, 0, 0, time.UTC)},
|
||||
{Date: time.Date(2023, 3, 5, 0, 0, 0, 0, time.UTC)},
|
||||
}, nil
|
||||
}),
|
||||
clock: then,
|
||||
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 2)
|
||||
|
||||
cnt, err := db.From("prayer_times").Count()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), cnt)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provider fails, empty db",
|
||||
provider: mockProvider(func() ([]prayer.Times, error) {
|
||||
return nil, fmt.Errorf("no")
|
||||
}),
|
||||
clock: then,
|
||||
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, times)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provider fails, populated db",
|
||||
setupDB: func(t *testing.T, db *goqu.Database) {
|
||||
_, err := db.Insert("prayer_times").Rows(
|
||||
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: time.Date(2023, 3, 4, 0, 0, 0, 0, time.UTC), Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
|
||||
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: time.Date(2023, 3, 5, 0, 0, 0, 0, time.UTC), Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
|
||||
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: time.Date(2023, 3, 6, 0, 0, 0, 0, time.UTC), Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
|
||||
).Executor().Exec()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
provider: mockProvider(func() ([]prayer.Times, error) {
|
||||
return nil, fmt.Errorf("no")
|
||||
}),
|
||||
clock: then,
|
||||
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 2)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db := testDB(t)
|
||||
p := Provider{
|
||||
db: db,
|
||||
provider: tt.provider,
|
||||
clockFunc: func() time.Time { return tt.clock },
|
||||
}
|
||||
if tt.setupDB != nil {
|
||||
tt.setupDB(t, db)
|
||||
}
|
||||
|
||||
actual, err := p.Get(context.Background(), "1")
|
||||
tt.assertRes(t, db, actual, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE IF NOT EXISTS locations
|
||||
(
|
||||
id text PRIMARY KEY,
|
||||
country text,
|
||||
city text,
|
||||
region text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS providers
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prayer_times
|
||||
(
|
||||
provider_id integer NOT NULL REFERENCES providers (id),
|
||||
location_id text NOT NULL REFERENCES locations (id),
|
||||
date datetime NOT NULL,
|
||||
fajr text NOT NULL,
|
||||
sunrise text NOT NULL,
|
||||
dhuhr text NOT NULL,
|
||||
asr text NOT NULL,
|
||||
maghrib text NOT NULL,
|
||||
isha text NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS prayer_times__provider__location ON prayer_times (provider_id, location_id, date);
|
||||
@@ -62,3 +62,7 @@ func (d Diyanet) Get(ctx context.Context, location string) ([]prayer.Times, erro
|
||||
|
||||
return times, err
|
||||
}
|
||||
|
||||
func (d Diyanet) Name() string {
|
||||
return "diyanet"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ var ErrInvalidLocation = errors.New("invalid location")
|
||||
|
||||
type TimesProvider interface {
|
||||
Get(ctx context.Context, location string) ([]Times, error)
|
||||
Name() string
|
||||
}
|
||||
|
||||
type Times struct {
|
||||
|
||||
Reference in New Issue
Block a user