├── .editorconfig ├── Dockerfile ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum ├── http ├── routers.go └── server.go ├── lemonade ├── filter.go ├── global.go └── trie.go ├── main.go ├── rpc ├── protos │ ├── lemonade.pb.go │ └── lemonade.proto └── server.go └── trie ├── node.go └── trie.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml,graphql,gql}] 15 | indent_size = 2 16 | 17 | [*.go] 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | 3 | ADD . /src 4 | 5 | RUN apk -U add git && \ 6 | cd /src && \ 7 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app 8 | 9 | FROM scratch 10 | 11 | COPY --from=build /src/app / 12 | 13 | EXPOSE 8080 14 | 15 | ENTRYPOINT ["/app"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Karl Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lemonade 2 | 3 | A sensitive word filter service based on [DFA](https://www.wikiwand.com/en/Deterministic_finite_automaton) 4 | 5 | ## Install 6 | ```shell script 7 | go get -u -v github.com/killtw/lemonade 8 | ``` 9 | 10 | ## Usage 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "github.com/killtw/lemonade/lemonade" 18 | "log" 19 | ) 20 | 21 | func main() { 22 | if err := lemonade.InitTrie(); err != nil { 23 | log.Fatalln(err) 24 | } 25 | 26 | lemonade.Add("test") 27 | 28 | f1, m1 := lemonade.Replace("123test321") 29 | fmt.Printf("filtered: %s, matches: %s\n", f1, m1) 30 | 31 | f2, m2 := lemonade.Replace("123te!@#$%st321") 32 | fmt.Printf("filtered: %s, matches: %s\n", f2, m2) 33 | } 34 | ``` 35 | 36 | ### Output 37 | ```shell script 38 | filtered: 123****321, matches: [test] 39 | filtered: 123*********321, matches: [te!@#$%st] 40 | ``` 41 | 42 | ## Credits 43 | 44 | - [Karl Li](https://github.com/killtw) 45 | - [All Contributors](../../contributors) 46 | 47 | ## License 48 | 49 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/killtw/lemonade 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.5.0 7 | github.com/golang/protobuf v1.3.2 8 | github.com/jinzhu/gorm v1.9.11 9 | github.com/kr/pretty v0.1.0 // indirect 10 | golang.org/x/net v0.0.0-20190311183353-d8887717615a 11 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e 12 | google.golang.org/grpc v1.19.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= 4 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 7 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 10 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 11 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 12 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= 17 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= 18 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 19 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 20 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 21 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 22 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 23 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 24 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 25 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 26 | github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= 27 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 28 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 29 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 30 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 31 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 32 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 33 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 34 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 35 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 36 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 37 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 38 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 39 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 40 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 41 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 42 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 43 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 45 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 47 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 48 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 49 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 50 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 51 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 52 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 53 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 54 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 55 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 56 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 57 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 58 | github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE= 59 | github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw= 60 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 61 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 62 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 63 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 64 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 65 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 66 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 67 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 68 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 69 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 70 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 71 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 72 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 73 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 74 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 75 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 76 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 77 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 78 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 79 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 80 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 81 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 82 | github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= 83 | github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 84 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 85 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 86 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 87 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 88 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 89 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 90 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 91 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 92 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 93 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 94 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 95 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 96 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 97 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 99 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 100 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 101 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 102 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 103 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 104 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 105 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 106 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 107 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 108 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 109 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 110 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 111 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 112 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 113 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 114 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 115 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 116 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 117 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 118 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 119 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 120 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= 121 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 122 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 123 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 124 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 125 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 126 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 129 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 130 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 133 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 134 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 135 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 136 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 137 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 142 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 143 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 147 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 149 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 150 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 152 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= 153 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 154 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 155 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 156 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 158 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 159 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 160 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 161 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 162 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 163 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 164 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 165 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo= 166 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 167 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 168 | google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= 169 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 170 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 171 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 172 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 173 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 174 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 175 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 176 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 177 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 178 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= 179 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 180 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 181 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 183 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 185 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 186 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 187 | -------------------------------------------------------------------------------- /http/routers.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/killtw/lemonade/lemonade" 6 | "net/http" 7 | ) 8 | 9 | func rootHandler(c *gin.Context) { 10 | c.JSON(http.StatusOK, gin.H{ 11 | "message": "Welcome to lemonade server", 12 | }) 13 | } 14 | 15 | func heartbeatHandler(c *gin.Context) { 16 | c.AbortWithStatus(http.StatusOK) 17 | } 18 | 19 | func replaceHandler(c *gin.Context) { 20 | message := c.PostForm("message") 21 | filtered, matches := lemonade.Replace(message) 22 | 23 | c.JSON(http.StatusOK, gin.H{ 24 | "original": message, 25 | "message": filtered, 26 | "matches": matches, 27 | }) 28 | } 29 | 30 | func addWordHandler(c *gin.Context) { 31 | word := c.PostForm("word") 32 | lemonade.Add(word) 33 | 34 | c.JSON(http.StatusOK, gin.H{ 35 | "status": "ok", 36 | "word": word, 37 | }) 38 | } 39 | 40 | func routers() *gin.Engine { 41 | router := gin.Default() 42 | 43 | router.POST("/replace", replaceHandler) 44 | router.POST("/addWord", addWordHandler) 45 | router.GET("/healthz", heartbeatHandler) 46 | router.GET("/", rootHandler) 47 | 48 | return router 49 | } 50 | 51 | -------------------------------------------------------------------------------- /http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "net/http" 4 | 5 | func RunHttpServer() error { 6 | server := &http.Server{ 7 | Addr: ":8080", 8 | Handler: routers(), 9 | } 10 | 11 | return server.ListenAndServe() 12 | } 13 | -------------------------------------------------------------------------------- /lemonade/filter.go: -------------------------------------------------------------------------------- 1 | package lemonade 2 | 3 | import ( 4 | "strings" 5 | "unicode/utf8" 6 | ) 7 | 8 | func Replace(message string) (string, []string) { 9 | matches := Trie.Search(message) 10 | r := make([]string, 2*len(matches)) 11 | 12 | for i, word := range matches { 13 | r[i*2] = word 14 | r[i*2+1] = strings.Repeat("*", utf8.RuneCountInString(word)) 15 | } 16 | 17 | return strings.NewReplacer(r...).Replace(message), matches 18 | } 19 | 20 | func Add(word string) { 21 | Trie.Add(word) 22 | } 23 | -------------------------------------------------------------------------------- /lemonade/global.go: -------------------------------------------------------------------------------- 1 | package lemonade 2 | 3 | import "github.com/killtw/lemonade/trie" 4 | 5 | var ( 6 | Trie *trie.Trie 7 | ) -------------------------------------------------------------------------------- /lemonade/trie.go: -------------------------------------------------------------------------------- 1 | package lemonade 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/mysql" 7 | "github.com/killtw/lemonade/trie" 8 | "os" 9 | ) 10 | 11 | var dbHost = getenv("DB_HOST", "") 12 | 13 | type Word struct { 14 | Keyword string 15 | } 16 | 17 | func InitTrie() (err error) { 18 | t := trie.New() 19 | 20 | if dbHost != "" { 21 | words, err := getWords() 22 | 23 | if err != nil { 24 | return err 25 | } 26 | 27 | for _, word := range words { 28 | t.Add(word.Keyword) 29 | } 30 | 31 | fmt.Println("Words imported") 32 | } 33 | 34 | Trie = t 35 | 36 | return 37 | } 38 | 39 | func getWords() (words []Word, err error) { 40 | db, err := gorm.Open("mysql", dbHost) 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | defer db.Close() 47 | 48 | db.Find(&words) 49 | 50 | return 51 | } 52 | 53 | func getenv(key, fallback string) string { 54 | if value, ok := os.LookupEnv(key); ok { 55 | return value 56 | } 57 | 58 | return fallback 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/killtw/lemonade/http" 5 | "github.com/killtw/lemonade/lemonade" 6 | "github.com/killtw/lemonade/rpc" 7 | "golang.org/x/sync/errgroup" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | var err error 13 | var e errgroup.Group 14 | 15 | if err:= lemonade.InitTrie(); err != nil { 16 | log.Fatalln(err) 17 | } 18 | 19 | e.Go(http.RunHttpServer) 20 | e.Go(rpc.RunGRPCServer) 21 | 22 | if err = e.Wait(); err != nil { 23 | log.Fatalln(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rpc/protos/lemonade.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: lemonade.proto 3 | 4 | package protos 5 | 6 | import ( 7 | context "context" 8 | fmt "fmt" 9 | proto "github.com/golang/protobuf/proto" 10 | grpc "google.golang.org/grpc" 11 | codes "google.golang.org/grpc/codes" 12 | status "google.golang.org/grpc/status" 13 | math "math" 14 | ) 15 | 16 | // Reference imports to suppress errors if they are not otherwise used. 17 | var _ = proto.Marshal 18 | var _ = fmt.Errorf 19 | var _ = math.Inf 20 | 21 | // This is a compile-time assertion to ensure that this generated file 22 | // is compatible with the proto package it is being compiled against. 23 | // A compilation error at this line likely means your copy of the 24 | // proto package needs to be updated. 25 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 26 | 27 | type ReplaceRequest struct { 28 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 29 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 30 | XXX_unrecognized []byte `json:"-"` 31 | XXX_sizecache int32 `json:"-"` 32 | } 33 | 34 | func (m *ReplaceRequest) Reset() { *m = ReplaceRequest{} } 35 | func (m *ReplaceRequest) String() string { return proto.CompactTextString(m) } 36 | func (*ReplaceRequest) ProtoMessage() {} 37 | func (*ReplaceRequest) Descriptor() ([]byte, []int) { 38 | return fileDescriptor_fb743e4414269ad4, []int{0} 39 | } 40 | 41 | func (m *ReplaceRequest) XXX_Unmarshal(b []byte) error { 42 | return xxx_messageInfo_ReplaceRequest.Unmarshal(m, b) 43 | } 44 | func (m *ReplaceRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 45 | return xxx_messageInfo_ReplaceRequest.Marshal(b, m, deterministic) 46 | } 47 | func (m *ReplaceRequest) XXX_Merge(src proto.Message) { 48 | xxx_messageInfo_ReplaceRequest.Merge(m, src) 49 | } 50 | func (m *ReplaceRequest) XXX_Size() int { 51 | return xxx_messageInfo_ReplaceRequest.Size(m) 52 | } 53 | func (m *ReplaceRequest) XXX_DiscardUnknown() { 54 | xxx_messageInfo_ReplaceRequest.DiscardUnknown(m) 55 | } 56 | 57 | var xxx_messageInfo_ReplaceRequest proto.InternalMessageInfo 58 | 59 | func (m *ReplaceRequest) GetMessage() string { 60 | if m != nil { 61 | return m.Message 62 | } 63 | return "" 64 | } 65 | 66 | type ReplaceReply struct { 67 | Original string `protobuf:"bytes,1,opt,name=original,proto3" json:"original,omitempty"` 68 | Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` 69 | Matches []string `protobuf:"bytes,3,rep,name=matches,proto3" json:"matches,omitempty"` 70 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 71 | XXX_unrecognized []byte `json:"-"` 72 | XXX_sizecache int32 `json:"-"` 73 | } 74 | 75 | func (m *ReplaceReply) Reset() { *m = ReplaceReply{} } 76 | func (m *ReplaceReply) String() string { return proto.CompactTextString(m) } 77 | func (*ReplaceReply) ProtoMessage() {} 78 | func (*ReplaceReply) Descriptor() ([]byte, []int) { 79 | return fileDescriptor_fb743e4414269ad4, []int{1} 80 | } 81 | 82 | func (m *ReplaceReply) XXX_Unmarshal(b []byte) error { 83 | return xxx_messageInfo_ReplaceReply.Unmarshal(m, b) 84 | } 85 | func (m *ReplaceReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 86 | return xxx_messageInfo_ReplaceReply.Marshal(b, m, deterministic) 87 | } 88 | func (m *ReplaceReply) XXX_Merge(src proto.Message) { 89 | xxx_messageInfo_ReplaceReply.Merge(m, src) 90 | } 91 | func (m *ReplaceReply) XXX_Size() int { 92 | return xxx_messageInfo_ReplaceReply.Size(m) 93 | } 94 | func (m *ReplaceReply) XXX_DiscardUnknown() { 95 | xxx_messageInfo_ReplaceReply.DiscardUnknown(m) 96 | } 97 | 98 | var xxx_messageInfo_ReplaceReply proto.InternalMessageInfo 99 | 100 | func (m *ReplaceReply) GetOriginal() string { 101 | if m != nil { 102 | return m.Original 103 | } 104 | return "" 105 | } 106 | 107 | func (m *ReplaceReply) GetMessage() string { 108 | if m != nil { 109 | return m.Message 110 | } 111 | return "" 112 | } 113 | 114 | func (m *ReplaceReply) GetMatches() []string { 115 | if m != nil { 116 | return m.Matches 117 | } 118 | return nil 119 | } 120 | 121 | func init() { 122 | proto.RegisterType((*ReplaceRequest)(nil), "protos.ReplaceRequest") 123 | proto.RegisterType((*ReplaceReply)(nil), "protos.ReplaceReply") 124 | } 125 | 126 | func init() { proto.RegisterFile("lemonade.proto", fileDescriptor_fb743e4414269ad4) } 127 | 128 | var fileDescriptor_fb743e4414269ad4 = []byte{ 129 | // 163 bytes of a gzipped FileDescriptorProto 130 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcb, 0x49, 0xcd, 0xcd, 131 | 0xcf, 0x4b, 0x4c, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03, 0x53, 0xc5, 0x4a, 132 | 0x5a, 0x5c, 0x7c, 0x41, 0xa9, 0x05, 0x39, 0x89, 0xc9, 0xa9, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 133 | 0x25, 0x42, 0x12, 0x5c, 0xec, 0xb9, 0xa9, 0xc5, 0xc5, 0x89, 0xe9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 134 | 0x1a, 0x9c, 0x41, 0x30, 0xae, 0x52, 0x1c, 0x17, 0x0f, 0x5c, 0x6d, 0x41, 0x4e, 0xa5, 0x90, 0x14, 135 | 0x17, 0x47, 0x7e, 0x51, 0x66, 0x7a, 0x66, 0x5e, 0x62, 0x0e, 0x54, 0x29, 0x9c, 0x8f, 0x6c, 0x0a, 136 | 0x13, 0x8a, 0x29, 0x60, 0x99, 0xc4, 0x92, 0xe4, 0x8c, 0xd4, 0x62, 0x09, 0x66, 0x05, 0x66, 0xb0, 137 | 0x0c, 0x84, 0x6b, 0xe4, 0xca, 0xc5, 0xe1, 0x03, 0x75, 0xa5, 0x90, 0x25, 0x17, 0x3b, 0xd4, 0x2e, 138 | 0x21, 0x31, 0x88, 0x93, 0x8b, 0xf5, 0x50, 0x1d, 0x2a, 0x25, 0x82, 0x21, 0x5e, 0x90, 0x53, 0xa9, 139 | 0xc4, 0x90, 0x04, 0xf1, 0x9a, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xbd, 0x68, 0xac, 0x54, 0xf3, 140 | 0x00, 0x00, 0x00, 141 | } 142 | 143 | // Reference imports to suppress errors if they are not otherwise used. 144 | var _ context.Context 145 | var _ grpc.ClientConn 146 | 147 | // This is a compile-time assertion to ensure that this generated file 148 | // is compatible with the grpc package it is being compiled against. 149 | const _ = grpc.SupportPackageIsVersion4 150 | 151 | // LemonadeClient is the client API for Lemonade service. 152 | // 153 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 154 | type LemonadeClient interface { 155 | Replace(ctx context.Context, in *ReplaceRequest, opts ...grpc.CallOption) (*ReplaceReply, error) 156 | } 157 | 158 | type lemonadeClient struct { 159 | cc *grpc.ClientConn 160 | } 161 | 162 | func NewLemonadeClient(cc *grpc.ClientConn) LemonadeClient { 163 | return &lemonadeClient{cc} 164 | } 165 | 166 | func (c *lemonadeClient) Replace(ctx context.Context, in *ReplaceRequest, opts ...grpc.CallOption) (*ReplaceReply, error) { 167 | out := new(ReplaceReply) 168 | err := c.cc.Invoke(ctx, "/protos.Lemonade/Replace", in, out, opts...) 169 | if err != nil { 170 | return nil, err 171 | } 172 | return out, nil 173 | } 174 | 175 | // LemonadeServer is the server API for Lemonade service. 176 | type LemonadeServer interface { 177 | Replace(context.Context, *ReplaceRequest) (*ReplaceReply, error) 178 | } 179 | 180 | // UnimplementedLemonadeServer can be embedded to have forward compatible implementations. 181 | type UnimplementedLemonadeServer struct { 182 | } 183 | 184 | func (*UnimplementedLemonadeServer) Replace(ctx context.Context, req *ReplaceRequest) (*ReplaceReply, error) { 185 | return nil, status.Errorf(codes.Unimplemented, "method Replace not implemented") 186 | } 187 | 188 | func RegisterLemonadeServer(s *grpc.Server, srv LemonadeServer) { 189 | s.RegisterService(&_Lemonade_serviceDesc, srv) 190 | } 191 | 192 | func _Lemonade_Replace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 193 | in := new(ReplaceRequest) 194 | if err := dec(in); err != nil { 195 | return nil, err 196 | } 197 | if interceptor == nil { 198 | return srv.(LemonadeServer).Replace(ctx, in) 199 | } 200 | info := &grpc.UnaryServerInfo{ 201 | Server: srv, 202 | FullMethod: "/protos.Lemonade/Replace", 203 | } 204 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 205 | return srv.(LemonadeServer).Replace(ctx, req.(*ReplaceRequest)) 206 | } 207 | return interceptor(ctx, in, info, handler) 208 | } 209 | 210 | var _Lemonade_serviceDesc = grpc.ServiceDesc{ 211 | ServiceName: "protos.Lemonade", 212 | HandlerType: (*LemonadeServer)(nil), 213 | Methods: []grpc.MethodDesc{ 214 | { 215 | MethodName: "Replace", 216 | Handler: _Lemonade_Replace_Handler, 217 | }, 218 | }, 219 | Streams: []grpc.StreamDesc{}, 220 | Metadata: "lemonade.proto", 221 | } 222 | -------------------------------------------------------------------------------- /rpc/protos/lemonade.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protos; 4 | 5 | service Lemonade { 6 | rpc Replace (ReplaceRequest) returns (ReplaceReply) {} 7 | } 8 | 9 | message ReplaceRequest { 10 | string message = 1; 11 | } 12 | 13 | message ReplaceReply { 14 | string original = 1; 15 | string message = 2; 16 | repeated string matches = 3; 17 | } 18 | -------------------------------------------------------------------------------- /rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "fmt" 5 | "github.com/killtw/lemonade/lemonade" 6 | "github.com/killtw/lemonade/rpc/protos" 7 | "golang.org/x/net/context" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/reflection" 10 | "log" 11 | "net" 12 | ) 13 | 14 | type Server struct{} 15 | 16 | func (s *Server) Replace(ctx context.Context, req *protos.ReplaceRequest) (*protos.ReplaceReply, error) { 17 | original := req.Message 18 | filtered, matches := lemonade.Replace(original) 19 | 20 | return &protos.ReplaceReply{ 21 | Original: original, 22 | Message: filtered, 23 | Matches: matches, 24 | }, nil 25 | } 26 | 27 | func RunGRPCServer() error { 28 | listener, err := net.Listen("tcp", ":9000") 29 | 30 | if err != nil { 31 | log.Fatalln(err) 32 | 33 | return err 34 | } 35 | 36 | gs := grpc.NewServer() 37 | s := &Server{} 38 | protos.RegisterLemonadeServer(gs, s) 39 | 40 | reflection.Register(gs) 41 | fmt.Println("gRPC server is running") 42 | if err := gs.Serve(listener); err != nil { 43 | log.Fatalln("gRPC server err: ", err) 44 | 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /trie/node.go: -------------------------------------------------------------------------------- 1 | package trie 2 | 3 | type Node struct { 4 | Char rune 5 | Children []*Node 6 | End bool 7 | } 8 | 9 | func NewNode(char rune) *Node { 10 | return &Node{Char: char, End: false} 11 | } 12 | 13 | func (node *Node) get(char rune) *Node { 14 | for _, child := range node.Children { 15 | if char == child.Char { 16 | return child 17 | } 18 | } 19 | 20 | return nil 21 | } 22 | 23 | func (node *Node) put(char rune) *Node { 24 | child := node.get(char) 25 | 26 | if child == nil { 27 | child = NewNode(char) 28 | node.Children = append(node.Children, child) 29 | } 30 | 31 | return child 32 | } -------------------------------------------------------------------------------- /trie/trie.go: -------------------------------------------------------------------------------- 1 | package trie 2 | 3 | import "unicode" 4 | 5 | var symbols = []rune{} 6 | 7 | type Trie struct { 8 | Root *Node 9 | } 10 | 11 | func New() *Trie { 12 | var r rune 13 | 14 | return &Trie{Root: NewNode(r)} 15 | } 16 | 17 | func (trie *Trie) Add(word string) { 18 | if len(word) <= 0 { 19 | return 20 | } 21 | 22 | chars := []rune(word) 23 | node := trie.Root 24 | 25 | for _, char := range chars { 26 | node = node.put(char) 27 | } 28 | 29 | node.End = true 30 | } 31 | 32 | func (trie *Trie) Scan(word string) bool { 33 | chars := []rune(word) 34 | node := trie.Root 35 | 36 | for _, char := range chars { 37 | if isSymbol(char) { 38 | continue 39 | } 40 | 41 | found := node.get(char) 42 | 43 | if found == nil { 44 | continue 45 | } 46 | node = found 47 | 48 | if node.End { 49 | return true 50 | } 51 | } 52 | 53 | return false 54 | } 55 | 56 | func (trie *Trie) Search(word string) (matches []string) { 57 | if len(word) == 0 { 58 | return 59 | } 60 | 61 | chars := []rune(word) 62 | length := len(chars) 63 | 64 | for pointer := 0; pointer < length; pointer++ { 65 | node := trie.Root 66 | matchFlag := 0 67 | flag := false 68 | 69 | if isSymbol(chars[pointer]) { 70 | continue 71 | } 72 | 73 | for i := pointer; i < length; i++ { 74 | child := node.get(chars[i]) 75 | 76 | if isSymbol(chars[i]) { 77 | if matchFlag > 0 { 78 | matchFlag++ 79 | } 80 | 81 | continue 82 | } 83 | 84 | if child == nil { 85 | break 86 | } 87 | 88 | node = child 89 | matchFlag++ 90 | 91 | if node.End { 92 | flag = true 93 | continue 94 | } 95 | } 96 | 97 | if !flag { 98 | matchFlag = 0 99 | } 100 | if matchFlag == 0 { 101 | continue 102 | } 103 | 104 | matches = append(matches, string(chars[pointer:pointer+matchFlag])) 105 | pointer += matchFlag - 1 106 | } 107 | 108 | return unique(matches) 109 | } 110 | 111 | func unique(elements []string) (result []string) { 112 | encountered := map[string]bool{} 113 | 114 | for v := range elements { 115 | encountered[elements[v]] = true 116 | } 117 | 118 | for key := range encountered { 119 | result = append(result, key) 120 | } 121 | 122 | return 123 | } 124 | 125 | func isSymbol(char rune) bool { 126 | 127 | if unicode.IsSpace(char) || unicode.IsSymbol(char) || unicode.IsPunct(char) { 128 | return true 129 | } 130 | 131 | for _, r := range symbols { 132 | if char == r { 133 | return true 134 | } 135 | } 136 | 137 | return false 138 | } 139 | --------------------------------------------------------------------------------