├── .github └── FUNDING.yml ├── .gitignore ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── README_zh.md ├── Rakefile ├── app ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── application_controller.rb │ └── concerns │ │ └── .keep ├── jobs │ └── application_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── appointment.rb │ ├── concerns │ │ └── .keep │ ├── patient.rb │ ├── physician.rb │ └── picture.rb └── views │ └── layouts │ ├── mailer.html.erb │ └── mailer.text.erb ├── bin ├── bundle ├── rails ├── rake ├── setup └── update ├── changelogs └── README.md ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ ├── test.rb │ └── test_postgres.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── cors.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20170411090750_create_physicians.rb │ ├── 20170411090810_create_patients.rb │ ├── 20170411090919_create_appointments.rb │ ├── 20170501075343_add_introduction_to_physicians.rb │ └── 20170601041316_create_pictures.rb └── seeds.rb ├── go-on-rails.gemspec ├── go-on-rails.png ├── go_app └── src │ └── models │ └── models_test.go ├── lib ├── generators │ ├── USAGE │ ├── gor │ │ ├── association.rb │ │ ├── converter.rb │ │ └── validator.rb │ ├── gor_generator.rb │ └── templates │ │ ├── Dockerfile.go_app.erb │ │ ├── Makefile │ │ ├── db.go.erb │ │ ├── docker-compose.yml.erb │ │ ├── favicon.ico │ │ ├── gor_model_mysql.go.erb │ │ ├── gor_model_postgres.go.erb │ │ ├── home_controller.go.erb │ │ ├── index.tmpl │ │ ├── main.go │ │ └── utils.go.erb ├── go │ └── on │ │ ├── rails.rb │ │ └── rails │ │ └── railtie.rb └── tasks │ └── gor.rake └── public └── robots.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: go-on-rails # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /spec/reports/ 5 | /test/tmp/ 6 | /test/version_tmp/ 7 | /tmp/ 8 | /log/ 9 | 10 | ## Ignore generated Golang files 11 | ## Except the testing ones 12 | /go_app/ 13 | !/go_app/src/models/models_test.go 14 | docker-compose.yml 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | 21 | ## Environment normalisation: 22 | /.bundle/ 23 | /vendor/bundle 24 | /lib/bundler/man/ 25 | 26 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 27 | .rvmrc 28 | 29 | # Ignore the db schema 30 | db/schema.rb 31 | db/test.sqlite3 32 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.3 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This test focuses on the generated Go codes to make things simple: 2 | # 1. Ruby use v2.6.3, specified in .ruby-version configuration 3 | # 2. Rails is v5.1.7, specified in Gemfile, it'll be installed by bundler 4 | # 3. we just test MySQL database using the Travis-ci's default service version 5 | 6 | language: go 7 | 8 | # Here Go versions have to correspond with https://github.com/go-sql-driver/mysql/blob/master/.travis.yml 9 | # if we hope a green tag in our README 10 | go: 11 | - 1.10.x 12 | - 1.11.x 13 | - 1.12.x 14 | - 1.13.x 15 | - master 16 | 17 | env: 18 | - RAILS_ENV=test 19 | 20 | services: 21 | - mysql 22 | 23 | before_install: 24 | - rvm install 2.6.3 --default 25 | - mysql -e 'CREATE DATABASE go_on_rails_test;' 26 | 27 | install: 28 | - go get github.com/jmoiron/sqlx 29 | - go get github.com/go-sql-driver/mysql 30 | - go get github.com/asaskevich/govalidator 31 | - bundle install --jobs=3 --retry=3 32 | - rails db:migrate 33 | - rails db:seed 34 | - rails generate gor test 35 | 36 | script: go test ./go_app/src/models 37 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | #source 'https://gems.ruby-china.org' 3 | 4 | git_source(:github) do |repo_name| 5 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 6 | "https://github.com/#{repo_name}.git" 7 | end 8 | 9 | gem 'rails', '~> 5.1.7' 10 | gem 'mysql2', '>= 0.3.18', '< 0.5' 11 | gem 'puma', '~> 3.0' 12 | gem 'sqlite3' 13 | gem 'pg' 14 | 15 | group :development, :test do 16 | gem 'byebug', platform: :mri 17 | gem 'annotate' 18 | end 19 | 20 | group :development do 21 | gem 'listen', '~> 3.0.5' 22 | gem 'spring' 23 | gem 'spring-watcher-listen', '~> 2.0.0' 24 | end 25 | 26 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 27 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 28 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.1.7) 5 | actionpack (= 5.1.7) 6 | nio4r (~> 2.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.1.7) 9 | actionpack (= 5.1.7) 10 | actionview (= 5.1.7) 11 | activejob (= 5.1.7) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.1.7) 15 | actionview (= 5.1.7) 16 | activesupport (= 5.1.7) 17 | rack (~> 2.0) 18 | rack-test (>= 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.1.7) 22 | activesupport (= 5.1.7) 23 | builder (~> 3.1) 24 | erubi (~> 1.4) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.1.7) 28 | activesupport (= 5.1.7) 29 | globalid (>= 0.3.6) 30 | activemodel (5.1.7) 31 | activesupport (= 5.1.7) 32 | activerecord (5.1.7) 33 | activemodel (= 5.1.7) 34 | activesupport (= 5.1.7) 35 | arel (~> 8.0) 36 | activesupport (5.1.7) 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | i18n (>= 0.7, < 2) 39 | minitest (~> 5.1) 40 | tzinfo (~> 1.1) 41 | annotate (3.0.3) 42 | activerecord (>= 3.2, < 7.0) 43 | rake (>= 10.4, < 14.0) 44 | arel (8.0.0) 45 | builder (3.2.4) 46 | byebug (11.0.1) 47 | concurrent-ruby (1.1.5) 48 | crass (1.0.5) 49 | erubi (1.9.0) 50 | ffi (1.11.3) 51 | globalid (0.4.2) 52 | activesupport (>= 4.2.0) 53 | i18n (1.7.0) 54 | concurrent-ruby (~> 1.0) 55 | listen (3.0.8) 56 | rb-fsevent (~> 0.9, >= 0.9.4) 57 | rb-inotify (~> 0.9, >= 0.9.7) 58 | loofah (2.4.0) 59 | crass (~> 1.0.2) 60 | nokogiri (>= 1.5.9) 61 | mail (2.7.1) 62 | mini_mime (>= 0.1.1) 63 | method_source (0.9.2) 64 | mini_mime (1.0.2) 65 | mini_portile2 (2.4.0) 66 | minitest (5.13.0) 67 | mysql2 (0.4.10) 68 | nio4r (2.5.2) 69 | nokogiri (1.10.7) 70 | mini_portile2 (~> 2.4.0) 71 | pg (1.1.4) 72 | puma (3.12.2) 73 | rack (2.0.8) 74 | rack-test (1.1.0) 75 | rack (>= 1.0, < 3) 76 | rails (5.1.7) 77 | actioncable (= 5.1.7) 78 | actionmailer (= 5.1.7) 79 | actionpack (= 5.1.7) 80 | actionview (= 5.1.7) 81 | activejob (= 5.1.7) 82 | activemodel (= 5.1.7) 83 | activerecord (= 5.1.7) 84 | activesupport (= 5.1.7) 85 | bundler (>= 1.3.0) 86 | railties (= 5.1.7) 87 | sprockets-rails (>= 2.0.0) 88 | rails-dom-testing (2.0.3) 89 | activesupport (>= 4.2.0) 90 | nokogiri (>= 1.6) 91 | rails-html-sanitizer (1.3.0) 92 | loofah (~> 2.3) 93 | railties (5.1.7) 94 | actionpack (= 5.1.7) 95 | activesupport (= 5.1.7) 96 | method_source 97 | rake (>= 0.8.7) 98 | thor (>= 0.18.1, < 2.0) 99 | rake (13.0.1) 100 | rb-fsevent (0.10.3) 101 | rb-inotify (0.10.0) 102 | ffi (~> 1.0) 103 | spring (2.1.0) 104 | spring-watcher-listen (2.0.1) 105 | listen (>= 2.7, < 4.0) 106 | spring (>= 1.2, < 3.0) 107 | sprockets (4.0.0) 108 | concurrent-ruby (~> 1.0) 109 | rack (> 1, < 3) 110 | sprockets-rails (3.2.1) 111 | actionpack (>= 4.0) 112 | activesupport (>= 4.0) 113 | sprockets (>= 3.0.0) 114 | sqlite3 (1.4.2) 115 | thor (1.0.1) 116 | thread_safe (0.3.6) 117 | tzinfo (1.2.5) 118 | thread_safe (~> 0.1) 119 | websocket-driver (0.6.5) 120 | websocket-extensions (>= 0.1.0) 121 | websocket-extensions (0.1.4) 122 | 123 | PLATFORMS 124 | ruby 125 | 126 | DEPENDENCIES 127 | annotate 128 | byebug 129 | listen (~> 3.0.5) 130 | mysql2 (>= 0.3.18, < 0.5) 131 | pg 132 | puma (~> 3.0) 133 | rails (~> 5.1.7) 134 | spring 135 | spring-watcher-listen (~> 2.0.0) 136 | sqlite3 137 | tzinfo-data 138 | 139 | BUNDLED WITH 140 | 1.17.2 141 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 B1nj0y 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Go on Rails

5 |

Use Rails to generate Golang code or manage Go app development

6 | 7 |

8 | Gem Version 9 | Build Status 10 | Maintainability 11 | MIT License 12 |

13 | 14 | --- 15 | 16 | [用 Rails 开发 Go 应用:中文文档](./README_zh.md) 17 | 18 | `go-on-rails` is a Rails generator aims to: 19 | 20 | 1. Help to develop and integrate some APIs written in Golang to existing Rails app for high performance 21 | 2. Use your familiar Rails tools to develop and manage a Golang app project 22 | 3. Convert a *not very complicated* Rails app to Golang equivalent 23 | 24 | Here's some examples: 25 | * A simple [example(tutorial)](https://github.com/railstack/example_simple) on the basic usage of go-on-rails generator 26 | * [An advanced example](https://github.com/railstack/example_with_admin) shows how to integrate Go APIs in a Rails project 27 | * [Another example](https://github.com/railstack/example_read_rails_session) shows how to handle a Rails session to get a user's info in a go-on-rails generated Go API 28 | 29 | ## Prerequisites 30 | 31 | * Rails 4.2+ (Rails 6 not supported and need your help) 32 | * Golang 1.10.x(mainly be compatible with github.com/go-sql-driver/mysql) 33 | 34 | ## Installation 35 | 36 | Add this line to your application's Gemfile: 37 | 38 | ```ruby 39 | gem 'go-on-rails', '~> 0.4.0' 40 | ``` 41 | 42 | And then execute: 43 | ```bash 44 | $ bundle 45 | ``` 46 | 47 | Or install it yourself as: 48 | ```bash 49 | $ gem install go-on-rails 50 | ``` 51 | ## Usage 52 | 53 | You must have an existing Rails app or to create a new one before you try go-on-rails to generate Golang codes. 54 | 55 | After that you can run the command just as other Rails generators: 56 | 57 | ```bash 58 | rails g gor [dev(elopment) | pro(duction) | test | ...] [-m model_a model_b model_c ...] 59 | 60 | # OR (on rails version < 5.0) 61 | rake g gor ... 62 | ``` 63 | 64 | here we take generating all models for the `development` environment for example: 65 | 66 | ```bash 67 | rails g gor dev 68 | ``` 69 | 70 | Then a folder named `go_app` that includes Golang codes will be generated under your Rails app root path. 71 | 72 | Install the dependent Golang packages for this Go project: 73 | 74 | ```bash 75 | rails gor:deps 76 | ``` 77 | 78 | Then change to the `go_app` directory and run: 79 | 80 | ```bash 81 | go run main.go 82 | ``` 83 | 84 | You can visit the page in http://localhost:4000 by default. 85 | 86 | More command details about go-on-rails generator: 87 | 88 | ```bash 89 | rails g gor --help 90 | ``` 91 | 92 | ## What will be generated? 93 | 94 | * Go project directory layout (all under the `go_app` directory, like `views`, `controllers`, `public`) 95 | * A Go data struct corresponding to each activerecord model 96 | * And each struct related CRUD functions/methods like FindModel, UpdateModel, DestroyModel etc. All these models related program files under the `go_app/models` directory 97 | * And godoc files for all the functions under `go_app/models/doc` 98 | * We use [Gin](https://github.com/gin-gonic/gin) as the default web framework, but you can change it to anyone that you favor in `main.go` and `controllers` programs 99 | 100 | ### View the godoc of all functions 101 | 102 | You can view the godoc page of all functions in http://localhost:7979/doc/models.html after run: 103 | 104 | ```bash 105 | rails gor:doc 106 | ``` 107 | 108 | Besides, there's [a sample project](https://github.com/railstack/gor_models_sample) generated by go-on-rails, you can view its [godoc](https://godoc.org/github.com/railstack/gor_models_sample) at godoc.org just now to get a picture of what functions are generated. 109 | 110 | 111 | ## Known issues and TODOs 112 | 113 | The gem is still under development, so there're some known issues. You're welcomed to [contribute](#contributing). 👏 114 | 115 | * databases specific functions between MySQL and Postgres or other databases are not covered completely 116 | * sql.NullType not supported yet, so you'd better in the migrations set those columns "not null" with a default value that's consistent with Golang's zero value specification, such as "" default for string and text typed column, and 0 default for int, etc. And now we have an alternative approach for manipulating the database nullable fields, see the wiki [Working with database nullable fields](https://github.com/railstack/go-on-rails/wiki/Working-with-database-nullable-fields) 117 | 118 | - [x] Associations 119 | - [x] has_many 120 | - [x] has_one 121 | - [x] belongs_to 122 | - [x] dependent 123 | - [x] Validations 124 | - [x] length 125 | - [x] presence 126 | - [x] format(string only) 127 | - [x] numericality(partially) 128 | - [ ] other validations 129 | - [x] Pagination(details see [wiki](https://github.com/railstack/go-on-rails/wiki/Pagination)) 130 | - [ ] Callbacks 131 | - [ ] Transactions 132 | 133 | ## Supported databases 134 | 135 | * SQLite 136 | * MySQL 137 | * Postgres 138 | 139 | ## Wiki 140 | 141 | * [Built-in Pagination](https://github.com/railstack/go-on-rails/wiki/Pagination) 142 | * [Working with database nullable fields](https://github.com/railstack/go-on-rails/wiki/Working-with-database-nullable-fields) 143 | * [Some Make commands](https://github.com/railstack/go-on-rails/wiki/Some-Make-commands) 144 | * [Dockerize a Go-on-Rails application](https://github.com/railstack/go-on-rails/wiki/Dockerize-a-Go-on-Rails-application) 145 | 146 | ## Golang dependencies by default 147 | 148 | * [github.com/jmoiron/sqlx](https://github.com/jmoiron/sqlx): an extension on the standard `database/sql` database API library 149 | * [github.com/railstack/go-sqlite3](https://github.com/railstack/go-sqlite3): a SQLite driver(This's a forked version of [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3), and This is [why we use this forked version](https://github.com/mattn/go-sqlite3/pull/468)) 150 | * [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql): a MySQL driver 151 | * [github.com/lib/pq](https://github.com/lib/pq): a PostgreSQL driver 152 | * [github.com/asaskevich/govalidator](https://github.com/asaskevich/govalidator): for the struct validation 153 | * [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin): a HTTP web framework 154 | 155 | ## Acknowledgement 156 | 157 | When I had the idea to convert Rails app or build Golang app with Rails tools, I searched github and found the project: https://github.com/t-k/ar2gostruct. And from ar2gostruct I copied some codes on handling data structure conversion and models association, it made my idea come true faster than I imagined. 158 | 159 | ## Contributing 160 | 161 | There're two branches at present: `master` and `dev`. 162 | 163 | The `dev` branch has a whole Rails environment for development: models, seeds for testing, and under `go_app` directory there's a file named `models_test.go` used to test generated Golang codes. 164 | 165 | - Fork the project switch to branch `dev`. 166 | - Make your feature addition or bug fix. 167 | - Add tests for it. This is important so I don't break it in a future version unintentionally. 168 | - Commit, do not mess with Rakefile or version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 169 | - Send me a pull request. Bonus points for topic branches. 170 | 171 | ### Testing 172 | 173 | We create four models for testing: 174 | 175 | - Physician 176 | - Patient 177 | - Appointment 178 | - Picture 179 | 180 | Run `rails db:seed` to use the data defined in `db/seeds.rb`. And change to `go_app/models` directory to run `go test` to test generated models-related functions. The test covers a small part of the functions currently. More will be added later on. 181 | 182 | ## License 183 | 184 | See the [LICENSE](https://github.com/railstack/go-on-rails/blob/master/MIT-LICENSE) file. 185 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Go on Rails

5 |

Use Rails to generate Golang code or manage Go app development

6 | 7 |

8 | Gem Version 9 | Build Status 10 | Maintainability 11 | MIT License 12 |

13 | 14 | --- 15 | 16 | `go-on-rails` 是一个 Rails 的 generator,其目标功能主要有: 17 | 18 | 1. 对于 Rails 应用中的某些需要高性能的接口,使用 go-on-rails 来生成代码,并把生成的 Go 接口集成进 Rails 项目进行替换 19 | 2. Rails 程序员可以使用熟悉的工具链开发和管理一个 Golang 的项目 20 | 21 | 下面是几个示例教程: 22 | * [简单示例](https://github.com/railstack/example_simple) 仿照 Rails guides 里那个入门的 [demo](http://guides.rubyonrails.org/getting_started.html),演示如何使用 go-on-rails 创建和生成一个简单 blog 的 Go API。 23 | * [高级教程](https://github.com/railstack/example_with_admin) 如何创建一个 Go 项目,并和 rails_admin, devise, cancancan 等集成,为 Go 项目快速增加一个管理后台。同时该项目演示了如何使用 Rails 5.1 新发布的 webpacker 工具,并利用 React 制作独立的前端界面在 Rails 中调用 Go 接口。 24 | * [如何从 Go API 读取 Rails session](https://github.com/railstack/example_read_rails_session) 讲述如何在一个 go-on-rails 生成的 Go 接口中读取 Rails 的 session 做用户验证,以便于将需要用户验证的 Go API 集成进 Rails 项目。 25 | 26 | ## 安装环境要求 27 | 28 | * Rails 4.2 及以上(Rails 6 尚未支持,需要您的加入) 29 | * Go 1.10 及以上(主要是为了兼容 github.com/go-sql-driver/mysql,如果你不用该驱动,可以考虑更低版本) 30 | 31 | ## 安装 32 | 33 | 在 Rails 项目的 Gemfile 中添加下面一行: 34 | 35 | ```ruby 36 | gem 'go-on-rails', '~> 0.4.0' 37 | ``` 38 | 39 | 然后运行: 40 | ```bash 41 | $ bundle 42 | ``` 43 | 44 | ## 用法 45 | 46 | 在运行生成 Go 代码的命令之前,你得保证在 Rails 中至少已经创建了一个 Model,然后运行如下格式的命令来生成 Go 代码: 47 | 48 | ```bash 49 | rails g gor [dev(elopment) | pro(duction) | test | ...] [-m model_a model_b model_c ...] 50 | ``` 51 | 52 | 这里比如我们为 `development` 环境所有的 model 进行操作: 53 | 54 | ```bash 55 | rails g gor dev 56 | ``` 57 | 58 | 运行后会在 Rails 项目的根目录生成一个 `go_app` 的文件夹。 59 | 60 | 接下来运行命令安装这个 Go 项目默认依赖的包: 61 | 62 | ```bash 63 | rails gor:deps 64 | ``` 65 | 66 | 然后进入到 `go_app` 目录下,启动 Go 服务器: 67 | 68 | ```bash 69 | cd go_app 70 | go run main.go 71 | ``` 72 | 73 | 这时可以在浏览器中访问 http://localhost:4000 ,正常的话会看到一个类似新建的 Rails 项目的默认首页。 74 | 75 | 关于这个生成器命令行的更多用法可以查看 go-on-rails 的帮助选项: 76 | 77 | ```bash 78 | rails g gor --help 79 | ``` 80 | 81 | ## 该命令都会生成些什么? 82 | 83 | * 一个 Go 项目的目录布局(所有程序都在 `go_app` 文件夹下,在其下面包括如 `views`、 `controllers`、 `public` 等文件夹) 84 | * 针对每个 activerecord 的 Model 生成相应的 Go 数据结构 85 | * 同时针对每个数据结构生成相关的 CRUD 函数/方法,如 FindModel, UpdateModel, DestroyModle 等等。所有这些 Model 相关的代码都生成在 `go_app/models` 目录下。 86 | * 在文件夹 `go_app/models/doc` 下生成的所有函数的 godoc 文档 87 | * 我们默认使用 [Gin](https://github.com/gin-gonic/gin) 作为我们的 Web 框架,你也可以通过改动 `main.go` 以及 `controllers` 等文件来使用你喜欢的框架,同时配合使用生成的 Model 相关的函数 88 | 89 | ### 查看生成函数(方法)的 godoc 90 | 91 | 可以通过运行如下命令在浏览器中查看所有生成的函数文档: 92 | 93 | ```bash 94 | rails gor:doc 95 | ``` 96 | 97 | 浏览器地址为:http://localhost:7979/doc/models.html 。 98 | 99 | 另外,这里有一个生成好的[示例项目](https://github.com/railstack/gor_models_sample),在 godoc.org 可以浏览该示例项目的 godoc 文档,[详情](https://github.com/railstack/gor_models_sample)。 100 | 101 | ## 已知问题 102 | 103 | 目前该项目还在持续开发中,还有很多不完善的方面。非常欢迎发现问题时提 issue 或者贡献代码👏 。 104 | 105 | * 针对不同的数据库,比如 MySQL, Postgres 分别生成的函数还需完善 106 | * 没有支持 sql.NullType 的数据类型,所以如果某个表中的字段出现 Null 时程序可能会出错,所以目前临时的做法是:你最好在 migration 中定义好 "not null",给一个和 Go 的数据类型的零值相一致的默认值。同时,我们给出了一个使用零值的解决方案,详见 wiki: [Working with database nullable fields](https://github.com/railstack/go-on-rails/wiki/Working-with-database-nullable-fields) 。 107 | 108 | - [x] Associations 109 | - [x] has_many 110 | - [x] has_one 111 | - [x] belongs_to 112 | - [x] dependent 113 | - [x] Validations 114 | - [x] length 115 | - [x] presence 116 | - [x] format(string only) 117 | - [x] numericality(partially) 118 | - [ ] other validations 119 | - [x] Pagination(details see [wiki](https://github.com/railstack/go-on-rails/wiki/Pagination)) 120 | - [ ] Callbacks 121 | - [ ] Transactions 122 | 123 | ## 已支持的数据库 124 | 125 | * SQLite 126 | * MySQL 127 | * Postgres 128 | 129 | ## Wiki 130 | 131 | * [内置的分页 Helper](https://github.com/railstack/go-on-rails/wiki/Pagination) 132 | * [如何处理数据库的 NULL 值](https://github.com/railstack/go-on-rails/wiki/Working-with-database-nullable-fields) 133 | * [几条 Make 命令](https://github.com/railstack/go-on-rails/wiki/Some-Make-commands) 134 | * [如何使用 Docker 发布 Go on Rails 应用](https://github.com/railstack/go-on-rails/wiki/Dockerize-a-Go-on-Rails-application) 135 | 136 | ## 默认需要的 Go 依赖包 137 | 138 | * [github.com/jmoiron/sqlx](https://github.com/jmoiron/sqlx): 标准 `database/sql` API 库的一个扩展版本 139 | * [github.com/railstack/go-sqlite3](https://github.com/railstack/go-sqlite3): SQLite driver(这是 [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) 的一个 fork 版本,这里有说明[为什么我们使用这个 fork 的版本](https://github.com/mattn/go-sqlite3/pull/468)) 140 | * [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql): 一个 MySQL driver 141 | * [github.com/lib/pq](https://github.com/lib/pq): 一个 PostgreSQL driver 142 | * [github.com/asaskevich/govalidator](https://github.com/asaskevich/govalidator): 一个用作数据验证的包 143 | * [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin): 一个易用的高性能 Web 框架 144 | 145 | 146 | ## 许可证 147 | 148 | 详见 [许可文件](https://github.com/railstack/go-on-rails/blob/master/MIT-LICENSE)。 149 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railstack/go-on-rails/681d5ee53e55f898a639b25e699cde4700a5cd45/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/appointment.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: appointments 4 | # 5 | # id :integer not null, primary key 6 | # appointment_date :datetime 7 | # physician_id :integer 8 | # patient_id :integer 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Appointment < ApplicationRecord 14 | belongs_to :physician 15 | belongs_to :patient 16 | end 17 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railstack/go-on-rails/681d5ee53e55f898a639b25e699cde4700a5cd45/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/patient.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: patients 4 | # 5 | # id :integer not null, primary key 6 | # name :string(255) 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # 10 | 11 | class Patient < ApplicationRecord 12 | has_many :appointments 13 | has_many :physicians, through: :appointments 14 | end 15 | -------------------------------------------------------------------------------- /app/models/physician.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: physicians 4 | # 5 | # id :integer not null, primary key 6 | # name :string(255) 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # introduction :text(65535) 10 | # 11 | 12 | class Physician < ApplicationRecord 13 | has_many :appointments 14 | has_many :patients, through: :appointments 15 | has_many :pictures, as: :imageable 16 | 17 | validates :name, presence: true, length: { in: 6..15 } 18 | validates :introduction, presence: true 19 | end 20 | -------------------------------------------------------------------------------- /app/models/picture.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: pictures 4 | # 5 | # id :integer not null, primary key 6 | # name :string(255) 7 | # url :string(255) 8 | # imageable_id :integer 9 | # imageable_type :string(255) 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | class Picture < ApplicationRecord 15 | belongs_to :imageable, polymorphic: true 16 | end 17 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /changelogs/README.md: -------------------------------------------------------------------------------- 1 | ## 4.0 (2019-06-03) 2 | 3 | - NOTE: v4.0 is a break update 4 | - move generated `models` under a new created `src` folder 5 | - fix `godoc` can't generate doc for `models` package after Go version 1.12 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | # require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module GoOnRails 20 | class Application < Rails::Application 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | # Only loads a smaller set of middleware suitable for API only apps. 26 | # Middleware like session, flash, cookies can be added back manually. 27 | # Skip views, helpers and assets when generating a new resource. 28 | config.api_only = true 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: mysql2 3 | encoding: utf8 4 | pool: 5 5 | username: root 6 | password: 7 | socket: /tmp/mysql.sock 8 | 9 | development: 10 | <<: *default 11 | database: go-on-rails_development 12 | 13 | test: 14 | <<: *default 15 | host: 127.0.0.1 16 | database: go_on_rails_test 17 | 18 | production: 19 | <<: *default 20 | database: go-on-rails_production 21 | username: go-on-rails 22 | password: <%= ENV['GO-ON-RAILS_DATABASE_PASSWORD'] %> 23 | 24 | test_sqlite: 25 | adapter: sqlite3 26 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 27 | timeout: 5000 28 | database: db/test.sqlite3 29 | 30 | test_postgres: 31 | adapter: postgresql 32 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 33 | timeout: 5000 34 | database: go-on-rails_test_postgres 35 | username: test_postgres 36 | password: secret 37 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | 41 | # Raises error for missing translations 42 | # config.action_view.raise_on_missing_translations = true 43 | 44 | # Use an evented file watcher to asynchronously detect changes in source code, 45 | # routes, locales, etc. This feature depends on the listen gem. 46 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 47 | end 48 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | 22 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 23 | # config.action_controller.asset_host = 'http://assets.example.com' 24 | 25 | # Specifies the header that your server uses for sending files. 26 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 27 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 28 | 29 | # Mount Action Cable outside main process or domain 30 | # config.action_cable.mount_path = nil 31 | # config.action_cable.url = 'wss://example.com/cable' 32 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 33 | 34 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 35 | # config.force_ssl = true 36 | 37 | # Use the lowest log level to ensure availability of diagnostic information 38 | # when problems arise. 39 | config.log_level = :debug 40 | 41 | # Prepend all log lines with the following tags. 42 | config.log_tags = [ :request_id ] 43 | 44 | # Use a different cache store in production. 45 | # config.cache_store = :mem_cache_store 46 | 47 | # Use a real queuing backend for Active Job (and separate queues per environment) 48 | # config.active_job.queue_adapter = :resque 49 | # config.active_job.queue_name_prefix = "go-on-rails_#{Rails.env}" 50 | config.action_mailer.perform_caching = false 51 | 52 | # Ignore bad email addresses and do not raise email delivery errors. 53 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 54 | # config.action_mailer.raise_delivery_errors = false 55 | 56 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 57 | # the I18n.default_locale when a translation cannot be found). 58 | config.i18n.fallbacks = true 59 | 60 | # Send deprecation notices to registered listeners. 61 | config.active_support.deprecation = :notify 62 | 63 | # Use default logging formatter so that PID and timestamp are not suppressed. 64 | config.log_formatter = ::Logger::Formatter.new 65 | 66 | # Use a different logger for distributed setups. 67 | # require 'syslog/logger' 68 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 69 | 70 | if ENV["RAILS_LOG_TO_STDOUT"].present? 71 | logger = ActiveSupport::Logger.new(STDOUT) 72 | logger.formatter = config.log_formatter 73 | config.logger = ActiveSupport::TaggedLogging.new(logger) 74 | end 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/environments/test_postgres.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | 41 | # Raises error for missing translations 42 | # config.action_view.raise_on_missing_translations = true 43 | 44 | # Use an evented file watcher to asynchronously detect changes in source code, 45 | # routes, locales, etc. This feature depends on the listen gem. 46 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 47 | end 48 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 6 | 7 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 8 | # Previous versions had false. 9 | ActiveSupport.to_time_preserves_timezone = true 10 | 11 | # Require `belongs_to` associations by default. Previous versions had false. 12 | Rails.application.config.active_record.belongs_to_required_by_default = true 13 | 14 | # Do not halt callback chains when a callback returns false. Previous versions had true. 15 | ActiveSupport.halt_callback_chains_on_return_false = false 16 | 17 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 18 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 19 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | end 4 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 772575d6c38b669314158d26869b340b765b0769d1821f6ca13e9629b267a8129cd75d233be7345e49c90fad4c696768453f4ed13bf34c2546d9f89d971a866a 15 | 16 | test: 17 | secret_key_base: 2940f3407f8f08ac32bca36aa11604afa58e56caf98d85dcd81051fa11567fab2347ab1c66a765153190fdf33afc18e62495b0fd7498c0eb1e7f5d5f31020f57 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20170411090750_create_physicians.rb: -------------------------------------------------------------------------------- 1 | class CreatePhysicians < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :physicians do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170411090810_create_patients.rb: -------------------------------------------------------------------------------- 1 | class CreatePatients < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :patients do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170411090919_create_appointments.rb: -------------------------------------------------------------------------------- 1 | class CreateAppointments < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :appointments do |t| 4 | t.datetime :appointment_date 5 | t.integer :physician_id 6 | t.integer :patient_id 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170501075343_add_introduction_to_physicians.rb: -------------------------------------------------------------------------------- 1 | class AddIntroductionToPhysicians < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :physicians, :introduction, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170601041316_create_pictures.rb: -------------------------------------------------------------------------------- 1 | class CreatePictures < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :pictures do |t| 4 | t.string :name 5 | t.string :url 6 | t.integer :imageable_id 7 | t.string :imageable_type 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Clear the records firstly 2 | Physician.delete_all 3 | 4 | # Here are some of famous physicians in ancient China: 李时珍, 张仲景, 扁鹊 5 | Physician.create([ 6 | { name: 'ShizhenLi', introduction: 'A famous doctor in Ming Dynasty, the author of Compendium of Materia Medica.' }, 7 | { name: 'ZhongjingZhang', introduction: "A famous doctor in Eastern Han Dynasty, wrote the book Treatise on Febrile and Miscellaneous Diseases" }, 8 | { name: 'QueBian', introduction: "A highly skilled doctor in the Warring States Period" } 9 | ]) 10 | 11 | # and 华佗, 12 | p = Physician.create(name: 'TuoHua', introduction: "A very famous doctor in ancient China, his name had been a symbol of magic doctor" ) 13 | # He ever treated some named patients in his time, Three Kingdoms: 14 | # 曹操, 关羽, 陈登, 周泰 15 | p.patients.create([{ name: 'CaoCao' }, { name: 'YuGuan' }, { name: 'DengChen' }, { name: 'TaiZhou' }]) 16 | 17 | puts 'Physician seeded!' 18 | -------------------------------------------------------------------------------- /go-on-rails.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'go-on-rails' 6 | s.version = '0.4.0' 7 | s.date = '2019-08-03' 8 | s.summary = "Use Rails to Develop or Generate a Golang application" 9 | s.description = "Modeling, developing and testing your Golang app with your familiar Rails tools like rails generate, db migration, console etc. It is more meant to help integrating some APIs written in Golang to existed Rails app for high performance." 10 | s.authors = ["B1nj0y"] 11 | s.email = 'idegorepl@gmail.com' 12 | s.files = Dir['MIT-LICENSE', 'README.md', 'lib/**/*'] 13 | s.homepage = 'https://github.com/railstack/go-on-rails' 14 | s.license = 'MIT' 15 | s.require_paths = ['lib'] 16 | end 17 | -------------------------------------------------------------------------------- /go-on-rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railstack/go-on-rails/681d5ee53e55f898a639b25e699cde4700a5cd45/go-on-rails.png -------------------------------------------------------------------------------- /go_app/src/models/models_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // TestFindPhysicianBy test the function FindPhysicianBy() 9 | func TestFindPhysicianBy(t *testing.T) { 10 | p, err := FindPhysicianBy("name", "TuoHua") 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | if p.Name != "TuoHua" { 15 | t.Error("FindPhysicianBy error!") 16 | } 17 | } 18 | 19 | // TestFindPhysician test the function FindPhysician() 20 | func TestFindPhysician(t *testing.T) { 21 | p, err := FindPhysicianBy("name", "TuoHua") 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | pp, err := FindPhysician(p.Id) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | if p.Name != pp.Name { 30 | t.Error("FindPhysician error!") 31 | } 32 | } 33 | 34 | // TestPhysicianGetPatients test the method GetPatients() 35 | func TestPhysicianGetPatients(t *testing.T) { 36 | p, err := FindPhysicianBy("name", "TuoHua") 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | err = p.GetPatients() 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | if len(p.Patients) != 4 { 45 | t.Error("Physician's GetPatients error!") 46 | } 47 | } 48 | 49 | // TestPhysicianIncludesWhere test the function PhysicianIncludesWhere() 50 | func TestPhysicianIncludesWhere(t *testing.T) { 51 | ps, err := PhysicianIncludesWhere([]string{"patients"}, "name = ?", "TuoHua") 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | if len(ps[0].Patients) != 4 { 56 | t.Error("PhysicianIncludesWhere error!") 57 | } 58 | } 59 | 60 | // TestPhysicianCreateValidationFail will fail: 61 | // The name "Jack" can't pass string length restrict validation: 4 is not range in 6..15 62 | func TestPhysicianCreateValidationFail(t *testing.T) { 63 | p := &Physician{Name: "Jack", Introduction: "Jack is a new Doctor."} 64 | _, err := p.Create() 65 | if err != nil { 66 | fmt.Printf("Create Physician Failure: %v\n", err) 67 | } else { 68 | t.Error("String length validation failed") 69 | } 70 | _, err = FindPhysicianBy("name", "Jack") 71 | if err != nil { 72 | fmt.Println("No New Physician is created") 73 | } 74 | } 75 | 76 | // TestPhysicianObjectCreateValidationPass will pass: 77 | // The name "New Doctor" can pass string length restrict validation: 10 is in range 6..15 78 | // The physician created hear will be for further testing of destroying a physician 79 | func TestPhysicianObjectCreateValidationPass(t *testing.T) { 80 | p := &Physician{Name: "New Doctor", Introduction: "A new doctor is welcomed!"} 81 | id, err := p.Create() 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | _, err = FindPhysician(id) 86 | if err != nil { 87 | t.Error("Create physician failure") 88 | } 89 | } 90 | 91 | // TestPhysicianCreateValidationPass will pass: 92 | // The name "New Doctor 2" can pass string length restrict validation: 10 is in range 6..15 93 | func TestPhysicianCreateValidationPass(t *testing.T) { 94 | p := map[string]interface{}{"name": "New Doctor 2", "introduction": "A new doctor is welcomed!"} 95 | id, err := CreatePhysician(p) 96 | if err != nil { 97 | t.Error(err) 98 | } 99 | _, err = FindPhysician(id) 100 | if err != nil { 101 | t.Error("Create physician failure") 102 | } 103 | err = DestroyPhysician(id) 104 | if err != nil { 105 | t.Error("Delete physician failure") 106 | } 107 | } 108 | 109 | // TestPhysicianDestroy test the function PhysicianDestroyBy() 110 | func TestDestroyPhysician(t *testing.T) { 111 | p, err := FindPhysicianBy("name", "New Doctor") 112 | if err != nil { 113 | t.Error("Create physician failure") 114 | } 115 | fmt.Printf("New Physician is: %v\n", p.Name) 116 | err = DestroyPhysician(p.Id) 117 | if err != nil { 118 | t.Error("Delete physician failure") 119 | } 120 | fmt.Printf("A physician %v is deleted\n", p.Name) 121 | } 122 | 123 | // TestFirstPhysician test the function FirstPhysician() 124 | func TestFirstPhysician(t *testing.T) { 125 | p, err := FirstPhysician() 126 | if err != nil { 127 | t.Error("Find first physician failure") 128 | } 129 | if p.Name == "ShizhenLi" { 130 | fmt.Printf("The First Physician is: %v\n", p.Name) 131 | } else { 132 | t.Error("The First Physician record is not right") 133 | } 134 | } 135 | 136 | // TestFirstPhysicians test the function FirstPhysician() 137 | func TestFirstPhysicians(t *testing.T) { 138 | ps, err := FirstPhysicians(3) 139 | if err != nil { 140 | t.Error("Get the First 3 Physicians failed") 141 | } 142 | if len(ps) == 3 { 143 | fmt.Println("Get the First 3 Physicians success") 144 | } else { 145 | t.Error("Get the First 3 Physicians failed") 146 | } 147 | } 148 | 149 | // TestLastPhysician test the function LastPhysician() 150 | func TestLastPhysician(t *testing.T) { 151 | p, err := LastPhysician() 152 | if err != nil { 153 | t.Error("Find Last physician failure") 154 | } 155 | if p.Name == "TuoHua" { 156 | fmt.Printf("The Last Physician is: %v\n", p.Name) 157 | } else { 158 | t.Error("The Last Physician record is not right") 159 | } 160 | } 161 | 162 | // TestLastPhysicians test the function LastPhysician() 163 | func TestLastPhysicians(t *testing.T) { 164 | ps, err := LastPhysicians(3) 165 | if err != nil { 166 | t.Error("Get the Last 3 Physicians failed") 167 | } 168 | if len(ps) == 3 { 169 | fmt.Println("Get the Last 3 Physicians success") 170 | } else { 171 | t.Error("Get the Last 3 Physicians failed") 172 | } 173 | } 174 | 175 | // TestPhysicianCount test the function PhysicianCount 176 | func TestPhysicianCount(t *testing.T) { 177 | c, err := PhysicianCount() 178 | if err != nil { 179 | t.Error("Phsicians count error") 180 | } 181 | if c == 4 { 182 | fmt.Println("Get Physician count success") 183 | } else { 184 | t.Error("Get Physician count failed") 185 | } 186 | } 187 | 188 | // TestPhysicianCountWhere test the function PhysicianCountWhere 189 | func TestPhysicianCountWhere(t *testing.T) { 190 | c, err := PhysicianCountWhere("name = ?", "TuoHua") 191 | if err != nil { 192 | t.Error("Phsicians count error") 193 | } 194 | if c == 1 { 195 | fmt.Println("Test PhysicianCountWhere success") 196 | } else { 197 | t.Error("Test PhysicianCountWhere success failed") 198 | } 199 | } 200 | 201 | // TestFindPhysicianBySql test the function PhysicianCountWhere 202 | func TestFindPhysicianBySql(t *testing.T) { 203 | c, err := FindPhysicianBySql("SELECT id, name FROM physicians WHERE name = ? ORDER BY id DESC LIMIT 1", "TuoHua") 204 | if err != nil { 205 | t.Error("Test FindPhsicianBySql error") 206 | } 207 | if c.Name == "TuoHua" { 208 | fmt.Println("Test FindPhsicianBySql success") 209 | } else { 210 | t.Error("Test FindPhsicianBySql failed") 211 | } 212 | } 213 | 214 | // TestGetPage test the funciton GetPage() for the pagination 215 | func TestGetPage(t *testing.T) { 216 | pm := PhysicianPage{ 217 | WhereString: "name LIKE ?", 218 | WhereParams: []interface{}{"%QueBian%"}, 219 | Order: map[string]string{"id": "desc"}, 220 | PerPage: 2} 221 | ps, err := pm.GetPage("current") 222 | if err != nil { 223 | t.Error("Test GetPage error") 224 | } 225 | if len(ps) == 1 && ps[0].Name == "QueBian" { 226 | fmt.Println("Test GetPage success") 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /lib/generators/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generate Golang app by Rails models: use Rails as a modeling and migration tool to build a Golang app, or migrate some of existed Rails APIs to Golang to decouple and high-perform your services. 3 | 4 | Read more: https://github.com/railstack/go-on-rails 5 | 6 | 7 | Example: 8 | rails generate gor development -m user post 9 | 10 | Here are valid ENV_NAME arguments by default for a Rails generated database configuration by default: [dev, development, pro, production, test]. The default ENV_NAME is "development" if you omit one. Obviously, you can reconfig the config/database.yml to define your own environments. 11 | 12 | So you can also run: 13 | 14 | rails g gor dev -m user post 15 | 16 | This will mainly create: 17 | go_app/Dockerfile 18 | go_app/Makefile 19 | go_app/main.go 20 | go_app/models/doc/models.html 21 | go_app/models/db.go 22 | go_app/models/gor_user.go 23 | go_app/models/gor_post.go 24 | 25 | Notice: If you omit the -m option, all the models of the Rails app will be generated. 26 | 27 | If you omit all the options, just run: 28 | 29 | rails g gor 30 | 31 | This will generate code of all the models with the database connection in Rails development environment configuration. 32 | 33 | -------------------------------------------------------------------------------- /lib/generators/gor/association.rb: -------------------------------------------------------------------------------- 1 | module GoOnRails 2 | class Association 3 | def initialize(klass, models) 4 | @klass = klass 5 | @models = models 6 | end 7 | attr_reader :klass, :models 8 | 9 | def get_schema_info 10 | info = {struct_body: "", has_assoc_dependent: false, assoc_info: {has_many: {}, has_one: {}, belongs_to: {}}} 11 | self.klass.reflect_on_all_associations.each do |assoc| 12 | tags = ["json:\"#{assoc.name.to_s},omitempty\" db:\"#{assoc.name.to_s}\" valid:\"-\""] 13 | case assoc.macro 14 | when :has_many 15 | col_name = assoc.name.to_s.camelize 16 | class_name = assoc.name.to_s.singularize.camelize 17 | unless assoc.options.empty? 18 | if assoc.options.key? :class_name 19 | class_name = assoc.options[:class_name] 20 | elsif assoc.options.key? :source 21 | class_name = assoc.options[:source].to_s.camelize 22 | end 23 | end 24 | type_name = "[]#{class_name}" 25 | if col_name && type_name && self.models.include?(class_name) 26 | info[:struct_body] << sprintf("%s %s `%s`\n", col_name, type_name, tags.join(" ")) 27 | info[:assoc_info][:has_many][col_name] = {class_name: class_name} 28 | info[:assoc_info][:has_many][col_name].merge!(assoc.options) unless assoc.options.empty? 29 | end 30 | 31 | when :has_one, :belongs_to 32 | col_name = class_name = assoc.name.to_s.camelize 33 | unless assoc.options.empty? 34 | if assoc.options.key? :class_name 35 | class_name = assoc.options[:class_name] 36 | elsif assoc.options.key? :source 37 | class_name = assoc.options[:source].to_s.camelize 38 | end 39 | end 40 | type_name = class_name 41 | if col_name && type_name && self.models.include?(class_name) 42 | info[:struct_body] << sprintf("%s %s `%s`\n", col_name, type_name, tags.join(" ")) 43 | info[:assoc_info][assoc.macro][col_name] = {class_name: class_name} 44 | info[:assoc_info][assoc.macro][col_name].merge!(assoc.options) unless assoc.options.empty? 45 | end 46 | end 47 | info[:has_assoc_dependent] = true if assoc.options.key? :dependent 48 | end 49 | info 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/generators/gor/converter.rb: -------------------------------------------------------------------------------- 1 | module GoOnRails 2 | class Convertor 3 | TYPE_MAP = { 4 | "string" => "string", 5 | "text" => "string", 6 | "boolean" => "bool", 7 | "integer(1)" => "int8", 8 | "integer(2)" => "int16", 9 | "integer(3)" => "int32", 10 | "integer(4)" => "int64", 11 | "integer(8)" => "int64", 12 | "float" => "float64", 13 | "datetime" => "time.Time", 14 | "date" => "time.Time", 15 | "inet" => "string" 16 | }.freeze 17 | 18 | # COALESCE datetime typed field for different databases 19 | # sqlite3 is dependent on the driver: https://github.com/railstack/go-sqlite3, details see: https://github.com/mattn/go-sqlite3/pull/468 20 | DATETIME_COALESCE_MAP = { 21 | "sqlite3" => "CAST(COALESCE(%s, '0001-01-01T00:00:00Z') as text) AS %s", 22 | "mysql" => "COALESCE(%s, CONVERT_TZ('0001-01-01 00:00:00','+00:00','UTC')) AS %s", 23 | "postgres" => "COALESCE(%s, (TIMESTAMP WITH TIME ZONE '0001-01-01 00:00:00+00') AT TIME ZONE 'UTC') AS %s" 24 | }.freeze 25 | 26 | # types need special treatment in the nullable_map() method 27 | SPECIAL_COALESCE_TYPES = %w[inet].freeze 28 | 29 | def initialize(klass, models, database) 30 | @klass = klass 31 | @models = models 32 | @database = database 33 | end 34 | attr_accessor :klass, :models, :database 35 | 36 | def convert 37 | get_schema_info 38 | end 39 | 40 | private 41 | 42 | def get_schema_info 43 | struct_info = { 44 | col_names: [], 45 | timestamp_cols: [], 46 | has_datetime_type: false, 47 | struct_body: "", 48 | } 49 | 50 | validation = GoOnRails::Validator.new(self.klass) 51 | # store fields by if nullable 52 | fields = { yes: [], no: [] } 53 | 54 | self.klass.columns.each_with_index do |col, index| 55 | tags = [] 56 | 57 | # add struct tag 58 | tags << struct_tag(col, validation) 59 | 60 | col_type = col.type.to_s 61 | struct_info[:has_datetime_type] = true if %w(datetime time).include? col_type 62 | if col_type == "datetime" and %w(created_at updated_at).include? col.name 63 | struct_info[:timestamp_cols] << col.name 64 | end 65 | 66 | case col_type 67 | when "integer" 68 | type = TYPE_MAP["integer(#{col.limit})"] || "int64" 69 | type = "u#{type}" if col.sql_type.match("unsigned").present? 70 | else 71 | type = TYPE_MAP[col_type] || "string" 72 | end 73 | 74 | # check the fields if nullable 75 | if col.null == true 76 | if SPECIAL_COALESCE_TYPES.include?(col_type) 77 | fields[:yes] << [col.name, col_type] 78 | else 79 | fields[:yes] << [col.name, type] 80 | end 81 | else 82 | fields[:no] << col.name 83 | end 84 | 85 | struct_info[:col_names] << col.name unless col.name == "id" 86 | struct_info[:struct_body] << sprintf("%s %s `%s`\n", col.name.camelize, type, tags.join(" ")) 87 | end 88 | 89 | assoc = get_associations 90 | struct_info[:struct_body] << assoc[:struct_body] 91 | struct_info[:assoc_info] = assoc[:assoc_info] 92 | struct_info[:has_assoc_dependent] = assoc[:has_assoc_dependent] 93 | struct_info[:select_fields] = nullable_select_str(fields) 94 | 95 | return struct_info 96 | end 97 | 98 | def get_struct_name 99 | self.klass.table_name.camelize 100 | end 101 | 102 | def get_associations 103 | builder = GoOnRails::Association.new(self.klass, self.models) 104 | builder.get_schema_info 105 | end 106 | 107 | def struct_tag(col, validation) 108 | valid_tags = validation.build_validator_tag(col) 109 | "json:\"#{col.name},omitempty\" db:\"#{col.name}\" #{valid_tags}" 110 | end 111 | 112 | def nullable_select_str(fields) 113 | fields[:yes].map do |f| 114 | sprintf(nullable_map(f[1]), "#{self.klass.table_name}.#{f[0]}", f[0]) 115 | end.concat( 116 | fields[:no].map do |f| 117 | sprintf("%s.%s", self.klass.table_name, f) 118 | end 119 | ).join(", ") 120 | end 121 | 122 | def nullable_map(type) 123 | case type 124 | when "string" 125 | "COALESCE(%s, '') AS %s" 126 | when "int8", "int16", "int32", "int64" 127 | "COALESCE(%s, 0) AS %s" 128 | when "float64" 129 | "COALESCE(%s, 0.0) AS %s" 130 | when "bool" 131 | "COALESCE(%s, FALSE) AS %s" 132 | when "time.Time" 133 | DATETIME_COALESCE_MAP[self.database] 134 | when "inet" 135 | "COALESCE(%s, '0.0.0.0') AS %s" 136 | else 137 | # FIXME: here just return the column name, may skip some nullable field and cause an error in a query 138 | "%s" 139 | end 140 | end 141 | end 142 | end 143 | 144 | require_relative 'association' 145 | require_relative 'validator' 146 | -------------------------------------------------------------------------------- /lib/generators/gor/validator.rb: -------------------------------------------------------------------------------- 1 | module GoOnRails 2 | class Validator 3 | TAG_DELIM = "," 4 | 5 | def initialize(klass) 6 | @klass = klass 7 | end 8 | attr_reader :klass 9 | 10 | def build_validator_tag(col) 11 | # ignore checking for some automaticly created fields 12 | return "valid:\"-\"" if %w(id created_at updated_at).include?(col.name) 13 | tags = [] 14 | 15 | validators = self.klass.validators_on col.name 16 | 17 | validators.each do |validator| 18 | tags.concat get_validation(validator, col) 19 | end 20 | 21 | tags << 'optional' if col.null && !tags.empty? && tags.exclude?('required') 22 | 23 | if tags.present? 24 | return "valid:\"#{tags.uniq.join(TAG_DELIM)}\"" 25 | else 26 | return "valid:\"-\"" 27 | end 28 | end 29 | 30 | def get_validation(validator, col) 31 | rules = [] 32 | case validator.class.to_s 33 | when "ActiveRecord::Validations::PresenceValidator" 34 | rules << "required" 35 | when "ActiveModel::Validations::FormatValidator" 36 | if validator.options && validator.options[:with] && ['string', 'text'].include?(col.type.to_s) 37 | re = $1 || ".*" if validator.options[:with].inspect =~ /\/(.*)\// 38 | rules << "matches(#{re})" 39 | end 40 | when "ActiveModel::Validations::NumericalityValidator" 41 | if validator.options && validator.options[:only_integer] 42 | rules << "IsInt" 43 | else 44 | rules << "IsNumeric" 45 | end 46 | when "ActiveModel::Validations::InclusionValidator" 47 | [:in, :within].each do |i| 48 | if validator.options && validator.options[i] 49 | rules << "in(#{validator.options[i].join('|')})" if ['string', 'text'].include?(col.type.to_s) 50 | end 51 | end 52 | when "ActiveRecord::Validations::LengthValidator" 53 | if validator.options && ['string', 'text'].include?(col.type.to_s) 54 | if validator.options[:is] 55 | min = max = validator.options[:is] 56 | else 57 | min = validator.options[:minimum] ? validator.options[:minimum] : 0 58 | max = validator.options[:maximum] ? validator.options[:maximum] : 4_294_967_295 59 | end 60 | rules << sprintf("length(%d|%d)", min, max) 61 | end 62 | end 63 | rules 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/generators/gor_generator.rb: -------------------------------------------------------------------------------- 1 | class GorGenerator < Rails::Generators::Base 2 | source_root File.expand_path('../templates', __FILE__) 3 | argument :env_name, type: :string, default: "development" 4 | class_option :models, type: :array, default: [], aliases: '-m' 5 | class_option :only_models, type: :boolean, default: false, aliases: '-o', description: "only generate models" 6 | 7 | def generate_gor 8 | env_names = ActiveRecord::Base.configurations.keys 9 | rails_env = case env_name 10 | when "dev" 11 | "development" 12 | when "pro" 13 | "production" 14 | else 15 | env_name 16 | end 17 | 18 | unless env_names.include? rails_env 19 | printf("Invalid env argument \"%s\": Not in the available list %p\n\n", rails_env, env_names) 20 | exit 21 | end 22 | 23 | @models = options[:models] 24 | if @models.empty? 25 | @models = get_all_models "app/models" 26 | else 27 | @models.map!(&:camelize) 28 | end 29 | puts "Rails env: [#{rails_env}]" 30 | puts "The models: #{@models} will be converted to a Golang App!" 31 | 32 | # read the database configuration 33 | @db_config = {} 34 | read_database_config(rails_env) 35 | 36 | @all_structs_info = {} 37 | # iterate the models to get all the structs' info 38 | @models.each do |m| 39 | begin 40 | klass = m.split('::').inject(Object) { |kls, part| kls.const_get(part) } 41 | if klass < ActiveRecord::Base && !klass.abstract_class? 42 | convertor = GoOnRails::Convertor.new(klass, @models, @db_config[:driver_name]) 43 | @all_structs_info[klass.to_s] = convertor.convert 44 | end 45 | rescue Exception => e 46 | puts "Failed to convert the model [#{m}]: #{e.message}" 47 | end 48 | end 49 | 50 | # iterate the structs info to generate codes for each model 51 | @all_structs_info.each do |k, v| 52 | @model_name, @struct_info = k, v 53 | if @db_config[:driver_name] == "postgres" 54 | template "gor_model_postgres.go.erb", "go_app/src/models/gor_#{@model_name.underscore}.go" 55 | else 56 | template "gor_model_mysql.go.erb", "go_app/src/models/gor_#{@model_name.underscore}.go" 57 | end 58 | end 59 | 60 | # generate program for database connection 61 | template "db.go.erb", "go_app/src/models/db.go" 62 | # and utils 63 | template "utils.go.erb", "go_app/src/models/utils.go" 64 | 65 | unless options[:only_models] 66 | # generate the main.go 67 | copy_file "main.go", "go_app/main.go" 68 | # generate the controllers and views dir 69 | template "home_controller.go.erb", "go_app/controllers/home_controller.go" 70 | copy_file "index.tmpl", "go_app/views/index.tmpl" 71 | copy_file "favicon.ico", "go_app/public/favicon.ico" 72 | # generate config files for make and dockerization 73 | template "docker-compose.yml.erb", "docker-compose.yml" 74 | template "Dockerfile.go_app.erb", "go_app/Dockerfile" 75 | copy_file "Makefile", "go_app/Makefile" 76 | end 77 | 78 | # use gofmt to prettify the generated Golang files 79 | gofmt_go_files 80 | 81 | # generate go docs for models 82 | generate_go_docs 83 | end 84 | 85 | private 86 | 87 | def get_all_models model_dir 88 | Dir.chdir(model_dir) do 89 | Dir["**/*.rb"] 90 | end.map { |m| m.sub(/\.rb$/,'').camelize } - ["ApplicationRecord"] 91 | end 92 | 93 | def read_database_config rails_env 94 | @db_config = Rails.configuration.database_configuration[rails_env].symbolize_keys 95 | @db_config[:host] ||= "localhost" 96 | case @db_config[:adapter] 97 | when "sqlite3" 98 | @db_config[:driver_name] = "sqlite3" 99 | @db_config[:dsn] = Rails.root.join(@db_config[:database]).to_s 100 | @db_config[:driver_package] = "_ \"github.com/railstack/go-sqlite3\"" 101 | when "mysql2" 102 | @db_config[:driver_name] = "mysql" 103 | @db_config[:port] ||= "3306" 104 | # MySQL DSN format: username:password@protocol(address)/dbname?param=value 105 | # See more: https://github.com/go-sql-driver/mysql 106 | format = "%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local" 107 | @db_config[:dsn] = sprintf(format, *@db_config.values_at(:username, :password, :host, :port, :database, :encoding)) 108 | @db_config[:driver_package] = "_ \"github.com/go-sql-driver/mysql\"" 109 | when "postgresql" 110 | @db_config[:driver_name] = "postgres" 111 | @db_config[:port] ||= "5432" 112 | format = "host=%s port=%s user=%s dbname=%s sslmode=disable password=%s" 113 | @db_config[:dsn] = sprintf(format, *@db_config.values_at(:host, :port, :username, :database, :password)) 114 | @db_config[:driver_package] = "_ \"github.com/lib/pq\"" 115 | end 116 | end 117 | 118 | def gofmt_go_files 119 | go_files = Rails.root.join('go_app', 'models/*.go').to_s 120 | system "gofmt -w #{go_files} > /dev/null 2>&1" 121 | end 122 | 123 | def generate_go_docs 124 | models_dir = Rails.root.join('go_app', 'src/models').to_s 125 | return unless Dir.exist?(File.expand_path(models_dir)) 126 | doc_dir = File.join(models_dir, 'doc') 127 | Dir.mkdir(doc_dir) unless Dir.exist?(doc_dir) 128 | Dir.chdir Rails.root.join('go_app').to_s 129 | system "GOPATH=./ godoc -url /pkg/models > #{doc_dir}/index.html" 130 | end 131 | end 132 | 133 | require 'generators/gor/converter' 134 | -------------------------------------------------------------------------------- /lib/generators/templates/Dockerfile.go_app.erb: -------------------------------------------------------------------------------- 1 | # Multi-stage builds require Docker 17.05 or higher on the daemon and client. 2 | # see: https://docs.docker.com/engine/userguide/eng-image/multistage-build/ 3 | 4 | # build the go app binary 5 | FROM golang:1.9.2 as builder 6 | WORKDIR /root/ 7 | COPY . /root/ 8 | <%- if %w[127.0.0.1 localhost].include? @db_config[:host] and @db_config[:driver_name] != "sqlite3" -%> 9 | RUN perl -pi -e "s/tcp\(.*?:/tcp\(db:/; s/host=\S+? /host=db /" src/models/db.go 10 | <%- end -%> 11 | RUN make deps 12 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp . 13 | 14 | # use the binary app to build the target image 15 | FROM alpine:latest 16 | WORKDIR /root/ 17 | ADD views /root/views 18 | ADD public /root/public 19 | COPY --from=builder /root/myapp . 20 | CMD ["./myapp"] 21 | -------------------------------------------------------------------------------- /lib/generators/templates/Makefile: -------------------------------------------------------------------------------- 1 | GO := go 2 | MYAPP := myapp 3 | IMAGE := $(MYAPP) 4 | TAG := latest 5 | 6 | $(MYAPP): 7 | $(GO) build -o $(MYAPP) 8 | 9 | clean: 10 | -rm $(MYAPP) 11 | 12 | build: $(MYAPP) 13 | @: 14 | 15 | deps: 16 | $(GO) get -u github.com/jmoiron/sqlx \ 17 | github.com/gin-gonic/gin \ 18 | github.com/railstack/go-sqlite3 \ 19 | github.com/go-sql-driver/mysql \ 20 | github.com/lib/pq \ 21 | github.com/asaskevich/govalidator 22 | 23 | test: 24 | $(GO) test -v ./... 25 | 26 | run: $(MYAPP) 27 | ./$(MYAPP) 28 | 29 | image: clean 30 | docker build -t $(USER)/$(IMAGE):$(TAG) . 31 | 32 | .PHONY: build clean deps test run image 33 | -------------------------------------------------------------------------------- /lib/generators/templates/db.go.erb: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "log" 5 | 6 | <%= @db_config[:driver_package] %> 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | var DB *sqlx.DB 11 | 12 | func init() { 13 | var err error 14 | driver_name := "<%= @db_config[:driver_name] %>" 15 | if driver_name == "" { 16 | log.Fatal("Invalid driver name") 17 | } 18 | dsn := "<%= @db_config[:dsn] %>" 19 | if dsn == "" { 20 | log.Fatal("Invalid DSN") 21 | } 22 | DB, err = sqlx.Connect(driver_name, dsn) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/generators/templates/docker-compose.yml.erb: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | <%- if %w[127.0.0.1 localhost].include? @db_config[:host] and @db_config[:driver_name] != "sqlite3" -%> 5 | # generated according to the config/database.yml 6 | db: 7 | image: <%= @db_config[:driver_name] %> 8 | ports: 9 | - '<%= @db_config[:port] %>:<%= @db_config[:port] %>' 10 | <%- end -%> 11 | 12 | # rails app part. You need to modify this part by your project 13 | # and create a Dockerfile for it before you run any docker-compose command 14 | rails_app: 15 | build: . 16 | command: bundle exec rails s -p 3000 -b '0.0.0.0' 17 | ports: 18 | - "3000:3000" 19 | <%- if %w[127.0.0.1 localhost].include? @db_config[:host] and @db_config[:driver_name] != "sqlite3" -%> 20 | depends_on: 21 | - db 22 | <%- end -%> 23 | 24 | # golang app part. It depends on the rails_app to initialize the database 25 | go_app: 26 | build: ./go_app 27 | command: ./myapp -port 4000 28 | environment: 29 | # Gin webserver run mode. Or "debug" for debugging 30 | - GIN_MODE=release 31 | ports: 32 | - "4000:4000" 33 | depends_on: 34 | <%- if %w[127.0.0.1 localhost].include? @db_config[:host] and @db_config[:driver_name] != "sqlite3" -%> 35 | - db 36 | <%- end -%> 37 | - rails_app 38 | -------------------------------------------------------------------------------- /lib/generators/templates/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railstack/go-on-rails/681d5ee53e55f898a639b25e699cde4700a5cd45/lib/generators/templates/favicon.ico -------------------------------------------------------------------------------- /lib/generators/templates/gor_model_mysql.go.erb: -------------------------------------------------------------------------------- 1 | <%#- var_pmn stands for pluralized model name, its format is same as the table name -#%> 2 | <%- var_pmn = table_name = @model_name.tableize -%> 3 | <%#- var_umn stands for underscored model name -#%> 4 | <%- var_umn = @model_name.underscore -%> 5 | <%- fields = @struct_info[:select_fields] -%> 6 | <%- col_names = @struct_info[:col_names] -%> 7 | <%- has_assoc = !@struct_info[:assoc_info][:has_many].empty? || !@struct_info[:assoc_info][:has_one].empty? -%> 8 | // Package models includes the functions on the model <%= @model_name %>. 9 | package models 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | "log" 15 | "math" 16 | "strings" 17 | <%- if @struct_info[:has_datetime_type] -%> 18 | "time" 19 | <%- end -%> 20 | 21 | "github.com/asaskevich/govalidator" 22 | ) 23 | 24 | // set flags to output more detailed log 25 | func init() { 26 | log.SetFlags(log.LstdFlags | log.Lshortfile) 27 | } 28 | 29 | type <%= @model_name %> struct { 30 | <%= @struct_info[:struct_body] -%> 31 | } 32 | 33 | // DataStruct for the pagination 34 | type <%= @model_name %>Page struct { 35 | WhereString string 36 | WhereParams []interface{} 37 | Order map[string]string 38 | FirstId int64 39 | LastId int64 40 | PageNum int 41 | PerPage int 42 | TotalPages int 43 | TotalItems int64 44 | orderStr string 45 | } 46 | 47 | // Current get the current page of <%= @model_name %>Page object for pagination. 48 | func (_p *<%= @model_name %>Page) Current() ([]<%= @model_name %>, error) { 49 | if _, exist := _p.Order["id"]; !exist { 50 | return nil, errors.New("No id order specified in Order map") 51 | } 52 | err := _p.buildPageCount() 53 | if err != nil { 54 | return nil, fmt.Errorf("Calculate page count error: %v", err) 55 | } 56 | if _p.orderStr == "" { 57 | _p.buildOrder() 58 | } 59 | idStr, idParams := _p.buildIdRestrict("current") 60 | whereStr := fmt.Sprintf("%s %s %s LIMIT %v", _p.WhereString, idStr, _p.orderStr, _p.PerPage) 61 | whereParams := []interface{}{} 62 | whereParams = append(append(whereParams, _p.WhereParams...), idParams...) 63 | <%= var_pmn %>, err := Find<%= @model_name.pluralize %>Where(whereStr, whereParams...) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if len(<%= var_pmn %>) != 0 { 68 | _p.FirstId, _p.LastId = <%= var_pmn %>[0].Id, <%= var_pmn %>[len(<%= var_pmn %>)-1].Id 69 | } 70 | return <%= var_pmn %>, nil 71 | } 72 | 73 | // Previous get the previous page of <%= @model_name %>Page object for pagination. 74 | func (_p *<%= @model_name %>Page) Previous() ([]<%= @model_name %>, error) { 75 | if _p.PageNum == 0 { 76 | return nil, errors.New("This's the first page, no previous page yet") 77 | } 78 | if _, exist := _p.Order["id"]; !exist { 79 | return nil, errors.New("No id order specified in Order map") 80 | } 81 | err := _p.buildPageCount() 82 | if err != nil { 83 | return nil, fmt.Errorf("Calculate page count error: %v", err) 84 | } 85 | if _p.orderStr == "" { 86 | _p.buildOrder() 87 | } 88 | idStr, idParams := _p.buildIdRestrict("previous") 89 | whereStr := fmt.Sprintf("%s %s %s LIMIT %v", _p.WhereString, idStr, _p.orderStr, _p.PerPage) 90 | whereParams := []interface{}{} 91 | whereParams = append(append(whereParams, _p.WhereParams...), idParams...) 92 | <%= var_pmn %>, err := Find<%= @model_name.pluralize %>Where(whereStr, whereParams...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | if len(<%= var_pmn %>) != 0 { 97 | _p.FirstId, _p.LastId = <%= var_pmn %>[0].Id, <%= var_pmn %>[len(<%= var_pmn %>)-1].Id 98 | } 99 | _p.PageNum -= 1 100 | return <%= var_pmn %>, nil 101 | } 102 | 103 | // Next get the next page of <%= @model_name %>Page object for pagination. 104 | func (_p *<%= @model_name %>Page) Next() ([]<%= @model_name %>, error) { 105 | if _p.PageNum == _p.TotalPages-1 { 106 | return nil, errors.New("This's the last page, no next page yet") 107 | } 108 | if _, exist := _p.Order["id"]; !exist { 109 | return nil, errors.New("No id order specified in Order map") 110 | } 111 | err := _p.buildPageCount() 112 | if err != nil { 113 | return nil, fmt.Errorf("Calculate page count error: %v", err) 114 | } 115 | if _p.orderStr == "" { 116 | _p.buildOrder() 117 | } 118 | idStr, idParams := _p.buildIdRestrict("next") 119 | whereStr := fmt.Sprintf("%s %s %s LIMIT %v", _p.WhereString, idStr, _p.orderStr, _p.PerPage) 120 | whereParams := []interface{}{} 121 | whereParams = append(append(whereParams, _p.WhereParams...), idParams...) 122 | <%= var_pmn %>, err := Find<%= @model_name.pluralize %>Where(whereStr, whereParams...) 123 | if err != nil { 124 | return nil, err 125 | } 126 | if len(<%= var_pmn %>) != 0 { 127 | _p.FirstId, _p.LastId = <%= var_pmn %>[0].Id, <%= var_pmn %>[len(<%= var_pmn %>)-1].Id 128 | } 129 | _p.PageNum += 1 130 | return <%= var_pmn %>, nil 131 | } 132 | 133 | // GetPage is a helper function for the <%= @model_name %>Page object to return a corresponding page due to 134 | // the parameter passed in, i.e. one of "previous, current or next". 135 | func (_p *<%= @model_name %>Page) GetPage(direction string) (ps []<%= @model_name %>, err error) { 136 | switch direction { 137 | case "previous": 138 | ps, _ = _p.Previous() 139 | case "next": 140 | ps, _ = _p.Next() 141 | case "current": 142 | ps, _ = _p.Current() 143 | default: 144 | return nil, errors.New("Error: wrong dircetion! None of previous, current or next!") 145 | } 146 | return 147 | } 148 | 149 | // buildOrder is for <%= @model_name %>Page object to build a SQL ORDER BY clause. 150 | func (_p *<%= @model_name %>Page) buildOrder() { 151 | tempList := []string{} 152 | for k, v := range _p.Order { 153 | tempList = append(tempList, fmt.Sprintf("%v %v", k, v)) 154 | } 155 | _p.orderStr = " ORDER BY " + strings.Join(tempList, ", ") 156 | } 157 | 158 | // buildIdRestrict is for <%= @model_name %>Page object to build a SQL clause for ID restriction, 159 | // implementing a simple keyset style pagination. 160 | func (_p *<%= @model_name %>Page) buildIdRestrict(direction string) (idStr string, idParams []interface{}) { 161 | switch direction { 162 | case "previous": 163 | if strings.ToLower(_p.Order["id"]) == "desc" { 164 | idStr += "id > ? " 165 | idParams = append(idParams, _p.FirstId) 166 | } else { 167 | idStr += "id < ? " 168 | idParams = append(idParams, _p.FirstId) 169 | } 170 | case "current": 171 | // trick to make Where function work 172 | if _p.PageNum == 0 && _p.FirstId == 0 && _p.LastId == 0 { 173 | idStr += "id > ? " 174 | idParams = append(idParams, 0) 175 | } else { 176 | if strings.ToLower(_p.Order["id"]) == "desc" { 177 | idStr += "id <= ? AND id >= ? " 178 | idParams = append(idParams, _p.FirstId, _p.LastId) 179 | } else { 180 | idStr += "id >= ? AND id <= ? " 181 | idParams = append(idParams, _p.FirstId, _p.LastId) 182 | } 183 | } 184 | case "next": 185 | if strings.ToLower(_p.Order["id"]) == "desc" { 186 | idStr += "id < ? " 187 | idParams = append(idParams, _p.LastId) 188 | } else { 189 | idStr += "id > ? " 190 | idParams = append(idParams, _p.LastId) 191 | } 192 | } 193 | if _p.WhereString != "" { 194 | idStr = " AND " + idStr 195 | } 196 | return 197 | } 198 | 199 | // buildPageCount calculate the TotalItems/TotalPages for the <%= @model_name %>Page object. 200 | func (_p *<%= @model_name %>Page) buildPageCount() error { 201 | count, err := <%= @model_name %>CountWhere(_p.WhereString, _p.WhereParams...) 202 | if err != nil { 203 | return err 204 | } 205 | _p.TotalItems = count 206 | if _p.PerPage == 0 { 207 | _p.PerPage = 10 208 | } 209 | _p.TotalPages = int(math.Ceil(float64(_p.TotalItems) / float64(_p.PerPage))) 210 | return nil 211 | } 212 | 213 | 214 | // Find<%= @model_name %> find a single <%= var_umn %> by an ID. 215 | func Find<%= @model_name %>(id int64) (*<%= @model_name %>, error) { 216 | if id == 0 { 217 | return nil, errors.New("Invalid ID: it can't be zero") 218 | } 219 | _<%= var_umn %> := <%= @model_name %>{} 220 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(`SELECT <%= fields %> FROM <%= table_name %> WHERE <%= table_name %>.id = ? LIMIT 1`), id) 221 | if err != nil { 222 | log.Printf("Error: %v\n", err) 223 | return nil, err 224 | } 225 | return &_<%= var_umn %>, nil 226 | } 227 | 228 | // First<%= @model_name %> find the first one <%= var_umn %> by ID ASC order. 229 | func First<%= @model_name %>() (*<%= @model_name %>, error) { 230 | _<%= var_umn %> := <%= @model_name %>{} 231 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(`SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id ASC LIMIT 1`)) 232 | if err != nil { 233 | log.Printf("Error: %v\n", err) 234 | return nil, err 235 | } 236 | return &_<%= var_umn %>, nil 237 | } 238 | 239 | // First<%= @model_name.pluralize %> find the first N <%= var_umn.pluralize %> by ID ASC order. 240 | func First<%= @model_name.pluralize %>(n uint32) ([]<%= @model_name %>, error) { 241 | _<%= var_pmn %> := []<%= @model_name %>{} 242 | sql := fmt.Sprintf("SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id ASC LIMIT %v", n) 243 | err := DB.Select(&_<%= var_pmn %>, DB.Rebind(sql)) 244 | if err != nil { 245 | log.Printf("Error: %v\n", err) 246 | return nil, err 247 | } 248 | return _<%= var_pmn %>, nil 249 | } 250 | 251 | // Last<%= @model_name %> find the last one <%= var_umn %> by ID DESC order. 252 | func Last<%= @model_name %>() (*<%= @model_name %>, error) { 253 | _<%= var_umn %> := <%= @model_name %>{} 254 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(`SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id DESC LIMIT 1`)) 255 | if err != nil { 256 | log.Printf("Error: %v\n", err) 257 | return nil, err 258 | } 259 | return &_<%= var_umn %>, nil 260 | } 261 | 262 | // Last<%= @model_name.pluralize %> find the last N <%= var_umn.pluralize %> by ID DESC order. 263 | func Last<%= @model_name.pluralize %>(n uint32) ([]<%= @model_name %>, error) { 264 | _<%= var_pmn %> := []<%= @model_name %>{} 265 | sql := fmt.Sprintf("SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id DESC LIMIT %v", n) 266 | err := DB.Select(&_<%= var_pmn %>, DB.Rebind(sql)) 267 | if err != nil { 268 | log.Printf("Error: %v\n", err) 269 | return nil, err 270 | } 271 | return _<%= var_pmn %>, nil 272 | } 273 | 274 | // Find<%= @model_name.pluralize %> find one or more <%= var_umn.pluralize %> by the given ID(s). 275 | func Find<%= @model_name.pluralize %>(ids ...int64) ([]<%= @model_name %>, error) { 276 | if len(ids) == 0 { 277 | msg := "At least one or more ids needed" 278 | log.Println(msg) 279 | return nil, errors.New(msg) 280 | } 281 | _<%= var_pmn %> := []<%= @model_name %>{} 282 | idsHolder := strings.Repeat(",?", len(ids)-1) 283 | sql := DB.Rebind(fmt.Sprintf(`SELECT <%= fields %> FROM <%= table_name %> WHERE <%= table_name %>.id IN (?%s)`, idsHolder)) 284 | idsT := []interface{}{} 285 | for _,id := range ids { 286 | idsT = append(idsT, interface{}(id)) 287 | } 288 | err := DB.Select(&_<%= var_pmn %>, sql, idsT...) 289 | if err != nil { 290 | log.Printf("Error: %v\n", err) 291 | return nil, err 292 | } 293 | return _<%= var_pmn %>, nil 294 | } 295 | 296 | // Find<%= @model_name %>By find a single <%= var_umn %> by a field name and a value. 297 | func Find<%= @model_name %>By(field string, val interface{}) (*<%= @model_name %>, error) { 298 | _<%= var_umn %> := <%= @model_name %>{} 299 | sqlFmt := `SELECT <%= fields %> FROM <%= table_name %> WHERE %s = ? LIMIT 1` 300 | sqlStr := fmt.Sprintf(sqlFmt, field) 301 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(sqlStr), val) 302 | if err != nil { 303 | log.Printf("Error: %v\n", err) 304 | return nil, err 305 | } 306 | return &_<%= var_umn %>, nil 307 | } 308 | 309 | // Find<%= @model_name.pluralize %>By find all <%= var_pmn %> by a field name and a value. 310 | func Find<%= @model_name.pluralize %>By(field string, val interface{}) (_<%= var_pmn %> []<%= @model_name %>, err error) { 311 | sqlFmt := `SELECT <%= fields %> FROM <%= table_name %> WHERE %s = ?` 312 | sqlStr := fmt.Sprintf(sqlFmt, field) 313 | err = DB.Select(&_<%= var_pmn %>, DB.Rebind(sqlStr), val) 314 | if err != nil { 315 | log.Printf("Error: %v\n", err) 316 | return nil, err 317 | } 318 | return _<%= var_pmn %>, nil 319 | } 320 | 321 | // All<%= @model_name.pluralize %> get all the <%= @model_name %> records. 322 | func All<%= @model_name.pluralize %>() (<%= var_pmn %> []<%= @model_name %>, err error) { 323 | err = DB.Select(&<%= var_pmn %>, "SELECT <%= fields %> FROM <%= table_name %>") 324 | if err != nil { 325 | log.Println(err) 326 | return nil, err 327 | } 328 | return <%= var_pmn %>, nil 329 | } 330 | 331 | // <%= @model_name %>Count get the count of all the <%= @model_name %> records. 332 | func <%= @model_name %>Count() (c int64, err error) { 333 | err = DB.Get(&c, "SELECT count(*) FROM <%= table_name %>") 334 | if err != nil { 335 | log.Println(err) 336 | return 0, err 337 | } 338 | return c, nil 339 | } 340 | 341 | // <%= @model_name %>CountWhere get the count of all the <%= @model_name %> records with a where clause. 342 | func <%= @model_name %>CountWhere(where string, args ...interface{}) (c int64, err error) { 343 | sql := "SELECT count(*) FROM <%= table_name %>" 344 | if len(where) > 0 { 345 | sql = sql + " WHERE " + where 346 | } 347 | stmt, err := DB.Preparex(DB.Rebind(sql)) 348 | if err != nil { 349 | log.Println(err) 350 | return 0, err 351 | } 352 | err = stmt.Get(&c, args...) 353 | if err != nil { 354 | log.Println(err) 355 | return 0, err 356 | } 357 | return c, nil 358 | } 359 | 360 | // <%= @model_name %>IncludesWhere get the <%= @model_name %> associated models records, currently it's not same as the corresponding "includes" function but "preload" instead in Ruby on Rails. It means that the "sql" should be restricted on <%= @model_name %> model. 361 | func <%= @model_name %>IncludesWhere(assocs []string, sql string, args ...interface{}) (_<%= var_pmn %> []<%= @model_name %>, err error) { 362 | _<%= var_pmn %>, err = Find<%= @model_name.pluralize %>Where(sql, args...) 363 | if err != nil { 364 | log.Println(err) 365 | return nil, err 366 | } 367 | if len(assocs) == 0 { 368 | log.Println("No associated fields ard specified") 369 | return _<%= var_pmn %>, err 370 | } 371 | if len(_<%= var_pmn %>) <= 0 { 372 | return nil, errors.New("No results available") 373 | } 374 | ids := make([]interface{}, len(_<%= var_pmn %>)) 375 | for _, v := range _<%= var_pmn %> { 376 | ids = append(ids, interface{}(v.Id)) 377 | } 378 | <%- if has_assoc -%> 379 | idsHolder := strings.Repeat(",?", len(ids)-1) 380 | for _, assoc := range assocs { 381 | switch assoc { 382 | <%- unless @struct_info[:assoc_info][:has_many].empty? -%> 383 | <%- has_many = @struct_info[:assoc_info][:has_many] -%> 384 | <%- has_many.each do |k, v| -%> 385 | case "<%= k.underscore.pluralize %>": 386 | <%- if v[:through] || v[:as] -%> 387 | // FIXME: optimize the query 388 | for i, vvv := range _<%= var_pmn %> { 389 | _<%= k.underscore.pluralize %>, err := <%= @model_name %>Get<%= k.pluralize %>(vvv.Id) 390 | if err != nil { 391 | continue 392 | } 393 | vvv.<%= k %> = _<%= k.underscore.pluralize %> 394 | _<%= var_pmn %>[i] = vvv 395 | } 396 | <%- else -%> 397 | <%- if v[:foreign_key] -%> 398 | where := fmt.Sprintf("<%= v[:foreign_key] %> IN (?%s)", idsHolder) 399 | <%- else -%> 400 | where := fmt.Sprintf("<%= var_umn %>_id IN (?%s)", idsHolder) 401 | <%- end -%> 402 | _<%= v[:class_name].underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>Where(where, ids...) 403 | if err != nil { 404 | log.Printf("Error when query associated objects: %v\n", assoc) 405 | continue 406 | } 407 | for _, vv := range _<%= v[:class_name].underscore.pluralize %> { 408 | for i, vvv := range _<%= var_pmn %> { 409 | <%- if v[:foreign_key] -%> 410 | if vv.<%= v[:foreign_key].to_s.camelize %> == vvv.Id { 411 | vvv.<%= k %> = append(vvv.<%= k %>, vv) 412 | } 413 | <%- else -%> 414 | if vv.<%= @model_name.camelize %>Id == vvv.Id { 415 | vvv.<%= k %> = append(vvv.<%= k %>, vv) 416 | } 417 | <%- end -%> 418 | _<%= var_pmn %>[i].<%= k %> = vvv.<%= k %> 419 | } 420 | } 421 | <%- end -%> 422 | <%- end -%> 423 | <%- end -%> 424 | <%- unless @struct_info[:assoc_info][:has_one].empty? -%> 425 | <%- has_one = @struct_info[:assoc_info][:has_one] -%> 426 | <%- has_one.each do |k, v| -%> 427 | case "<%= k.underscore %>": 428 | <%- if v[:foreign_key] -%> 429 | where := fmt.Sprintf("<%= v[:foreign_key] %> IN (?%s)", idsHolder) 430 | <%- else -%> 431 | where := fmt.Sprintf("<%= var_umn %>_id IN (?%s)", idsHolder) 432 | <%- end -%> 433 | _<%= v[:class_name].underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>Where(where, ids...) 434 | if err != nil { 435 | log.Printf("Error when query associated objects: %v\n", assoc) 436 | continue 437 | } 438 | for _, vv := range _<%= v[:class_name].underscore.pluralize %> { 439 | for i, vvv := range _<%= var_pmn %> { 440 | <%- if v[:foreign_key] -%> 441 | if vv.<%= v[:foreign_key].to_s.camelize %> == vvv.Id { 442 | vvv.<%= k %> = vv 443 | } 444 | <%- else -%> 445 | if vv.<%= @model_name.camelize %>Id == vvv.Id { 446 | vvv.<%= k %> = vv 447 | } 448 | <%- end -%> 449 | _<%= var_pmn %>[i].<%= k %> = vvv.<%= k %> 450 | } 451 | } 452 | <%- end -%> 453 | <%- end -%> 454 | } 455 | } 456 | <%- end -%> 457 | return _<%= var_pmn %>, nil 458 | } 459 | 460 | // <%= @model_name %>Ids get all the IDs of <%= @model_name %> records. 461 | func <%= @model_name %>Ids() (ids []int64, err error) { 462 | err = DB.Select(&ids, "SELECT id FROM <%= table_name %>") 463 | if err != nil { 464 | log.Println(err) 465 | return nil, err 466 | } 467 | return ids, nil 468 | } 469 | 470 | // <%= @model_name %>IdsWhere get all the IDs of <%= @model_name %> records by where restriction. 471 | func <%= @model_name %>IdsWhere(where string, args ...interface{}) ([]int64, error) { 472 | ids, err := <%= @model_name %>IntCol("id", where, args...) 473 | return ids, err 474 | } 475 | 476 | // <%= @model_name %>IntCol get some int64 typed column of <%= @model_name %> by where restriction. 477 | func <%= @model_name %>IntCol(col, where string, args ...interface{}) (intColRecs []int64, err error) { 478 | sql := "SELECT " + col + " FROM <%= table_name %>" 479 | if len(where) > 0 { 480 | sql = sql + " WHERE " + where 481 | } 482 | stmt, err := DB.Preparex(DB.Rebind(sql)) 483 | if err != nil { 484 | log.Println(err) 485 | return nil, err 486 | } 487 | err = stmt.Select(&intColRecs, args...) 488 | if err != nil { 489 | log.Println(err) 490 | return nil, err 491 | } 492 | return intColRecs, nil 493 | } 494 | 495 | // <%= @model_name %>StrCol get some string typed column of <%= @model_name %> by where restriction. 496 | func <%= @model_name %>StrCol(col, where string, args ...interface{}) (strColRecs []string, err error) { 497 | sql := "SELECT " + col + " FROM <%= table_name %>" 498 | if len(where) > 0 { 499 | sql = sql + " WHERE " + where 500 | } 501 | stmt, err := DB.Preparex(DB.Rebind(sql)) 502 | if err != nil { 503 | log.Println(err) 504 | return nil, err 505 | } 506 | err = stmt.Select(&strColRecs, args...) 507 | if err != nil { 508 | log.Println(err) 509 | return nil, err 510 | } 511 | return strColRecs, nil 512 | } 513 | 514 | // Find<%= @model_name.pluralize %>Where query use a partial SQL clause that usually following after WHERE 515 | // with placeholders, eg: FindUsersWhere("first_name = ? AND age > ?", "John", 18) 516 | // will return those records in the table "users" whose first_name is "John" and age elder than 18. 517 | func Find<%= @model_name.pluralize %>Where(where string, args ...interface{}) (<%= var_pmn %> []<%= @model_name %>, err error) { 518 | sql := "SELECT <%= fields %> FROM <%= table_name %>" 519 | if len(where) > 0 { 520 | sql = sql + " WHERE " + where 521 | } 522 | stmt, err := DB.Preparex(DB.Rebind(sql)) 523 | if err != nil { 524 | log.Println(err) 525 | return nil, err 526 | } 527 | err = stmt.Select(&<%= var_pmn %>, args...) 528 | if err != nil { 529 | log.Println(err) 530 | return nil, err 531 | } 532 | return <%= var_pmn %>, nil 533 | } 534 | 535 | // Find<%= @model_name %>BySql query use a complete SQL clause 536 | // with placeholders, eg: FindUserBySql("SELECT * FROM users WHERE first_name = ? AND age > ? ORDER BY DESC LIMIT 1", "John", 18) 537 | // will return only One record in the table "users" whose first_name is "John" and age elder than 18. 538 | func Find<%= @model_name %>BySql(sql string, args ...interface{}) (*<%= @model_name %>, error) { 539 | stmt, err := DB.Preparex(DB.Rebind(sql)) 540 | if err != nil { 541 | log.Println(err) 542 | return nil, err 543 | } 544 | _<%= var_umn %> := &<%= @model_name %>{} 545 | err = stmt.Get(_<%= var_umn %>, args...) 546 | if err != nil { 547 | log.Println(err) 548 | return nil, err 549 | } 550 | return _<%= var_umn %>, nil 551 | } 552 | 553 | // Find<%= @model_name.pluralize %>BySql query use a complete SQL clause 554 | // with placeholders, eg: FindUsersBySql("SELECT * FROM users WHERE first_name = ? AND age > ?", "John", 18) 555 | // will return those records in the table "users" whose first_name is "John" and age elder than 18. 556 | func Find<%= @model_name.pluralize %>BySql(sql string, args ...interface{}) (<%= var_pmn %> []<%= @model_name %>, err error) { 557 | stmt, err := DB.Preparex(DB.Rebind(sql)) 558 | if err != nil { 559 | log.Println(err) 560 | return nil, err 561 | } 562 | err = stmt.Select(&<%= var_pmn %>, args...) 563 | if err != nil { 564 | log.Println(err) 565 | return nil, err 566 | } 567 | return <%= var_pmn %>, nil 568 | } 569 | 570 | // Create<%= @model_name %> use a named params to create a single <%= @model_name %> record. 571 | // A named params is key-value map like map[string]interface{}{"first_name": "John", "age": 23} . 572 | func Create<%= @model_name %>(am map[string]interface{}) (int64, error) { 573 | if len(am) == 0 { 574 | return 0, fmt.Errorf("Zero key in the attributes map!") 575 | } 576 | <%- unless @struct_info[:timestamp_cols].empty? -%> 577 | t := time.Now() 578 | for _, v := range []string{<%= @struct_info[:timestamp_cols].map(&:inspect).join(", ") %>} { 579 | if am[v] == nil { 580 | am[v] = t 581 | } 582 | } 583 | <%- end -%> 584 | keys := allKeys(am) 585 | sqlFmt := `INSERT INTO <%= table_name %> (%s) VALUES (%s)` 586 | sql := fmt.Sprintf(sqlFmt, strings.Join(keys, ","), ":"+strings.Join(keys, ",:")) 587 | result, err := DB.NamedExec(sql, am) 588 | if err != nil { 589 | log.Println(err) 590 | return 0, err 591 | } 592 | lastId, err := result.LastInsertId() 593 | if err != nil { 594 | log.Println(err) 595 | return 0, err 596 | } 597 | return lastId, nil 598 | } 599 | 600 | // Create is a method for <%= @model_name %> to create a record. 601 | func (_<%= var_umn %> *<%= @model_name %>) Create() (int64, error) { 602 | ok, err := govalidator.ValidateStruct(_<%= var_umn %>) 603 | if !ok { 604 | errMsg := "Validate <%= @model_name %> struct error: Unknown error" 605 | if err != nil { 606 | errMsg = "Validate <%= @model_name %> struct error: " + err.Error() 607 | } 608 | log.Println(errMsg) 609 | return 0, errors.New(errMsg) 610 | } 611 | <%- unless @struct_info[:timestamp_cols].empty? -%> 612 | t := time.Now() 613 | <%- @struct_info[:timestamp_cols].each do |c| -%> 614 | _<%= var_umn %>.<%= c.camelize %> = t 615 | <%- end -%> 616 | <%- end -%> 617 | sql := `INSERT INTO <%= table_name %> (<%= col_names.join(",") %>) VALUES (:<%= col_names.join(",:") %>)` 618 | result, err := DB.NamedExec(sql, _<%= var_umn %>) 619 | if err != nil { 620 | log.Println(err) 621 | return 0, err 622 | } 623 | lastId, err := result.LastInsertId() 624 | if err != nil { 625 | log.Println(err) 626 | return 0, err 627 | } 628 | return lastId, nil 629 | } 630 | 631 | <%- unless @struct_info[:assoc_info][:has_many].empty? -%> 632 | <%- has_many = @struct_info[:assoc_info][:has_many] -%> 633 | <%- has_many.each do |k, v| -%> 634 | // <%= k.pluralize %>Create is used for <%= @model_name %> to create the associated objects <%= k.pluralize %> 635 | func (_<%= var_umn %> *<%= @model_name %>) <%= k.pluralize %>Create(am map[string]interface{}) error { 636 | <%- if v[:through] -%> 637 | // FIXME: use transaction to create these associated objects 638 | <%= v[:class_name].underscore %>Id, err := Create<%= v[:class_name] %>(am) 639 | if err != nil { 640 | return err 641 | } 642 | _, err = Create<%= v[:through].to_s.singularize.camelize %>(map[string]interface{}{"<%= var_umn %>_id": _<%= var_umn %>.Id, "<%= v[:class_name].underscore %>_id": <%= v[:class_name].underscore %>Id}) 643 | <%- elsif v[:as] -%> 644 | am["<%= v[:as] %>_id"] = _<%= var_umn %>.Id 645 | am["<%= v[:as] %>_type"] = "<%= @model_name %>" 646 | _, err := Create<%= v[:class_name] %>(am) 647 | <%- else -%> 648 | <%- if v[:foreign_key] -%> 649 | am["<%= v[:foreign_key] %>"] = _<%= var_umn %>.Id 650 | <%- else -%> 651 | am["<%= var_umn %>_id"] = _<%= var_umn %>.Id 652 | <%- end -%> 653 | _, err := Create<%= v[:class_name] %>(am) 654 | <%- end -%> 655 | return err 656 | } 657 | 658 | // Get<%= k.pluralize %> is used for <%= @model_name %> to get associated objects <%= k.pluralize %> 659 | // Say you have a <%= @model_name %> object named <%= var_umn %>, when you call <%= var_umn %>.Get<%= k.pluralize %>(), 660 | // the object will get the associated <%= k.pluralize %> attributes evaluated in the struct. 661 | func (_<%= var_umn %> *<%= @model_name %>) Get<%= k.pluralize %>() error { 662 | _<%= k.underscore.pluralize %>, err := <%= @model_name %>Get<%= k.pluralize %>(_<%= var_umn %>.Id) 663 | if err == nil { 664 | _<%= var_umn %>.<%= k %> = _<%= k.underscore.pluralize %> 665 | } 666 | return err 667 | } 668 | 669 | // <%= @model_name %>Get<%= k.pluralize %> a helper fuction used to get associated objects for <%= @model_name %>IncludesWhere(). 670 | func <%= @model_name %>Get<%= k.pluralize %>(id int64) ([]<%= v[:class_name] %>, error) { 671 | <%- if v[:through] -%> 672 | // FIXME: use transaction to create these associated objects 673 | <%- target_table = v[:class_name].underscore.pluralize -%> 674 | <%- through_table = v[:through].to_s.pluralize -%> 675 | sql := `SELECT <%= @all_structs_info[v[:class_name]][:select_fields] %> 676 | FROM <%= target_table %> 677 | INNER JOIN <%= through_table %> 678 | ON <%= target_table %>.id = <%= through_table %>.<%= v[:class_name].underscore %>_id 679 | WHERE <%= through_table %>.<%= var_umn %>_id = ?` 680 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>BySql(sql, id) 681 | <%- elsif v[:as] -%> 682 | where := `<%= v[:as] %>_type = "<%= @model_name %>" AND <%= v[:as] %>_id = ?` 683 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>Where(where, id) 684 | <%- else -%> 685 | <%- if v[:foreign_key] -%> 686 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>By("<%= v[:foreign_key] %>", id) 687 | <%- else -%> 688 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>By("<%= var_umn %>_id", id) 689 | <%- end -%> 690 | <%- end -%> 691 | return _<%= k.underscore.pluralize %>, err 692 | } 693 | 694 | <%- end -%> 695 | <%- end -%> 696 | 697 | <%- unless @struct_info[:assoc_info][:has_one].empty? -%> 698 | <%- has_one = @struct_info[:assoc_info][:has_one] -%> 699 | <%- has_one.each do |k, v| -%> 700 | // Create<%= k %> is a method for the <%= @model_name %> model object to create an associated <%= k %> record. 701 | func (_<%= var_umn %> *<%= @model_name %>) Create<%= k %>(am map[string]interface{}) error { 702 | <%- if v[:foreign_key] -%> 703 | am["<%= v[:foreign_key] %>"] = _<%= var_umn %>.Id 704 | <%- else -%> 705 | am["<%= var_umn %>_id"] = _<%= var_umn %>.Id 706 | <%- end -%> 707 | _, err := Create<%= v[:class_name] %>(am) 708 | return err 709 | } 710 | <%- end -%> 711 | <%- end -%> 712 | 713 | <%- unless @struct_info[:assoc_info][:belongs_to].empty? -%> 714 | <%- belongs_to = @struct_info[:assoc_info][:belongs_to] -%> 715 | <%- belongs_to.each do |k, v| -%> 716 | <%-# don't create virtual table for polymorphic model -%> 717 | <%- next if v[:polymorphic] -%> 718 | // Create<%= k %> is a method for a <%= @model_name %> object to create an associated <%= k %> record. 719 | func (_<%= var_umn %> *<%= @model_name %>) Create<%= k %>(am map[string]interface{}) error { 720 | <%- if v[:foreign_key] -%> 721 | am["<%= v[:foreign_key] %>"] = _<%= var_umn %>.Id 722 | <%- else -%> 723 | am["<%= var_umn %>_id"] = _<%= var_umn %>.Id 724 | <%- end -%> 725 | _, err := Create<%= v[:class_name] %>(am) 726 | return err 727 | } 728 | <%- end -%> 729 | <%- end -%> 730 | 731 | // Destroy is method used for a <%= @model_name %> object to be destroyed. 732 | func (_<%= var_umn %> *<%= @model_name %>) Destroy() error { 733 | if _<%= var_umn %>.Id == 0 { 734 | return errors.New("Invalid Id field: it can't be a zero value") 735 | } 736 | err := Destroy<%= @model_name %>(_<%= var_umn %>.Id) 737 | return err 738 | } 739 | 740 | // Destroy<%= @model_name %> will destroy a <%= @model_name %> record specified by the id parameter. 741 | func Destroy<%= @model_name %>(id int64) error { 742 | <%- if @struct_info[:has_assoc_dependent] -%> 743 | // Destroy association objects at first 744 | // Not care if exec properly temporarily 745 | destroy<%= @model_name %>Associations(id) 746 | <%- end -%> 747 | stmt, err := DB.Preparex(DB.Rebind(`DELETE FROM <%= table_name %> WHERE id = ?`)) 748 | _, err = stmt.Exec(id) 749 | if err != nil { 750 | return err 751 | } 752 | return nil 753 | } 754 | 755 | // Destroy<%= @model_name.pluralize %> will destroy <%= @model_name %> records those specified by the ids parameters. 756 | func Destroy<%= @model_name.pluralize %>(ids ...int64) (int64, error) { 757 | if len(ids) == 0 { 758 | msg := "At least one or more ids needed" 759 | log.Println(msg) 760 | return 0, errors.New(msg) 761 | } 762 | <%- if @struct_info[:has_assoc_dependent] -%> 763 | // Destroy association objects at first 764 | // Not care if exec properly temporarily 765 | destroy<%= @model_name %>Associations(ids...) 766 | <%- end -%> 767 | idsHolder := strings.Repeat(",?", len(ids)-1) 768 | sql := fmt.Sprintf(`DELETE FROM <%= table_name %> WHERE id IN (?%s)`, idsHolder) 769 | idsT := []interface{}{} 770 | for _,id := range ids { 771 | idsT = append(idsT, interface{}(id)) 772 | } 773 | stmt, err := DB.Preparex(DB.Rebind(sql)) 774 | result, err := stmt.Exec(idsT...) 775 | if err != nil { 776 | return 0, err 777 | } 778 | cnt, err := result.RowsAffected() 779 | if err != nil { 780 | return 0, err 781 | } 782 | return cnt, nil 783 | } 784 | 785 | // Destroy<%= @model_name.pluralize %>Where delete records by a where clause restriction. 786 | // e.g. Destroy<%= @model_name.pluralize %>Where("name = ?", "John") 787 | // And this func will not call the association dependent action 788 | func Destroy<%= @model_name.pluralize %>Where(where string, args ...interface{}) (int64, error) { 789 | sql := `DELETE FROM <%= table_name %> WHERE ` 790 | if len(where) > 0 { 791 | sql = sql + where 792 | } else { 793 | return 0, errors.New("No WHERE conditions provided") 794 | } 795 | <%- if @struct_info[:has_assoc_dependent] -%> 796 | ids, x_err := <%= @model_name %>IdsWhere(where, args...) 797 | if x_err != nil { 798 | log.Printf("Delete associated objects error: %v\n", x_err) 799 | } else { 800 | destroy<%= @model_name %>Associations(ids...) 801 | } 802 | <%- end -%> 803 | stmt, err := DB.Preparex(DB.Rebind(sql)) 804 | result, err := stmt.Exec(args...) 805 | if err != nil { 806 | return 0, err 807 | } 808 | cnt, err := result.RowsAffected() 809 | if err != nil { 810 | return 0, err 811 | } 812 | return cnt, nil 813 | } 814 | 815 | <%- if @struct_info[:has_assoc_dependent] -%> 816 | // destroy<%= @model_name %>Associations is a private function used to destroy a <%= @model_name %> record's associated objects. 817 | // The func not return err temporarily. 818 | func destroy<%= @model_name %>Associations(ids ...int64) { 819 | idsHolder := "" 820 | if len(ids) > 1 { 821 | idsHolder = strings.Repeat(",?", len(ids)-1) 822 | } 823 | idsT := []interface{}{} 824 | for _, id := range ids { 825 | idsT = append(idsT, interface{}(id)) 826 | } 827 | var err error 828 | // make sure no declared-and-not-used exception 829 | _, _, _ = idsHolder, idsT, err 830 | <%- [:has_many, :has_one].each do |ass| -%> 831 | <%- ass_cols = @struct_info[:assoc_info][ass] -%> 832 | <%- unless ass_cols.empty? -%> 833 | <%- ass_cols.each_value do |opts| -%> 834 | <%- if opts.key? :dependent -%> 835 | <%- case opts[:dependent] -%> 836 | <%- when :destroy, :delete_all -%> 837 | <%- if opts[:through] -%> 838 | where := fmt.Sprintf("id IN (SELECT id FROM <%= opts[:through].to_s %> WHERE <%= var_umn %>_id IN (?%s))", idsHolder) 839 | _, err = Destroy<%= opts[:class_name].pluralize %>Where(where, idsT...) 840 | if err != nil { 841 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 842 | } 843 | where = fmt.Sprintf("<%= var_umn %>_id IN (?%s)", idsHolder) 844 | _, err = Destroy<%= opts[:through].to_s.pluralize.camelize %>Where(where, idsT...) 845 | if err != nil { 846 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:through].to_s.singularize.camelize %>", err) 847 | } 848 | <%- elsif opts[:as] -%> 849 | where := fmt.Sprintf(`<%= opts[:as] %>_type = "<%= @model_name %>" AND <%= opts[:as] %>_id IN (?%s)`, idsHolder) 850 | _, err = Destroy<%= opts[:class_name].pluralize %>Where(where, idsT...) 851 | if err != nil { 852 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 853 | } 854 | <%- else -%> 855 | <%- if opts.key? :foreign_key -%> 856 | where := fmt.Sprintf("<%= opts[:foreign_key] %> IN (?%s)", idsHolder) 857 | <%- else -%> 858 | where := fmt.Sprintf("<%= var_umn %>_id IN (?%s)", idsHolder) 859 | <%- end -%> 860 | _, err = Destroy<%= opts[:class_name].pluralize %>Where(where, idsT...) 861 | if err != nil { 862 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 863 | } 864 | <%- end -%> 865 | <%- when :nullify -%> 866 | // no sql.NULLType supported, just set the associated field to zero value of int64 867 | <%- if opts[:through] -%> 868 | sql := fmt.Sprintf("UPDATE <%= opts[:through].to_s %> SET <%= opts[:class_name].underscore %>_id = 0 WHERE <%= opts[:class_name].underscore %>_id IN (?%s)", idsHolder) 869 | _, err = Update<%= opts[:through].to_s.camelize %>BySql(sql, idsT...) 870 | if err != nil { 871 | log.Printf("Delete associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 872 | } 873 | <%- else -%> 874 | <%- if opts.key? :foreign_key -%> 875 | sql := fmt.Sprintf("UPDATE <%= opts[:class_name].underscore.pluralize %> SET <%= opts[:foreign_key] %> = 0 WHERE <%= opts[:foreign_key] %> IN (?%s)", idsHolder) 876 | <%- else -%> 877 | sql := fmt.Sprintf("UPDATE <%= opts[:class_name].underscore.pluralize %> SET <%= var_umn %>_id = 0 WHERE <%= var_umn %>_id IN (?%s)", idsHolder) 878 | <%- end -%> 879 | _, err = Update<%= opts[:class_name].pluralize %>BySql(sql, idsT...) 880 | if err != nil { 881 | log.Printf("Delete associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 882 | } 883 | <%- end -%> 884 | <%- end -%> 885 | <%- end -%> 886 | <%- end -%> 887 | <%- end -%> 888 | <%- end -%> 889 | } 890 | <%- end -%> 891 | 892 | // Save method is used for a <%= @model_name %> object to update an existed record mainly. 893 | // If no id provided a new record will be created. FIXME: A UPSERT action will be implemented further. 894 | func (_<%= var_umn %> *<%= @model_name %>) Save() error { 895 | ok, err := govalidator.ValidateStruct(_<%= var_umn %>) 896 | if !ok { 897 | errMsg := "Validate <%= @model_name %> struct error: Unknown error" 898 | if err != nil { 899 | errMsg = "Validate <%= @model_name %> struct error: " + err.Error() 900 | } 901 | log.Println(errMsg) 902 | return errors.New(errMsg) 903 | } 904 | if _<%= var_umn %>.Id == 0 { 905 | _, err = _<%= var_umn %>.Create() 906 | return err 907 | } 908 | <%- if @struct_info[:timestamp_cols].include? "updated_at" -%> 909 | _<%= var_umn %>.UpdatedAt = time.Now() 910 | <%- end -%> 911 | sqlFmt := `UPDATE <%= table_name %> SET %s WHERE id = %v` 912 | <%- save_col_names = col_names - ["created_at"] -%> 913 | sqlStr := fmt.Sprintf(sqlFmt, "<%= save_col_names.zip(save_col_names).map{|c| c.join(" = :")}.join(", ") %>", _<%= var_umn %>.Id) 914 | _, err = DB.NamedExec(sqlStr, _<%= var_umn %>) 915 | return err 916 | } 917 | 918 | // Update<%= @model_name %> is used to update a record with a id and map[string]interface{} typed key-value parameters. 919 | func Update<%= @model_name %>(id int64, am map[string]interface{}) error { 920 | if len(am) == 0 { 921 | return errors.New("Zero key in the attributes map!") 922 | } 923 | <%- if @struct_info[:timestamp_cols].include? "updated_at" -%> 924 | am["updated_at"] = time.Now() 925 | <%- end -%> 926 | keys := allKeys(am) 927 | sqlFmt := `UPDATE <%= table_name %> SET %s WHERE id = %v` 928 | setKeysArr := []string{} 929 | for _,v := range keys { 930 | s := fmt.Sprintf(" %s = :%s", v, v) 931 | setKeysArr = append(setKeysArr, s) 932 | } 933 | sqlStr := fmt.Sprintf(sqlFmt, strings.Join(setKeysArr, ", "), id) 934 | _, err := DB.NamedExec(sqlStr, am) 935 | if err != nil { 936 | log.Println(err) 937 | return err 938 | } 939 | return nil 940 | } 941 | 942 | // Update is a method used to update a <%= @model_name %> record with the map[string]interface{} typed key-value parameters. 943 | func (_<%= var_umn %> *<%= @model_name %>) Update(am map[string]interface{}) error { 944 | if _<%= var_umn %>.Id == 0 { 945 | return errors.New("Invalid Id field: it can't be a zero value") 946 | } 947 | err := Update<%= @model_name %>(_<%= var_umn %>.Id, am) 948 | return err 949 | } 950 | 951 | // UpdateAttributes method is supposed to be used to update <%= @model_name %> records as corresponding update_attributes in Ruby on Rails. 952 | func (_<%= var_umn %> *<%= @model_name %>) UpdateAttributes(am map[string]interface{}) error { 953 | if _<%= var_umn %>.Id == 0 { 954 | return errors.New("Invalid Id field: it can't be a zero value") 955 | } 956 | err := Update<%= @model_name %>(_<%= var_umn %>.Id, am) 957 | return err 958 | } 959 | 960 | // UpdateColumns method is supposed to be used to update <%= @model_name %> records as corresponding update_columns in Ruby on Rails. 961 | func (_<%= var_umn %> *<%= @model_name %>) UpdateColumns(am map[string]interface{}) error { 962 | if _<%= var_umn %>.Id == 0 { 963 | return errors.New("Invalid Id field: it can't be a zero value") 964 | } 965 | err := Update<%= @model_name %>(_<%= var_umn %>.Id, am) 966 | return err 967 | } 968 | 969 | // Update<%= @model_name.pluralize %>BySql is used to update <%= @model_name %> records by a SQL clause 970 | // using the '?' binding syntax. 971 | func Update<%= @model_name.pluralize %>BySql(sql string, args ...interface{}) (int64, error) { 972 | if sql == "" { 973 | return 0, errors.New("A blank SQL clause") 974 | } 975 | stmt, err := DB.Preparex(DB.Rebind(sql)) 976 | result, err := stmt.Exec(args...) 977 | if err != nil { 978 | return 0, err 979 | } 980 | cnt, err := result.RowsAffected() 981 | if err != nil { 982 | return 0, err 983 | } 984 | return cnt, nil 985 | } 986 | -------------------------------------------------------------------------------- /lib/generators/templates/gor_model_postgres.go.erb: -------------------------------------------------------------------------------- 1 | <%#- var_pmn stands for pluralized model name, its format is same as the table name -#%> 2 | <%- var_pmn = table_name = @model_name.tableize -%> 3 | <%#- var_umn stands for underscored model name -#%> 4 | <%- var_umn = @model_name.underscore -%> 5 | <%- fields = @struct_info[:select_fields] -%> 6 | <%- col_names = @struct_info[:col_names] -%> 7 | <%- has_assoc = !@struct_info[:assoc_info][:has_many].empty? || !@struct_info[:assoc_info][:has_one].empty? -%> 8 | // Package models includes the functions on the model <%= @model_name %>. 9 | package models 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | "log" 15 | "math" 16 | "strings" 17 | <%- if @struct_info[:has_datetime_type] -%> 18 | "time" 19 | <%- end -%> 20 | 21 | "github.com/asaskevich/govalidator" 22 | ) 23 | 24 | // set flags to output more detailed log 25 | func init() { 26 | log.SetFlags(log.LstdFlags | log.Lshortfile) 27 | } 28 | 29 | type <%= @model_name %> struct { 30 | <%= @struct_info[:struct_body] -%> 31 | } 32 | 33 | // DataStruct for the pagination 34 | type <%= @model_name %>Page struct { 35 | WhereString string 36 | WhereParams []interface{} 37 | Order map[string]string 38 | FirstId int64 39 | LastId int64 40 | PageNum int 41 | PerPage int 42 | TotalPages int 43 | TotalItems int64 44 | orderStr string 45 | } 46 | 47 | // Current get the current page of <%= @model_name %>Page object for pagination. 48 | func (_p *<%= @model_name %>Page) Current() ([]<%= @model_name %>, error) { 49 | if _, exist := _p.Order["id"]; !exist { 50 | return nil, errors.New("No id order specified in Order map") 51 | } 52 | err := _p.buildPageCount() 53 | if err != nil { 54 | return nil, fmt.Errorf("Calculate page count error: %v", err) 55 | } 56 | if _p.orderStr == "" { 57 | _p.buildOrder() 58 | } 59 | idStr, idParams := _p.buildIdRestrict("current") 60 | whereStr := fmt.Sprintf("%s %s %s FETCH NEXT %v ROWS ONLY", _p.WhereString, idStr, _p.orderStr, _p.PerPage) 61 | whereParams := []interface{}{} 62 | whereParams = append(append(whereParams, _p.WhereParams...), idParams...) 63 | <%= var_pmn %>, err := Find<%= @model_name.pluralize %>Where(whereStr, whereParams...) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if len(<%= var_pmn %>) != 0 { 68 | _p.FirstId, _p.LastId = <%= var_pmn %>[0].Id, <%= var_pmn %>[len(<%= var_pmn %>)-1].Id 69 | } 70 | return <%= var_pmn %>, nil 71 | } 72 | 73 | // Previous get the previous page of <%= @model_name %>Page object for pagination. 74 | func (_p *<%= @model_name %>Page) Previous() ([]<%= @model_name %>, error) { 75 | if _p.PageNum == 0 { 76 | return nil, errors.New("This's the first page, no previous page yet") 77 | } 78 | if _, exist := _p.Order["id"]; !exist { 79 | return nil, errors.New("No id order specified in Order map") 80 | } 81 | err := _p.buildPageCount() 82 | if err != nil { 83 | return nil, fmt.Errorf("Calculate page count error: %v", err) 84 | } 85 | if _p.orderStr == "" { 86 | _p.buildOrder() 87 | } 88 | idStr, idParams := _p.buildIdRestrict("previous") 89 | whereStr := fmt.Sprintf("%s %s %s FETCH NEXT %v ROWS ONLY", _p.WhereString, idStr, _p.orderStr, _p.PerPage) 90 | whereParams := []interface{}{} 91 | whereParams = append(append(whereParams, _p.WhereParams...), idParams...) 92 | <%= var_pmn %>, err := Find<%= @model_name.pluralize %>Where(whereStr, whereParams...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | if len(<%= var_pmn %>) != 0 { 97 | _p.FirstId, _p.LastId = <%= var_pmn %>[0].Id, <%= var_pmn %>[len(<%= var_pmn %>)-1].Id 98 | } 99 | _p.PageNum -= 1 100 | return <%= var_pmn %>, nil 101 | } 102 | 103 | // Next get the next page of <%= @model_name %>Page object for pagination. 104 | func (_p *<%= @model_name %>Page) Next() ([]<%= @model_name %>, error) { 105 | if _p.PageNum == _p.TotalPages-1 { 106 | return nil, errors.New("This's the last page, no next page yet") 107 | } 108 | if _, exist := _p.Order["id"]; !exist { 109 | return nil, errors.New("No id order specified in Order map") 110 | } 111 | err := _p.buildPageCount() 112 | if err != nil { 113 | return nil, fmt.Errorf("Calculate page count error: %v", err) 114 | } 115 | if _p.orderStr == "" { 116 | _p.buildOrder() 117 | } 118 | idStr, idParams := _p.buildIdRestrict("next") 119 | whereStr := fmt.Sprintf("%s %s %s FETCH NEXT %v ROWS ONLY", _p.WhereString, idStr, _p.orderStr, _p.PerPage) 120 | whereParams := []interface{}{} 121 | whereParams = append(append(whereParams, _p.WhereParams...), idParams...) 122 | <%= var_pmn %>, err := Find<%= @model_name.pluralize %>Where(whereStr, whereParams...) 123 | if err != nil { 124 | return nil, err 125 | } 126 | if len(<%= var_pmn %>) != 0 { 127 | _p.FirstId, _p.LastId = <%= var_pmn %>[0].Id, <%= var_pmn %>[len(<%= var_pmn %>)-1].Id 128 | } 129 | _p.PageNum += 1 130 | return <%= var_pmn %>, nil 131 | } 132 | 133 | // GetPage is a helper function for the <%= @model_name %>Page object to return a corresponding page due to 134 | // the parameter passed in, i.e. one of "previous, current or next". 135 | func (_p *<%= @model_name %>Page) GetPage(direction string) (ps []<%= @model_name %>, err error) { 136 | switch direction { 137 | case "previous": 138 | ps, _ = _p.Previous() 139 | case "next": 140 | ps, _ = _p.Next() 141 | case "current": 142 | ps, _ = _p.Current() 143 | default: 144 | return nil, errors.New("Error: wrong dircetion! None of previous, current or next!") 145 | } 146 | return 147 | } 148 | 149 | // buildOrder is for <%= @model_name %>Page object to build a SQL ORDER BY clause. 150 | func (_p *<%= @model_name %>Page) buildOrder() { 151 | tempList := []string{} 152 | for k, v := range _p.Order { 153 | tempList = append(tempList, fmt.Sprintf("%v %v", k, v)) 154 | } 155 | _p.orderStr = " ORDER BY " + strings.Join(tempList, ", ") 156 | } 157 | 158 | // buildIdRestrict is for <%= @model_name %>Page object to build a SQL clause for ID restriction, 159 | // implementing a simple keyset style pagination. 160 | func (_p *<%= @model_name %>Page) buildIdRestrict(direction string) (idStr string, idParams []interface{}) { 161 | bindVarNumber := len(_p.WhereParams) + 1 162 | switch direction { 163 | case "previous": 164 | if strings.ToLower(_p.Order["id"]) == "desc" { 165 | idStr += fmt.Sprintf("id > $%v ", bindVarNumber) 166 | idParams = append(idParams, _p.FirstId) 167 | } else { 168 | idStr += fmt.Sprintf("id < $%v ", bindVarNumber) 169 | idParams = append(idParams, _p.FirstId) 170 | } 171 | case "current": 172 | // trick to make Where function work 173 | if _p.PageNum == 0 && _p.FirstId == 0 && _p.LastId == 0 { 174 | idStr += fmt.Sprintf("id > $%v ", bindVarNumber) 175 | idParams = append(idParams, 0) 176 | } else { 177 | if strings.ToLower(_p.Order["id"]) == "desc" { 178 | idStr += fmt.Sprintf("id <= $%v AND id >= $%v ", bindVarNumber, bindVarNumber + 1) 179 | idParams = append(idParams, _p.FirstId, _p.LastId) 180 | } else { 181 | idStr += fmt.Sprintf("id >= $%v AND id <= $%v ", bindVarNumber, bindVarNumber + 1) 182 | idParams = append(idParams, _p.FirstId, _p.LastId) 183 | } 184 | } 185 | case "next": 186 | if strings.ToLower(_p.Order["id"]) == "desc" { 187 | idStr += fmt.Sprintf("id < $%v ", bindVarNumber) 188 | idParams = append(idParams, _p.LastId) 189 | } else { 190 | idStr += fmt.Sprintf("id > $%v ", bindVarNumber) 191 | idParams = append(idParams, _p.LastId) 192 | } 193 | } 194 | if _p.WhereString != "" { 195 | idStr = " AND " + idStr 196 | } 197 | return 198 | } 199 | 200 | // buildPageCount calculate the TotalItems/TotalPages for the <%= @model_name %>Page object. 201 | func (_p *<%= @model_name %>Page) buildPageCount() error { 202 | count, err := <%= @model_name %>CountWhere(_p.WhereString, _p.WhereParams...) 203 | if err != nil { 204 | return err 205 | } 206 | _p.TotalItems = count 207 | if _p.PerPage == 0 { 208 | _p.PerPage = 10 209 | } 210 | _p.TotalPages = int(math.Ceil(float64(_p.TotalItems) / float64(_p.PerPage))) 211 | return nil 212 | } 213 | 214 | 215 | // Find<%= @model_name %> find a single <%= var_umn %> by an ID. 216 | func Find<%= @model_name %>(id int64) (*<%= @model_name %>, error) { 217 | if id == 0 { 218 | return nil, errors.New("Invalid ID: it can't be zero") 219 | } 220 | _<%= var_umn %> := <%= @model_name %>{} 221 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(`SELECT <%= fields %> FROM <%= table_name %> WHERE <%= table_name %>.id = $1 LIMIT 1`), id) 222 | if err != nil { 223 | log.Printf("Error: %v\n", err) 224 | return nil, err 225 | } 226 | return &_<%= var_umn %>, nil 227 | } 228 | 229 | // First<%= @model_name %> find the first one <%= var_umn %> by ID ASC order. 230 | func First<%= @model_name %>() (*<%= @model_name %>, error) { 231 | _<%= var_umn %> := <%= @model_name %>{} 232 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(`SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id ASC LIMIT 1`)) 233 | if err != nil { 234 | log.Printf("Error: %v\n", err) 235 | return nil, err 236 | } 237 | return &_<%= var_umn %>, nil 238 | } 239 | 240 | // First<%= @model_name.pluralize %> find the first N <%= var_umn.pluralize %> by ID ASC order. 241 | func First<%= @model_name.pluralize %>(n uint32) ([]<%= @model_name %>, error) { 242 | _<%= var_pmn %> := []<%= @model_name %>{} 243 | sql := fmt.Sprintf("SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id ASC LIMIT %v", n) 244 | err := DB.Select(&_<%= var_pmn %>, DB.Rebind(sql)) 245 | if err != nil { 246 | log.Printf("Error: %v\n", err) 247 | return nil, err 248 | } 249 | return _<%= var_pmn %>, nil 250 | } 251 | 252 | // Last<%= @model_name %> find the last one <%= var_umn %> by ID DESC order. 253 | func Last<%= @model_name %>() (*<%= @model_name %>, error) { 254 | _<%= var_umn %> := <%= @model_name %>{} 255 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(`SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id DESC LIMIT 1`)) 256 | if err != nil { 257 | log.Printf("Error: %v\n", err) 258 | return nil, err 259 | } 260 | return &_<%= var_umn %>, nil 261 | } 262 | 263 | // Last<%= @model_name.pluralize %> find the last N <%= var_umn.pluralize %> by ID DESC order. 264 | func Last<%= @model_name.pluralize %>(n uint32) ([]<%= @model_name %>, error) { 265 | _<%= var_pmn %> := []<%= @model_name %>{} 266 | sql := fmt.Sprintf("SELECT <%= fields %> FROM <%= table_name %> ORDER BY <%= table_name %>.id DESC LIMIT %v", n) 267 | err := DB.Select(&_<%= var_pmn %>, DB.Rebind(sql)) 268 | if err != nil { 269 | log.Printf("Error: %v\n", err) 270 | return nil, err 271 | } 272 | return _<%= var_pmn %>, nil 273 | } 274 | 275 | // Find<%= @model_name.pluralize %> find one or more <%= var_umn.pluralize %> by the given ID(s). 276 | func Find<%= @model_name.pluralize %>(ids ...int64) ([]<%= @model_name %>, error) { 277 | if len(ids) == 0 { 278 | msg := "At least one or more ids needed" 279 | log.Println(msg) 280 | return nil, errors.New(msg) 281 | } 282 | _<%= var_pmn %> := []<%= @model_name %>{} 283 | idsHolder := buildIdsHolder(len(ids)) 284 | sql := DB.Rebind(fmt.Sprintf(`SELECT <%= fields %> FROM <%= table_name %> WHERE <%= table_name %>.id IN (%s)`, idsHolder)) 285 | idsT := []interface{}{} 286 | for _,id := range ids { 287 | idsT = append(idsT, interface{}(id)) 288 | } 289 | err := DB.Select(&_<%= var_pmn %>, sql, idsT...) 290 | if err != nil { 291 | log.Printf("Error: %v\n", err) 292 | return nil, err 293 | } 294 | return _<%= var_pmn %>, nil 295 | } 296 | 297 | // Find<%= @model_name %>By find a single <%= var_umn %> by a field name and a value. 298 | func Find<%= @model_name %>By(field string, val interface{}) (*<%= @model_name %>, error) { 299 | _<%= var_umn %> := <%= @model_name %>{} 300 | sqlFmt := `SELECT <%= fields %> FROM <%= table_name %> WHERE %s = ? LIMIT 1` 301 | sqlStr := fmt.Sprintf(sqlFmt, field) 302 | err := DB.Get(&_<%= var_umn %>, DB.Rebind(sqlStr), val) 303 | if err != nil { 304 | log.Printf("Error: %v\n", err) 305 | return nil, err 306 | } 307 | return &_<%= var_umn %>, nil 308 | } 309 | 310 | // Find<%= @model_name.pluralize %>By find all <%= var_pmn %> by a field name and a value. 311 | func Find<%= @model_name.pluralize %>By(field string, val interface{}) (_<%= var_pmn %> []<%= @model_name %>, err error) { 312 | sqlFmt := `SELECT <%= fields %> FROM <%= table_name %> WHERE %s = ?` 313 | sqlStr := fmt.Sprintf(sqlFmt, field) 314 | err = DB.Select(&_<%= var_pmn %>, DB.Rebind(sqlStr), val) 315 | if err != nil { 316 | log.Printf("Error: %v\n", err) 317 | return nil, err 318 | } 319 | return _<%= var_pmn %>, nil 320 | } 321 | 322 | // All<%= @model_name.pluralize %> get all the <%= @model_name %> records. 323 | func All<%= @model_name.pluralize %>() (<%= var_pmn %> []<%= @model_name %>, err error) { 324 | err = DB.Select(&<%= var_pmn %>, "SELECT <%= fields %> FROM <%= table_name %>") 325 | if err != nil { 326 | log.Println(err) 327 | return nil, err 328 | } 329 | return <%= var_pmn %>, nil 330 | } 331 | 332 | // <%= @model_name %>Count get the count of all the <%= @model_name %> records. 333 | func <%= @model_name %>Count() (c int64, err error) { 334 | err = DB.Get(&c, "SELECT count(*) FROM <%= table_name %>") 335 | if err != nil { 336 | log.Println(err) 337 | return 0, err 338 | } 339 | return c, nil 340 | } 341 | 342 | // <%= @model_name %>CountWhere get the count of all the <%= @model_name %> records with a where clause. 343 | func <%= @model_name %>CountWhere(where string, args ...interface{}) (c int64, err error) { 344 | sql := "SELECT count(*) FROM <%= table_name %>" 345 | if len(where) > 0 { 346 | sql = sql + " WHERE " + where 347 | } 348 | stmt, err := DB.Preparex(DB.Rebind(sql)) 349 | if err != nil { 350 | log.Println(err) 351 | return 0, err 352 | } 353 | err = stmt.Get(&c, args...) 354 | if err != nil { 355 | log.Println(err) 356 | return 0, err 357 | } 358 | return c, nil 359 | } 360 | 361 | // <%= @model_name %>IncludesWhere get the <%= @model_name %> associated models records, currently it's not same as the corresponding "includes" function but "preload" instead in Ruby on Rails. It means that the "sql" should be restricted on <%= @model_name %> model. 362 | func <%= @model_name %>IncludesWhere(assocs []string, sql string, args ...interface{}) (_<%= var_pmn %> []<%= @model_name %>, err error) { 363 | _<%= var_pmn %>, err = Find<%= @model_name.pluralize %>Where(sql, args...) 364 | if err != nil { 365 | log.Println(err) 366 | return nil, err 367 | } 368 | if len(assocs) == 0 { 369 | log.Println("No associated fields ard specified") 370 | return _<%= var_pmn %>, err 371 | } 372 | if len(_<%= var_pmn %>) <= 0 { 373 | return nil, errors.New("No results available") 374 | } 375 | ids := make([]interface{}, len(_<%= var_pmn %>)) 376 | for _, v := range _<%= var_pmn %> { 377 | ids = append(ids, interface{}(v.Id)) 378 | } 379 | <%- if has_assoc -%> 380 | idsHolder := buildIdsHolder(len(ids)) 381 | for _, assoc := range assocs { 382 | switch assoc { 383 | <%- unless @struct_info[:assoc_info][:has_many].empty? -%> 384 | <%- has_many = @struct_info[:assoc_info][:has_many] -%> 385 | <%- has_many.each do |k, v| -%> 386 | case "<%= k.underscore.pluralize %>": 387 | <%- if v[:through] || v[:as] -%> 388 | // FIXME: optimize the query 389 | for i, vvv := range _<%= var_pmn %> { 390 | _<%= k.underscore.pluralize %>, err := <%= @model_name %>Get<%= k.pluralize %>(vvv.Id) 391 | if err != nil { 392 | continue 393 | } 394 | vvv.<%= k %> = _<%= k.underscore.pluralize %> 395 | _<%= var_pmn %>[i] = vvv 396 | } 397 | <%- else -%> 398 | <%- if v[:foreign_key] -%> 399 | where := fmt.Sprintf("<%= v[:foreign_key] %> IN (%s)", idsHolder) 400 | <%- else -%> 401 | where := fmt.Sprintf("<%= var_umn %>_id IN (%s)", idsHolder) 402 | <%- end -%> 403 | _<%= v[:class_name].underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>Where(where, ids...) 404 | if err != nil { 405 | log.Printf("Error when query associated objects: %v\n", assoc) 406 | continue 407 | } 408 | for _, vv := range _<%= v[:class_name].underscore.pluralize %> { 409 | for i, vvv := range _<%= var_pmn %> { 410 | <%- if v[:foreign_key] -%> 411 | if vv.<%= v[:foreign_key].to_s.camelize %> == vvv.Id { 412 | vvv.<%= k %> = append(vvv.<%= k %>, vv) 413 | } 414 | <%- else -%> 415 | if vv.<%= @model_name.camelize %>Id == vvv.Id { 416 | vvv.<%= k %> = append(vvv.<%= k %>, vv) 417 | } 418 | <%- end -%> 419 | _<%= var_pmn %>[i].<%= k %> = vvv.<%= k %> 420 | } 421 | } 422 | <%- end -%> 423 | <%- end -%> 424 | <%- end -%> 425 | <%- unless @struct_info[:assoc_info][:has_one].empty? -%> 426 | <%- has_one = @struct_info[:assoc_info][:has_one] -%> 427 | <%- has_one.each do |k, v| -%> 428 | case "<%= k.underscore %>": 429 | <%- if v[:foreign_key] -%> 430 | where := fmt.Sprintf("<%= v[:foreign_key] %> IN (%s)", idsHolder) 431 | <%- else -%> 432 | where := fmt.Sprintf("<%= var_umn %>_id IN (%s)", idsHolder) 433 | <%- end -%> 434 | _<%= v[:class_name].underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>Where(where, ids...) 435 | if err != nil { 436 | log.Printf("Error when query associated objects: %v\n", assoc) 437 | continue 438 | } 439 | for _, vv := range _<%= v[:class_name].underscore.pluralize %> { 440 | for i, vvv := range _<%= var_pmn %> { 441 | <%- if v[:foreign_key] -%> 442 | if vv.<%= v[:foreign_key].to_s.camelize %> == vvv.Id { 443 | vvv.<%= k %> = vv 444 | } 445 | <%- else -%> 446 | if vv.<%= @model_name.camelize %>Id == vvv.Id { 447 | vvv.<%= k %> = vv 448 | } 449 | <%- end -%> 450 | _<%= var_pmn %>[i].<%= k %> = vvv.<%= k %> 451 | } 452 | } 453 | <%- end -%> 454 | <%- end -%> 455 | } 456 | } 457 | <%- end -%> 458 | return _<%= var_pmn %>, nil 459 | } 460 | 461 | // <%= @model_name %>Ids get all the IDs of <%= @model_name %> records. 462 | func <%= @model_name %>Ids() (ids []int64, err error) { 463 | err = DB.Select(&ids, "SELECT id FROM <%= table_name %>") 464 | if err != nil { 465 | log.Println(err) 466 | return nil, err 467 | } 468 | return ids, nil 469 | } 470 | 471 | // <%= @model_name %>IdsWhere get all the IDs of <%= @model_name %> records by where restriction. 472 | func <%= @model_name %>IdsWhere(where string, args ...interface{}) ([]int64, error) { 473 | ids, err := <%= @model_name %>IntCol("id", where, args...) 474 | return ids, err 475 | } 476 | 477 | // <%= @model_name %>IntCol get some int64 typed column of <%= @model_name %> by where restriction. 478 | func <%= @model_name %>IntCol(col, where string, args ...interface{}) (intColRecs []int64, err error) { 479 | sql := "SELECT " + col + " FROM <%= table_name %>" 480 | if len(where) > 0 { 481 | sql = sql + " WHERE " + where 482 | } 483 | stmt, err := DB.Preparex(DB.Rebind(sql)) 484 | if err != nil { 485 | log.Println(err) 486 | return nil, err 487 | } 488 | err = stmt.Select(&intColRecs, args...) 489 | if err != nil { 490 | log.Println(err) 491 | return nil, err 492 | } 493 | return intColRecs, nil 494 | } 495 | 496 | // <%= @model_name %>StrCol get some string typed column of <%= @model_name %> by where restriction. 497 | func <%= @model_name %>StrCol(col, where string, args ...interface{}) (strColRecs []string, err error) { 498 | sql := "SELECT " + col + " FROM <%= table_name %>" 499 | if len(where) > 0 { 500 | sql = sql + " WHERE " + where 501 | } 502 | stmt, err := DB.Preparex(DB.Rebind(sql)) 503 | if err != nil { 504 | log.Println(err) 505 | return nil, err 506 | } 507 | err = stmt.Select(&strColRecs, args...) 508 | if err != nil { 509 | log.Println(err) 510 | return nil, err 511 | } 512 | return strColRecs, nil 513 | } 514 | 515 | // Find<%= @model_name.pluralize %>Where query use a partial SQL clause that usually following after WHERE 516 | // with placeholders, eg: FindUsersWhere("first_name = ? AND age > ?", "John", 18) 517 | // will return those records in the table "users" whose first_name is "John" and age elder than 18. 518 | func Find<%= @model_name.pluralize %>Where(where string, args ...interface{}) (<%= var_pmn %> []<%= @model_name %>, err error) { 519 | sql := "SELECT <%= fields %> FROM <%= table_name %>" 520 | if len(where) > 0 { 521 | sql = sql + " WHERE " + where 522 | } 523 | stmt, err := DB.Preparex(DB.Rebind(sql)) 524 | if err != nil { 525 | log.Println(err) 526 | return nil, err 527 | } 528 | err = stmt.Select(&<%= var_pmn %>, args...) 529 | if err != nil { 530 | log.Println(err) 531 | return nil, err 532 | } 533 | return <%= var_pmn %>, nil 534 | } 535 | 536 | // Find<%= @model_name %>BySql query use a complete SQL clause 537 | // with placeholders, eg: FindUserBySql("SELECT * FROM users WHERE first_name = ? AND age > ? ORDER BY DESC LIMIT 1", "John", 18) 538 | // will return only One record in the table "users" whose first_name is "John" and age elder than 18. 539 | func Find<%= @model_name %>BySql(sql string, args ...interface{}) (*<%= @model_name %>, error) { 540 | stmt, err := DB.Preparex(DB.Rebind(sql)) 541 | if err != nil { 542 | log.Println(err) 543 | return nil, err 544 | } 545 | _<%= var_umn %> := &<%= @model_name %>{} 546 | err = stmt.Get(_<%= var_umn %>, args...) 547 | if err != nil { 548 | log.Println(err) 549 | return nil, err 550 | } 551 | return _<%= var_umn %>, nil 552 | } 553 | 554 | // Find<%= @model_name.pluralize %>BySql query use a complete SQL clause 555 | // with placeholders, eg: FindUsersBySql("SELECT * FROM users WHERE first_name = ? AND age > ?", "John", 18) 556 | // will return those records in the table "users" whose first_name is "John" and age elder than 18. 557 | func Find<%= @model_name.pluralize %>BySql(sql string, args ...interface{}) (<%= var_pmn %> []<%= @model_name %>, err error) { 558 | stmt, err := DB.Preparex(DB.Rebind(sql)) 559 | if err != nil { 560 | log.Println(err) 561 | return nil, err 562 | } 563 | err = stmt.Select(&<%= var_pmn %>, args...) 564 | if err != nil { 565 | log.Println(err) 566 | return nil, err 567 | } 568 | return <%= var_pmn %>, nil 569 | } 570 | 571 | // Create<%= @model_name %> use a named params to create a single <%= @model_name %> record. 572 | // A named params is key-value map like map[string]interface{}{"first_name": "John", "age": 23} . 573 | func Create<%= @model_name %>(am map[string]interface{}) (int64, error) { 574 | if len(am) == 0 { 575 | return 0, fmt.Errorf("Zero key in the attributes map!") 576 | } 577 | <%- unless @struct_info[:timestamp_cols].empty? -%> 578 | t := time.Now() 579 | for _, v := range []string{<%= @struct_info[:timestamp_cols].map(&:inspect).join(", ") %>} { 580 | if am[v] == nil { 581 | am[v] = t 582 | } 583 | } 584 | <%- end -%> 585 | keys := allKeys(am) 586 | sqlFmt := `INSERT INTO <%= table_name %> (%s) VALUES (%s) RETURNING id` 587 | sql := fmt.Sprintf(sqlFmt, strings.Join(keys, ","), ":"+strings.Join(keys, ",:")) 588 | stmt, err := DB.PrepareNamed(sql) 589 | if err != nil { 590 | log.Println(err) 591 | return 0, err 592 | } 593 | var lastId int64 594 | err = stmt.Get(&lastId, am) 595 | if err != nil { 596 | log.Println(err) 597 | return 0, err 598 | } 599 | return lastId, nil 600 | } 601 | 602 | // Create is a method for <%= @model_name %> to create a record. 603 | func (_<%= var_umn %> *<%= @model_name %>) Create() (int64, error) { 604 | ok, err := govalidator.ValidateStruct(_<%= var_umn %>) 605 | if !ok { 606 | errMsg := "Validate <%= @model_name %> struct error: Unknown error" 607 | if err != nil { 608 | errMsg = "Validate <%= @model_name %> struct error: " + err.Error() 609 | } 610 | log.Println(errMsg) 611 | return 0, errors.New(errMsg) 612 | } 613 | <%- unless @struct_info[:timestamp_cols].empty? -%> 614 | t := time.Now() 615 | <%- @struct_info[:timestamp_cols].each do |c| -%> 616 | _<%= var_umn %>.<%= c.camelize %> = t 617 | <%- end -%> 618 | <%- end -%> 619 | sql := `INSERT INTO <%= table_name %> (<%= col_names.join(",") %>) VALUES (:<%= col_names.join(",:") %>) RETURNING id` 620 | stmt, err := DB.PrepareNamed(sql) 621 | if err != nil { 622 | log.Println(err) 623 | return 0, err 624 | } 625 | var lastId int64 626 | err = stmt.Get(&lastId, _<%= var_umn %>) 627 | if err != nil { 628 | log.Println(err) 629 | return 0, err 630 | } 631 | return lastId, nil 632 | } 633 | 634 | <%- unless @struct_info[:assoc_info][:has_many].empty? -%> 635 | <%- has_many = @struct_info[:assoc_info][:has_many] -%> 636 | <%- has_many.each do |k, v| -%> 637 | // <%= k.pluralize %>Create is used for <%= @model_name %> to create the associated objects <%= k.pluralize %> 638 | func (_<%= var_umn %> *<%= @model_name %>) <%= k.pluralize %>Create(am map[string]interface{}) error { 639 | <%- if v[:through] -%> 640 | // FIXME: use transaction to create these associated objects 641 | <%= v[:class_name].underscore %>Id, err := Create<%= v[:class_name] %>(am) 642 | if err != nil { 643 | return err 644 | } 645 | _, err = Create<%= v[:through].to_s.singularize.camelize %>(map[string]interface{}{"<%= var_umn %>_id": _<%= var_umn %>.Id, "<%= v[:class_name].underscore %>_id": <%= v[:class_name].underscore %>Id}) 646 | <%- elsif v[:as] -%> 647 | am["<%= v[:as] %>_id"] = _<%= var_umn %>.Id 648 | am["<%= v[:as] %>_type"] = "<%= @model_name %>" 649 | _, err := Create<%= v[:class_name] %>(am) 650 | <%- else -%> 651 | <%- if v[:foreign_key] -%> 652 | am["<%= v[:foreign_key] %>"] = _<%= var_umn %>.Id 653 | <%- else -%> 654 | am["<%= var_umn %>_id"] = _<%= var_umn %>.Id 655 | <%- end -%> 656 | _, err := Create<%= v[:class_name] %>(am) 657 | <%- end -%> 658 | return err 659 | } 660 | 661 | // Get<%= k.pluralize %> is used for <%= @model_name %> to get associated objects <%= k.pluralize %> 662 | // Say you have a <%= @model_name %> object named <%= var_umn %>, when you call <%= var_umn %>.Get<%= k.pluralize %>(), 663 | // the object will get the associated <%= k.pluralize %> attributes evaluated in the struct. 664 | func (_<%= var_umn %> *<%= @model_name %>) Get<%= k.pluralize %>() error { 665 | _<%= k.underscore.pluralize %>, err := <%= @model_name %>Get<%= k.pluralize %>(_<%= var_umn %>.Id) 666 | if err == nil { 667 | _<%= var_umn %>.<%= k %> = _<%= k.underscore.pluralize %> 668 | } 669 | return err 670 | } 671 | 672 | // <%= @model_name %>Get<%= k.pluralize %> a helper fuction used to get associated objects for <%= @model_name %>IncludesWhere(). 673 | func <%= @model_name %>Get<%= k.pluralize %>(id int64) ([]<%= v[:class_name] %>, error) { 674 | <%- if v[:through] -%> 675 | // FIXME: use transaction to create these associated objects 676 | <%- target_table = v[:class_name].underscore.pluralize -%> 677 | <%- through_table = v[:through].to_s.pluralize -%> 678 | sql := `SELECT <%= @all_structs_info[v[:class_name]][:select_fields] %> 679 | FROM <%= target_table %> 680 | INNER JOIN <%= through_table %> 681 | ON <%= target_table %>.id = <%= through_table %>.<%= v[:class_name].underscore %>_id 682 | WHERE <%= through_table %>.<%= var_umn %>_id = ?` 683 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>BySql(sql, id) 684 | <%- elsif v[:as] -%> 685 | where := `<%= v[:as] %>_type = "<%= @model_name %>" AND <%= v[:as] %>_id = ?` 686 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>Where(where, id) 687 | <%- else -%> 688 | <%- if v[:foreign_key] -%> 689 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>By("<%= v[:foreign_key] %>", id) 690 | <%- else -%> 691 | _<%= k.underscore.pluralize %>, err := Find<%= v[:class_name].pluralize %>By("<%= var_umn %>_id", id) 692 | <%- end -%> 693 | <%- end -%> 694 | return _<%= k.underscore.pluralize %>, err 695 | } 696 | 697 | <%- end -%> 698 | <%- end -%> 699 | 700 | <%- unless @struct_info[:assoc_info][:has_one].empty? -%> 701 | <%- has_one = @struct_info[:assoc_info][:has_one] -%> 702 | <%- has_one.each do |k, v| -%> 703 | // Create<%= k %> is a method for the <%= @model_name %> model object to create an associated <%= k %> record. 704 | func (_<%= var_umn %> *<%= @model_name %>) Create<%= k %>(am map[string]interface{}) error { 705 | <%- if v[:foreign_key] -%> 706 | am["<%= v[:foreign_key] %>"] = _<%= var_umn %>.Id 707 | <%- else -%> 708 | am["<%= var_umn %>_id"] = _<%= var_umn %>.Id 709 | <%- end -%> 710 | _, err := Create<%= v[:class_name] %>(am) 711 | return err 712 | } 713 | <%- end -%> 714 | <%- end -%> 715 | 716 | <%- unless @struct_info[:assoc_info][:belongs_to].empty? -%> 717 | <%- belongs_to = @struct_info[:assoc_info][:belongs_to] -%> 718 | <%- belongs_to.each do |k, v| -%> 719 | <%-# don't create virtual table for polymorphic model -%> 720 | <%- next if v[:polymorphic] -%> 721 | // Create<%= k %> is a method for a <%= @model_name %> object to create an associated <%= k %> record. 722 | func (_<%= var_umn %> *<%= @model_name %>) Create<%= k %>(am map[string]interface{}) error { 723 | <%- if v[:foreign_key] -%> 724 | am["<%= v[:foreign_key] %>"] = _<%= var_umn %>.Id 725 | <%- else -%> 726 | am["<%= var_umn %>_id"] = _<%= var_umn %>.Id 727 | <%- end -%> 728 | _, err := Create<%= v[:class_name] %>(am) 729 | return err 730 | } 731 | <%- end -%> 732 | <%- end -%> 733 | 734 | // Destroy is method used for a <%= @model_name %> object to be destroyed. 735 | func (_<%= var_umn %> *<%= @model_name %>) Destroy() error { 736 | if _<%= var_umn %>.Id == 0 { 737 | return errors.New("Invalid Id field: it can't be a zero value") 738 | } 739 | err := Destroy<%= @model_name %>(_<%= var_umn %>.Id) 740 | return err 741 | } 742 | 743 | // Destroy<%= @model_name %> will destroy a <%= @model_name %> record specified by the id parameter. 744 | func Destroy<%= @model_name %>(id int64) error { 745 | <%- if @struct_info[:has_assoc_dependent] -%> 746 | // Destroy association objects at first 747 | // Not care if exec properly temporarily 748 | destroy<%= @model_name %>Associations(id) 749 | <%- end -%> 750 | stmt, err := DB.Preparex(DB.Rebind(`DELETE FROM <%= table_name %> WHERE id = ?`)) 751 | _, err = stmt.Exec(id) 752 | if err != nil { 753 | return err 754 | } 755 | return nil 756 | } 757 | 758 | // Destroy<%= @model_name.pluralize %> will destroy <%= @model_name %> records those specified by the ids parameters. 759 | func Destroy<%= @model_name.pluralize %>(ids ...int64) (int64, error) { 760 | if len(ids) == 0 { 761 | msg := "At least one or more ids needed" 762 | log.Println(msg) 763 | return 0, errors.New(msg) 764 | } 765 | <%- if @struct_info[:has_assoc_dependent] -%> 766 | // Destroy association objects at first 767 | // Not care if exec properly temporarily 768 | destroy<%= @model_name %>Associations(ids...) 769 | <%- end -%> 770 | idsHolder := buildIdsHolder(len(ids)) 771 | sql := fmt.Sprintf(`DELETE FROM <%= table_name %> WHERE id IN (%s)`, idsHolder) 772 | idsT := []interface{}{} 773 | for _,id := range ids { 774 | idsT = append(idsT, interface{}(id)) 775 | } 776 | stmt, err := DB.Preparex(DB.Rebind(sql)) 777 | result, err := stmt.Exec(idsT...) 778 | if err != nil { 779 | return 0, err 780 | } 781 | cnt, err := result.RowsAffected() 782 | if err != nil { 783 | return 0, err 784 | } 785 | return cnt, nil 786 | } 787 | 788 | // Destroy<%= @model_name.pluralize %>Where delete records by a where clause restriction. 789 | // e.g. Destroy<%= @model_name.pluralize %>Where("name = ?", "John") 790 | // And this func will not call the association dependent action 791 | func Destroy<%= @model_name.pluralize %>Where(where string, args ...interface{}) (int64, error) { 792 | sql := `DELETE FROM <%= table_name %> WHERE ` 793 | if len(where) > 0 { 794 | sql = sql + where 795 | } else { 796 | return 0, errors.New("No WHERE conditions provided") 797 | } 798 | <%- if @struct_info[:has_assoc_dependent] -%> 799 | ids, x_err := <%= @model_name %>IdsWhere(where, args...) 800 | if x_err != nil { 801 | log.Printf("Delete associated objects error: %v\n", x_err) 802 | } else { 803 | destroy<%= @model_name %>Associations(ids...) 804 | } 805 | <%- end -%> 806 | stmt, err := DB.Preparex(DB.Rebind(sql)) 807 | result, err := stmt.Exec(args...) 808 | if err != nil { 809 | return 0, err 810 | } 811 | cnt, err := result.RowsAffected() 812 | if err != nil { 813 | return 0, err 814 | } 815 | return cnt, nil 816 | } 817 | 818 | <%- if @struct_info[:has_assoc_dependent] -%> 819 | // destroy<%= @model_name %>Associations is a private function used to destroy a <%= @model_name %> record's associated objects. 820 | // The func not return err temporarily. 821 | func destroy<%= @model_name %>Associations(ids ...int64) { 822 | idsHolder := "" 823 | if len(ids) > 1 { 824 | idsHolder := buildIdsHolder(len(ids)) 825 | } 826 | idsT := []interface{}{} 827 | for _, id := range ids { 828 | idsT = append(idsT, interface{}(id)) 829 | } 830 | var err error 831 | // make sure no declared-and-not-used exception 832 | _, _, _ = idsHolder, idsT, err 833 | <%- [:has_many, :has_one].each do |ass| -%> 834 | <%- ass_cols = @struct_info[:assoc_info][ass] -%> 835 | <%- unless ass_cols.empty? -%> 836 | <%- ass_cols.each_value do |opts| -%> 837 | <%- if opts.key? :dependent -%> 838 | <%- case opts[:dependent] -%> 839 | <%- when :destroy, :delete_all -%> 840 | <%- if opts[:through] -%> 841 | where := fmt.Sprintf("id IN (SELECT id FROM <%= opts[:through].to_s %> WHERE <%= var_umn %>_id IN (%s))", idsHolder) 842 | _, err = Destroy<%= opts[:class_name].pluralize %>Where(where, idsT...) 843 | if err != nil { 844 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 845 | } 846 | where = fmt.Sprintf("<%= var_umn %>_id IN (%s)", idsHolder) 847 | _, err = Destroy<%= opts[:through].to_s.pluralize.camelize %>Where(where, idsT...) 848 | if err != nil { 849 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:through].to_s.singularize.camelize %>", err) 850 | } 851 | <%- elsif opts[:as] -%> 852 | where := fmt.Sprintf(`<%= opts[:as] %>_type = "<%= @model_name %>" AND <%= opts[:as] %>_id IN (%s)`, idsHolder) 853 | _, err = Destroy<%= opts[:class_name].pluralize %>Where(where, idsT...) 854 | if err != nil { 855 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 856 | } 857 | <%- else -%> 858 | <%- if opts.key? :foreign_key -%> 859 | where := fmt.Sprintf("<%= opts[:foreign_key] %> IN (%s)", idsHolder) 860 | <%- else -%> 861 | where := fmt.Sprintf("<%= var_umn %>_id IN (%s)", idsHolder) 862 | <%- end -%> 863 | _, err = Destroy<%= opts[:class_name].pluralize %>Where(where, idsT...) 864 | if err != nil { 865 | log.Printf("Destroy associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 866 | } 867 | <%- end -%> 868 | <%- when :nullify -%> 869 | // no sql.NULLType supported, just set the associated field to zero value of int64 870 | <%- if opts[:through] -%> 871 | sql := fmt.Sprintf("UPDATE <%= opts[:through].to_s %> SET <%= opts[:class_name].underscore %>_id = 0 WHERE <%= opts[:class_name].underscore %>_id IN (%s)", idsHolder) 872 | _, err = Update<%= opts[:through].to_s.camelize %>BySql(sql, idsT...) 873 | if err != nil { 874 | log.Printf("Delete associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 875 | } 876 | <%- else -%> 877 | <%- if opts.key? :foreign_key -%> 878 | sql := fmt.Sprintf("UPDATE <%= opts[:class_name].underscore.pluralize %> SET <%= opts[:foreign_key] %> = 0 WHERE <%= opts[:foreign_key] %> IN (%s)", idsHolder) 879 | <%- else -%> 880 | sql := fmt.Sprintf("UPDATE <%= opts[:class_name].underscore.pluralize %> SET <%= var_umn %>_id = 0 WHERE <%= var_umn %>_id IN (%s)", idsHolder) 881 | <%- end -%> 882 | _, err = Update<%= opts[:class_name].pluralize %>BySql(sql, idsT...) 883 | if err != nil { 884 | log.Printf("Delete associated object %s error: %v\n", "<%= opts[:class_name].pluralize %>", err) 885 | } 886 | <%- end -%> 887 | <%- end -%> 888 | <%- end -%> 889 | <%- end -%> 890 | <%- end -%> 891 | <%- end -%> 892 | } 893 | <%- end -%> 894 | 895 | // Save method is used for a <%= @model_name %> object to update an existed record mainly. 896 | // If no id provided a new record will be created. FIXME: A UPSERT action will be implemented further. 897 | func (_<%= var_umn %> *<%= @model_name %>) Save() error { 898 | ok, err := govalidator.ValidateStruct(_<%= var_umn %>) 899 | if !ok { 900 | errMsg := "Validate <%= @model_name %> struct error: Unknown error" 901 | if err != nil { 902 | errMsg = "Validate <%= @model_name %> struct error: " + err.Error() 903 | } 904 | log.Println(errMsg) 905 | return errors.New(errMsg) 906 | } 907 | if _<%= var_umn %>.Id == 0 { 908 | _, err = _<%= var_umn %>.Create() 909 | return err 910 | } 911 | <%- if @struct_info[:timestamp_cols].include? "updated_at" -%> 912 | _<%= var_umn %>.UpdatedAt = time.Now() 913 | <%- end -%> 914 | sqlFmt := `UPDATE <%= table_name %> SET %s WHERE id = %v` 915 | <%- save_col_names = col_names - ["created_at"] -%> 916 | sqlStr := fmt.Sprintf(sqlFmt, "<%= save_col_names.zip(save_col_names).map{|c| c.join(" = :")}.join(", ") %>", _<%= var_umn %>.Id) 917 | _, err = DB.NamedExec(sqlStr, _<%= var_umn %>) 918 | return err 919 | } 920 | 921 | // Update<%= @model_name %> is used to update a record with a id and map[string]interface{} typed key-value parameters. 922 | func Update<%= @model_name %>(id int64, am map[string]interface{}) error { 923 | if len(am) == 0 { 924 | return errors.New("Zero key in the attributes map!") 925 | } 926 | <%- if @struct_info[:timestamp_cols].include? "updated_at" -%> 927 | am["updated_at"] = time.Now() 928 | <%- end -%> 929 | keys := allKeys(am) 930 | sqlFmt := `UPDATE <%= table_name %> SET %s WHERE id = %v` 931 | setKeysArr := []string{} 932 | for _,v := range keys { 933 | s := fmt.Sprintf(" %s = :%s", v, v) 934 | setKeysArr = append(setKeysArr, s) 935 | } 936 | sqlStr := fmt.Sprintf(sqlFmt, strings.Join(setKeysArr, ", "), id) 937 | _, err := DB.NamedExec(sqlStr, am) 938 | if err != nil { 939 | log.Println(err) 940 | return err 941 | } 942 | return nil 943 | } 944 | 945 | // Update is a method used to update a <%= @model_name %> record with the map[string]interface{} typed key-value parameters. 946 | func (_<%= var_umn %> *<%= @model_name %>) Update(am map[string]interface{}) error { 947 | if _<%= var_umn %>.Id == 0 { 948 | return errors.New("Invalid Id field: it can't be a zero value") 949 | } 950 | err := Update<%= @model_name %>(_<%= var_umn %>.Id, am) 951 | return err 952 | } 953 | 954 | // UpdateAttributes method is supposed to be used to update <%= @model_name %> records as corresponding update_attributes in Ruby on Rails. 955 | func (_<%= var_umn %> *<%= @model_name %>) UpdateAttributes(am map[string]interface{}) error { 956 | if _<%= var_umn %>.Id == 0 { 957 | return errors.New("Invalid Id field: it can't be a zero value") 958 | } 959 | err := Update<%= @model_name %>(_<%= var_umn %>.Id, am) 960 | return err 961 | } 962 | 963 | // UpdateColumns method is supposed to be used to update <%= @model_name %> records as corresponding update_columns in Ruby on Rails. 964 | func (_<%= var_umn %> *<%= @model_name %>) UpdateColumns(am map[string]interface{}) error { 965 | if _<%= var_umn %>.Id == 0 { 966 | return errors.New("Invalid Id field: it can't be a zero value") 967 | } 968 | err := Update<%= @model_name %>(_<%= var_umn %>.Id, am) 969 | return err 970 | } 971 | 972 | // Update<%= @model_name.pluralize %>BySql is used to update <%= @model_name %> records by a SQL clause 973 | // using the '$1...$n' binding syntax. 974 | func Update<%= @model_name.pluralize %>BySql(sql string, args ...interface{}) (int64, error) { 975 | if sql == "" { 976 | return 0, errors.New("A blank SQL clause") 977 | } 978 | stmt, err := DB.Preparex(DB.Rebind(sql)) 979 | result, err := stmt.Exec(args...) 980 | if err != nil { 981 | return 0, err 982 | } 983 | cnt, err := result.RowsAffected() 984 | if err != nil { 985 | return 0, err 986 | } 987 | return cnt, nil 988 | } 989 | -------------------------------------------------------------------------------- /lib/generators/templates/home_controller.go.erb: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | // you can import models 8 | //m "../models" 9 | ) 10 | 11 | func HomeHandler(c *gin.Context) { 12 | // you can use model functions to do CRUD 13 | // 14 | // user, _ := m.FindUser(1) 15 | // u, err := json.Marshal(user) 16 | // if err != nil { 17 | // log.Printf("JSON encoding error: %v\n", err) 18 | // u = []byte("Get data error!") 19 | // } 20 | 21 | type Envs struct { 22 | GoOnRailsVer string 23 | GolangVer string 24 | } 25 | 26 | gorVer := "<%= Gem.loaded_specs["go-on-rails"].version.to_s rescue "Failed to get" %>" 27 | golangVer := "<%= `go version`.chomp rescue "Failed to get" %>" 28 | 29 | envs := Envs{GoOnRailsVer: gorVer, GolangVer: golangVer} 30 | c.HTML(http.StatusOK, "index.tmpl", envs) 31 | } 32 | -------------------------------------------------------------------------------- /lib/generators/templates/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | c "./controllers" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | // The app will run on port 4000 by default, you can custom it with the flag -port 12 | servePort := flag.String("port", "4000", "Http Server Port") 13 | flag.Parse() 14 | 15 | // Here we are instantiating the router 16 | r := gin.Default() 17 | // Switch to "release" mode in production 18 | // gin.SetMode(gin.ReleaseMode) 19 | r.LoadHTMLGlob("views/*") 20 | // Create a static assets router 21 | // r.Static("/assets", "./public/assets") 22 | r.StaticFile("/favicon.ico", "./public/favicon.ico") 23 | // Then we bind some route to some handler(controller action) 24 | r.GET("/", c.HomeHandler) 25 | // Let's start the server 26 | r.Run(":" + *servePort) 27 | } 28 | -------------------------------------------------------------------------------- /lib/generators/templates/utils.go.erb: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func buildIdsHolder(n int) (idsHolder string) { 8 | idsHolder = "$1" 9 | for i := 2; i <= n; i++ { 10 | idsHolder += fmt.Sprintf(",$%d", i) 11 | } 12 | return 13 | } 14 | 15 | func allKeys(am map[string]interface{}) []string { 16 | keys := make([]string, len(am)) 17 | i := 0 18 | for k := range am { 19 | keys[i] = k 20 | i++ 21 | } 22 | return keys 23 | } 24 | -------------------------------------------------------------------------------- /lib/go/on/rails.rb: -------------------------------------------------------------------------------- 1 | module Go 2 | module On 3 | module Rails 4 | end 5 | end 6 | end 7 | 8 | require 'go/on/rails/railtie' 9 | -------------------------------------------------------------------------------- /lib/go/on/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | class Go::On::Rails::Railtie < Rails::Railtie 2 | rake_tasks do 3 | load "tasks/gor.rake" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/tasks/gor.rake: -------------------------------------------------------------------------------- 1 | namespace :gor do 2 | desc 'Install dependent Golang packages' 3 | task :deps do 4 | puts 'Beginning to install Go deps...' 5 | system "go get -u \ 6 | github.com/jmoiron/sqlx \ 7 | github.com/gin-gonic/gin \ 8 | github.com/railstack/go-sqlite3 \ 9 | github.com/go-sql-driver/mysql \ 10 | github.com/lib/pq \ 11 | github.com/asaskevich/govalidator" 12 | puts 'Installation completed!' 13 | end 14 | 15 | desc 'Gofmt the generated codes on models' 16 | task :fmt do 17 | go_files = Rails.root.join('go_app', 'models/*.go').to_s 18 | system "gofmt -w #{go_files} > /dev/null 2>&1" 19 | end 20 | 21 | desc 'View the doc of all the functions generated on models' 22 | task :doc do 23 | models_dir = Rails.root.join('go_app', 'src/models').to_s 24 | puts 'Please open "http://localhost:7979/doc/" to view the doc of all functions generated on models.' 25 | puts 'Use Ctrl-C to terminate this server!' 26 | if RUBY_PLATFORM =~ /darwin/ 27 | system 'open http://localhost:7979/doc/' 28 | elsif RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/ 29 | system 'start http://localhost:7979/doc/' 30 | end 31 | system "GOPATH=#{Rails.root.join('go_app').to_s} godoc -goroot #{models_dir} -http=:7979" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | --------------------------------------------------------------------------------