├── LICENSE ├── README.md ├── create_templates.go ├── examples └── animals.scaffold.json ├── process.templates.sh ├── scaffolder.go ├── setenv.sh └── templates ├── controller.go.template ├── controller.test.go.template ├── form.concrete.list.go.template ├── form.concrete.single.item.go.template ├── form.concrete.single.item.test.go.template ├── form.list.go.template ├── form.single.item.go.template ├── gorp.concrete.go.template ├── main.go.template ├── model.concrete.go.template ├── model.concrete.test.go.template ├── model.interface.go.template ├── repository.concrete.gorp.go.template ├── repository.concrete.gorp.test.go.template ├── repository.interface.go.template ├── retrofit.template.go.template ├── script.install.bat.template ├── script.install.sh.template ├── script.test.bat.template ├── script.test.sh.template ├── services.concrete.go.template ├── services.go.template ├── sql.create.db.template ├── test.go.template ├── utilities.go.template ├── view.base.ghtml.template ├── view.error.html.template ├── view.index.ghtml.template ├── view.resource.create.ghtml.template ├── view.resource.edit.ghtml.template ├── view.resource.index.ghtml.template ├── view.resource.show.ghtml.template └── view.stylesheets.scaffold.css.template /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Simon Ritchie 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 | # Scaffolder 2 | Given a description of a database and its tables, the Goblimey Scaffolder creates that database 3 | and generates a Go web server that provides the Create, Read, Update and Delete (CRUD) operations 4 | on it. 5 | The server is designed according to the Model, View, Controller (MVC) architecture and is 6 | implemented using RESTful requests. 7 | The result is presented as a complete prototype Go project with all source code included 8 | along with unit and integration tests to check that the whole thing hangs together. 9 | 10 | The idea of the scaffolder is taken from the Ruby-on-Rails scaffold generator. 11 | 12 | The scaffolder has all sorts of uses, the most obvious being a prototyping tool 13 | for designing database tables. 14 | Producing the right design for the database often takes several attempts. 15 | The scaffolder gives you a quick and easy way to experiment with different versions.. 16 | 17 | Software testers may also find the scaffolder useful. 18 | Testers often have to create carefully-crafted database content 19 | to drive a set of tests. 20 | The scaffolder provides bespoke tools for doing that. 21 | 22 | Producing a complete piece of working source code also makes the scaffolder 23 | a very useful aid to learning Go. 24 | That means that it may be used by people who are new to Go and 25 | possibly new to programming. 26 | This document assumes a fair amount of specialist knowledge. 27 | [These notes](http://www.goblimey.com/scaffolder/) 28 | describe the scaffolder at a gentler pace and 29 | covers basic issues such as installing Go and MySQL. 30 | 31 | In this version 32 | the scaffolder doesn't handle relations between tables. 33 | That is a serious omission 34 | which I plan to fix in a future version. 35 | 36 | 37 | For the Impatient 38 | ============ 39 | 40 | Get the dependencies and install the scaffolder: 41 | 42 | $ go get github.com/go-sql-driver/mysql 43 | $ go get gopkg.in/gorp.v1 44 | $ go get github.com/emicklei/go-restful 45 | $ go get github.com/onsi/gomega 46 | $ go get golang.org/x/tools/cmd/goimports 47 | $ go get github.com/petergtz/pegomock/pegomock 48 | $ go get github.com/goblimey/scaffolder 49 | 50 | By default, "go get" doesn't update any projects that you have already downloaded. 51 | If you downloaded any of those projects a long time ago, 52 | you may wish to update it to the latest version using the -u flag, for example: 53 | 54 | go get -u github.com/petergtz/pegomock/pegomock 55 | 56 | Once you have downloaded the scaffolder, you can find an example table specification in the examples directory. 57 | You can use this to create a simple web server like so: 58 | 59 | * create an empty database 60 | * create a Go project directory and cd to it 61 | * Copy the example specification file to this directory and call it "scaffold.json" 62 | * $ scaffolder 63 | * $ ./install.sh 64 | * $ animals # start your web server 65 | * in your web browser, navigate to 66 | * create some cats and dogs 67 | 68 | 69 | Creating Your Project 70 | ================ 71 | 72 | The How to Write Go Code document 73 | (which you can find [here](https://golang.org/doc/code.html)) 74 | suggests that you structure your project as if you are going to store it in a repository, 75 | even if you don't ever store it there. 76 | 77 | I'm going to assume that you will use the GitHub 78 | to store your project. 79 | If your GitHub account was called alunsmithie, 80 | your home page on the github would be 81 | https://github.com/alunsmithie. 82 | If your project is called 'animals' 83 | then it would be stored in 84 | https://github.com/alunsmithie/animals. 85 | 86 | If you follow this stucture 87 | but you don't want to put the result on the Github, 88 | you can just create a directory to hold your project - 89 | on Linux it's the directory: 90 | 91 | $GOPATH/src/github.com/alunsmithie/animals 92 | 93 | on Windows it's: 94 | 95 | %GOPATH%\src\github.com\alunsmithie\animals 96 | 97 | and you can skip the rest of this section. 98 | 99 | If you are actually going to store 100 | your project on the GitHub, rather than just structuring it so that you could, 101 | it's much easier to create an empty project first 102 | and add files later. 103 | Use the '+' button at the top of your github home page 104 | to create the project. 105 | In this example, 106 | it's called 'animals' 107 | 108 | Now create a clone of this project on your computer. You can do all this in a command window. 109 | (On Windows 7 use the Command Prompt option in the Start menu) 110 | 111 | On Linux: 112 | 113 | $ mkdir -p $GOPATH/src/github.com/alunsmithie 114 | $ cd $GOPATH/src/github.com/alunsmithie 115 | $ git clone https://github.com/alunsmithie/animals 116 | 117 | On Windows: 118 | 119 | mkdir %GOPATH%\src 120 | mkdir %GOPATH%\src\github.com 121 | mkdir %GOPATH%\src\github.com\alunsmithie 122 | cd %GOPATH%\src\github.com\alunsmithie 123 | git clone https://github.com/alunsmithie/animals 124 | 125 | That creates a Go project directory "animals" 126 | which is also a local Git repository. 127 | As you create files 128 | you can add, commit and push them. 129 | 130 | 131 | The JSON Specification 132 | ====================== 133 | 134 | The scaffolder is driven by a text file in JSON format that specifies a database and a set of tables. 135 | 136 | When you are writing JSON, it's very easy to make a simple mistake such as missing out a comma. 137 | The resulting error messages may not be very helpful. 138 | You will save yourself a lot of pain if you prepare the file 139 | using an editor that understands JSON and warns you about obvious errors. 140 | Most Integrated development Environments (liteIDE, Eclipse, IntelliJ, VSCode etc) have editors that will do this. Text editors such as Windows Notepad++ will do the same. 141 | 142 | The scaffolder includes an example specification file so you can use that for a quick experiment. 143 | Copy goprojects/scaffolder/examples/animals.scaffold.json into your project directory and rename it scaffold.json. 144 | 145 | The example specification defines a MySQL database called "animals" containing tables "cats" and "mice": 146 | 147 | { 148 | "name": "animals", 149 | "db": "mysql", 150 | "dbuser": "webuser", 151 | "dbpassword": "secret", 152 | "dbserver": "localhost", 153 | "orm": "gorp", 154 | "sourcebase": "github.com/alunsmithie/animals", 155 | "Resources": [ 156 | { 157 | "name": "cat", 158 | "fields": [ 159 | { 160 | "name": "name", "type": "string", "mandatory": true, 161 | "testValues": ["a","b"] 162 | }, 163 | { 164 | "name": "breed", "type": "string", "mandatory": true 165 | }, 166 | { 167 | "name": "age", "type": "int", "mandatory": true, 168 | "excludeFromDisplay": true 169 | }, 170 | { 171 | "name": "weight", "type": "float", "mandatory": true, 172 | "excludeFromDisplay": true 173 | }, 174 | { 175 | "name": "chipped", "type": "bool", 176 | "excludeFromDisplay": true 177 | } 178 | ] 179 | }, 180 | { 181 | "name": "mouse", "plural": "mice", 182 | "fields": [ 183 | { "name": "name", "type": "string", "mandatory": true }, 184 | { "name": "breed", "type": "string", "excludeFromDisplay": true } 185 | ] 186 | } 187 | ] 188 | } 189 | 190 | In the example, the first few lines of the JSON define the project and its database. 191 | The project is the one we created earlier - animals . 192 | The resulting server uses a MySQL database called "animals". 193 | 194 | The sourcebase defines the location of the project. 195 | In this example the sourcebase is "github.com/alunsmithie/animals", 196 | so the project is stored in 197 | src/github.com/alunsmithie/animals within your workspace. 198 | You created that directory in the previous section. 199 | 200 | When the scaffolder creates files, it creates them within this directory. 201 | 202 | The database definition specifies the user name and password 203 | ("webuser" and "secret" in this example), 204 | the name of the database server and the port that it is listening on. 205 | In this case the server machine is "localhost" (this computer) 206 | and the MySQL server is listening on its default port. 207 | (If not you can specify the port like so: "dbport": "1234".) 208 | 209 | Go has a number of Object-Relational Mapping (ORM) tools 210 | to manage the connection with a database. 211 | The ORM value says which one to use. 212 | At present 213 | the only one supported is [GORP](https://github.com/coopernurse/gorp) version 1. 214 | I plan to add support for other ORMs in the future. 215 | 216 | The Resources section defines a list of resources. 217 | When you run the scaffolder, 218 | for each resource it produces a database table, a model, a repository, 219 | a controller and a set of views. 220 | This example describes the "cat" resource and the "mouse" resource supported by the table with the same name as its resource. 221 | 222 | Traditionally, database tables are named using the plural of the data that they contain. 223 | By default the scaffolder just takes the name of the resource and adds an "s" 224 | so the table for the cat resource is called "cats". 225 | If that won't do, you can specify the table name like so: 226 | 227 | "name": "mouse", "plural": "mice", 228 | 229 | Each resource contains a list of fields. The cat resource has fields "name" and "breed" which contain strings, 230 | "age" containing an integer 231 | "weight" containing a floating point number 232 | and "chipped" containing a boolean value, 233 | recording whether or not the cat has been microchipped. 234 | The "chipped" field is optional by default. 235 | The rest are marked as mandatory. 236 | 237 | The mouse resource has just two fields, 238 | "name" which is mandatory and "breed" which is optional. 239 | Both contain strings. 240 | 241 | Given this JSON spec, 242 | the scaffolder generates a set of unit and integration test programs to check that the generated source code works properly. 243 | A unit test takes a module of the source code and runs it in isolation, supplying it with test values and checking that the module produces the expected result. An integration tests is similar, but checks that a set of modules work together properly. 244 | Each field in the JSON can have an optiona; list of testValues to be used by the tests. 245 | If you don't specify an test values, they are all generated automatically. 246 | If you don't specify enough, the rest are generated automatically. 247 | If you specify too many, the extra ones are ignored. 248 | Currently none of the the generated tests use more than two values, 249 | so a list of two values is always sufficient. 250 | 251 | The optional excludeFromDisplay value in the JSON 252 | controls the contents of the display label. 253 | This identifies each database record in the generated web pages 254 | and it's used in all sorts of ways. 255 | For example, 256 | the index page shows a list of all records in the table. 257 | It uses the display label to represent each record. 258 | By default the display label contains the values of all the fields, 259 | so if no fields were excluded, 260 | a record in the index page for cats would look something something like this: 261 | 262 | 1 Tommy Siamese 2 5 true 263 | 264 | If there are a lot of fields the label can become unwieldy, 265 | Excluding some of them from the label 266 | makes it more manageable. 267 | In the cats resource in the example, 268 | the fields "age", "weight" and "chipped" are excluded, 269 | so the display label will be something like: 270 | 271 | 1 Tommy Siamese 272 | 273 | If you view the HTML for the index page, 274 | you can see that it's a series of links, 275 | one to show each record and one to edit each record. 276 | Each link has a unique ID, 277 | made up using the display label: 278 | 279 | 280 | 1 Tommy Siamese 281 | 282 | 283 | Edit 284 | 285 | 286 | Giving each of the the objects on the page a unique ID 287 | makes it easier to test the solution using 288 | web testing tools such as a Selenium. 289 | 290 | If you press the edit button 291 | and then view the HTML for that page, 292 | you can see that 293 | the title and the h3 heading are also made from the display label: 294 | 295 | 296 | 297 | 298 | Edit Cat 1 Tommy Siamese 299 | 300 | 301 | 302 |

Animals

303 |

Edit Cat 1 Tommy Siamese

304 | 305 | 306 | Creating a Database 307 | ================== 308 | 309 | The JSON in the previous section expects a database called "animals" which can be accessed by the MySQL user "webuser" using the password "secret". 310 | Before you run the generated web server for the first time 311 | you need to create an empty database and give the user access rights: 312 | 313 | Run the MySQL client in a command window: 314 | 315 | mysql -u root -p 316 | {type the root password that you set when you installed mysql} 317 | 318 | mysql> create database animals; 319 | mysql> grant all on animals.* to 'webuser' identified by 'secret'; 320 | mysql> quit 321 | 322 | The web server will connect to this database 323 | and create the tables if they don't already exist. 324 | Each table will have the fields specified in the JSON, plus an auto-incremented unique numeric ID. 325 | The cats table will look like this: 326 | 327 | mysql> describe cats; 328 | +---------+---------------------+------+-----+---------+----------------+ 329 | | Field | Type | Null | Key | Default | Extra | 330 | +---------+---------------------+------+-----+---------+----------------+ 331 | | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | 332 | | name | varchar(255) | YES | | NULL | | 333 | | breed | varchar(255) | YES | | NULL | | 334 | | age | bigint(20) | YES | | NULL | | 335 | | weight | double | YES | | NULL | | 336 | | chipped | tinyint(1) | YES | | NULL | | 337 | +---------+---------------------+------+-----+---------+----------------+ 338 | 339 | When you create a record, 340 | its ID field will be set automatically to a unique value. 341 | 342 | Building the Server 343 | ====================== 344 | 345 | When you run the scaffolder, by default it looks for a specification file "scaffold.json" in the current directory - something like the example above. You can specify a different file if you want to. 346 | 347 | By default the scaffolder generates the server in the current directory, which should be your github project directory (in the example, goprojects/src/github.com/alunsmithie/animals). 348 | Alternatively you can run it from another directory and tell it where to find the project directory. 349 | 350 | In your command window, change directory to your project and run the scaffolder: 351 | 352 | $ cd $HOME/goprojects/src/github.com/alunsmithie/animals 353 | $ scaffolder 354 | 355 | That creates the web server source code and some scripts. 356 | 357 | To use a different specification file: 358 | 359 | $ scaffolder ../specs/animals.json 360 | 361 | To specify the workspace directory as well: 362 | 363 | $ scaffolder workspace=/home/simon/goprojects ../specs/animals.json 364 | 365 | Run the scaffolder program like so to see all of the options: 366 | 367 | $ scaffolder -h 368 | Usage of scaffolder: 369 | -overwrite 370 | overwrite all files, not just the generated directory 371 | -projectdir string 372 | the project directory (default ".") 373 | -templatedir string 374 | the directory containing the scaffold templates (normally this is not specified and built in templates are used) 375 | -v enable verbose logging (shorthand) 376 | -verbose 377 | enable verbose logging 378 | 379 | The generated script install.sh builds and installs the server on Linux: 380 | 381 | $ ./install.sh 382 | 383 | install.bat does the same on Windows: 384 | 385 | install 386 | 387 | There is also test.sh and test.bat. 388 | These run the tests to ensure that all the generated parts work properly: 389 | 390 | $ ./test.sh 391 | 392 | If all the tests pass, you can start the web server. 393 | 394 | 395 | 396 | Running the Server 397 | ================== 398 | 399 | If your Go bin directory goprojects/bin is in your path, 400 | you can run your server like so: 401 | 402 | $ animals 403 | 404 | or you can run it in verbose mode and see tracing messages in your command window: 405 | 406 | $ animals -v 407 | 408 | The first time you run the server it will create the database tables. 409 | (Assuming that you have created an empty database 410 | and permitted the web server's user to create tables 411 | as described earlier.) 412 | 413 | The server runs on port 4000. In your web browser, navigate to 414 | 415 | That display the home page. It has two links "Manage cats" and "Manage mice". 416 | The first takes you to the index page for the cat resource. 417 | The cats table is currently empty. Use the Create button to create some. 418 | 419 | Once you've done that, 420 | the index page lists the cats with links and buttons 421 | to edit and delete the records, and a link back to the home page. 422 | 423 | To add some mice, use the link to the home page and then the "Manage Mice" link. 424 | 425 | To stop the server, type ctrl/c in the command window. (Hold down the ctrl key and type a single "c", you don't need to press the enter key.) 426 | 427 | 428 | Changing the JSON 429 | ================== 430 | 431 | The scaffolder creates these files 432 | 433 | * install.sh - a shell script to build the animals server 434 | * install.bat batch script to do the same on Windows 435 | * test.sh - a shell script to run the test suite 436 | * test.bat same for Windows 437 | * animals.go - the source code of the main module 438 | * generated - the source code of the models, views, controllers, repositories and support software 439 | * views - the templates used to create the html views. 440 | 441 | You can edit the JSON and add some fields. For example, you could add a field "favouritefood" to the cats table. Run the scaffolder again and it will produce a new version of the server. Run the install script to build and install it. 442 | 443 | It's assumed that you may want to tweak things like the build scripts, the main program, the home page and so on. If you run the scaffolder over this project again, by default only the stuff in the "generated" directories is overwritten. 444 | 445 | If you run the scaffolder with the overwrite option, it replaces everything: 446 | 447 | $scaffolder --overwrite 448 | 449 | The server only creates the database tables 450 | if they are missing, 451 | so if you change the JSON and add some fields, 452 | they won't be added to the database tables. 453 | You can add the extra fields to the tables using the MySQL client 454 | or you can simply drop the tables 455 | and then restart the server. 456 | It will create any missing tables using the new specification, 457 | but they will be empty. 458 | If you have created a lot of test data 459 | you might want to use the first option 460 | of adding the fields by hand, 461 | or maybe create a new project connected to a 462 | different database. 463 | 464 | If you change the JSON it's a good idea to run the tests again to make sure that nothing has been broken. However, some of the integration tests write to the database and they will also trash any existing data if you run them. If you want to avoid that, you can run just the unit tests: 465 | 466 | $ ./test.sh unit 467 | 468 | 469 | 470 | MVC 471 | ===================================== 472 | 473 | Given a description of a database, the scaffolder writes a Go program that creates the database 474 | and provides a web server that allows you to create, read, update and delete records. The web server builds HTML pages to order, depending on the data in the database. Such a server is sometimes called a web application server, to distinguish it from a web server that simply feeds out static pages. 475 | 476 | The web application server provides controlled access to data in the database. Each response is manufactured to order, based on the data. In a production system, it's usually impossible for a user to access the database directly. They can only do it via the web server and they can only do what the web server allows. 477 | 478 | The web server generated by the scaffolder 479 | is designed using Model, View, Controller architecture(MVC). 480 | The benfits of MVC include: 481 | 482 | * Isolation of business logic from the user interface 483 | * Ease of keeping code DRY 484 | * Clarifying where different types of code belong for easier maintenance 485 | 486 | (DRY means Don't Repeat Yourself - don't write the same source code more than once.) 487 | 488 | A model represents the information in the database 489 | and the rules to manipulate that data. 490 | In this case, models are used to manage the interaction with a corresponding database table. 491 | Each table in your database will correspond to one model in your application. 492 | There is also a corresponding Repository (AKA a Data Access Object (DAO)), 493 | which is software used to fetch and store data. 494 | (Not to be confused with the GIT repository.) 495 | 496 | The Views represent the user interface of your application. 497 | They handle the presentation of the data. Views provide data to the web browser or other tool that is used to make requests from your application. 498 | This web server generates an HTML response page for each request on the fly from a template, so its views are Go HTML templates. 499 | 500 | The controller provides the “glue” between models and views. The controller is responsible for processing the incoming requests from the web browser, interrogating the models for data, and passing that data on to the views for presentation. There is one controller for each table. 501 | 502 | 503 | Restful Requests 504 | ================ 505 | 506 | The web server created by the scaffolder handles requests 507 | that conform to the 508 | REpresentational State Transfer (REST) model. 509 | REST is described [here](https://en.wikipedia.org/wiki/Representational_state_transfer). 510 | 511 | The REST model stresses the use of resource identifiers such as URIs to represent resources. 512 | In the generated web server, each resource has an associated database table 513 | with the same name. 514 | If we have a database table called "cats" then we have web resource called "cats". 515 | 516 | GET /cats 517 | GET /cats/ 518 | 519 | are both RESTful HTTP request to display a list of all cat resources 520 | (all records in the cats table) 521 | This is the "index" page for the cat resource. 522 | 523 | GET /cats/42 524 | 525 | is a RESTful HTTP request to display the data for the cat with ID 42. 526 | 527 | DELETE /cats/42 528 | 529 | is a RESTful HTTP request to delete that record. 530 | 531 | REST requires that all GET requests are idempotent ("having equal effect"). 532 | This simply means that if some browsers issue the same GET request many times and 533 | the data in the database is not changed by some other agent, 534 | each request will produce the same response. 535 | 536 | The upshot of that is that we use GET requests to read data 537 | from the database and other requests (PUT, DELETE etc) to change the data. 538 | 539 | One advantage of the REST approach is that search engines respect this rule. If your web site is public it will be crawled repeatedly by lots of search engines. When a search engine crawls a site it attempts to visit all the pages. It scans the home page and looks through it for HTTP requests to other pages. Then it scans those pages and so on. If it finds a GET request, 540 | it will attempt to issue it, 541 | but it will avoid issuing any other requests that it finds. The crawler assumes that a GET requests won't change your data but any other request might. 542 | 543 | REST requires that requests containing parameters are only used to submit form data. For example if this request deletes the record with ID 42: 544 | 545 | GET /cats?operation=delete&id=42 546 | 547 | it doesn't follow the REST rules, firstly because it's using a GET request to change the database and secondly because it uses parameters but it's not carrying form data. 548 | 549 | 550 | -------------------------------------------------------------------------------- /examples/animals.scaffold.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animals", 3 | "sourcebase": "github.com/goblimey/animals", 4 | "db": "mysql", 5 | "dbuser": "webuser", 6 | "dbpassword": "secret", 7 | "dbserver": "localhost", 8 | "orm": "gorp", 9 | "Resources": [ 10 | { 11 | "name": "cat", 12 | "fields": [ 13 | { 14 | "name": "name", 15 | "type": "string", 16 | "mandatory": true, 17 | "testValues": [ 18 | "a", 19 | "b" 20 | ] 21 | }, 22 | { 23 | "name": "breed", 24 | "type": "string", 25 | "mandatory": true 26 | }, 27 | { 28 | "name": "age", 29 | "type": "int", 30 | "mandatory": true, 31 | "excludeFromDisplay": true 32 | }, 33 | { 34 | "name": "weight", 35 | "type": "float", 36 | "mandatory": true, 37 | "excludeFromDisplay": true 38 | }, 39 | { 40 | "name": "chipped", 41 | "type": "bool", 42 | "excludeFromDisplay": true 43 | } 44 | ] 45 | }, 46 | { 47 | "name": "mouse", 48 | "plural": "mice", 49 | "fields": [ 50 | { 51 | "name": "name", 52 | "type": "string", 53 | "mandatory": true 54 | }, 55 | { 56 | "name": "breed", 57 | "type": "string", 58 | "excludeFromDisplay": true 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /process.templates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Takes a set of templates stored in files and creates Go code that specifies the 4 | # same templates inline. Normally the scaffolder uses these inline versions and so 5 | # the user doesn't need to specify where to find the tenplates when they run the 6 | # scaffolder. 7 | # 8 | # usage: 9 | # ./process_templates.sh 10 | 11 | process() { 12 | echo 'package main' >$1 13 | echo >>$1 14 | echo 'import (' >>$1 15 | echo ' "io/ioutil"' >>$1 16 | echo ' "strings"' >>$1 17 | echo ' "text/template"' >>$1 18 | echo ' "log"' >>$1 19 | echo ' "os"' >>$1 20 | echo ')' >>$1 21 | echo >>$1 22 | echo '// substituteGraves replaces each occurence of the sequence "%%GRAVE%%" with a' >>$1 23 | echo '// single grave (backtick) rune. In this source file, all templates are quoted in' >>$1 24 | echo '// graves, but some templates contain graves, and a grave within a grave causes a' >>$1 25 | echo '// syntax error. The solution is to replace the graves in the template with' >>$1 26 | echo '// "%%GRAVE%% and then pre-process the template before use.' >>$1 27 | echo 'func substituteGraves(s string) string {' >>$1 28 | echo ' return strings.Replace(s, "%%GRAVE%%", "\x60", -1)' >>$1 29 | echo '}' >>$1 30 | echo >>$1 31 | echo '// createTemplateFromFile creates a template from a file. The file is in the' >>$1 32 | echo '// templates directory wherever the scaffolder is installed, and that is out of our' >>$1 33 | echo '// control, so this should only be called when the "templatedir" command line' >>$1 34 | echo '// argument is specified. ' >>$1 35 | echo 'func createTemplateFromFile(templateName string) *template.Template {' >>$1 36 | echo ' log.SetPrefix("createTemplate() ")' >>$1 37 | echo ' templateFile := templateDir + templateName' >>$1 38 | echo ' buf, err := ioutil.ReadFile(templateFile)' >>$1 39 | echo ' if err != nil {' >>$1 40 | echo ' log.Printf("cannot open template file %s - %s ",' >>$1 41 | echo ' templateFile, err.Error())' >>$1 42 | echo ' os.Exit(-1)' >>$1 43 | echo ' }' >>$1 44 | echo ' tp := string(buf)' >>$1 45 | echo ' tp = substituteGraves(tp)' >>$1 46 | echo ' return template.Must(template.New(templateName).Parse(tp))' >>$1 47 | echo '}' >>$1 48 | echo >>$1 49 | echo 'func createTemplates(useBuiltIn bool) {' >>$1 50 | first=1 51 | for file in * 52 | do 53 | echo 54 | if test $first -eq 1 55 | then 56 | echo 'templateName := "'$file'"' 57 | else 58 | echo 'templateName = "'$file'"' 59 | fi 60 | first=0 61 | echo ' if useBuiltIn {' 62 | echo ' if verbose {' 63 | echo ' log.Printf("creating template %s from builtin template", templateName)' 64 | echo ' }' 65 | echo ' templateText := `' 66 | cat $file 67 | echo '`' 68 | echo ' templateText = substituteGraves(templateText)' 69 | echo ' templateMap[templateName] =' 70 | echo ' template.Must(template.New(templateName).Parse(templateText))' 71 | echo ' } else {' 72 | echo ' if verbose {' 73 | echo ' log.Printf("creating template %s from file %s", templateName, templateDir+templateName)' 74 | echo ' }' 75 | echo ' templateMap[templateName] = createTemplateFromFile(templateName)' 76 | echo ' }' 77 | done >>$1 78 | echo '}' >>$1 79 | } 80 | 81 | cd $GOPATH/src/github.com/goblimey/scaffolder/templates 82 | 83 | process "../create_templates.go" 84 | -------------------------------------------------------------------------------- /scaffolder.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | "unicode" 13 | "unicode/utf8" 14 | ) 15 | 16 | // The Goblimey scaffolder reads a specification file written in JSON describing 17 | // a set of database tables. It generates a web application server that implements 18 | // the Create, Read, Update and Delete (CRUD) operations on those tables. The 19 | // idea is based on the Ruby-on-Rails scaffold generator. 20 | // 21 | // The examples directory contains example JSON specification files. 22 | // 23 | // Run the scaffolder like so: 24 | // scaffolder // uses spec file scaffold.json 25 | // or 26 | // scaffolder 27 | 28 | type Field struct { 29 | Name string `json:"name"` 30 | Type string `json: "type"` 31 | ExcludeFromDisplay bool `json: "excludeFromDisplay"` 32 | Mandatory bool `json: "mandatory"` 33 | TestValues []string `json: "testValues"` 34 | GoType string 35 | NameWithUpperFirst string 36 | NameWithLowerFirst string 37 | NameAllLower string 38 | LastItem bool 39 | } 40 | 41 | func (f Field) String() string { 42 | testValues := "" 43 | for _, s := range f.TestValues { 44 | if f.Type == "string" { 45 | testValues += fmt.Sprintf("\"%s\",", s) 46 | } else { 47 | testValues += s 48 | } 49 | } 50 | 51 | status := "optional" 52 | if f.Mandatory { 53 | status = "mandatory" 54 | } 55 | return fmt.Sprintf("{Name=%s,Type=%s,GoType=%s, ExcludeFromDisplay=%v,%s,TestValues=%s,NameWithLowerFirst=%s,NameWithUpperFirst=%s,NameAllLower=%s,LastItem=%v}", 56 | f.Name, f.Type, f.GoType, f.ExcludeFromDisplay, status, testValues, 57 | f.NameWithLowerFirst, f.NameWithUpperFirst, f.NameAllLower, f.LastItem) 58 | } 59 | 60 | type Resource struct { 61 | Name string `json:"name"` 62 | PluralName string `json:"plural"` 63 | TableName string `json:"tableName"` 64 | NameWithUpperFirst string 65 | NameWithLowerFirst string 66 | NameAllLower string 67 | PluralNameWithUpperFirst string 68 | PluralNameWithLowerFirst string 69 | ProjectName string // copied from the name field of the spec record 70 | ProjectNameWithUpperFirst string 71 | Imports string 72 | SourceBase string // copied from the spec record 73 | DB string // copied from the spec record 74 | DBURL string // copied from the spec record 75 | Fields []Field 76 | } 77 | 78 | func (r Resource) String() string { 79 | var fields string 80 | for _, f := range r.Fields { 81 | fields += f.String() + "\n" 82 | } 83 | return fmt.Sprintf("{Name=%s,PluralName=%s,TableName=%s,NameWithLowerFirst=%s,NameWithUpperFirst=%s,PluralNameWithLowerFirst=%s,PluralNameWithUpperFirst=%s,NameAllLower=%s,ProjectName=%s,imports=%s,DB=%s,DBURL=%s,fields=%s}", 84 | r.Name, r.PluralName, r.TableName, 85 | r.NameWithLowerFirst, r.NameWithUpperFirst, 86 | r.PluralNameWithLowerFirst, r.PluralNameWithUpperFirst, r.NameAllLower, 87 | r.ProjectName, r.Imports, r.DB, r.DBURL, fields) 88 | } 89 | 90 | type Spec struct { 91 | Name string `json:"name"` 92 | SourceBase string `json:"sourcebase"` 93 | DB string `json:"db"` 94 | DBUser string `json:"dbuser"` 95 | DBPassword string `json:"dbpassword"` 96 | DBServer string `json:dbserver` 97 | DBPort string `json:dbport` 98 | ORM string `json:orm` 99 | DBURL string 100 | NameWithUpperFirst string 101 | NameWithLowerFirst string 102 | NameAllUpper string 103 | Imports string 104 | CurrentDir string 105 | Resources []Resource 106 | } 107 | 108 | func (s Spec) String() string { 109 | var resources string 110 | for _, r := range s.Resources { 111 | resources += r.String() + "\n" 112 | } 113 | return fmt.Sprintf("{name=%s sourceBase=%s db=%s dbserver=%s dbport=%s dbuser=%s dbpassword=%s dburl=%s %d resources={%s}}", 114 | s.Name, s.SourceBase, s.DB, s.DBServer, s.DBPort, s.DBUser, s.DBPassword, 115 | s.DBURL, len(s.Resources), resources) 116 | } 117 | 118 | var templateMap map[string]*template.Template 119 | 120 | var verbose bool 121 | var overwriteMode bool 122 | var templateDir string 123 | var projectDir string 124 | 125 | func init() { 126 | const ( 127 | defaultVerbose = false 128 | usage = "enable verbose logging" 129 | ) 130 | flag.BoolVar(&verbose, "verbose", defaultVerbose, usage) 131 | flag.BoolVar(&verbose, "v", defaultVerbose, usage+" (shorthand)") 132 | 133 | flag.BoolVar(&overwriteMode, "overwrite", false, "overwrite all files, not just the generated directory") 134 | flag.StringVar(&templateDir, "templatedir", "", "the directory containing the scaffold templates (normally this is not specified and built in templates are used)") 135 | flag.StringVar(&projectDir, "projectdir", ".", "the project directory") 136 | 137 | templateMap = make(map[string]*template.Template) 138 | } 139 | 140 | func main() { 141 | log.SetPrefix("main() ") 142 | 143 | flag.Parse() 144 | 145 | // Find the scaffold spec. By default it's "scaffold.json" but it can be 146 | // specified by the first (and only) command line argument. 147 | // 148 | // Do this before changing directory to the project. 149 | 150 | specFile := "scaffold.json" 151 | if len(flag.Args()) >= 1 { 152 | specFile = flag.Args()[0] 153 | } 154 | 155 | if verbose { 156 | log.Printf("specification file %s", specFile) 157 | } 158 | 159 | // Check that file exists and can be read 160 | path, err := filepath.Abs(specFile) 161 | if err != nil { 162 | log.Printf("cannot find path for JSON specification file %s - %s", specFile, 163 | err.Error()) 164 | os.Exit(-1) 165 | } 166 | 167 | if verbose { 168 | log.Printf("spec file path %s", path) 169 | } 170 | 171 | jsonFile, err := os.Open(path) 172 | if err != nil { 173 | log.Printf("cannot open JSON specification file %s - %s", specFile, 174 | err.Error()) 175 | os.Exit(-1) 176 | } 177 | defer jsonFile.Close() 178 | 179 | // By default, the projectDir is the current directory but it can 180 | // be specified on the command line. 181 | 182 | if projectDir != "." { 183 | err := os.Chdir(projectDir) 184 | if err != nil { 185 | log.Printf("cannot change directory to the project %s - %s", 186 | err.Error(), projectDir) 187 | os.Exit(-1) 188 | } 189 | } 190 | 191 | var spec Spec 192 | 193 | jsonParser := json.NewDecoder(jsonFile) 194 | if err = jsonParser.Decode(&spec); err != nil { 195 | log.Printf("cannot read JSON from specification file %s - %s", specFile, err.Error()) 196 | os.Exit(-1) 197 | } 198 | 199 | if verbose { 200 | log.Printf("specification\n%s", spec.String()) 201 | } 202 | 203 | data, err := json.MarshalIndent(&spec, "", " ") 204 | if err != nil { 205 | log.Printf("internal error - cannot convert specification structure back to JSON - %s", 206 | err.Error()) 207 | os.Exit(-1) 208 | } 209 | if verbose { 210 | log.Printf("formatted specification\n%s\n", data) 211 | } 212 | 213 | // Get the full pathname of the current working directory and add it to 214 | // the spec. 215 | 216 | spec.CurrentDir, err = os.Getwd() 217 | if err != nil { 218 | log.Printf("cannot get current directory - %s", 219 | err.Error()) 220 | os.Exit(-1) 221 | } 222 | 223 | // If the templateDir is not specified, produce templates from the built-in 224 | // prototypes. Otherwise produce templates using the files in the templateDir 225 | // directory as prototypes. The second choice is intended for use only during 226 | // development of the scaffolder. 227 | 228 | if templateDir == "" { 229 | createTemplates(true) 230 | } else { 231 | createTemplates(false) 232 | } 233 | 234 | // Enhance the data by setting the derived fields. 235 | 236 | if spec.DBPort == "" { 237 | if spec.DB == "mysql" { 238 | spec.DBPort = "3306" 239 | } 240 | } 241 | 242 | // "webuser:secret@tcp(localhost:3306)/animals" 243 | spec.DBURL = spec.DBUser + ":" + spec.DBPassword + "@tcp(" + 244 | spec.DBServer + ":" + spec.DBPort + ")/" + spec.Name 245 | 246 | // "animals" => "Animals" 247 | spec.NameWithUpperFirst = upperFirstRune(spec.Name) 248 | // Animals" => animals" 249 | spec.NameWithLowerFirst = lowerFirstRune(spec.Name) 250 | //"animals" => "ANIMALS" 251 | spec.NameAllUpper = strings.ToUpper(spec.Name) 252 | 253 | for i, _ := range spec.Resources { 254 | // Set the last item flag in each field list. For all but the last 255 | // field in a resource, the LastItem flag is false. For the last field 256 | // it's true. This helps the templates to construct things like lists 257 | // where the fields are separated by commas but the last field is not 258 | // followed by a comma, for example: 259 | // return MakeInitialisedPerson(source.ID(), source.Forename(), source.Surname()) 260 | for j, _ := range spec.Resources[i].Fields { 261 | // Set LastItem true, then set it false on the next iteration. 262 | if j > 0 { 263 | spec.Resources[i].Fields[j].LastItem = true 264 | spec.Resources[i].Fields[j-1].LastItem = false 265 | } 266 | } 267 | 268 | spec.NameWithUpperFirst = upperFirstRune(spec.Name) 269 | spec.NameWithLowerFirst = lowerFirstRune(spec.Name) 270 | 271 | // These are supplied once in the spec, but each resource needs them, 272 | // so copy them into each resource record. 273 | spec.Resources[i].ProjectName = spec.Name 274 | spec.Resources[i].SourceBase = spec.SourceBase 275 | // "animals" => "Animals" 276 | spec.Resources[i].ProjectNameWithUpperFirst = 277 | upperFirstRune(spec.Name) 278 | spec.Resources[i].DB = spec.DB 279 | spec.Resources[i].DBURL = spec.DBURL 280 | 281 | // "CatAndDog" => "catAndDog" 282 | spec.Resources[i].NameWithLowerFirst = lowerFirstRune(spec.Resources[i].Name) 283 | // "cat" => "Cat" 284 | spec.Resources[i].NameWithUpperFirst = upperFirstRune(spec.Resources[i].Name) 285 | 286 | // "CatAndDog" => "catanddog" 287 | spec.Resources[i].NameAllLower = 288 | strings.ToLower(spec.Resources[i].Name) 289 | 290 | if spec.Resources[i].PluralName == "" { 291 | // "cat" => "cats" 292 | spec.Resources[i].PluralName = spec.Resources[i].NameWithLowerFirst + "s" 293 | } 294 | 295 | if spec.Resources[i].PluralName == "" { 296 | // "cat" => "cats" 297 | spec.Resources[i].PluralName = spec.Resources[i].NameWithLowerFirst + "s" 298 | } 299 | 300 | // "CatAndDogs" => "catAndDogs" 301 | spec.Resources[i].PluralNameWithLowerFirst = 302 | lowerFirstRune(spec.Resources[i].PluralName) 303 | 304 | // "catAndDogs" => "CatAndDogs" 305 | spec.Resources[i].PluralNameWithUpperFirst = 306 | upperFirstRune(spec.Resources[i].PluralName) 307 | 308 | // The table name is the plural of the lowered resource name (eg "cats") 309 | // but the JSON can specify it (eg resource name is "mouse" and table 310 | // name is "mice". 311 | if spec.Resources[i].TableName == "" { 312 | spec.Resources[i].TableName = spec.Resources[i].PluralName 313 | } 314 | 315 | // Set the fields that are set from other fields. 316 | nextTestValue := 1 317 | 318 | for j, _ := range spec.Resources[i].Fields { 319 | 320 | // In the JSON, the types are "int", "uint", "float",or "bool". In 321 | // the generated Go code use int64 for int, unit64 for uint and 322 | // float64 for float. Other types are OK. 323 | if spec.Resources[i].Fields[j].Type == "int" { 324 | spec.Resources[i].Fields[j].GoType = "int64" 325 | } else if spec.Resources[i].Fields[j].Type == "uint" { 326 | spec.Resources[i].Fields[j].GoType = "uint64" 327 | } else if spec.Resources[i].Fields[j].Type == "float" { 328 | spec.Resources[i].Fields[j].GoType = "float64" 329 | } else { 330 | spec.Resources[i].Fields[j].GoType = 331 | spec.Resources[i].Fields[j].Type 332 | } 333 | 334 | spec.Resources[i].Fields[j].NameWithUpperFirst = 335 | upperFirstRune(spec.Resources[i].Fields[j].Name) 336 | spec.Resources[i].Fields[j].NameWithLowerFirst = 337 | lowerFirstRune(spec.Resources[i].Fields[j].Name) 338 | spec.Resources[i].Fields[j].NameAllLower = 339 | strings.ToLower(spec.Resources[i].Fields[j].Name) 340 | 341 | // The test values are optional. We need two values for each 342 | // field, because some tests create two objects. If only one 343 | // value is supplied, then use that and create the second. If 344 | // none are supplied, then create both. To create all values, 345 | // use a sequence such as: 346 | // {"s1", "s2}, {"s3", "s4"}, {"s5", "s6"} for three string types, 347 | // or {"1.1", "2.1"}, {"s3", "s4"} for a float type followed by a 348 | // string type. 349 | // 350 | // For booleans, generate {true, false}, {true, false} .... 351 | 352 | CreateFirstTestValue := false 353 | CreateSecondTestValue := false 354 | if spec.Resources[i].Fields[j].TestValues == nil { 355 | spec.Resources[i].Fields[j].TestValues = make([]string, 2) 356 | CreateFirstTestValue = true 357 | CreateSecondTestValue = true 358 | } else { 359 | if len(spec.Resources[i].Fields[j].TestValues) == 0 { 360 | CreateFirstTestValue = true 361 | CreateSecondTestValue = true 362 | } else if len(spec.Resources[i].Fields[j].TestValues) == 1 { 363 | // Got the first value, need the second. 364 | CreateSecondTestValue = true 365 | } else { 366 | // Got both values already 367 | } 368 | } 369 | 370 | switch spec.Resources[i].Fields[j].Type { 371 | case "string": 372 | if CreateFirstTestValue { 373 | spec.Resources[i].Fields[j].TestValues[0] = 374 | fmt.Sprintf("s%d", nextTestValue) 375 | } 376 | if CreateSecondTestValue { 377 | spec.Resources[i].Fields[j].TestValues[1] = 378 | fmt.Sprintf("s%d", nextTestValue+1) 379 | } 380 | case "int": 381 | if CreateFirstTestValue { 382 | spec.Resources[i].Fields[j].TestValues[0] = 383 | fmt.Sprintf("%d", nextTestValue) 384 | } 385 | if CreateSecondTestValue { 386 | spec.Resources[i].Fields[j].TestValues[1] = 387 | fmt.Sprintf("%d", nextTestValue+1) 388 | } 389 | case "uint": 390 | if CreateFirstTestValue { 391 | spec.Resources[i].Fields[j].TestValues[0] = 392 | fmt.Sprintf("%d", nextTestValue) 393 | } 394 | if CreateSecondTestValue { 395 | spec.Resources[i].Fields[j].TestValues[1] = 396 | fmt.Sprintf("%d", nextTestValue+1) 397 | } 398 | case "float": 399 | if CreateFirstTestValue { 400 | spec.Resources[i].Fields[j].TestValues[0] = 401 | fmt.Sprintf("%d.1", nextTestValue) 402 | } 403 | if CreateSecondTestValue { 404 | spec.Resources[i].Fields[j].TestValues[1] = 405 | fmt.Sprintf("%d.1", nextTestValue+1) 406 | } 407 | case "bool": 408 | if CreateFirstTestValue { 409 | spec.Resources[i].Fields[j].TestValues[0] = "true" 410 | } 411 | if CreateSecondTestValue { 412 | spec.Resources[i].Fields[j].TestValues[1] = "false" 413 | } 414 | default: 415 | log.Printf("cannot handle type %s ", spec.Resources[i].Fields[j].Type) 416 | os.Exit(-1) 417 | } 418 | 419 | nextTestValue += 2 // 1, 3, 5 ... 420 | } 421 | } 422 | 423 | data, err = json.MarshalIndent(&spec, "", " ") 424 | if err != nil { 425 | log.Printf("internal error - cannot convert the spec structure back to JSON after enhancement - %s", 426 | err.Error()) 427 | os.Exit(-1) 428 | } 429 | 430 | if verbose { 431 | log.Printf("enhanced spec:\n%s\n", data) 432 | } 433 | 434 | // Build the project from the templates and the JSON spec. 435 | 436 | // install.sh script with permission u+rwx 437 | templateName := "script.install.sh.template" 438 | targetName := "install.sh" 439 | createFileFromTemplateAndSpec(projectDir, targetName, templateName, spec, 440 | overwriteMode) 441 | 442 | var permisssions os.FileMode = 0700 443 | os.Chmod(projectDir+"/"+targetName, permisssions) 444 | 445 | // test.sh script with permission u+rwx 446 | templateName = "script.test.sh.template" 447 | targetName = "test.sh" 448 | createFileFromTemplateAndSpec(projectDir, targetName, templateName, spec, 449 | overwriteMode) 450 | os.Chmod(projectDir+"/"+targetName, permisssions) 451 | 452 | // Windoze batch files 453 | templateName = "script.install.bat.template" 454 | targetName = "install.bat" 455 | createFileFromTemplateAndSpec(projectDir, targetName, templateName, spec, 456 | overwriteMode) 457 | 458 | templateName = "script.test.bat.template" 459 | targetName = "test.bat" 460 | createFileFromTemplateAndSpec(projectDir, targetName, templateName, spec, 461 | overwriteMode) 462 | 463 | // Build the main program. 464 | templateName = "main.go.template" 465 | targetName = spec.NameWithLowerFirst + ".go" 466 | 467 | spec.Imports = ` 468 | import ( 469 | "flag" 470 | "fmt" 471 | "log" 472 | "net/http" 473 | "os" 474 | "regexp" 475 | "strconv" 476 | "strings" 477 | restful "github.com/emicklei/go-restful" 478 | retrofitTemplate "` + spec.SourceBase + 479 | "/generated/crud/retrofit/template" + `" 480 | "` + spec.SourceBase + "/generated/crud/services" + `" 481 | "` + spec.SourceBase + "/generated/crud/utilities" + `" 482 | ` 483 | 484 | for _, resource := range spec.Resources { 485 | // personForms "github.com/goblimey/films/generated/crud/forms/people" 486 | spec.Imports += resource.NameWithLowerFirst + `Forms "` + 487 | spec.SourceBase + "/generated/crud/forms/" + resource.NameWithLowerFirst + `" 488 | ` 489 | // personController "github.com/goblimey/films/generated/crud/controllers/person" 490 | spec.Imports += resource.NameWithLowerFirst + `Controller "` + 491 | spec.SourceBase + "/generated/crud/controllers/" + 492 | resource.NameWithLowerFirst + `" 493 | ` 494 | // personRepository "github.com/goblimey/films/generated/crud/repositories/person/gorpmysql" 495 | spec.Imports += resource.NameWithLowerFirst + `Repository "` + 496 | spec.SourceBase + "/generated/crud/repositories/" + 497 | resource.NameWithLowerFirst + `/gorpmysql" 498 | ` 499 | } 500 | 501 | spec.Imports += ` 502 | )` 503 | createFileFromTemplateAndSpec(projectDir, targetName, templateName, spec, 504 | overwriteMode) 505 | 506 | // Build the static views. It's assumed that the user may want to edit 507 | // these and add their own stuff, so they are not overwritten. 508 | 509 | // views/stylesheets/scaffold.css - the static stylesheet. 510 | stylesheetDir := projectDir + "/views/stylesheets" 511 | targetName = "scaffold.css" 512 | templateName = "view.stylesheets.scaffold.css.template" 513 | createFileFromTemplateAndSpec(stylesheetDir, targetName, templateName, spec, 514 | overwriteMode) 515 | 516 | // views/html/index.html - the static application home page. 517 | htmlDir := projectDir + "/views/html" 518 | targetName = "index.html" 519 | templateName = "view.index.ghtml.template" 520 | createFileFromTemplateAndSpec(htmlDir, targetName, templateName, spec, 521 | overwriteMode) 522 | 523 | // views/html/error.html - the static error page. 524 | targetName = "error.html" 525 | templateName = "view.error.html.template" 526 | createFileFromTemplateAndSpec(htmlDir, targetName, templateName, spec, 527 | overwriteMode) 528 | 529 | // views/_base.ghtml - the prototype for all generated pages 530 | generatedDir := projectDir + "/views" 531 | targetName = "_base.ghtml" 532 | templateName = "view.base.ghtml.template" 533 | createFileFromTemplateAndSpec(generatedDir, targetName, templateName, spec, 534 | overwriteMode) 535 | 536 | // These files are always overwritten. 537 | 538 | // Generate the sql scripts. 539 | sqlDir := projectDir + "/generated/sql" 540 | templateName = "sql.create.db.template" 541 | targetName = "create.db.sql" 542 | createFileFromTemplateAndSpec(sqlDir, targetName, templateName, spec, true) 543 | 544 | // Generate the utilities. 545 | 546 | crudBase := projectDir + "/generated/crud" 547 | utilitiesDir := crudBase + "/utilities" 548 | templateName = "utilities.go.template" 549 | targetName = "utilities.go" 550 | 551 | spec.Imports = ` 552 | import ( 553 | "fmt" 554 | "html/template" 555 | "log" 556 | "net/http" 557 | "strings" 558 | restful "github.com/emicklei/go-restful" 559 | retrofitTemplate "` + spec.SourceBase + 560 | "/generated/crud/retrofit/template" + `" 561 | )` 562 | createFileFromTemplateAndSpec(utilitiesDir, targetName, templateName, spec, 563 | true) 564 | 565 | retrofitDir := crudBase + "/retrofit/template" 566 | templateName = "retrofit.template.go.template" 567 | targetName = "template.go" 568 | createFileFromTemplateAndSpec(retrofitDir, targetName, templateName, 569 | spec, true) 570 | 571 | // Generate the services object. 572 | 573 | // Interface. 574 | servicesDir := crudBase + "/services" 575 | templateName = "services.go.template" 576 | targetName = "services.go" 577 | 578 | spec.Imports = ` 579 | import ( 580 | retrofitTemplate "` + spec.SourceBase + 581 | "/generated/crud/retrofit/template" + `" 582 | ` 583 | for _, resource := range spec.Resources { 584 | // personForms "github.com/goblimey/films/generated/crud/forms/people" 585 | spec.Imports += resource.NameWithLowerFirst + `Forms "` + 586 | spec.SourceBase + "/generated/crud/forms/" + 587 | resource.NameWithLowerFirst + `" 588 | ` 589 | // "github.com/goblimey/films/generated/crud/models/person" 590 | spec.Imports += `"` + spec.SourceBase + "/generated/crud/models/" + 591 | resource.NameWithLowerFirst + `" 592 | ` 593 | // peopleRepo "github.com/goblimey/films/generated/crud/repositories/people" 594 | spec.Imports += resource.NameWithLowerFirst + `Repo "` + spec.SourceBase + 595 | "/generated/crud/repositories/" + resource.NameWithLowerFirst + `" 596 | ` 597 | } 598 | spec.Imports += ")" 599 | 600 | createFileFromTemplateAndSpec(servicesDir, targetName, templateName, spec, 601 | true) 602 | 603 | // Concrete type. 604 | templateName = "services.concrete.go.template" 605 | targetName = "concrete_services.go" 606 | 607 | spec.Imports = ` 608 | import ( 609 | retrofitTemplate "` + spec.SourceBase + 610 | "/generated/crud/retrofit/template" + `" 611 | ` 612 | for _, resource := range spec.Resources { 613 | // personForms "github.com/goblimey/films/generated/crud/forms/people" 614 | spec.Imports += resource.NameWithLowerFirst + `Forms "` + 615 | spec.SourceBase + "/generated/crud/forms/" + 616 | resource.NameWithLowerFirst + `" 617 | ` 618 | // "github.com/goblimey/films/generated/crud/models/person" 619 | spec.Imports += `"` + spec.SourceBase + 620 | "/generated/crud/models/" + resource.NameWithLowerFirst + `" 621 | ` 622 | // gorpPerson "github.com/goblimey/films/generated/crud/models/person/gorp" 623 | spec.Imports += "gorp" + resource.NameWithUpperFirst + ` "` + 624 | spec.SourceBase + "/generated/crud/models/" + 625 | resource.NameWithLowerFirst + `/gorp" 626 | ` 627 | // peopleRepo "github.com/goblimey/films/generated/crud/repositories/people" 628 | spec.Imports += resource.NameWithLowerFirst + `Repo "` + spec.SourceBase + 629 | "/generated/crud/repositories/" + resource.NameWithLowerFirst + `" 630 | ` 631 | } 632 | spec.Imports += ")" 633 | createFileFromTemplateAndSpec(servicesDir, targetName, templateName, spec, 634 | true) 635 | 636 | // Generate the models. 637 | 638 | for _, resource := range spec.Resources { 639 | 640 | // Generate the interface and concrete objects for the model. 641 | 642 | modelDir := crudBase + "/models/" + resource.NameAllLower 643 | targetName = resource.NameAllLower + ".go" 644 | templateName = "model.interface.go.template" 645 | createFileFromTemplateAndResource(modelDir, targetName, templateName, 646 | resource) 647 | 648 | // concrete model object 649 | targetName = "concrete_" + resource.NameAllLower + ".go" 650 | templateName = "model.concrete.go.template" 651 | createFileFromTemplateAndResource(modelDir, targetName, templateName, 652 | resource) 653 | 654 | // test for concrete model object 655 | targetName = "concrete_" + resource.NameAllLower + "_test.go" 656 | templateName = "model.concrete.test.go.template" 657 | createFileFromTemplateAndResource(modelDir, targetName, templateName, 658 | resource) 659 | 660 | // concrete model object using gorp to access the database 661 | modelDir += "/gorp" 662 | targetName = "concrete_" + resource.NameAllLower + ".go" 663 | templateName = "gorp.concrete.go.template" 664 | 665 | resource.Imports = ` 666 | import ( 667 | "errors" 668 | "fmt" 669 | "strings" 670 | "` + 671 | spec.SourceBase + "/generated/crud/models/" + 672 | resource.NameWithLowerFirst + `" 673 | )` 674 | 675 | createFileFromTemplateAndResource(modelDir, targetName, templateName, 676 | resource) 677 | 678 | // The test for the gorp version of the model is the same test as for the 679 | // concrete model object, but in the appropriate directory. 680 | targetName = "concrete_" + resource.NameAllLower + "_test.go" 681 | templateName = "model.concrete.test.go.template" 682 | createFileFromTemplateAndResource(modelDir, targetName, templateName, 683 | resource) 684 | 685 | // Generate the repository. 686 | 687 | // interface 688 | interfaceDir := crudBase + "/repositories/" + resource.NameAllLower 689 | targetName = "repository.go" 690 | templateName = "repository.interface.go.template" 691 | 692 | resource.Imports = ` 693 | import ("` + spec.SourceBase + "/generated/crud/models/" + 694 | resource.NameAllLower + `")` 695 | 696 | createFileFromTemplateAndResource(interfaceDir, targetName, templateName, 697 | resource) 698 | 699 | // concrete repository using gorp to access the mysql database 700 | interfaceDir += "/gorpmysql" 701 | targetName = "concrete_repository.go" 702 | templateName = "repository.concrete.gorp.go.template" 703 | 704 | resource.Imports = ` 705 | import ( 706 | "database/sql" 707 | "errors" 708 | "fmt" 709 | "log" 710 | "strconv" 711 | "strings" 712 | // This import must be present to satisfy a dependency in the GORP library. 713 | _ "github.com/go-sql-driver/mysql" 714 | gorp "gopkg.in/gorp.v1" 715 | ` + 716 | resource.NameWithLowerFirst + ` "` + 717 | spec.SourceBase + "/generated/crud/models/" + 718 | resource.NameAllLower + `" 719 | ` + 720 | "gorp" + resource.NameWithUpperFirst + ` "` + 721 | spec.SourceBase + "/generated/crud/models/" + resource.NameAllLower + 722 | `/gorp" 723 | ` + 724 | resource.NameWithLowerFirst + "Repo " + `"` + 725 | spec.SourceBase + "/generated/crud/repositories/" + 726 | resource.NameWithLowerFirst + `" 727 | )` 728 | 729 | createFileFromTemplateAndResource(interfaceDir, targetName, templateName, 730 | resource) 731 | 732 | // Unit test 733 | targetName = "concrete_repository_test.go" 734 | templateName = "repository.concrete.gorp.test.go.template" 735 | 736 | resource.Imports = ` 737 | import ( 738 | "fmt" 739 | "log" 740 | "os" 741 | "strconv" 742 | "testing" 743 | gorp` + resource.NameWithUpperFirst + 744 | ` "` + 745 | spec.SourceBase + "/generated/crud/models/" + 746 | resource.NameAllLower + `/gorp" 747 | "` + spec.SourceBase + "/generated/crud/repositories/" + 748 | resource.NameWithLowerFirst + `" 749 | )` 750 | 751 | createFileFromTemplateAndResource(interfaceDir, targetName, templateName, 752 | resource) 753 | 754 | // generated the forms. 755 | 756 | // interface for single object form - single_object_form.go 757 | 758 | formsDir := crudBase + "/forms/" + resource.NameAllLower 759 | targetName = "single_item_form.go" 760 | templateName = "form.single.item.go.template" 761 | 762 | // import ("github.com/goblimey/films/models/person") 763 | resource.Imports = ` 764 | import ( 765 | "` + 766 | spec.SourceBase + "/generated/crud/models/" + 767 | resource.NameAllLower + `" 768 | )` 769 | 770 | createFileFromTemplateAndResource(formsDir, targetName, templateName, 771 | resource) 772 | 773 | // interface for list form - list_form.go 774 | 775 | targetName = "list_form.go" 776 | templateName = "form.list.go.template" 777 | // import ("github.com/goblimey/films/generated/crud/models/person") 778 | resource.Imports = `import ("` + spec.SourceBase + 779 | "/generated/crud/models/" + resource.NameWithLowerFirst + `")` 780 | 781 | createFileFromTemplateAndResource(formsDir, targetName, templateName, 782 | resource) 783 | 784 | // concrete structure for single item form concrete_person_form.go 785 | targetName = "concrete_single_item_form.go" 786 | templateName = "form.concrete.single.item.go.template" 787 | 788 | resource.Imports = ` 789 | import ( 790 | "fmt" 791 | "strings" 792 | "` + spec.SourceBase + `/generated/crud/utilities" 793 | "` + spec.SourceBase + "/generated/crud/models/" + 794 | resource.NameAllLower + `" 795 | )` 796 | 797 | createFileFromTemplateAndResource(formsDir, targetName, templateName, 798 | resource) 799 | 800 | // test for concrete single item form - concrete_single_item_form_test.go 801 | targetName = "concrete_single_item_form_test.go" 802 | templateName = "form.concrete.single.item.test.go.template" 803 | 804 | resource.Imports = ` 805 | import ( 806 | "testing" 807 | ` + resource.NameAllLower + `Model "` + spec.SourceBase + 808 | "/generated/crud/models/" + resource.NameAllLower + `" 809 | )` 810 | 811 | createFileFromTemplateAndResource(formsDir, targetName, templateName, 812 | resource) 813 | 814 | resource.Imports = ` 815 | import ( 816 | "testing" 817 | "` + spec.SourceBase + "/generated/crud/models/" + 818 | resource.NameAllLower + `" 819 | "` + spec.SourceBase + "/generated/crud/repositories/" + 820 | resource.PluralNameWithLowerFirst + `" 821 | )` 822 | 823 | // concrete structure for list form concrete_list_form.go 824 | targetName = "concrete_list_form.go" 825 | templateName = "form.concrete.list.go.template" 826 | // import ("github.com/goblimey/films/generated/crud/models/person") 827 | resource.Imports = `import ("` + spec.SourceBase + 828 | `/generated/crud/models/` + resource.NameAllLower + `")` 829 | createFileFromTemplateAndResource(formsDir, targetName, templateName, 830 | resource) 831 | 832 | // Generate the controller. 833 | controllerDir := crudBase + "/controllers/" + resource.NameAllLower 834 | targetName = "controller.go" 835 | templateName = "controller.go.template" 836 | 837 | resource.Imports = ` 838 | import ( 839 | "fmt" 840 | "log" 841 | restful "github.com/emicklei/go-restful" 842 | "` + spec.SourceBase + "/generated/crud/utilities" + `" 843 | ` + resource.NameWithLowerFirst + `Forms "` + spec.SourceBase + 844 | "/generated/crud/forms/" + resource.NameWithLowerFirst + `" 845 | "` + spec.SourceBase + "/generated/crud/services" + `" 846 | )` 847 | 848 | createFileFromTemplateAndResource(controllerDir, targetName, templateName, 849 | resource) 850 | 851 | // Controller test. 852 | targetName = "controller_test.go" 853 | templateName = "controller.test.go.template" 854 | 855 | resource.Imports = ` 856 | import ( 857 | "errors" 858 | "fmt" 859 | "log" 860 | "net/http" 861 | "net/url" 862 | "strings" 863 | "testing" 864 | restful "github.com/emicklei/go-restful" 865 | "github.com/petergtz/pegomock" 866 | retrofitTemplate "` + spec.SourceBase + 867 | "/generated/crud/retrofit/template" + `" 868 | "` + spec.SourceBase + "/generated/crud/services" + `" 869 | mocks "` + spec.SourceBase + "/generated/crud/mocks/pegomock" + `" 870 | mock` + resource.NameWithUpperFirst + ` "` + 871 | spec.SourceBase + "/generated/crud/mocks/pegomock/" + 872 | resource.NameWithLowerFirst + `" 873 | ` + 874 | // personForms "github.com/goblimey/films/generated/crud/forms/person" 875 | resource.NameWithLowerFirst + `Forms "` + spec.SourceBase + 876 | "/generated/crud/forms/" + resource.NameWithLowerFirst + `" 877 | ` + 878 | // person "github.com/goblimey/films/generated/crud/models/person" 879 | resource.NameWithLowerFirst + ` "` + spec.SourceBase + 880 | "/generated/crud/models/" + resource.NameWithLowerFirst + `" 881 | )` 882 | 883 | createFileFromTemplateAndResource(controllerDir, targetName, templateName, 884 | resource) 885 | 886 | // Build the views for each model. 887 | 888 | // views/generated/crud/templates/index.ghtml - html template for the index 889 | // page for the model. 890 | ghtmlDir := projectDir + "/views/generated/crud/templates/" + 891 | resource.NameAllLower 892 | targetName = "index.ghtml" 893 | templateName = "view.resource.index.ghtml.template" 894 | createFileFromTemplateAndResource(ghtmlDir, targetName, templateName, 895 | resource) 896 | 897 | // views/generated/crud/templates/create.ghtml - html template for the 898 | // create page for the model. 899 | targetName = "create.ghtml" 900 | templateName = "view.resource.create.ghtml.template" 901 | createFileFromTemplateAndResource(ghtmlDir, targetName, templateName, 902 | resource) 903 | 904 | // views/generated/crud/templates/edit.ghtml - html template for the edit 905 | // page for the model. 906 | targetName = "edit.ghtml" 907 | templateName = "view.resource.edit.ghtml.template" 908 | createFileFromTemplateAndResource(ghtmlDir, targetName, templateName, 909 | resource) 910 | 911 | // views/generated/crud/templates/edit.ghtml - html template for the show 912 | // page for each model. 913 | targetName = "show.ghtml" 914 | templateName = "view.resource.show.ghtml.template" 915 | createFileFromTemplateAndResource(ghtmlDir, targetName, templateName, 916 | resource) 917 | } 918 | } 919 | 920 | func createFileFromTemplateAndSpec(targetDir string, targetName string, 921 | templateName string, spec Spec, overwrite bool) { 922 | 923 | log.SetPrefix("createFileFromTemplateAndSpec ") 924 | 925 | file, err := createAndOpenFile(targetDir, targetName, overwrite) 926 | if err != nil { 927 | log.Println(err.Error()) 928 | os.Exit(-1) 929 | } 930 | 931 | // Special case, only happens if overwrite is true and file exists - nothing 932 | // to do. 933 | 934 | if file == nil { 935 | return 936 | } 937 | 938 | defer file.Close() 939 | 940 | err = templateMap[templateName].Execute(file, spec) 941 | if err != nil { 942 | log.Printf("error creating file %s from template %s - %s ", 943 | targetDir+"/"+targetName, templateName, err.Error()) 944 | os.Exit(-1) 945 | } 946 | } 947 | 948 | func createFileFromTemplateAndResource(targetDir string, targetName string, 949 | templateName string, resource Resource) { 950 | 951 | log.SetPrefix("createFileFromTemplateAndResource ") 952 | 953 | targetPathName := targetDir + "/" + targetName 954 | if verbose { 955 | log.Printf("creating file %s from template %s", targetPathName, 956 | templateName) 957 | } 958 | conn, err := createAndOpenFile(targetDir, targetName, true) 959 | if err != nil { 960 | log.Println(err.Error()) 961 | os.Exit(-1) 962 | } 963 | defer conn.Close() 964 | 965 | err = templateMap[templateName].Execute(conn, resource) 966 | if err != nil { 967 | log.Printf("error creating file %s from template %s - %s ", 968 | targetDir+"/"+targetName, templateName, err.Error()) 969 | os.Exit(-1) 970 | } 971 | } 972 | 973 | // CreateAndOpenfile creates a file if it doesn't exist, opens it and returns 974 | // a file descriptor, or any error. An existing file is only overwritten 975 | // if overwrite is true. 976 | func createAndOpenFile(targetDir string, targetName string, 977 | overwrite bool) (*os.File, error) { 978 | 979 | log.SetPrefix("createAndOpenFile ") 980 | 981 | if verbose { 982 | log.Printf("%s/%s verbose %v", targetDir, targetName, verbose) 983 | } 984 | 985 | // Ensure that the target directory exists. 986 | err := os.MkdirAll(targetDir, 0777) 987 | if err != nil { 988 | log.Printf("cannot create target directory %s - %s ", targetDir, err.Error()) 989 | return nil, err 990 | } 991 | 992 | // If the file already exists, do not write to it except in overwrite 993 | // mode. 994 | 995 | if !overwrite { 996 | path, err := filepath.Abs(targetDir) 997 | if err != nil { 998 | log.Printf("cannot find path for target directory %s - %s", targetDir, 999 | err.Error()) 1000 | os.Exit(-1) 1001 | } 1002 | dir, err := os.Open(path) 1003 | if err != nil { 1004 | log.Printf("cannot open target directory %s - %s ", 1005 | targetDir, err.Error()) 1006 | } 1007 | 1008 | defer dir.Close() 1009 | 1010 | // Get the contents of the target directory 1011 | fileInfoList, err := dir.Readdir(0) 1012 | if err != nil { 1013 | log.Printf("cannot scan target directory %s - %s ", 1014 | targetDir, err.Error()) 1015 | return nil, err 1016 | } 1017 | 1018 | // Scan the target directory to see if the file already exists. 1019 | for _, fileInfo := range fileInfoList { 1020 | if fileInfo.Name() == targetName { 1021 | if verbose { 1022 | log.Printf("file %s/%s already exists and overwrite mode is off.", 1023 | targetDir, targetName) 1024 | } 1025 | return nil, nil 1026 | } 1027 | } 1028 | } 1029 | 1030 | targetPathName := targetDir + "/" + targetName 1031 | 1032 | if verbose { 1033 | log.Printf("Creating file %s - overwrite %v.", 1034 | targetPathName, overwrite) 1035 | } 1036 | file, err := os.Create(targetPathName) 1037 | if err != nil { 1038 | log.Println("cannot create target file %s for writing - %s ", 1039 | targetPathName, err.Error()) 1040 | return nil, err 1041 | } 1042 | 1043 | return file, nil 1044 | } 1045 | 1046 | // lowerFirstRune takes a string and ensures that the first rune is lower case. 1047 | // From https://play.golang.org/p/D8cYDgfZr8 via 1048 | // https://groups.google.com/forum/#!topic/golang-nuts/WfpmVDQFecU 1049 | func lowerFirstRune(s string) string { 1050 | if s == "" { 1051 | return "" 1052 | } 1053 | r, n := utf8.DecodeRuneInString(s) 1054 | if r == utf8.RuneError { 1055 | return s 1056 | } 1057 | return string(unicode.ToLower(r)) + s[n:] 1058 | } 1059 | 1060 | // upperFirstRune takes a string and ensures that the first rune is upper case. 1061 | // For origins, see lowerFirstRune. 1062 | func upperFirstRune(s string) string { 1063 | if s == "" { 1064 | return "" 1065 | } 1066 | r, n := utf8.DecodeRuneInString(s) 1067 | if r == utf8.RuneError { 1068 | return s 1069 | } 1070 | return string(unicode.ToUpper(r)) + s[n:] 1071 | } 1072 | -------------------------------------------------------------------------------- /setenv.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # 3 | # Set the environment variables for building the scaffolder. 4 | # 5 | # Change directory to the one containing this file and run it, for example: 6 | # 7 | # cd $HOME/workspaces/films 8 | # . setenv.sh 9 | 10 | if test -z $GOPATH 11 | then 12 | GOPATH=`pwd` 13 | export GOPATH 14 | else 15 | GOPATH=$GOPATH:`pwd` 16 | export GOPATH 17 | fi 18 | 19 | PATH=`pwd`/bin:$PATH 20 | export PATH 21 | -------------------------------------------------------------------------------- /templates/controller.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{.NameWithLowerFirst}} 4 | 5 | {{.Imports}} 6 | 7 | // Generated by the goblimey scaffold generator. You are STRONGLY 8 | // recommended not to alter this file, as it will be overwritten next time the 9 | // scaffolder is run. For the same reason, do not commit this file to a 10 | // source code repository. Commit the json specification which was used to 11 | // produce it. 12 | 13 | // Package {{.PluralNameWithLowerFirst}} provides the controller for the {{.PluralNameWithLowerFirst}} resource. It provides a 14 | // set of action functions that are triggered by HTTP requests and implement the 15 | // Create, Read, Update and Delete (CRUD) operations on the {{.PluralNameWithLowerFirst}} resource: 16 | // 17 | // GET {{.PluralNameWithLowerFirst}}/ - runs Index() to list all {{.PluralNameWithLowerFirst}} 18 | // GET {{.PluralNameWithLowerFirst}}/n - runs Show() to display the details of the {{.NameWithLowerFirst}} with ID n 19 | // GET {{.PluralNameWithLowerFirst}}/create - runs New() to display the page to create a {{.NameWithLowerFirst}} using any data in the form to pre-populate it 20 | // PUT {{.PluralNameWithLowerFirst}}/n - runs Create() to create a new {{.NameWithLowerFirst}} using the data in the supplied form 21 | // GET {{.PluralNameWithLowerFirst}}/n/edit - runs Edit() to display the page to edit the {{.NameWithLowerFirst}} with ID n, using any data in the form to pre-populate it 22 | // PUT {{.PluralNameWithLowerFirst}}/n - runs Update() to update the {{.NameWithLowerFirst}} with ID n using the data in the form 23 | // DELETE {{.PluralNameWithLowerFirst}}/n - runs Delete() to delete the {{.NameWithLowerFirst}} with id n 24 | 25 | type Controller struct { 26 | services services.Services 27 | verbose bool 28 | } 29 | 30 | // MakeController is a factory that creates a {{.PluralNameWithLowerFirst}} controller 31 | func MakeController(services services.Services, verbose bool) Controller { 32 | var controller Controller 33 | controller.SetServices(services) 34 | controller.SetVerbose(verbose) 35 | return controller 36 | } 37 | 38 | // Index fetches a list of all valid {{.PluralNameWithLowerFirst}} and displays the index page. 39 | func (c Controller) Index(req *restful.Request, resp *restful.Response, 40 | form {{.NameWithLowerFirst}}Forms.ListForm) { 41 | 42 | log.SetPrefix("Index()") 43 | 44 | c.List{{.PluralNameWithUpperFirst}}(req, resp, form) 45 | return 46 | } 47 | 48 | // Show displays the details of the {{.NameWithLowerFirst}} with the ID given in the URI. 49 | func (c Controller) Show(req *restful.Request, resp *restful.Response, 50 | form {{.NameWithLowerFirst}}Forms.SingleItemForm) { 51 | 52 | log.SetPrefix("Show()") 53 | 54 | repository := c.services.{{.NameWithUpperFirst}}Repository() 55 | 56 | // Get the details of the {{.NameWithLowerFirst}} with the given ID. 57 | {{.NameWithLowerFirst}}, err := repository.FindByID(form.{{.NameWithUpperFirst}}().ID()) 58 | if err != nil { 59 | // no such {{.NameWithLowerFirst}}. Display index page with error message 60 | em := "no such {{.NameWithLowerFirst}}" 61 | log.Printf("%s\n", em) 62 | c.ErrorHandler(req, resp, em) 63 | return 64 | } 65 | 66 | // The {{.NameWithLowerFirst}} in the form contains just an ID. Replace it with the 67 | // complete {{.NameWithLowerFirst}} record that we just fetched. 68 | form.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 69 | 70 | page := c.services.Template("{{.NameWithLowerFirst}}", "Show") 71 | if page == nil { 72 | em := fmt.Sprintf("internal error displaying Show page - no HTML template") 73 | log.Printf("%s\n", em) 74 | c.ErrorHandler(req, resp, em) 75 | return 76 | } 77 | 78 | err = page.Execute(resp.ResponseWriter, form) 79 | if err != nil { 80 | em := fmt.Sprintf("error displaying page - %s", err.Error()) 81 | log.Printf("%s\n", em) 82 | c.ErrorHandler(req, resp, em) 83 | return 84 | } 85 | return 86 | } 87 | 88 | // New displays the page to create a {{.NameWithLowerFirst}}, 89 | func (c Controller) New(req *restful.Request, resp *restful.Response, 90 | form {{.NameWithLowerFirst}}Forms.SingleItemForm) { 91 | 92 | log.SetPrefix("New()") 93 | 94 | // Display the page. 95 | page := c.services.Template("{{.NameWithLowerFirst}}", "Create") 96 | if page == nil { 97 | em := fmt.Sprintf("internal error displaying Create page - no HTML template") 98 | log.Printf("%s\n", em) 99 | c.ErrorHandler(req, resp, em) 100 | return 101 | } 102 | err := page.Execute(resp.ResponseWriter, form) 103 | if err != nil { 104 | log.Printf("error displaying new page - %s", err.Error()) 105 | em := fmt.Sprintf("error displaying page - %s", err.Error()) 106 | c.ErrorHandler(req, resp, em) 107 | return 108 | } 109 | } 110 | 111 | // Create creates a {{.NameWithLowerFirst}} using the data from the HTTP form displayed 112 | // by a previous NEW request. 113 | func (c Controller) Create(req *restful.Request, resp *restful.Response, 114 | form {{.NameWithLowerFirst}}Forms.SingleItemForm) { 115 | 116 | log.SetPrefix("Create()") 117 | 118 | if !(form.Valid()) { 119 | // validation errors. Return to create screen with error messages in the form data 120 | if c.verbose { 121 | log.Printf("Validation failed\n") 122 | } 123 | page := c.services.Template("{{.NameWithLowerFirst}}", "Create") 124 | if page == nil { 125 | em := fmt.Sprintf("internal error displaying Create page - no HTML template") 126 | log.Printf("%s\n", em) 127 | c.ErrorHandler(req, resp, em) 128 | return 129 | } 130 | err := page.Execute(resp.ResponseWriter, &form) 131 | if err != nil { 132 | em := fmt.Sprintf("Internal error while preparing create form after failed validation - %s", 133 | err.Error()) 134 | log.Printf("%s\n", em) 135 | c.ErrorHandler(req, resp, em) 136 | return 137 | } 138 | return 139 | } 140 | 141 | // Create a {{.NameWithLowerFirst}} in the database using the validated data in the form 142 | repository := c.services.{{.NameWithUpperFirst}}Repository() 143 | 144 | created{{.NameWithUpperFirst}}, err := repository.Create(form.{{.NameWithUpperFirst}}()) 145 | if err != nil { 146 | // Failed to create {{.NameWithLowerFirst}}. Display index page with error message. 147 | em := fmt.Sprintf("Could not create {{.NameWithLowerFirst}} %s - %s", form.{{.NameWithUpperFirst}}().DisplayName(), err.Error()) 148 | c.ErrorHandler(req, resp, em) 149 | return 150 | } 151 | 152 | // Success! {{.NameWithUpperFirst}} created. Display index page with confirmation notice 153 | notice := fmt.Sprintf("created {{.NameWithLowerFirst}} %s", created{{.NameWithUpperFirst}}.DisplayName()) 154 | if c.verbose { 155 | log.Printf("%s\n", notice) 156 | } 157 | listForm := c.services.Make{{.NameWithUpperFirst}}ListForm() 158 | listForm.SetNotice(notice) 159 | c.List{{.PluralNameWithUpperFirst}}(req, resp, listForm) 160 | return 161 | } 162 | 163 | // Edit fetches the data for the {{.PluralNameWithLowerFirst}} record with the given ID and displays 164 | // the edit page, populated with that data. 165 | func (c Controller) Edit(req *restful.Request, resp *restful.Response, 166 | form {{.NameWithLowerFirst}}Forms.SingleItemForm) { 167 | 168 | log.SetPrefix("Edit() ") 169 | 170 | id := form.{{.NameWithUpperFirst}}().ID() 171 | 172 | repository := c.services.{{.NameWithUpperFirst}}Repository() 173 | // Get the existing data for the {{.NameWithLowerFirst}} 174 | {{.NameWithLowerFirst}}, err := repository.FindByID(id) 175 | if err != nil { 176 | // No such {{.NameWithLowerFirst}}. Display index page with error message. 177 | em := err.Error() 178 | log.Printf("%s\n", em) 179 | c.ErrorHandler(req, resp, em) 180 | return 181 | } 182 | // Got the {{.NameWithLowerFirst}} with the given ID. Put it into the form and validate it. 183 | // If the data is invalid, continue - the user may be trying to fix it. 184 | 185 | form.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 186 | if c.verbose && !form.Validate() { 187 | em := fmt.Sprintf("invalid record in the {{.PluralNameWithLowerFirst}} database - %s", 188 | {{.NameWithLowerFirst}}.String()) 189 | log.Printf("%s\n", em) 190 | } 191 | 192 | // Display the edit page 193 | page := c.services.Template("{{.NameWithLowerFirst}}", "Edit") 194 | if page == nil { 195 | em := fmt.Sprintf("internal error displaying Edit page - no HTML template") 196 | log.Printf("%s\n", em) 197 | c.ErrorHandler(req, resp, em) 198 | return 199 | } 200 | err = page.Execute(resp.ResponseWriter, form) 201 | if err != nil { 202 | // error while preparing edit page 203 | log.Printf("%s: error displaying edit page - %s", err.Error()) 204 | em := fmt.Sprintf("error displaying page - %s", err.Error()) 205 | c.ErrorHandler(req, resp, em) 206 | } 207 | } 208 | 209 | // Update responds to a PUT request. For example: 210 | // PUT /{{.PluralNameWithLowerFirst}}/1 211 | // It's invoked by the form displayed by a previous Edit request. If the ID in the URI is 212 | // valid and the request parameters from the form specify valid {{.PluralNameWithLowerFirst}} data, it updates the 213 | // record and displays the index page with a confirmation message, otherwise it displays 214 | // the edit page again with the given data and some error messages. 215 | func (c Controller) Update(req *restful.Request, resp *restful.Response, 216 | form {{.NameWithLowerFirst}}Forms.SingleItemForm) { 217 | 218 | log.SetPrefix("Update() ") 219 | 220 | if !form.Valid() { 221 | // The supplied data is invalid. The validator has set error messages. 222 | // Return to the edit screen. 223 | if c.verbose { 224 | log.Printf("Validation failed\n") 225 | } 226 | page := c.services.Template("{{.NameWithLowerFirst}}", "Edit") 227 | if page == nil { 228 | em := fmt.Sprintf("internal error displaying Edit page - no HTML template") 229 | log.Printf("%s\n", em) 230 | c.ErrorHandler(req, resp, em) 231 | return 232 | } 233 | err := page.Execute(resp.ResponseWriter, form) 234 | if err != nil { 235 | log.Printf("%s: error displaying edit page - %s", err.Error()) 236 | em := fmt.Sprintf("error displaying page - %s", err.Error()) 237 | c.ErrorHandler(req, resp, em) 238 | return 239 | } 240 | return 241 | } 242 | 243 | if form.{{.NameWithUpperFirst}}() == nil { 244 | em := fmt.Sprint("internal error - form should contain an updated {{.NameWithLowerFirst}} record") 245 | log.Printf("%s\n", em) 246 | c.ErrorHandler(req, resp, em) 247 | return 248 | } 249 | 250 | // Get the {{.NameWithLowerFirst}} specified in the form from the DB. 251 | // If that fails, the id in the form doesn't match any record. 252 | repository := c.services.{{.NameWithUpperFirst}}Repository() 253 | {{.NameWithLowerFirst}}, err := repository.FindByID(form.{{.NameWithUpperFirst}}().ID()) 254 | if err != nil { 255 | // There is no {{.NameWithLowerFirst}} with this ID. The ID is chosen by the user from a 256 | // supplied list and it should always be valid, so there's something screwy 257 | // going on. Display the index page with an error message. 258 | em := fmt.Sprintf("error searching for {{.NameWithLowerFirst}} with id %s - %s", 259 | form.{{.NameWithUpperFirst}}().ID(), err.Error()) 260 | log.Printf("%s\n", em) 261 | c.ErrorHandler(req, resp, em) 262 | return 263 | } 264 | 265 | // We have a matching {{.NameWithLowerFirst}} from the DB. 266 | if c.verbose { 267 | log.Printf("got {{.NameWithLowerFirst}} %v\n", {{.NameWithLowerFirst}}) 268 | } 269 | 270 | // we have a record and valid new values. Update. 271 | {{range .Fields}} 272 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}(form.{{$resourceNameUpper}}().{{.NameWithUpperFirst}}()) 273 | {{end}} 274 | if c.verbose { 275 | log.Printf("updating {{.NameWithLowerFirst}} to %v\n", {{.NameWithLowerFirst}}) 276 | } 277 | _, err = repository.Update({{.NameWithLowerFirst}}) 278 | if err != nil { 279 | // The commit failed. Display the edit page with an error message 280 | em := fmt.Sprintf("Could not update {{.NameWithLowerFirst}} - %s", err.Error()) 281 | log.Printf("%s\n", em) 282 | form.SetErrorMessage(em) 283 | 284 | page := c.services.Template("{{.NameWithLowerFirst}}", "Edit") 285 | if page == nil { 286 | em := fmt.Sprintf("internal error displaying Edit page - no HTML template") 287 | log.Printf("%s\n", em) 288 | c.ErrorHandler(req, resp, em) 289 | return 290 | } 291 | err = page.Execute(resp.ResponseWriter, form) 292 | if err != nil { 293 | // Error while recovering from another error. This is looking like a habit! 294 | em := fmt.Sprintf("Internal error while preparing edit page after failing to update {{.NameWithLowerFirst}} in DB - %s", err.Error()) 295 | log.Printf("%s\n", em) 296 | c.ErrorHandler(req, resp, em) 297 | } else { 298 | return 299 | } 300 | } 301 | 302 | // Success! Display the index page with a confirmation notice 303 | notice := fmt.Sprintf("updated {{.NameWithLowerFirst}} %s", form.{{.NameWithUpperFirst}}().DisplayName()) 304 | if c.verbose { 305 | log.Printf("%s:\n", notice) 306 | } 307 | listForm := c.services.Make{{.NameWithUpperFirst}}ListForm() 308 | listForm.SetNotice(notice) 309 | c.List{{.PluralNameWithUpperFirst}}(req, resp, listForm) 310 | return 311 | } 312 | 313 | // Delete responds to a DELETE request and deletes the record with the given ID, 314 | // eg DELETE http://server:port/{{.PluralNameWithLowerFirst}}/1. 315 | func (c Controller) Delete(req *restful.Request, resp *restful.Response, 316 | form {{.NameWithLowerFirst}}Forms.SingleItemForm) { 317 | 318 | log.SetPrefix("Delete()") 319 | 320 | repository := c.services.{{.NameWithUpperFirst}}Repository() 321 | // Attempt the delete 322 | _, err := repository.DeleteByID(form.{{.NameWithUpperFirst}}().ID()) 323 | if err != nil { 324 | // failed - cannot delete {{.NameWithLowerFirst}} 325 | em := fmt.Sprintf("Cannot delete {{.NameWithLowerFirst}} with id %d - %s", 326 | form.{{.NameWithUpperFirst}}().ID(), err.Error()) 327 | log.Printf("%s\n", em) 328 | c.ErrorHandler(req, resp, em) 329 | return 330 | } 331 | // Success - {{.NameWithLowerFirst}} deleted. Display the index view with a notification. 332 | listForm := c.services.Make{{.NameWithUpperFirst}}ListForm() 333 | notice := fmt.Sprintf("deleted {{.NameWithLowerFirst}} with id %d", 334 | form.{{.NameWithUpperFirst}}().ID()) 335 | if c.verbose { 336 | log.Printf("%s:\n", notice) 337 | } 338 | listForm.SetNotice(notice) 339 | c.List{{.PluralNameWithUpperFirst}}(req, resp, listForm) 340 | return 341 | } 342 | 343 | // ErrorHandler displays the index page with an error message 344 | func (c Controller) ErrorHandler(req *restful.Request, resp *restful.Response, 345 | errormessage string) { 346 | 347 | form := c.services.Make{{.NameWithUpperFirst}}ListForm() 348 | form.SetErrorMessage(errormessage) 349 | c.List{{.PluralNameWithUpperFirst}}(req, resp, form) 350 | } 351 | 352 | // SetServices sets the services. 353 | func (c *Controller) SetServices(services services.Services) { 354 | c.services = services 355 | } 356 | 357 | // SetVerbose sets the verbosity level. 358 | func (c *Controller) SetVerbose(verbose bool) { 359 | c.verbose = verbose 360 | } 361 | 362 | /* 363 | * The List{{.PluralNameWithUpperFirst}} helper method fetches a list of {{.PluralNameWithLowerFirst}} and displays the 364 | * index page. It's used to fulfil an index request but the index page is 365 | * also used as the last page of a sequence of requests (for example new, 366 | * create, index). If the sequence was successful, the form may contain a 367 | * confirmation note. If the sequence failed, the form should contain an error 368 | * message. 369 | */ 370 | func (c Controller) List{{.PluralNameWithUpperFirst}}(req *restful.Request, resp *restful.Response, 371 | form {{.NameWithLowerFirst}}Forms.ListForm) { 372 | 373 | log.SetPrefix("Controller.List{{.PluralNameWithUpperFirst}}() ") 374 | 375 | repository := c.services.{{.NameWithUpperFirst}}Repository() 376 | 377 | {{.PluralNameWithLowerFirst}}List, err := repository.FindAll() 378 | if err != nil { 379 | em := fmt.Sprintf("error getting the list of {{.PluralNameWithLowerFirst}} - %s", err.Error()) 380 | log.Printf("%s\n", em) 381 | form.SetErrorMessage(em) 382 | } 383 | if c.verbose{ 384 | log.Printf("%d {{.PluralNameWithLowerFirst}}", len({{.PluralNameWithLowerFirst}}List)) 385 | } 386 | if len({{.PluralNameWithLowerFirst}}List) <= 0 { 387 | form.SetNotice("there are no {{.PluralNameWithLowerFirst}} currently set up") 388 | } 389 | form.Set{{.PluralNameWithUpperFirst}}({{.PluralNameWithLowerFirst}}List) 390 | 391 | // Display the index page 392 | page := c.services.Template("{{.NameWithLowerFirst}}", "Index") 393 | if page == nil { 394 | log.Printf("no Index page for {{.NameWithLowerFirst}} controller") 395 | utilities.Dead(resp) 396 | return 397 | } 398 | err = page.Execute(resp.ResponseWriter, form) 399 | if err != nil { 400 | /* 401 | * Error while displaying the index page. We handle most internal 402 | * errors by displaying the controller's index page. That's just failed, 403 | * so fall back to the static error page. 404 | */ 405 | log.Printf(err.Error()) 406 | page = c.services.Template("html", "Error") 407 | if page == nil { 408 | log.Printf("no Error page") 409 | utilities.Dead(resp) 410 | return 411 | } 412 | err = page.Execute(resp.ResponseWriter, form) 413 | if err != nil { 414 | // Can't display the static error page either. Bale out. 415 | em := fmt.Sprintf("fatal error - failed to display error page for error %s\n", err.Error()) 416 | log.Printf(em) 417 | panic(em) 418 | } 419 | return 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /templates/controller.test.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | {{$resourceNamePluralUpper := .PluralNameWithUpperFirst}} 4 | package {{.NameWithLowerFirst}} 5 | 6 | {{.Imports}} 7 | 8 | // Generated by the goblimey scaffold generator. You are STRONGLY 9 | // recommended not to alter this file, as it will be overwritten next time the 10 | // scaffolder is run. For the same reason, do not commit this file to a 11 | // source code repository. Commit the json specification which was used to 12 | // produce it. 13 | 14 | // Unit tests for the {{.NameWithLowerFirst}} controller. Uses mock objects 15 | // created by pegomock. 16 | 17 | var panicValue string 18 | 19 | {{/* This creates the expected values using the field names and the test 20 | values, something like: 21 | var expectedName1 string = "s1" 22 | var expectedAge1 int64 = 2 23 | var expectedName2 string = "s3" 24 | var expectedAge2 int64 = 4 */}} 25 | {{range $index, $element := .Fields}} 26 | {{if eq .Type "string"}} 27 | var expected{{.NameWithUpperFirst}}1 {{.GoType}} = "{{index .TestValues 0}}" 28 | {{else}} 29 | var expected{{.NameWithUpperFirst}}1 {{.GoType}} = {{index .TestValues 0}} 30 | {{end}} 31 | {{if eq .Type "string"}} 32 | var expected{{.NameWithUpperFirst}}2 {{.GoType}} = "{{index .TestValues 1}}" 33 | {{else}} 34 | var expected{{.NameWithUpperFirst}}2 {{.GoType}} = {{index .TestValues 1}} 35 | {{end}} 36 | {{end}} 37 | 38 | // TestUnitIndexWithOne{{.NameWithUpperFirst}} checks that the Index method of the 39 | // {{.NameWithLowerFirst}} controller handles a list of {{.PluralNameWithLowerFirst}} from FindAll() containing one {{.NameWithLowerFirst}}. 40 | func TestUnitIndexWithOne{{.NameWithUpperFirst}}(t *testing.T) { 41 | 42 | var expectedID1 uint64 = 42 43 | 44 | pegomock.RegisterMockTestingT(t) 45 | 46 | // Create a list containing one {{.NameWithLowerFirst}}. 47 | expected{{.NameWithUpperFirst}}1 := {{.NameWithLowerFirst}}.MakeInitialised{{$resourceNameUpper}}(expectedID1, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 48 | expected{{.NameWithUpperFirst}}List := make([]{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, 1) 49 | expected{{.NameWithUpperFirst}}List[0] = expected{{.NameWithUpperFirst}}1 50 | 51 | // Create the mocks and dummy objects. 52 | var url url.URL 53 | url.Opaque = "/{{.PluralNameWithLowerFirst}}" // url.RequestURI() will return "/{{.PluralNameWithLowerFirst}}" 54 | var httpRequest http.Request 55 | httpRequest.URL = &url 56 | httpRequest.Method = "GET" 57 | var request restful.Request 58 | request.Request = &httpRequest 59 | writer := mocks.NewMockResponseWriter() 60 | var response restful.Response 61 | response.ResponseWriter = writer 62 | mockTemplate := mocks.NewMockTemplate() 63 | mockRepository := mock{{.NameWithUpperFirst}}.NewMockRepository() 64 | 65 | innerPageMap := make(map[string]retrofitTemplate.Template) 66 | innerPageMap["Index"] = mockTemplate 67 | pageMap := make(map[string]map[string]retrofitTemplate.Template) 68 | pageMap["{{.NameWithLowerFirst}}"] = innerPageMap 69 | 70 | // Create a service that returns the mock repository and templates. 71 | var services services.ConcreteServices 72 | services.Set{{.NameWithUpperFirst}}Repository(mockRepository) 73 | services.SetTemplates(&pageMap) 74 | 75 | // Create the form 76 | form := {{.NameWithLowerFirst}}Forms.MakeListForm() 77 | 78 | // Expect the controller to call the {{.NameWithLowerFirst}} repository's FindAll method. Return 79 | // the list containing one {{.NameWithLowerFirst}}. 80 | pegomock.When(mockRepository.FindAll()).ThenReturn(expected{{.NameWithUpperFirst}}List, nil) 81 | 82 | // The request supplies method "GET" and URI "/{{.PluralNameWithLowerFirst}}". Expect 83 | // template.Execute to be called and return nil (no error). 84 | pegomock.When(mockTemplate.Execute(writer, form)).ThenReturn(nil) 85 | 86 | // Run the test. 87 | var controller Controller 88 | controller.SetServices(&services) 89 | controller.Index(&request, &response, form) 90 | 91 | // We expect that the form contains the expected {{.NameWithLowerFirst}} list - 92 | // one {{.NameWithLowerFirst}} object with contents as expected. 93 | if form.{{.PluralNameWithUpperFirst}}() == nil { 94 | t.Errorf("Expected a list, got nil") 95 | } 96 | 97 | if len(form.{{.PluralNameWithUpperFirst}}()) != 1 { 98 | t.Errorf("Expected a list of 1, got %d", len(form.{{.PluralNameWithUpperFirst}}())) 99 | } 100 | 101 | if form.{{.PluralNameWithUpperFirst}}()[0].ID() != expectedID1 { 102 | t.Errorf("Expected ID %d, got %d", 103 | expectedID1, form.{{.PluralNameWithUpperFirst}}()[0].ID()) 104 | } 105 | {{range .Fields}} 106 | if form.{{$resourceNamePluralUpper}}()[0].{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}}1 { 107 | t.Errorf("Expected {{.NameWithLowerFirst}} %v, got %v", 108 | expected{{.NameWithUpperFirst}}1, form.{{$resourceNamePluralUpper}}()[0].{{.NameWithUpperFirst}}()) 109 | } 110 | {{end}} 111 | } 112 | 113 | // TestUnitIndexWithErrorWhenFetching{{.PluralNameWithUpperFirst}} checks that the {{.NameWithLowerFirst}} controller's 114 | // Index() method handles errors from FindAll() correctly. 115 | func TestUnitIndexWithErrorWhenFetching{{.PluralNameWithUpperFirst}}(t *testing.T) { 116 | 117 | log.SetPrefix("TestUnitIndexWithErrorWhenFetching{{.PluralNameWithUpperFirst}} ") 118 | log.Printf("This test is expected to provoke error messages in the log") 119 | 120 | expectedErr := errors.New("Test Error Message") 121 | expectedErrorMessage := "error getting the list of {{.PluralNameWithLowerFirst}} - Test Error Message" 122 | 123 | // Create the mocks and dummy objects. 124 | pegomock.RegisterMockTestingT(t) 125 | var url url.URL 126 | url.Opaque = "/{{.PluralNameWithLowerFirst}}" // url.RequestURI() will return "/{{.PluralNameWithLowerFirst}}" 127 | var httpRequest http.Request 128 | httpRequest.URL = &url 129 | httpRequest.Method = "GET" 130 | var request restful.Request 131 | request.Request = &httpRequest 132 | writer := mocks.NewMockResponseWriter() 133 | var response restful.Response 134 | response.ResponseWriter = writer 135 | mockTemplate := mocks.NewMockTemplate() 136 | mockRepository := mock{{.NameWithUpperFirst}}.NewMockRepository() 137 | 138 | // Create the form 139 | form := {{.NameWithLowerFirst}}Forms.MakeListForm() 140 | 141 | 142 | // Expect the controller to call the {{.NameWithLowerFirst}} repository's FindAll method. Return 143 | // the list containing one {{.NameWithLowerFirst}}. 144 | pegomock.When(mockRepository.FindAll()).ThenReturn(nil, expectedErr) 145 | 146 | // Expect the controller to call the tenmplate's Execute() method. Return 147 | // nil (no error). 148 | pegomock.When(mockTemplate.Execute(writer, form)).ThenReturn(nil) 149 | 150 | innerPageMap := make(map[string]retrofitTemplate.Template) 151 | innerPageMap["Index"] = mockTemplate 152 | pageMap := make(map[string]map[string]retrofitTemplate.Template) 153 | pageMap["{{.NameWithLowerFirst}}"] = innerPageMap 154 | 155 | // Create a service that returns the mock repository and templates. 156 | var services services.ConcreteServices 157 | services.Set{{.NameWithUpperFirst}}Repository(mockRepository) 158 | services.SetTemplates(&pageMap) 159 | 160 | // Create the controller and run the test. 161 | controller := MakeController(&services, false) 162 | controller.Index(&request, &response, form) 163 | 164 | // Verify that the form contains the expected error message. 165 | if form.ErrorMessage() != expectedErrorMessage { 166 | t.Errorf("Expected error message to be %s actually %s", expectedErrorMessage, form.ErrorMessage()) 167 | } 168 | } 169 | 170 | 171 | // TestUnitIndexWithManyFailures checks that the {{.PluralNameWithUpperFirst}} controller's 172 | // Index() method handles a series of errors correctly. 173 | // 174 | // Panic handling based on http://stackoverflow.com/questions/31595791/how-to-test-panics 175 | // 176 | func TestUnitIndexWithManyFailures(t *testing.T) { 177 | 178 | log.SetPrefix("TestUnitIndexWithManyFailures ") 179 | log.Printf("This test is expected to provoke error messages in the log") 180 | 181 | em1 := "first error message" 182 | 183 | expectedFirstErrorMessage := errors.New(em1) 184 | 185 | em2 := "second error message" 186 | expectedSecondErrorMessage := errors.New(em2) 187 | 188 | em3 := "final error message" 189 | finalErrorMessage := errors.New(em3) 190 | 191 | // Create the mocks and dummy objects. 192 | pegomock.RegisterMockTestingT(t) 193 | var url url.URL 194 | url.Opaque = "/{{.PluralNameWithLowerFirst}}" // url.RequestURI() will return "/{{.PluralNameWithLowerFirst}}" 195 | var httpRequest http.Request 196 | httpRequest.URL = &url 197 | httpRequest.Method = "GET" 198 | var request restful.Request 199 | request.Request = &httpRequest 200 | mockResponseWriter := mocks.NewMockResponseWriter() 201 | var response restful.Response 202 | response.ResponseWriter = mockResponseWriter 203 | mockIndexTemplate := mocks.NewMockTemplate() 204 | mockErrorTemplate := mocks.NewMockTemplate() 205 | mockRepository := mock{{.NameWithUpperFirst}}.NewMockRepository() 206 | 207 | // Create a template map containing the mock templates 208 | pageMap := make(map[string]map[string]retrofitTemplate.Template) 209 | pageMap["html"] = make(map[string]retrofitTemplate.Template) 210 | pageMap["html"]["Error"] = mockErrorTemplate 211 | pageMap["{{.NameWithLowerFirst}}"] = make(map[string]retrofitTemplate.Template) 212 | pageMap["{{.NameWithLowerFirst}}"]["Index"] = mockIndexTemplate 213 | 214 | // Create a service that returns the mock repository and templates. 215 | var services services.ConcreteServices 216 | services.Set{{.NameWithUpperFirst}}Repository(mockRepository) 217 | services.SetTemplates(&pageMap) 218 | 219 | // Create the form 220 | form := {{.NameWithLowerFirst}}Forms.MakeListForm() 221 | 222 | // Expectations: 223 | // Index will run List{{.PluralNameWithUpperFirst}} which will call the {{.NameWithLowerFirst}} 224 | // repository's FindAll(). Make that return an error, then List{{.PluralNameWithUpperFirst}} 225 | // will get the Index page from the template and call its Execute method. Make 226 | // that fail, and the controller will get the error page and call its Execute 227 | // method. Make that fail and the app will panic with a message "fatal error - 228 | // failed to display error page for error ", followed by the error message from 229 | // the last Execute call. 230 | 231 | pegomock.When(mockRepository.FindAll()).ThenReturn(nil, 232 | expectedFirstErrorMessage) 233 | pegomock.When(mockIndexTemplate.Execute(mockResponseWriter, form)). 234 | ThenReturn(expectedSecondErrorMessage) 235 | pegomock.When(mockErrorTemplate.Execute(mockResponseWriter, form)). 236 | ThenReturn(finalErrorMessage) 237 | 238 | // Expect a panic, catch it and check the value. (If there is no panic, 239 | // this raises an error.) 240 | 241 | defer func() { 242 | r := recover() 243 | if r == nil { 244 | t.Errorf("Expected the Index call to panic") 245 | } else { 246 | em := fmt.Sprintf("%s", r) 247 | // Verify that the panic value is as expected. 248 | if !strings.Contains(em, em3) { 249 | t.Errorf("Expected a panic with value containing \"%s\" actually \"%s\"", 250 | em3, em) 251 | } 252 | } 253 | }() 254 | 255 | // Run the test. 256 | controller := MakeController(&services, false) 257 | controller.Index(&request, &response, form) 258 | 259 | // Verify that the form has an error message containing the expected text. 260 | if strings.Contains(form.ErrorMessage(), em1) { 261 | t.Errorf("Expected error message to be \"%s\" actually \"%s\"", 262 | expectedFirstErrorMessage, form.ErrorMessage()) 263 | } 264 | 265 | // Verify that the list of {{.PluralNameWithLowerFirst}} is nil 266 | if form.{{.PluralNameWithUpperFirst}}() != nil { 267 | t.Errorf("Expected the list of {{.PluralNameWithLowerFirst}} to be nil. Actually contains %d entries", 268 | len(form.{{.PluralNameWithUpperFirst}}())) 269 | } 270 | 271 | } 272 | 273 | // TestUnitSuccessfulCreate checks that the {{.NameWithLowerFirst}} controller's Create method 274 | // correctly handles a successful attempt to create a {{.NameWithLowerFirst}} in the database. 275 | func TestUnitSuccessfulCreate(t *testing.T) { 276 | 277 | log.SetPrefix("TestUnitSuccessfulCreate ") 278 | 279 | expectedNoticeFragment := "created {{.NameWithLowerFirst}}" 280 | {{range .Fields}} 281 | {{if not .ExcludeFromDisplay}} 282 | {{if eq .Type "int"}} 283 | expected{{.NameWithUpperFirst}}1_str := fmt.Sprintf("%d", expected{{.NameWithUpperFirst}}1) 284 | {{end}} 285 | {{if eq .Type "float"}} 286 | expected{{.NameWithUpperFirst}}1_str := fmt.Sprintf("%f", expected{{.NameWithUpperFirst}}1) 287 | {{end}} 288 | {{if eq .Type "bool"}} 289 | expected{{.NameWithUpperFirst}}1_str := fmt.Sprintf("%v", expected{{.NameWithUpperFirst}}1) 290 | {{end}} 291 | {{end}} 292 | {{end}} 293 | pegomock.RegisterMockTestingT(t) 294 | 295 | // Create the mocks and dummy objects. 296 | var expectedID1 uint64 = 42 297 | expected{{.NameWithUpperFirst}}1 := {{.NameWithLowerFirst}}.MakeInitialised{{$resourceNameUpper}}(expectedID1, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 298 | singleItemForm := {{.NameWithLowerFirst}}Forms.MakeInitialisedSingleItemForm(expected{{.NameWithUpperFirst}}1) 299 | listForm := {{.NameWithLowerFirst}}Forms.MakeListForm() 300 | {{.PluralNameWithLowerFirst}} := make([]{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, 1) 301 | {{.PluralNameWithLowerFirst}}[0] = expected{{.NameWithUpperFirst}}1 302 | listForm.Set{{.PluralNameWithUpperFirst}}({{.PluralNameWithLowerFirst}}) 303 | var url url.URL 304 | url.Opaque = "/{{.PluralNameWithLowerFirst}}/42" // url.RequestURI() will return "/{{.PluralNameWithLowerFirst}}/42" 305 | var httpRequest http.Request 306 | httpRequest.URL = &url 307 | httpRequest.Method = "POST" 308 | var request restful.Request 309 | request.Request = &httpRequest 310 | writer := mocks.NewMockResponseWriter() 311 | var response restful.Response 312 | response.ResponseWriter = writer 313 | mockIndexTemplate := mocks.NewMockTemplate() 314 | mockCreateTemplate := mocks.NewMockTemplate() 315 | mockRepository := mock{{.NameWithUpperFirst}}.NewMockRepository() 316 | mockServices := mocks.NewMockServices() 317 | 318 | // Create a template map containing the mock templates 319 | pageMap := make(map[string]map[string]retrofitTemplate.Template) 320 | pageMap["{{.NameWithLowerFirst}}"] = make(map[string]retrofitTemplate.Template) 321 | pageMap["{{.NameWithLowerFirst}}"]["Index"] = mockIndexTemplate 322 | pageMap["{{.NameWithLowerFirst}}"]["Create"] = mockCreateTemplate 323 | 324 | // Set expectations. The controller will display the Create template, 325 | // get some data, create a repository and use it to create a model object. 326 | // Then it will use the Index template to display the index page. 327 | pegomock.When(mockServices.Template("{{.NameWithLowerFirst}}", "Create")).ThenReturn(mockCreateTemplate) 328 | pegomock.When(mockServices.{{.NameWithUpperFirst}}Repository()).ThenReturn(mockRepository) 329 | pegomock.When(mockRepository.Create(expected{{.NameWithUpperFirst}}1)). 330 | ThenReturn(expected{{.NameWithUpperFirst}}1, nil) 331 | pegomock.When(mockServices.Make{{.NameWithUpperFirst}}ListForm()).ThenReturn(listForm) 332 | pegomock.When(mockServices.Template("{{.NameWithLowerFirst}}", "Index")).ThenReturn(mockIndexTemplate) 333 | pegomock.When(mockRepository.FindAll()).ThenReturn({{.PluralNameWithLowerFirst}}, nil) 334 | pegomock.When(mockCreateTemplate.Execute(response.ResponseWriter, listForm)). 335 | ThenReturn(nil) 336 | 337 | // Run the test. 338 | controller := MakeController(mockServices, false) 339 | controller.Create(&request, &response, singleItemForm) 340 | 341 | // Verify that the form contains a notice with the expected contents. 342 | if !strings.Contains(listForm.Notice(), expectedNoticeFragment) { 343 | t.Errorf("Expected notice to contain \"%s\" actually \"%s\"", 344 | expectedNoticeFragment, listForm.Notice()) 345 | } 346 | {{range .Fields}} 347 | {{if not .ExcludeFromDisplay}} 348 | {{if eq .Type "string"}} 349 | if !strings.Contains(listForm.Notice(), expected{{.NameWithUpperFirst}}1) { 350 | t.Errorf("Expected notice to contain \"%s\" actually \"%s\"", 351 | expected{{.NameWithUpperFirst}}1, listForm.Notice()) 352 | } 353 | {{else}} 354 | if !strings.Contains(listForm.Notice(), expected{{.NameWithUpperFirst}}1_str) { 355 | t.Errorf("Expected notice to contain \"%s\" actually \"%s\"", 356 | expected{{.NameWithUpperFirst}}1_str, listForm.Notice()) 357 | } 358 | {{end}} 359 | {{end}} 360 | {{end}} 361 | } 362 | 363 | // TestUnitCreateFailsWithMissingFields checks that the {{.NameWithLowerFirst}} controller's 364 | // Create method correctly handles invalid data from the HTTP request. Note: by 365 | // the time the code under test runs, number and boolean fields have already been 366 | // extracted from the HTML form and converted, so the only fields that can be made 367 | // invalid are mandatory string fields. If there are none of those, the test will 368 | // run successfully but it will do nothing useful. 369 | // 370 | // The test uses pegomock to provide mocks. 371 | func TestUnitCreateFailsWithMissingFields(t *testing.T) { 372 | 373 | log.SetPrefix("TestUnitCreateFailsWithMissingFields ") 374 | 375 | // This test only makes sense when there are mandatory fields in the form. 376 | mandatoryFieldCount := 0 377 | {{range .Fields}} 378 | {{if and .Mandatory (eq .Type "string")}} 379 | mandatoryFieldCount++ // {{.NameWithLowerFirst}} is mandatory 380 | {{end}} 381 | {{end}} 382 | 383 | if mandatoryFieldCount > 0 { 384 | {{range .Fields}} 385 | {{if and .Mandatory (eq .Type "string")}} 386 | expectedErrorMessage{{.NameWithUpperFirst}} := "you must specify the {{.NameWithLowerFirst}}" 387 | {{end}} 388 | {{end}} 389 | pegomock.RegisterMockTestingT(t) 390 | 391 | var expectedID1 uint64 = 42 392 | // supply empty string for mandatory string fields, the given values for others. 393 | expected{{.NameWithUpperFirst}}1 := {{.NameWithLowerFirst}}.MakeInitialised{{$resourceNameUpper}}(expectedID1, {{range .Fields}}{{if and .Mandatory (eq .Type "string")}}" "{{else}}expected{{.NameWithUpperFirst}}1{{end}}{{if not .LastItem}}, {{end}}{{end}}) 394 | singleItemForm := {{.NameWithLowerFirst}}Forms.MakeInitialisedSingleItemForm(expected{{.NameWithUpperFirst}}1) 395 | 396 | // Create the mocks and dummy objects. 397 | 398 | var url url.URL 399 | url.Opaque = "/{{.PluralNameWithLowerFirst}}/42" // url.RequestURI() will return "/{{.PluralNameWithLowerFirst}}/42" 400 | var httpRequest http.Request 401 | httpRequest.URL = &url 402 | httpRequest.Method = "POST" 403 | var request restful.Request 404 | request.Request = &httpRequest 405 | writer := mocks.NewMockResponseWriter() 406 | var response restful.Response 407 | response.ResponseWriter = writer 408 | mockTemplate := mocks.NewMockTemplate() 409 | 410 | // Create a services layer that returns the other mocks. 411 | mockServices := mocks.NewMockServices() 412 | pegomock.When(mockServices.Template("{{.NameWithLowerFirst}}", "Create")).ThenReturn(mockTemplate) 413 | 414 | // Run the test. 415 | 416 | // In the app, the form is validated before the controller method is called. 417 | // Validate the fome and check that it fails. 418 | valid := singleItemForm.Validate() 419 | if valid { 420 | t.Errorf("Expected the validation method to return false (invalid)") 421 | } 422 | 423 | // Check that the validation method set the valid flag in the form 424 | if singleItemForm.Valid() { 425 | t.Errorf("Expected the form to be marked as invalid") 426 | } 427 | 428 | controller := MakeController(mockServices, false) 429 | controller.Create(&request, &response, singleItemForm) 430 | 431 | // If the {{.NameWithLowerFirst}} has mandatory string fields, verify that the 432 | // form contains the expected error messages. 433 | {{range .Fields}} 434 | {{if and .Mandatory (eq .Type "string")}} 435 | if singleItemForm.ErrorForField("{{.NameWithUpperFirst}}") != expectedErrorMessage{{.NameWithUpperFirst}} { 436 | t.Errorf("Expected error message to be %s actually %s", 437 | expectedErrorMessage{{.NameWithUpperFirst}}, singleItemForm.ErrorForField("{{.NameWithUpperFirst}}")) 438 | } 439 | {{end}} 440 | {{end}} 441 | } 442 | } 443 | 444 | // TestUnitCreateFailsWithDBError checks that the {{.NameWithLowerFirst}} handler's Create method 445 | // correctly handles an error from the repository while attempting to create a 446 | // {{.NameWithLowerFirst}} in the database. 447 | func TestUnitCreateFailsWithDBError(t *testing.T) { 448 | 449 | log.SetPrefix("TestUnitCreateFailsWithDBError ") 450 | 451 | expectedErrorMessage := "some error" 452 | expectedErrorMessageLeader := "Could not create {{.NameWithLowerFirst}}" 453 | 454 | pegomock.RegisterMockTestingT(t) 455 | 456 | // Create the mocks and dummy objects. 457 | var expectedID1 uint64 = 42 458 | expected{{.NameWithUpperFirst}}1 := {{.NameWithLowerFirst}}.MakeInitialised{{$resourceNameUpper}}(expectedID1, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 459 | singleItemForm := {{.NameWithLowerFirst}}Forms.MakeInitialisedSingleItemForm(expected{{.NameWithUpperFirst}}1) 460 | listForm := {{.NameWithLowerFirst}}Forms.MakeListForm() 461 | var url url.URL 462 | url.Opaque = "/{{.PluralNameWithLowerFirst}}/42" // url.RequestURI() will return "/{{.PluralNameWithLowerFirst}}/42" 463 | var httpRequest http.Request 464 | httpRequest.URL = &url 465 | httpRequest.Method = "POST" 466 | var request restful.Request 467 | request.Request = &httpRequest 468 | writer := mocks.NewMockResponseWriter() 469 | var response restful.Response 470 | response.ResponseWriter = writer 471 | mockIndexTemplate := mocks.NewMockTemplate() 472 | mockCreateTemplate := mocks.NewMockTemplate() 473 | 474 | // Create a services layer that returns the mock create template. 475 | mockRepository := mock{{.NameWithUpperFirst}}.NewMockRepository() 476 | mockServices := mocks.NewMockServices() 477 | pegomock.When(mockServices.Template("{{.NameWithLowerFirst}}", "Create")). 478 | ThenReturn(mockCreateTemplate) 479 | pegomock.When(mockServices.{{.NameWithUpperFirst}}Repository()).ThenReturn(mockRepository) 480 | pegomock.When(mockRepository.Create(expected{{.NameWithUpperFirst}}1)). 481 | ThenReturn(nil, errors.New(expectedErrorMessage)) 482 | pegomock.When(mockServices.Template("{{.NameWithLowerFirst}}", "Index")). 483 | ThenReturn(mockIndexTemplate) 484 | pegomock.When(mockServices.Make{{.NameWithUpperFirst}}ListForm()).ThenReturn(listForm) 485 | 486 | // Run the test. 487 | controller := MakeController(mockServices, false) 488 | 489 | controller.Create(&request, &response, singleItemForm) 490 | 491 | // Verify that the form contains the expected error message. 492 | if !strings.Contains(listForm.ErrorMessage(), expectedErrorMessageLeader) { 493 | t.Errorf("Expected error message to contain \"%s\" actually \"%s\"", 494 | expectedErrorMessageLeader, listForm.ErrorMessage()) 495 | } 496 | 497 | if !strings.Contains(listForm.ErrorMessage(), expectedErrorMessage) { 498 | t.Errorf("Expected error message to contain \"%s\" actually \"%s\"", 499 | expectedErrorMessage, listForm.ErrorMessage()) 500 | } 501 | } 502 | 503 | // Recover from any panic and record the error. 504 | func catchPanic() { 505 | log.SetPrefix("catchPanic ") 506 | if p := recover(); p != nil { 507 | em := fmt.Sprintf("%v", p) 508 | panicValue = em 509 | log.Printf(em) 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /templates/form.concrete.list.go.template: -------------------------------------------------------------------------------- 1 | package {{.NameWithLowerFirst}} 2 | 3 | {{.Imports}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // The {{.NameWithLowerFirst}} ConcreteListForm satisfies the ListForm interface and 12 | // holds view data including a list of {{.PluralNameWithLowerFirst}}. It's 13 | // approximately equivalent to a Struts form bean - it's used as a Data Transfer 14 | // Object to carry a list of {{.PluralNameWithLowerFirst}} from the {{.NameWithLowerFirst}} controller 15 | // to the web browser. 16 | 17 | type ConcreteListForm struct { 18 | {{.PluralNameWithLowerFirst}} []{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} 19 | notice string 20 | errorMessage string 21 | } 22 | 23 | // Define the factory functions. 24 | 25 | // MakeListForm creates and returns a new uninitialised ListForm object 26 | func MakeListForm() ListForm { 27 | var concreteListForm ConcreteListForm 28 | return &concreteListForm 29 | } 30 | 31 | // {{.PluralNameWithUpperFirst}} returns the list of {{.NameWithUpperFirst}} objects from the form 32 | func (clf *ConcreteListForm) {{.PluralNameWithUpperFirst}}() []{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} { 33 | return clf.{{.PluralNameWithLowerFirst}} 34 | } 35 | 36 | // Notice gets the notice. 37 | func (clf *ConcreteListForm) Notice() string { 38 | return clf.notice 39 | } 40 | 41 | // ErrorMessage gets the general error message. 42 | func (clf *ConcreteListForm) ErrorMessage() string { 43 | return clf.errorMessage 44 | } 45 | 46 | // Set{{.PluralNameWithUpperFirst}} sets the list of {{.NameWithUpperFirst}}s. 47 | func (clf *ConcreteListForm) Set{{.PluralNameWithUpperFirst}}({{.PluralNameWithLowerFirst}} []{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) { 48 | clf.{{.PluralNameWithLowerFirst}} = {{.PluralNameWithLowerFirst}} 49 | } 50 | 51 | // SetNotice sets the notice. 52 | func (clf *ConcreteListForm) SetNotice(notice string) { 53 | clf.notice = notice 54 | } 55 | 56 | // SetErrorMessage sets the error message. 57 | func (clf *ConcreteListForm) SetErrorMessage(errorMessage string) { 58 | clf.errorMessage = errorMessage 59 | } 60 | -------------------------------------------------------------------------------- /templates/form.concrete.single.item.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{.NameWithLowerFirst}} 4 | 5 | {{.Imports}} 6 | 7 | // Generated by the goblimey scaffold generator. You are STRONGLY 8 | // recommended not to alter this file, as it will be overwritten next time the 9 | // scaffolder is run. For the same reason, do not commit this file to a 10 | // source code repository. Commit the json specification which was used to 11 | // produce it. 12 | 13 | // ConcreteSingleItemForm satisfies the {{.NameWithLowerFirst}} SingleItemForm interface. 14 | // It's used as a Data Transfer Object to carry the data for a {{.NameWithLowerFirst}} 15 | // between the web browser and the {{.NameWithLowerFirst}} controller. 16 | 17 | type ConcreteSingleItemForm struct { 18 | {{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} 19 | errorMessage string 20 | notice string 21 | fieldError map[string]string 22 | isValid bool 23 | } 24 | 25 | // Define the factory functions. 26 | 27 | // MakeSingleItemForm creates and returns a new uninitialised form object 28 | func MakeSingleItemForm() SingleItemForm { 29 | var concreteSingleItemForm ConcreteSingleItemForm 30 | return &concreteSingleItemForm 31 | } 32 | 33 | // MakeInitialisedSingleItemForm creates and returns a new form object 34 | // containing the given {{.NameWithLowerFirst}}. 35 | func MakeInitialisedSingleItemForm({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) SingleItemForm { 36 | form := MakeSingleItemForm() 37 | form.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 38 | form.SetValid(true) 39 | return form 40 | } 41 | 42 | // Getters 43 | 44 | // {{.NameWithUpperFirst}} gets the {{.NameWithLowerFirst}} embedded in the form. 45 | func (form ConcreteSingleItemForm) {{.NameWithUpperFirst}}() {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} { 46 | return form.{{.NameWithLowerFirst}} 47 | } 48 | 49 | // Notice gets the notice. 50 | func (form ConcreteSingleItemForm) Notice() string { 51 | return form.notice 52 | } 53 | 54 | // ErrorMessage gets the general error message. 55 | func (form ConcreteSingleItemForm) ErrorMessage() string { 56 | return form.errorMessage 57 | } 58 | 59 | // FieldErrors returns all the field errors as a map. 60 | func (form ConcreteSingleItemForm) FieldErrors() map[string]string { 61 | return form.fieldError 62 | } 63 | 64 | // ErrorForField returns the error message about a field (may be an empty string). 65 | func (form ConcreteSingleItemForm) ErrorForField(key string) string { 66 | if form.fieldError == nil { 67 | // The field error map has not been set up. 68 | return "" 69 | } 70 | return form.fieldError[key] 71 | } 72 | 73 | // Valid returns true if the contents of the form is valid 74 | func (form ConcreteSingleItemForm) Valid() bool { 75 | return form.isValid 76 | } 77 | 78 | // String returns a string version of the {{.NameWithUpperFirst}}Form. 79 | func (form ConcreteSingleItemForm) String() string { 80 | return fmt.Sprintf("ConcreteSingleItemForm={{"{"}}{{.NameWithLowerFirst}}=%s, notice=%s,errorMessage=%s,fieldError=%s{{"}"}}", 81 | form.{{.NameWithLowerFirst}}, 82 | form.notice, 83 | form.errorMessage, 84 | utilities.Map2String(form.fieldError)) 85 | } 86 | 87 | // Setters 88 | 89 | // Set{{.NameWithUpperFirst}} sets the {{.NameWithUpperFirst}} in the form. 90 | func (form *ConcreteSingleItemForm) Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) { 91 | form.{{.NameWithLowerFirst}} = {{.NameWithLowerFirst}} 92 | } 93 | 94 | // SetNotice sets the notice. 95 | func (form *ConcreteSingleItemForm) SetNotice(notice string) { 96 | form.notice = notice 97 | } 98 | 99 | //SetErrorMessage sets the general error message. 100 | func (form *ConcreteSingleItemForm) SetErrorMessage(errorMessage string) { 101 | form.errorMessage = errorMessage 102 | } 103 | 104 | // SetErrorMessageForField sets the error message for a named field 105 | func (form *ConcreteSingleItemForm) SetErrorMessageForField(fieldname, errormessage string) { 106 | if form.fieldError == nil { 107 | form.fieldError = make(map[string]string) 108 | } 109 | form.fieldError[fieldname] = errormessage 110 | } 111 | 112 | // SetValid sets a warning that the data in the form is invalid 113 | func (form *ConcreteSingleItemForm) SetValid(value bool) { 114 | form.isValid = value 115 | } 116 | 117 | // Validate validates the data in the {{.NameWithUpperFirst}} and sets the various error messages. 118 | // It returns true if the data is valid, false if there are errors. 119 | func (form *ConcreteSingleItemForm) Validate() bool { 120 | form.isValid = true 121 | 122 | // Trim and test all mandatory string items. 123 | {{range .Fields}} 124 | {{if and .Mandatory (eq .Type "string")}} 125 | if len(strings.TrimSpace(form.{{$resourceNameLower}}.{{.NameWithUpperFirst}}())) <= 0 { 126 | form.SetErrorMessageForField("{{.NameWithUpperFirst}}", "you must specify the {{.NameWithLowerFirst}}") 127 | form.isValid = false 128 | } 129 | {{end}} 130 | {{end}} 131 | return form.isValid 132 | } -------------------------------------------------------------------------------- /templates/form.concrete.single.item.test.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{.NameWithLowerFirst}} 4 | 5 | {{.Imports}} 6 | 7 | var expectedID1 uint64 = 42 8 | var expectedID2 uint64 = 43 9 | {{/* This creates the expected values using the field names and the test 10 | values, something like: 11 | var expectedName1 string = "s1" 12 | var expectedAge1 int = 2 13 | var expectedName2 string = "s3" 14 | var expectedAge2 int = 4 */}} 15 | {{range $index, $element := .Fields}} 16 | {{if eq .Type "string"}} 17 | var expected{{.NameWithUpperFirst}}1 {{.GoType}} = "{{index .TestValues 0}}" 18 | {{else}} 19 | var expected{{.NameWithUpperFirst}}1 {{.GoType}} = {{index .TestValues 0}} 20 | {{end}} 21 | {{if eq .Type "string"}} 22 | var expected{{.NameWithUpperFirst}}2 {{.GoType}} = "{{index .TestValues 1}}" 23 | {{else}} 24 | var expected{{.NameWithUpperFirst}}2 {{.GoType}} = {{index .TestValues 1}} 25 | {{end}} 26 | {{end}} 27 | 28 | // Create a {{.NameWithLowerFirst}} and a ConcreteSingleItemForm containing it. Retrieve the {{.NameWithLowerFirst}}. 29 | func TestUnitCreate{{.NameWithUpperFirst}}FormAndRetrieve{{.NameWithUpperFirst}}(t *testing.T) { 30 | {{.NameWithLowerFirst}}Form := Create{{.NameWithUpperFirst}}Form(expectedID1, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 31 | if {{.NameWithLowerFirst}}Form.{{.NameWithUpperFirst}}().ID() != expectedID1 { 32 | t.Errorf("Expected ID to be %d actually %d", expectedID1, {{.NameWithLowerFirst}}Form.{{.NameWithUpperFirst}}().ID()) 33 | } 34 | {{range .Fields}} 35 | if {{$resourceNameLower}}Form.{{$resourceNameUpper}}().{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}}1 { 36 | t.Errorf("Expected {{.NameWithLowerFirst}} to be %s actually %s", expected{{.NameWithUpperFirst}}1, {{$resourceNameLower}}Form.{{$resourceNameUpper}}().{{.NameWithUpperFirst}}()) 37 | } 38 | {{end}} 39 | } 40 | 41 | {{$fields := .Fields}} 42 | {{range .Fields}} 43 | {{if .Mandatory}} 44 | {{if eq .Type "string"}} 45 | {{$thisField := .NameWithLowerFirst}} 46 | {{$thisFieldUpper := .NameWithUpperFirst}} 47 | // Create a {{$resourceNameUpper}}Form containing a {{$resourceNameLower}} with no {{.NameWithLowerFirst}}, and validate it. 48 | func TestUnitCreate{{$resourceNameUpper}}FormNo{{.NameWithUpperFirst}}(t *testing.T) { 49 | expectedError := "you must specify the {{.NameWithLowerFirst}}" 50 | {{$resourceNameLower}}Form := Create{{$resourceNameUpper}}Form(expectedID2, {{range $fields}}{{if eq $thisField .NameWithLowerFirst}}""{{if not .LastItem}}, {{end}}{{else}}expected{{.NameWithUpperFirst}}2{{if .LastItem}}){{else}}, {{end}}{{end}}{{end}} 51 | if {{$resourceNameLower}}Form.Validate() { 52 | t.Errorf("Expected the validation to fail with missing {{$thisField}}") 53 | } else { 54 | if {{$resourceNameLower}}Form.ErrorForField("{{$thisFieldUpper}}") != expectedError { 55 | t.Errorf("Expected \"%s\", got \"%s\"", expectedError, 56 | {{$resourceNameLower}}Form.ErrorForField("{{$thisFieldUpper}}")) 57 | } 58 | } 59 | errors := {{$resourceNameLower}}Form.FieldErrors() 60 | if len(errors) != 1 { 61 | t.Errorf("Expected 1 error, got %d", len(errors)) 62 | } 63 | } 64 | {{end}} 65 | {{end}} 66 | {{end}} 67 | 68 | 69 | func Create{{.NameWithUpperFirst}}Form(id uint64, {{range .Fields}}{{.NameWithLowerFirst}} {{.GoType}}{{if not .LastItem}}, {{end}}{{end}}) ConcreteSingleItemForm { 70 | var {{.NameWithLowerFirst}} {{.NameWithLowerFirst}}Model.Concrete{{.NameWithUpperFirst}} 71 | {{.NameWithLowerFirst}}.SetID(id) 72 | {{range .Fields}} 73 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 74 | {{end}} 75 | var {{.NameWithLowerFirst}}Form ConcreteSingleItemForm 76 | {{.NameWithLowerFirst}}Form.Set{{.NameWithUpperFirst}}(&{{.NameWithLowerFirst}}) 77 | return {{.NameWithLowerFirst}}Form 78 | } 79 | -------------------------------------------------------------------------------- /templates/form.list.go.template: -------------------------------------------------------------------------------- 1 | package {{.NameWithLowerFirst}} 2 | 3 | {{.Imports}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // The {{.NameWithLowerFirst}} ListForm holds view data including a list of {{.PluralNameWithLowerFirst}}. 12 | // It's approximately equivalent to a Struts form bean - it's used as a Data Transfer 13 | // Object to carry a list of {{.PluralNameWithLowerFirst}} from the {{.NameWithLowerFirst}} controller 14 | // to the web browser. 15 | 16 | type ListForm interface { 17 | // {{.PluralNameWithUpperFirst}} returns the list of {{.NameWithUpperFirst}} objects from the form 18 | {{.PluralNameWithUpperFirst}}() []{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} 19 | // Notice gets the notice. 20 | Notice() string 21 | // ErrorMessage gets the general error message. 22 | ErrorMessage() string 23 | // Set{{.PluralNameWithUpperFirst}} sets the list of {{.PluralNameWithLowerFirst}} in the form. 24 | Set{{.PluralNameWithUpperFirst}}([]{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) 25 | // SetNotice sets the notice. 26 | SetNotice(notice string) 27 | //SetErrorMessage sets the error message. 28 | SetErrorMessage(errorMessage string) 29 | } -------------------------------------------------------------------------------- /templates/form.single.item.go.template: -------------------------------------------------------------------------------- 1 | package {{.NameWithLowerFirst}} 2 | 3 | {{.Imports}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // SingleItemForm holds view data about a {{.NameWithLowerFirst}}. It's used as a data transfer object (DTO) 12 | // in particular for use with views that handle a {{.NameWithUpperFirst}}. (It's approximately equivalent to 13 | // a Struts form bean.) It contains a {{.NameWithUpperFirst}}; a validator function that validates the data 14 | // in the {{.NameWithUpperFirst}} and sets the various error messages; a general error message (for errors not 15 | // associated with an individual field of the {{.NameWithUpperFirst}}), a notice (for announcement that are 16 | // not about errors) and a set of error messages about individual fields of the {{.NameWithUpperFirst}}. It 17 | // offers getters and setters for the various attributes that it supports. 18 | 19 | type SingleItemForm interface { 20 | // {{.NameWithUpperFirst}} gets the {{.NameWithUpperFirst}} embedded in the form. 21 | {{.NameWithUpperFirst}}() {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} 22 | // Notice gets the notice. 23 | Notice() string 24 | // ErrorMessage gets the general error message. 25 | ErrorMessage() string 26 | // FieldErrors returns all the field errors as a map. 27 | FieldErrors() map[string]string 28 | // ErrorForField returns the error message about a field (may be an empty string). 29 | ErrorForField(key string) string 30 | // String returns a string version of the {{.NameWithUpperFirst}}Form. 31 | // Valid returns true if the contents of the form is valid 32 | Valid() bool 33 | String() string 34 | // Set{{.NameWithUpperFirst}} sets the {{.NameWithUpperFirst}} in the form. 35 | Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) 36 | // SetNotice sets the notice. 37 | SetNotice(notice string) 38 | //SetErrorMessage sets the general error message. 39 | SetErrorMessage(errorMessage string) 40 | // SetErrorMessageForField sets the error message for a named field 41 | SetErrorMessageForField(fieldname, errormessage string) 42 | // SetValid sets a warning that the data in the form is invalid 43 | SetValid(value bool) 44 | // Validate validates the data in the {{.NameWithUpperFirst}} and sets the various error messages. 45 | // It returns true if the data is valid, false if there are errors. 46 | Validate() bool 47 | } 48 | -------------------------------------------------------------------------------- /templates/gorp.concrete.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{$resourceNameLower}} 4 | 5 | {{.Imports}} 6 | 7 | // Generated by the goblimey scaffold generator. You are STRONGLY 8 | // recommended not to alter this file, as it will be overwritten next time the 9 | // scaffolder is run. For the same reason, do not commit this file to a 10 | // source code repository. Commit the json specification which was used to 11 | // produce it. 12 | 13 | // The Concrete{{$resourceNameUpper}} struct satisfies the {{$resourceNameUpper}} interface and holds a single row from 14 | // the PEOPLE table, accessed via the GORP library. 15 | // 16 | // The fields must be public for GORP to work but the names must not clash with 17 | // those of the getters, so for a field "name" call the getter Name() and the 18 | // field NameField. 19 | type Concrete{{$resourceNameUpper}} struct { 20 | IDField uint64 %%GRAVE%%db: "id, primarykey, autoincrement"%%GRAVE%% 21 | {{range .Fields}} 22 | {{.NameWithUpperFirst}}Field {{.GoType}} %%GRAVE%%db: "{{.NameWithLowerFirst}}"%%GRAVE%% 23 | {{end}} 24 | } 25 | 26 | // Factory functions 27 | 28 | // Make{{$resourceNameUpper}} creates and returns a new uninitialised {{$resourceNameUpper}} object 29 | func Make{{$resourceNameUpper}}() {{$resourceNameLower}}.{{$resourceNameUpper}} { 30 | var Concrete{{$resourceNameUpper}} Concrete{{$resourceNameUpper}} 31 | return &Concrete{{$resourceNameUpper}} 32 | } 33 | 34 | // MakeInitialised{{$resourceNameUpper}} creates and returns a new {{$resourceNameUpper}} object initialised from 35 | // the arguments 36 | func MakeInitialised{{$resourceNameUpper}}(id uint64, {{range .Fields}}{{.NameWithLowerFirst}} {{.GoType}}{{if not .LastItem}}, {{end}}{{end}}) {{$resourceNameLower}}.{{$resourceNameUpper}} { 37 | {{$resourceNameLower}} := Make{{$resourceNameUpper}}() 38 | {{$resourceNameLower}}.SetID(id) 39 | {{range .Fields}} 40 | {{if eq .Type "string"}} 41 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}(strings.TrimSpace({{.NameWithLowerFirst}})) 42 | {{else}} 43 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 44 | {{end}} 45 | {{end}} 46 | return {{$resourceNameLower}} 47 | } 48 | 49 | // Clone creates and returns a new {{$resourceNameUpper}} object initialised from a source {{$resourceNameUpper}}. 50 | func Clone({{$resourceNameLower}} {{$resourceNameLower}}.{{$resourceNameUpper}}) {{$resourceNameLower}}.{{$resourceNameUpper}} { 51 | return MakeInitialised{{$resourceNameUpper}}({{$resourceNameLower}}.ID(), {{range .Fields}}{{$resourceNameLower}}.{{.NameWithUpperFirst}}(){{if not .LastItem}}, {{end}}{{end}}) 52 | } 53 | 54 | // Methods to implement the {{$resourceNameUpper}} interface. 55 | 56 | // ID gets the id of the {{$resourceNameLower}}. 57 | func (o Concrete{{$resourceNameUpper}}) ID() uint64 { 58 | return o.IDField 59 | } 60 | {{range .Fields}} 61 | //{{.NameWithUpperFirst}} gets the {{.NameWithLowerFirst}} of the {{$resourceNameLower}}. 62 | func (o Concrete{{$resourceNameUpper}}) {{.NameWithUpperFirst}}() {{.GoType}} { 63 | return o.{{.NameWithUpperFirst}}Field 64 | } 65 | {{end}} 66 | // String gets the {{$resourceNameLower}} as a string. 67 | func (o Concrete{{$resourceNameUpper}}) String() string { 68 | return fmt.Sprintf("Concrete{{$resourceNameUpper}}={id=%d, {{range .Fields}}{{.NameWithLowerFirst}}=%v{{if not .LastItem}}, {{end}}{{end}}{{"}"}}", 69 | o.IDField, {{range .Fields}}o.{{.NameWithUpperFirst}}Field{{if not .LastItem}}, {{end}}{{end}}) 70 | } 71 | 72 | // DisplayName returns a name for the object composed of the values of the id and 73 | // any fields not marked as excluded from the display name. 74 | func (o Concrete{{$resourceNameUpper}}) DisplayName() string { 75 | return fmt.Sprintf("%d{{range .Fields}}{{if not .ExcludeFromDisplay}} %v{{end}}{{end}}", 76 | o.IDField{{range .Fields}}{{if not .ExcludeFromDisplay}}, o.{{.NameWithUpperFirst}}Field{{end}}{{end}}) 77 | } 78 | 79 | // SetID sets the {{$resourceNameLower}}'s id to the given value 80 | func (o *Concrete{{$resourceNameUpper}}) SetID(id uint64) { 81 | o.IDField = id 82 | } 83 | {{range .Fields}} 84 | {{if eq .Type "string"}} 85 | // Set{{.NameWithUpperFirst}} sets the {{.NameWithLowerFirst}} of the {{$resourceNameLower}}. 86 | func (o *Concrete{{$resourceNameUpper}}) Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}} {{.GoType}}) { 87 | o.{{.NameWithUpperFirst}}Field = strings.TrimSpace({{.NameWithLowerFirst}}) 88 | {{else}} 89 | // Set{{.NameWithUpperFirst}} sets the {{.NameWithLowerFirst}} of the {{$resourceNameLower}}. 90 | func (o *Concrete{{$resourceNameUpper}}) Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}} {{.GoType}}) { 91 | o.{{.NameWithUpperFirst}}Field = {{.NameWithLowerFirst}} 92 | {{end}} 93 | } 94 | {{end}} 95 | 96 | // Define the validation. 97 | func (o *Concrete{{$resourceNameUpper}}) Validate() error { 98 | 99 | // Trim and test all mandatory string fields 100 | 101 | errorMessage := "" 102 | {{range .Fields}} 103 | {{if and .Mandatory (eq .Type "string")}} 104 | if len(strings.TrimSpace(o.{{.NameWithUpperFirst}}())) <= 0 { 105 | errorMessage += "you must specify the {{.NameWithLowerFirst}} " 106 | } 107 | {{end}} 108 | {{end}} 109 | if len(errorMessage) > 0 { 110 | return errors.New(errorMessage) 111 | } 112 | return nil 113 | } -------------------------------------------------------------------------------- /templates/main.go.template: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | {{.Imports}} 4 | 5 | // Originally generated by the goblimey scaffold generator. It is safe to edit 6 | // this file. If you need to restore the original version, run the scaffolder with 7 | // the -overwrite option. 8 | 9 | // The {{.NameWithLowerFirst}} program provides the back end logic to provide 10 | // CRUD operations on the {{.NameWithLowerFirst}} resources. 11 | 12 | // resourceRE is the regular expression to extract the resource from the URI of 13 | // the request to be. For example in: "/people" and "/people/1/delete", the 14 | // resource is "people". 15 | var resourceRE = regexp.MustCompile(%%GRAVE%%^/([^/]+)%%GRAVE%%) 16 | 17 | // The following regular expressions are for specific request URIs, to work out 18 | // which controller method to call. For example, a GET request with URI "/people" 19 | // produces a call to the Index method of the people controller. 20 | // 21 | // The requests follow the REST model and therefore carry data such as IDs 22 | // in the request URI rather than in HTTP parameters, for example: 23 | // 24 | // GET /people/435 25 | // 26 | // rather than 27 | // 28 | // GET/people&id=435 29 | // 30 | // Only form data is supplied through HTTP parameters 31 | 32 | {{range .Resources}} 33 | // The {{.NameWithLowerFirst}}DeleteRequestRE is the regular expression for the URI of a delete 34 | // request containing a numeric ID - for example: "/{{.PluralNameWithLowerFirst}}/1/delete". 35 | var {{.NameWithLowerFirst}}DeleteRequestRE = regexp.MustCompile(%%GRAVE%%^/{{.PluralNameWithLowerFirst}}/[0-9]+/delete$%%GRAVE%%) 36 | 37 | // The {{.NameWithLowerFirst}}ShowRequestRE is the regular expression for the URI of a show 38 | // request containing a numeric ID - for example: "/{{.PluralNameWithLowerFirst}}/1". 39 | var {{.NameWithLowerFirst}}ShowRequestRE = regexp.MustCompile(%%GRAVE%%^/{{.PluralNameWithLowerFirst}}/[0-9]+$%%GRAVE%%) 40 | 41 | // The {{.NameWithLowerFirst}}EditRequestRE is the regular expression for the URI of an edit 42 | // request containing a numeric ID - for example: "/{{.PluralNameWithLowerFirst}}/1/edit". 43 | var {{.NameWithLowerFirst}}EditRequestRE = regexp.MustCompile(%%GRAVE%%^/{{.PluralNameWithLowerFirst}}/[0-9]+/edit$%%GRAVE%%) 44 | 45 | // The {{.NameWithLowerFirst}}UpdateRequestRE is the regular expression for the URI of an update 46 | // request containing a numeric ID - for example: "/{{.PluralNameWithLowerFirst}}/1". The URI 47 | // is the same as for the show request, but we give it a different name for 48 | // clarity. 49 | var {{.NameWithLowerFirst}}UpdateRequestRE = {{.NameWithLowerFirst}}ShowRequestRE 50 | {{end}} 51 | var templateMap *map[string]map[string]retrofitTemplate.Template 52 | 53 | // These values are set from the command line arguments. 54 | var homeDir string // app server's home directory 55 | var verbose bool // verbose mode 56 | 57 | func init() { 58 | const ( 59 | defaultVerbose = false 60 | usage = "enable verbose logging" 61 | ) 62 | flag.BoolVar(&verbose, "verbose", defaultVerbose, usage) 63 | flag.BoolVar(&verbose, "v", defaultVerbose, usage+" (shorthand)") 64 | flag.StringVar(&homeDir, "homedir", ".", "the application server's home directory (must contain the views directory)") 65 | } 66 | 67 | func main() { 68 | log.SetPrefix("main() ") 69 | // Find the home directory. This is specified by the first command line 70 | // argument. If that's not specified, the home is assumed to be the current 71 | //directory. 72 | 73 | flag.Parse() 74 | if len(flag.Args()) >= 1 { 75 | homeDir = flag.Args()[0] 76 | } 77 | err := os.Chdir(homeDir) 78 | if err != nil { 79 | log.Printf("cannot change directory to homeDir %s - %s", homeDir, 80 | err.Error()) 81 | os.Exit(-1) 82 | } 83 | 84 | // The home directory must contain a directory "views" containing the HTML and 85 | // the templates. If there is no views directory, give up. Most likely, the 86 | // user has not moved to the right directory before running this. 87 | fileInfo, err := os.Stat("views") 88 | if err != nil { 89 | if os.IsNotExist(err) { 90 | // views does not exist 91 | em := "cannot find the views directory" 92 | log.Println(em) 93 | fmt.Fprintln(os.Stderr, em) 94 | 95 | } else if !fileInfo.IsDir() { 96 | // views exists but is not a directory 97 | em := "the file views must be a directory" 98 | log.Println(em) 99 | fmt.Fprintln(os.Stderr, em) 100 | 101 | } else { 102 | // some other error 103 | log.Println(err.Error()) 104 | fmt.Fprintln(os.Stderr, err.Error()) 105 | } 106 | 107 | os.Exit(-1) 108 | } 109 | 110 | templateMap = utilities.CreateTemplates() 111 | 112 | // Set up the restful web service. Send all requests to marshal(). 113 | 114 | if verbose { 115 | log.Println("setting up routes") 116 | } 117 | ws := new(restful.WebService) 118 | http.Handle("/stylesheets/", http.StripPrefix("/stylesheets/", http.FileServer(http.Dir("views/stylesheets")))) 119 | http.Handle("/html/", http.StripPrefix("/html/", http.FileServer(http.Dir("views/html")))) 120 | // Handlers for static HTML pages. 121 | 122 | ws.Route(ws.GET("/").To(marshal)) 123 | ws.Route(ws.GET("/error.html").To(marshal)) 124 | {{range .Resources}} 125 | // Tie all expected requests to the marshal. 126 | ws.Route(ws.GET("/{{.PluralNameWithLowerFirst}}").To(marshal)) 127 | ws.Route(ws.GET("/{{.PluralNameWithLowerFirst}}/{id}/edit").To(marshal)) 128 | ws.Route(ws.GET("/{{.PluralNameWithLowerFirst}}/{id}").To(marshal)) 129 | ws.Route(ws.GET("/{{.PluralNameWithLowerFirst}}/create").To(marshal)) 130 | ws.Route(ws.POST("/{{.PluralNameWithLowerFirst}}").Consumes("application/x-www-form-urlencoded").To(marshal)) 131 | ws.Route(ws.POST("/{{.PluralNameWithLowerFirst}}/{id}").Consumes("application/x-www-form-urlencoded").To(marshal)) 132 | ws.Route(ws.POST("/{{.PluralNameWithLowerFirst}}/{id}/delete").Consumes("application/x-www-form-urlencoded").To(marshal)) 133 | {{end}} 134 | restful.Add(ws) 135 | 136 | if verbose { 137 | log.Println("starting the listener") 138 | } 139 | err = http.ListenAndServe(":4000", nil) 140 | log.Printf("baling out - %s" + err.Error()) 141 | } 142 | 143 | // marshal passes the request and response to the appropriate method of the 144 | // appropriate controller. 145 | func marshal(request *restful.Request, response *restful.Response) { 146 | 147 | log.SetPrefix("main.marshal() ") 148 | 149 | defer catchPanic() 150 | 151 | // Create a service supplier 152 | var services services.ConcreteServices 153 | services.SetTemplates(templateMap) 154 | {{range .Resources}} 155 | {{.NameWithUpperFirst}}Repository, err := {{.NameWithLowerFirst}}Repository.MakeRepository(verbose) 156 | if err != nil { 157 | log.Println(err.Error()) 158 | fmt.Fprintln(os.Stderr, err.Error()) 159 | os.Exit(-1) 160 | } 161 | services.Set{{.NameWithUpperFirst}}Repository({{.NameWithUpperFirst}}Repository) 162 | defer {{.NameWithUpperFirst}}Repository.Close() 163 | {{end}} 164 | 165 | // We get the HTTP request from the restful request via its public Request 166 | // attribute. Getting the method from that requires another public attribute. 167 | // These operations and others cannot be defined using an interface, which is 168 | // why the request and response are passed to the controller as pointers to 169 | // concrete objects rather than as retro-fitted interfaces. 170 | 171 | uri := request.Request.URL.RequestURI() 172 | 173 | // The REST model uses HTTP requests such as PUT and DELETE. The standard browsers do not support 174 | // these operations, so they are implemented using a POST request with a parameter "_method" 175 | // defining the operation. (A post with a parameter "_method=PUT" simulates a PUT, and so on.) 176 | 177 | method := request.Request.Method 178 | if method == "POST" { 179 | // handle simulated PUT, DELETE etc via the _method parameter 180 | simMethod := request.Request.FormValue("_method") 181 | if simMethod == "PUT" || simMethod == "DELETE" { 182 | method = simMethod 183 | } 184 | } 185 | if verbose { 186 | log.Printf("uri %s method %s", uri, method) 187 | } 188 | 189 | // The home page "/" or "/index.html" is dealt with using the special resource 190 | // "html". 191 | 192 | if uri == "/" || uri == "/index.html" { 193 | if verbose { 194 | log.Println("home page") 195 | } 196 | page := services.Template("html", "Index") 197 | if page == nil { 198 | log.Printf("no home Index page") 199 | utilities.Dead(response) 200 | return 201 | } 202 | // This template is just HTML, so it needs no data. 203 | err = page.Execute(response.ResponseWriter, nil) 204 | if err != nil { 205 | // Can't display the home index page. Bale out. 206 | em := fmt.Sprintf("fatal error - failed to display error page for error %s\n", err.Error()) 207 | log.Printf(em) 208 | panic(em) 209 | } 210 | return 211 | } 212 | 213 | // Extract the resource. For uris "/people", /people/1 etc, the resource is 214 | // "people". If the string matches the regular expression, result will have 215 | // at least two entries and the resource name will be in result[1]. 216 | 217 | result := resourceRE.FindStringSubmatch(uri) 218 | 219 | if len(result) < 2 { 220 | em := fmt.Sprintf("illegal request uri %v", uri) 221 | log.Println(em) 222 | utilities.BadError(em, response) 223 | return 224 | } 225 | 226 | resource := result[1] 227 | 228 | switch resource { 229 | {{range .Resources}} 230 | case "{{.PluralNameWithLowerFirst}}": 231 | 232 | if verbose { 233 | log.Printf("Sending request %s to {{.NameWithLowerFirst}} controller\n", uri) 234 | } 235 | 236 | var controller = {{.NameWithLowerFirst}}Controller.MakeController(&services, verbose) 237 | 238 | // Call the appropriate handler for the request 239 | 240 | switch method { 241 | 242 | case "GET": 243 | 244 | if uri == "/{{.PluralNameWithLowerFirst}}" { 245 | // "GET http://server:port/{{.PluralNameWithLowerFirst}}" - fetch all the valid {{.PluralNameWithLowerFirst}} 246 | // records and display them. 247 | form := services.Make{{.NameWithUpperFirst}}ListForm() 248 | controller.Index(request, response, form) 249 | 250 | } else if {{.NameWithLowerFirst}}EditRequestRE.MatchString(uri) { 251 | 252 | // "GET http://server:port/{{.PluralNameWithLowerFirst}}/1/edit" - fetch the {{.PluralNameWithLowerFirst}} record 253 | // given by the ID in the request and display the form to edit it. 254 | {{.NameWithLowerFirst}} := services.Make{{.NameWithUpperFirst}}() 255 | form := services.Make{{.NameWithUpperFirst}}Form() 256 | form.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 257 | // The URI should contain an ID as a string. Parse and copy it. 258 | var id uint64 = 0 259 | idStr := request.PathParameter("id") 260 | if verbose { 261 | log.Printf("id %s", idStr) 262 | } 263 | if idStr == "" { 264 | // This should never happen 265 | em := fmt.Sprintf("id is not set in the request, must be an unsigned integer") 266 | log.Printf("%s\n", em) 267 | form.SetErrorMessageForField("ID", "Internal error - " + em) 268 | } 269 | 270 | id, err = strconv.ParseUint(idStr, 10, 64) 271 | if err != nil { 272 | em := fmt.Sprintf("invalid id %s in request, must be an unsigned integer - %s", 273 | idStr, err.Error()) 274 | log.Printf("%s\n", em) 275 | form.SetErrorMessageForField("ID", "ID must be a whole number greater than 0") 276 | } 277 | {{.NameWithLowerFirst}}.SetID(id) 278 | 279 | controller.Edit(request, response, form) 280 | 281 | 282 | } else if uri == "/{{.PluralNameWithLowerFirst}}/create" { 283 | 284 | // "GET http://server:port/{{.PluralNameWithLowerFirst}}/create" - display the form to 285 | // create a new single item {{.NameWithLowerFirst}} record. 286 | 287 | // Create an empty {{.NameWithLowerFirst}} to get started. 288 | {{.NameWithLowerFirst}} := services.Make{{.NameWithUpperFirst}}() 289 | form := services.Make{{.NameWithUpperFirst}}Form() 290 | form.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 291 | controller.New(request, response, form) 292 | 293 | 294 | } else if {{.NameWithLowerFirst}}ShowRequestRE.MatchString(uri) { 295 | 296 | // "GET http://server:port/{{.PluralNameWithLowerFirst}}/435" - fetch the {{.PluralNameWithLowerFirst}} record 297 | // with ID 435 and display it. 298 | 299 | // Get the ID from the HTML form data . The data only contains the 300 | // ID so the resulting {{.NameWithUpperFirst}}Form may be marked 301 | // as invalid, but we are only interested in the ID. 302 | {{.NameWithLowerFirst}} := services.Make{{.NameWithUpperFirst}}() 303 | form := services.Make{{.NameWithUpperFirst}}Form() 304 | form.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 305 | 306 | // Get the ID from the request and create a form containing a 307 | // {{.NameWithLowerFirst}} with only the ID set. The resulting 308 | // form will be invalid, but we are only interested in the ID. 309 | var id uint64 = 0 310 | idStr := request.PathParameter("id") 311 | if verbose { 312 | log.Printf("id %s", idStr) 313 | } 314 | if idStr == "" { 315 | // This should never happen 316 | em := fmt.Sprintf("id is not set in the request, must be an unsigned integer") 317 | log.Printf("%s\n", em) 318 | form.SetErrorMessageForField("ID", "Internal error - " + em) 319 | } 320 | 321 | id, err = strconv.ParseUint(idStr, 10, 64) 322 | if err != nil { 323 | em := fmt.Sprintf("invalid id %s in request, must be an unsigned integer - %s", 324 | idStr, err.Error()) 325 | log.Printf("%s\n", em) 326 | form.SetErrorMessageForField("ID", "ID must be a whole number greater than 0") 327 | } 328 | {{.NameWithLowerFirst}}.SetID(id) 329 | 330 | controller.Show(request, response, form) 331 | 332 | 333 | } else { 334 | em := fmt.Sprintf("unexpected GET request - uri %v", uri) 335 | log.Println(em) 336 | controller.ErrorHandler(request, response, em) 337 | } 338 | 339 | case "PUT": 340 | if {{.NameWithLowerFirst}}UpdateRequestRE.MatchString(uri) { 341 | 342 | // POST http://server:port/{{.PluralNameWithLowerFirst}}/1" - update the single item {{.NameWithLowerFirst}} record with 343 | // the given ID from the URI using the form data in the body. 344 | form := makeValidated{{.NameWithUpperFirst}}FormFromRequest(request, &services) 345 | controller.Update(request, response, form) 346 | 347 | } else if uri == "/{{.PluralNameWithLowerFirst}}" { 348 | 349 | // POST http://server:port/{{.PluralNameWithLowerFirst}}" - create a new {{.PluralNameWithLowerFirst}} record from 350 | // the form data in the body. 351 | form := makeValidated{{.NameWithUpperFirst}}FormFromRequest(request, &services) 352 | controller.Create(request, response, form) 353 | 354 | } else { 355 | em := fmt.Sprintf("unexpected PUT request - uri %v", uri) 356 | log.Println(em) 357 | controller.ErrorHandler(request, response, em) 358 | } 359 | 360 | case "DELETE": 361 | if {{.NameWithLowerFirst}}DeleteRequestRE.MatchString(uri) { 362 | 363 | // "POST http://server:port/{{.PluralNameWithLowerFirst}}/1/delete" - delete the {{.PluralNameWithLowerFirst}} 364 | // record with the ID given in the request. 365 | 366 | {{.NameWithLowerFirst}} := services.Make{{.NameWithUpperFirst}}() 367 | form := services.Make{{.NameWithUpperFirst}}Form() 368 | form.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 369 | 370 | // Get the ID from the request and create a form containing a 371 | // {{.NameWithLowerFirst}} with only the ID set. The resulting 372 | // form will be invalid, but we are only interested in the ID. 373 | var id uint64 = 0 374 | idStr := request.PathParameter("id") 375 | if verbose { 376 | log.Printf("id %s", idStr) 377 | } 378 | if idStr == "" { 379 | // This should never happen 380 | em := fmt.Sprintf("id is not set in the request, must be an unsigned integer") 381 | log.Printf("%s\n", em) 382 | form.SetErrorMessageForField("ID", "Internal error - " + em) 383 | } 384 | 385 | id, err = strconv.ParseUint(idStr, 10, 64) 386 | if err != nil { 387 | em := fmt.Sprintf("invalid id %s in request, must be an unsigned integer - %s", 388 | idStr, err.Error()) 389 | log.Printf("%s\n", em) 390 | form.SetErrorMessageForField("ID", "ID must be a whole number greater than 0") 391 | } 392 | {{.NameWithLowerFirst}}.SetID(id) 393 | 394 | controller.Delete(request, response, form) 395 | } 396 | 397 | default: 398 | em := fmt.Sprintf("unexpected HTTP method %v", method) 399 | log.Println(em) 400 | controller.ErrorHandler(request, response, em) 401 | } 402 | {{end}} 403 | default: 404 | em := fmt.Sprintf("unexpected resource %v in uri %v", resource, uri) 405 | log.Println(em) 406 | utilities.BadError(em, response) 407 | } 408 | } 409 | 410 | {{$resourceNameLower := .NameWithLowerFirst}} 411 | {{$resourceNameUpper := .NameWithUpperFirst}} 412 | {{range .Resources}} 413 | // makeValidated{{.NameWithUpperFirst}}FormFromRequest gets the {{.NameWithLowerFirst}} data from the request, creates a 414 | // {{.NameWithUpperFirst}} and returns it in a single item {{.NameWithLowerFirst}} form. 415 | func makeValidated{{.NameWithUpperFirst}}FormFromRequest(request *restful.Request, services services.Services) {{.NameWithLowerFirst}}Forms.SingleItemForm { 416 | 417 | log.SetPrefix("makeValidated{{.NameWithUpperFirst}}FormFromRequest() ") 418 | 419 | {{.NameWithLowerFirst}} := services.Make{{.NameWithUpperFirst}}() 420 | {{.NameWithLowerFirst}}Form := services.MakeInitialised{{.NameWithUpperFirst}}Form({{.NameWithLowerFirst}}) 421 | 422 | // The Validate method validates the {{.NameWithUpperFirst}}Form. Fields 423 | //in the request that are destined for any object except a string could 424 | // also be invalid and we also have to check for that before we set 425 | // a field in the {{.NameWithUpperFirst}}Form. 426 | 427 | valid := true // This will be set false on any error. 428 | 429 | err := request.Request.ParseForm() 430 | if err != nil { 431 | valid = false 432 | em := fmt.Sprintf("cannot parse form - %s", err.Error()) 433 | log.Printf("%s\n", em) 434 | {{.NameWithLowerFirst}}Form.SetErrorMessage("Internal error while processing the last data input") 435 | // Cannot make any sense of the HTML form data - bale out. 436 | return {{.NameWithLowerFirst}}Form 437 | } 438 | 439 | // If the URI contains an ID, parse and copy it. 440 | var id uint64 = 0 441 | idStr := request.PathParameter("id") 442 | if idStr != "" { 443 | if verbose { 444 | log.Printf("id %s", idStr) 445 | } 446 | id, err = strconv.ParseUint(idStr, 10, 64) 447 | if err != nil { 448 | valid = false 449 | em := fmt.Sprintf("invalid id %s in request, must be an unsigned integer - %s", 450 | idStr, err.Error()) 451 | log.Printf("%s\n", em) 452 | {{.NameWithLowerFirst}}Form.SetErrorMessageForField("ID", "ID must be a whole number greater than 0") 453 | } 454 | {{.NameWithLowerFirst}}.SetID(id) 455 | } 456 | 457 | {{$resourceNameLower := .NameWithLowerFirst}} 458 | {{$resourceNameUpper := .NameWithUpperFirst}} 459 | {{range .Fields}} 460 | {{if eq .GoType "string"}} 461 | {{.NameWithLowerFirst}} := request.Request.FormValue("{{.NameWithLowerFirst}}") 462 | if verbose { 463 | log.Printf("{{.NameWithLowerFirst}} %s", {{.NameWithLowerFirst}}) 464 | } 465 | {{else}} 466 | {{.NameWithLowerFirst}}Str := strings.TrimSpace(request.Request.FormValue("{{.NameWithLowerFirst}}")) 467 | if verbose { 468 | log.Printf("{{.NameWithLowerFirst}} %s", {{.NameWithLowerFirst}}Str) 469 | } 470 | {{if eq .GoType "int64"}} 471 | {{.NameWithLowerFirst}}, err := strconv.ParseInt({{.NameWithLowerFirst}}Str, 10, 64) 472 | if err != nil { 473 | valid = false 474 | log.Println(fmt.Sprintf("HTTP form input for field {{.NameWithLowerFirst}} %s is not an integer - %s", 475 | {{.NameWithLowerFirst}}Str, err.Error())) 476 | {{$resourceNameLower}}Form.SetErrorMessageForField("{{.NameWithUpperFirst}}", "must be a whole number") 477 | } 478 | {{else if eq .GoType "uint64"}} 479 | {{.NameWithLowerFirst}}, err := strconv.ParseUint({{.NameWithLowerFirst}}Str, 10, 64) 480 | if err != nil { 481 | valid = false 482 | log.Println(fmt.Sprintf("HTTP form input for field {{.NameWithLowerFirst}} %s is not an unsigned integer - %s", 483 | {{.NameWithLowerFirst}}Str, err.Error())) 484 | {{$resourceNameLower}}Form.SetErrorMessageForField("{{.NameWithUpperFirst}}", "must be a whole number >= 0") 485 | } 486 | {{else if eq .GoType "float64"}} 487 | {{.NameWithLowerFirst}}, err := strconv.ParseFloat({{.NameWithLowerFirst}}Str, 64) 488 | if err != nil { 489 | valid = false 490 | log.Println(fmt.Sprintf("HTTP form input for field {{.NameWithLowerFirst}} %s is not a float value - %s", 491 | {{.NameWithLowerFirst}}Str, err.Error())) 492 | {{$resourceNameLower}}Form.SetErrorMessageForField("{{.NameWithUpperFirst}}", "must be a number") 493 | } 494 | {{else if eq .GoType "bool"}} 495 | {{.NameWithLowerFirst}} := false 496 | if len({{.NameWithLowerFirst}}Str) > 0 { 497 | {{.NameWithLowerFirst}}, err = strconv.ParseBool({{.NameWithLowerFirst}}Str) 498 | if err != nil { 499 | valid = false 500 | log.Println(fmt.Sprintf("HTTP form input for field {{.NameWithLowerFirst}} %s is not a bool - %s", 501 | {{.NameWithLowerFirst}}Str, err.Error())) 502 | {{$resourceNameLower}}Form.SetErrorMessageForField("{{.NameWithUpperFirst}}", "must be true or false") 503 | } 504 | } 505 | {{end}} 506 | {{end}} 507 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 508 | {{end}} 509 | if valid { 510 | // The HTML form data is valid so far - check the mandatory string fields. 511 | {{$resourceNameLower}}Form.SetValid({{.NameWithLowerFirst}}Form.Validate()) 512 | } else { 513 | // Syntax errors in the HTML form data. Validate the mandatory string 514 | // fields to set any remaining error messages, but set the form invalid 515 | // anyway. 516 | {{$resourceNameLower}}Form.Validate() 517 | {{$resourceNameLower}}Form.SetValid(false) 518 | } 519 | return {{.NameWithLowerFirst}}Form 520 | } 521 | {{end}} 522 | 523 | // Recover from any panic and log an error. 524 | func catchPanic() { 525 | if p := recover(); p != nil { 526 | log.Printf("unrecoverable internal error %v\n", p) 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /templates/model.concrete.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{$resourceNameLower}} 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | // Generated by the goblimey scaffold generator. You are STRONGLY 12 | // recommended not to alter this file, as it will be overwritten next time the 13 | // scaffolder is run. For the same reason, do not commit this file to a 14 | // source code repository. Commit the json specification which was used to 15 | // produce it. 16 | 17 | // Concrete{{$resourceNameUpper}} satisfies the {{$resourceNameUpper}} interface. 18 | // It's used to hold the data representing a {{$resourceNameLower}}. 19 | 20 | type Concrete{{$resourceNameUpper}} struct { 21 | id uint64 22 | {{range .Fields}} 23 | {{.NameWithLowerFirst}} {{.GoType}} 24 | {{end}} 25 | } 26 | 27 | // Define the factory functions. 28 | 29 | // Make{{$resourceNameUpper}} creates and returns a new uninitialised {{$resourceNameUpper}} object 30 | func Make{{$resourceNameUpper}}() {{$resourceNameUpper}} { 31 | var concrete{{$resourceNameUpper}} Concrete{{$resourceNameUpper}} 32 | return &concrete{{$resourceNameUpper}} 33 | } 34 | 35 | // MakeInitialised{{$resourceNameUpper}} creates and returns a new {{$resourceNameUpper}} object initialised from 36 | // the arguments 37 | func MakeInitialised{{$resourceNameUpper}}(id uint64, {{range .Fields}}{{.NameWithLowerFirst}} {{.GoType}}{{if not .LastItem}}, {{end}}{{end}}) {{$resourceNameUpper}} { 38 | {{$resourceNameLower}} := Make{{$resourceNameUpper}}() 39 | {{$resourceNameLower}}.SetID(id) 40 | {{range .Fields}} 41 | {{if eq .Type "string"}} 42 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}(strings.TrimSpace({{.NameWithLowerFirst}})) 43 | {{else}} 44 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}}) 45 | {{end}} 46 | {{end}} 47 | return {{$resourceNameLower}} 48 | } 49 | 50 | // Clone creates and returns a new {{$resourceNameUpper}} object initialised from a source {{$resourceNameUpper}}. 51 | func Clone({{$resourceNameLower}} {{$resourceNameUpper}}) {{$resourceNameUpper}} { 52 | return MakeInitialised{{$resourceNameUpper}}({{$resourceNameLower}}.ID(), {{range .Fields}}{{$resourceNameLower}}.{{.NameWithUpperFirst}}(){{if not .LastItem}}, {{end}}{{end}}) 53 | } 54 | 55 | // Define the getters. 56 | 57 | // ID() gets the id of the {{$resourceNameLower}}. 58 | func (o Concrete{{$resourceNameUpper}}) ID() uint64 { 59 | return o.id 60 | } 61 | {{range .Fields}} 62 | //{{.NameWithUpperFirst}} gets the {{.NameWithLowerFirst}} of the {{$resourceNameLower}}. 63 | func (o Concrete{{$resourceNameUpper}}) {{.NameWithUpperFirst}}() {{.GoType}} { 64 | return o.{{.NameWithLowerFirst}} 65 | } 66 | {{end}} 67 | // String gets the {{$resourceNameLower}} as a string. 68 | func (o Concrete{{$resourceNameUpper}}) String() string { 69 | return fmt.Sprintf("Concrete{{$resourceNameUpper}}={id=%d, {{range .Fields}}{{.NameWithLowerFirst}}=%v{{if not .LastItem}}, {{end}}{{end}}{{"}"}}", 70 | o.id, {{range .Fields}}o.{{.NameWithLowerFirst}}{{if not .LastItem}}, {{end}}{{end}}) 71 | } 72 | // DisplayName returns a name for the object composed of the values of the id and 73 | // the value of any field not marked as excluded. 74 | func (o Concrete{{$resourceNameUpper}}) DisplayName() string { 75 | return fmt.Sprintf("%d{{range .Fields}}{{if not .ExcludeFromDisplay}} %v{{end}}{{end}}", 76 | o.id{{range .Fields}}{{if not .ExcludeFromDisplay}}, o.{{.NameWithLowerFirst}}{{end}}{{end}}) 77 | } 78 | 79 | // Define the setters. 80 | 81 | // SetID sets the id to the given value. 82 | func (o *Concrete{{$resourceNameUpper}}) SetID(id uint64) { 83 | o.id = id 84 | } 85 | 86 | {{range .Fields}} 87 | // Set{{.NameWithUpperFirst}} sets the {{.NameWithLowerFirst}} of the {{$resourceNameLower}}. 88 | func (o *Concrete{{$resourceNameUpper}}) Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}} {{.GoType}}) { 89 | {{if eq .Type "string"}} 90 | o.{{.NameWithLowerFirst}} = strings.TrimSpace({{.NameWithLowerFirst}}) 91 | {{else}} 92 | o.{{.NameWithLowerFirst}} = {{.NameWithLowerFirst}} 93 | {{end}} 94 | } 95 | {{end}} 96 | 97 | // Define the validation. 98 | func (o *Concrete{{$resourceNameUpper}}) Validate() error { 99 | 100 | // Trim and test all mandatory string fields 101 | 102 | errorMessage := "" 103 | {{range .Fields}} 104 | {{if and .Mandatory (eq .Type "string")}} 105 | if len(strings.TrimSpace(o.{{.NameWithUpperFirst}}())) <= 0 { 106 | errorMessage += "you must specify the {{.NameWithLowerFirst}} " 107 | } 108 | {{end}} 109 | {{end}} 110 | if len(errorMessage) > 0 { 111 | return errors.New(errorMessage) 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /templates/model.concrete.test.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{.NameWithLowerFirst}} 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | // Generated by the goblimey scaffold generator. You are STRONGLY 10 | // recommended not to alter this file, as it will be overwritten next time the 11 | // scaffolder is run. For the same reason, do not commit this file to a 12 | // source code repository. Commit the json specification which was used to 13 | // produce it. 14 | 15 | // Unit tests for the Concrete{{$resourceNameUpper}} object. 16 | 17 | {{/* This creates the expected values using the field name and the first test 18 | value from each field, something like: 19 | var expectedForename string = "s1" 20 | var expectedSurname string = "s2" */}} 21 | {{range $index, $element := .Fields}} 22 | {{if eq .Type "string"}} 23 | var expected{{.NameWithUpperFirst}} {{.GoType}} = "{{index .TestValues 0}}" 24 | {{else}} 25 | var expected{{.NameWithUpperFirst}} {{.GoType}} = {{index .TestValues 0}} 26 | {{end}} 27 | {{end}} 28 | func TestUnitCreateConcrete{{$resourceNameUpper}}AndCheckContents(t *testing.T) { 29 | var expectedID uint64 = 42 30 | 31 | {{$resourceNameLower}} := MakeInitialised{{$resourceNameUpper}}(expectedID, {{range .Fields}}expected{{.NameWithUpperFirst}}{{if not .LastItem}}, {{end}}{{end}}) 32 | if {{$resourceNameLower}}.ID() != expectedID { 33 | t.Errorf("expected ID to be %d actually %d", expectedID, {{$resourceNameLower}}.ID()) 34 | } 35 | {{range .Fields}} 36 | if {{$resourceNameLower}}.{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}} { 37 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", expected{{.NameWithUpperFirst}}, {{$resourceNameLower}}.{{.NameWithUpperFirst}}()) 38 | } 39 | {{end}} 40 | } 41 | -------------------------------------------------------------------------------- /templates/model.interface.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{$resourceNameLower}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // {{$resourceNameUpper}} represents a {{$resourceNameLower}} object. 12 | 13 | type {{$resourceNameUpper}} interface { 14 | // ID() gets the id of the {{$resourceNameLower}} 15 | ID() uint64 16 | {{range .Fields}} 17 | //{{.NameWithUpperFirst}} gets the {{.NameWithLowerFirst}} of the {{$resourceNameLower}} 18 | {{.NameWithUpperFirst}}() {{.GoType}} 19 | {{end}} 20 | // String gets the {{$resourceNameLower}} as a string 21 | String() string 22 | // DisplayName gets a name composed of selected fields 23 | DisplayName() string 24 | // SetID sets the id to the given value 25 | SetID(id uint64) 26 | {{range .Fields}} 27 | // Set{{.NameWithUpperFirst}} sets the {{.NameWithLowerFirst}} of the {{$resourceNameLower}} 28 | Set{{.NameWithUpperFirst}}({{.NameWithLowerFirst}} {{.GoType}}) 29 | {{end}} 30 | // Valdate checks the data in the {{.NameWithLowerFirst}}. 31 | Validate() error 32 | } -------------------------------------------------------------------------------- /templates/repository.concrete.gorp.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package gorp 4 | 5 | {{.Imports}} 6 | 7 | // Generated by the goblimey scaffold generator. You are STRONGLY 8 | // recommended not to alter this file, as it will be overwritten next time the 9 | // scaffolder is run. For the same reason, do not commit this file to a 10 | // source code repository. Commit the json specification which was used to 11 | // produce it. 12 | 13 | // This package satisfies the {{.NameWithLowerFirst}} Repository interface and 14 | // provides Create, Read, Update and Delete (CRUD) operations on the {{.PluralNameWithLowerFirst}} resource. 15 | // In this case, the resource is a MySQL table accessed via the GORP ORM. 16 | 17 | type GorpMysqlRepository struct { 18 | dbmap *gorp.DbMap 19 | verbose bool 20 | } 21 | 22 | // MakeRepository is a factory function that creates a GorpMysqlRepository and 23 | // returns it as a Repository. 24 | func MakeRepository(verbose bool) ({{.NameWithLowerFirst}}Repo.Repository, error) { 25 | log.SetPrefix("{{.PluralNameWithLowerFirst}}.MakeRepository() ") 26 | 27 | db, err := sql.Open("{{.DB}}", "{{.DBURL}}") 28 | if err != nil { 29 | log.Printf("failed to get DB handle - %s\n" + err.Error()) 30 | return nil, errors.New("failed to get DB handle - " + err.Error()) 31 | } 32 | // check that the handle works 33 | err = db.Ping() 34 | if err != nil { 35 | log.Printf("cannot connect to DB. %s\n", err.Error()) 36 | return nil, err 37 | } 38 | // construct a gorp DbMap 39 | dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}} 40 | table := dbmap.AddTableWithName(gorp{{.NameWithUpperFirst}}.Concrete{{.NameWithUpperFirst}}{}, "{{.TableName}}").SetKeys(true, "IDField") 41 | if table == nil { 42 | em := "cannot add table {{.TableName}}" 43 | log.Println(em) 44 | return nil, errors.New(em) 45 | } 46 | 47 | table.ColMap("IDField").Rename("id") 48 | {{range .Fields}} 49 | table.ColMap("{{.NameWithUpperFirst}}Field").Rename("{{.NameWithLowerFirst}}") 50 | {{end}} 51 | // Create any missing tables. 52 | err = dbmap.CreateTablesIfNotExists() 53 | if err != nil { 54 | em := fmt.Sprintf("cannot create table - %s\n", err.Error()) 55 | log.Printf("em") 56 | return nil, errors.New(em) 57 | } 58 | 59 | repository := GorpMysqlRepository{dbmap, verbose} 60 | return repository, nil 61 | } 62 | 63 | // SetVerbosity sets the verbosity level. 64 | func (gmpd GorpMysqlRepository) SetVerbosity(verbose bool) { 65 | gmpd.verbose = verbose 66 | } 67 | 68 | // FindAll returns a list of all valid {{.NameWithUpperFirst}} records from the database in a slice. 69 | // The result may be an empty slice. If the database lookup fails, the error is 70 | // returned instead. 71 | func (gmpd GorpMysqlRepository) FindAll() ([]{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) { 72 | log.SetPrefix("FindAll() ") 73 | if gmpd.verbose { 74 | log.Println("") 75 | } 76 | 77 | transaction, err := gmpd.dbmap.Begin() 78 | if err != nil { 79 | em := fmt.Sprintf("cannot create transaction - %s", err.Error()) 80 | log.Println(em) 81 | return nil, errors.New(em) 82 | } 83 | var {{.NameWithLowerFirst}}List []gorp{{.NameWithUpperFirst}}.Concrete{{.NameWithUpperFirst}} 84 | 85 | _, err = transaction.Select(&{{.NameWithLowerFirst}}List, 86 | "select id, {{range .Fields}}{{.NameWithLowerFirst}}{{if not .LastItem}}, {{end}}{{end}} from {{.TableName}}") 87 | if err != nil { 88 | transaction.Rollback() 89 | return nil, err 90 | } 91 | transaction.Commit() 92 | 93 | valid{{.PluralNameWithUpperFirst}} := make([]{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, len({{.NameWithLowerFirst}}List)) 94 | 95 | // Validate and clone the {{.NameWithUpperFirst}} records 96 | 97 | next := 0 // Index of next valid{{.PluralNameWithUpperFirst}} entry 98 | for i, _ := range {{.NameWithLowerFirst}}List { 99 | // Check any mandatory string fields 100 | {{range .Fields}} 101 | {{if eq .Type "string" }} 102 | {{$resourceNameLower}}List[i].Set{{.NameWithUpperFirst}}(strings.TrimSpace({{$resourceNameLower}}List[i].{{.NameWithUpperFirst}}())) 103 | {{end}} 104 | {{if .Mandatory}} 105 | {{if eq .Type "string" }} 106 | if len({{$resourceNameLower}}List[i].{{.NameWithUpperFirst}}()) == 0 { 107 | continue 108 | } 109 | {{end}} 110 | {{end}} 111 | {{end}} 112 | 113 | // All mandatory string fields are set. Clone the data. 114 | valid{{.PluralNameWithUpperFirst}}[next] = gorp{{.NameWithUpperFirst}}.Clone(&{{.NameWithLowerFirst}}List[i]) 115 | next++ 116 | } 117 | 118 | return valid{{.PluralNameWithUpperFirst}}, nil 119 | } 120 | 121 | // FindByID fetches the row from the {{.TableName}} table with the given uint64 id. It 122 | // validates that data and, if it's valid, returns the {{.NameWithLowerFirst}}. If the data is not 123 | // valid the function returns an error message. 124 | func (gmpd GorpMysqlRepository) FindByID(id uint64) ({{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) { 125 | log.SetPrefix("FindByID() ") 126 | if gmpd.verbose{ 127 | log.Printf("id=%d", id) 128 | } 129 | 130 | var {{.NameWithLowerFirst}} gorp{{.NameWithUpperFirst}}.Concrete{{.NameWithUpperFirst}} 131 | transaction, err := gmpd.dbmap.Begin() 132 | if err != nil { 133 | em := fmt.Sprintf("cannot create transaction - %s", err.Error()) 134 | log.Println(em) 135 | return nil, errors.New(em) 136 | } 137 | 138 | err = transaction.SelectOne(&{{.NameWithLowerFirst}}, 139 | "select id, {{range .Fields}}{{.NameWithLowerFirst}}{{if not .LastItem}}, {{end}}{{end}} from {{.TableName}} where id = ?", id) 140 | if err != nil { 141 | transaction.Rollback() 142 | log.Println(err.Error()) 143 | return nil, err 144 | } 145 | transaction.Commit() 146 | if gmpd.verbose { 147 | log.Printf("found {{.NameWithLowerFirst}} %s", {{.NameWithLowerFirst}}.String()) 148 | } 149 | 150 | if err != nil { 151 | return nil, err 152 | } 153 | {{range .Fields}} 154 | {{if eq .Type "string" }} 155 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}(strings.TrimSpace({{$resourceNameLower}}.{{.NameWithUpperFirst}}())) 156 | {{end}} 157 | {{if .Mandatory}} 158 | {{if eq .Type "string" }} 159 | if len({{$resourceNameLower}}.{{.NameWithUpperFirst}}()) == 0 { 160 | em := "{{.NameWithUpperFirst}} must be set" 161 | log.Println(em) 162 | return nil, errors.New(em) 163 | } 164 | {{end}} 165 | {{end}} 166 | {{end}} 167 | return &{{.NameWithLowerFirst}}, nil 168 | } 169 | 170 | // FindByIDStr fetches the row from the {{.TableName}} table with the given string id. It 171 | // validates that data and, if it's valid, returns the {{.NameWithLowerFirst}}. If the data is not valid 172 | // the function returns an errormessage. The ID in the database is numeric and the method 173 | // checks that the given ID is also numeric before it makes the call. This avoids hitting 174 | // the DB when the id is obviously junk. 175 | func (gmpd GorpMysqlRepository) FindByIDStr(idStr string) ({{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) { 176 | log.SetPrefix("FindByIDStr() ") 177 | if gmpd.verbose { 178 | log.Printf("id=%s", idStr) 179 | } 180 | 181 | id, err := strconv.ParseUint(idStr, 10, 64) 182 | if err != nil { 183 | em := fmt.Sprintf("ID %s is not an unsigned integer", idStr) 184 | log.Println(em) 185 | return nil, fmt.Errorf("ID %s is not an unsigned integer", idStr) 186 | } 187 | return gmpd.FindByID(id) 188 | } 189 | 190 | // Create takes a {{.NameWithLowerFirst}}, creates a record in the {{.TableName}} table containing the same 191 | // data with an auto-incremented ID and returns any error that the DB call returns. 192 | // On a successful create, the method returns the created {{.NameWithLowerFirst}}, including 193 | // the assigned ID. This is all done within a transaction to ensure atomicity. 194 | func (gmpd GorpMysqlRepository) Create({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) ({{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) { 195 | log.SetPrefix("Create() ") 196 | if gmpd.verbose { 197 | log.Println("") 198 | } 199 | 200 | tx, err := gmpd.dbmap.Begin() 201 | if err != nil { 202 | log.Println(err.Error()) 203 | return nil, err 204 | } 205 | {{.NameWithLowerFirst}}.SetID(0) // provokes the auto-increment 206 | err = tx.Insert({{.NameWithLowerFirst}}) 207 | if err != nil { 208 | tx.Rollback() 209 | return nil, err 210 | } 211 | 212 | err = tx.Commit() 213 | if err != nil { 214 | tx.Rollback() 215 | return nil, err 216 | } 217 | 218 | if gmpd.verbose { 219 | log.Printf("created {{.NameWithLowerFirst}} %s", {{.NameWithLowerFirst}}.String()) 220 | } 221 | return {{.NameWithLowerFirst}}, nil 222 | } 223 | 224 | // Update takes a {{.NameWithLowerFirst}} record, updates the record in the {{.TableName}} table with the same ID 225 | // and returns the updated {{.NameWithLowerFirst}} or any error that the DB call supplies to it. The update 226 | // is done within a transaction 227 | func (gmpd GorpMysqlRepository) Update({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) (uint64, error) { 228 | log.SetPrefix("Update() ") 229 | 230 | tx, err := gmpd.dbmap.Begin() 231 | if err != nil { 232 | log.Println(err.Error()) 233 | return 0, err 234 | } 235 | rowsUpdated, err := tx.Update({{.NameWithLowerFirst}}) 236 | if err != nil { 237 | tx.Rollback() 238 | log.Println(err.Error()) 239 | return 0, err 240 | } 241 | if rowsUpdated != 1 { 242 | tx.Rollback() 243 | em := fmt.Sprintf("update failed - %d rows would have been updated, expected 1", rowsUpdated) 244 | log.Println(em) 245 | return 0, errors.New(em) 246 | } 247 | 248 | err = tx.Commit() 249 | if err != nil { 250 | tx.Rollback() 251 | log.Println(err.Error()) 252 | return 0, err 253 | } 254 | 255 | // Success! 256 | return 1, nil 257 | } 258 | 259 | // DeleteByID takes the given uint64 ID and deletes the record with that ID from the {{.TableName}} table. 260 | // The function returns the row count and error that the database supplies to it. On a successful 261 | // delete, it should return 1, having deleted one row. 262 | func (gmpd GorpMysqlRepository) DeleteByID(id uint64) (int64, error) { 263 | log.SetPrefix("DeleteByID() ") 264 | 265 | if gmpd.verbose { 266 | log.Printf("id=%d", id) 267 | } 268 | 269 | // Need a {{.NameWithUpperFirst}} record for the delete method, so fake one up. 270 | var {{.NameWithLowerFirst}} gorp{{.NameWithUpperFirst}}.Concrete{{.NameWithUpperFirst}} 271 | {{.NameWithLowerFirst}}.SetID(id) 272 | tx, err := gmpd.dbmap.Begin() 273 | if err != nil { 274 | log.Println(err.Error()) 275 | return 0, err 276 | } 277 | rowsDeleted, err := tx.Delete(&{{.NameWithLowerFirst}}) 278 | if err != nil { 279 | tx.Rollback() 280 | log.Println(err.Error()) 281 | return 0, err 282 | } 283 | if rowsDeleted != 1 { 284 | tx.Rollback() 285 | em := fmt.Sprintf("delete failed - %d rows would have been deleted, expected 1", rowsDeleted) 286 | log.Println(em) 287 | return 0, errors.New(em) 288 | } 289 | 290 | err = tx.Commit() 291 | if err != nil { 292 | tx.Rollback() 293 | log.Println(err.Error()) 294 | return 0, err 295 | } 296 | if err != nil { 297 | log.Println(err.Error()) 298 | } 299 | return rowsDeleted, nil 300 | } 301 | 302 | // DeleteByIDStr takes the given String ID and deletes the record with that ID from the {{.TableName}} table. 303 | // The ID in the database is numeric and the method checks that the given ID is also numeric before 304 | // it makes the call. If not, it returns an error. If the ID looks sensible, the function attempts 305 | // the delete and returns the row count and error that the database supplies to it. On a successful 306 | // delete, it should return 1, having deleted one row. 307 | func (gmpd GorpMysqlRepository) DeleteByIDStr(idStr string) (int64, error) { 308 | log.SetPrefix("DeleteByIDStr() ") 309 | if gmpd.verbose { 310 | log.Printf("ID %s", idStr) 311 | } 312 | // Check the id. 313 | id, err := strconv.ParseUint(idStr, 10, 64) 314 | if err != nil { 315 | em := fmt.Sprintf("ID %s is not an unsigned integer", idStr) 316 | log.Println(em) 317 | return 0, errors.New(em) 318 | } 319 | return gmpd.DeleteByID(id) 320 | } 321 | 322 | // Close closes the repository, reclaiming any redundant resources, in 323 | // particular, any open database connection and transactions. Anything that 324 | // creates a repository MUST call this when it's finished, to avoid resource 325 | // leaks. 326 | func (gmpd GorpMysqlRepository) Close() { 327 | log.SetPrefix("Close() ") 328 | if gmpd.verbose { 329 | log.Printf("closing the {{.NameWithLowerFirst}} repository") 330 | } 331 | gmpd.dbmap.Db.Close() 332 | } 333 | -------------------------------------------------------------------------------- /templates/repository.concrete.gorp.test.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package gorp 4 | 5 | {{.Imports}} 6 | 7 | // Generated by the goblimey scaffold generator. You are STRONGLY 8 | // recommended not to alter this file, as it will be overwritten next time the 9 | // scaffolder is run. For the same reason, do not commit this file to a 10 | // source code repository. Commit the json specification which was used to 11 | // produce it. 12 | 13 | // Integration tests for the Gorp MySQL {{.NameWithLowerFirst}} repository. 14 | 15 | {{/* This creates the expected values using the field names and the test 16 | values, something like: 17 | var expectedName1 string = "s1" 18 | var expectedAge1 int = 2 19 | var expectedName2 string = "s3" 20 | var expectedAge2 int = 4 */}} 21 | {{range $index, $element := .Fields}} 22 | {{if eq .Type "string"}} 23 | var expected{{.NameWithUpperFirst}}1 {{.GoType}} = "{{index .TestValues 0}}" 24 | {{else}} 25 | var expected{{.NameWithUpperFirst}}1 {{.GoType}} = {{index .TestValues 0}} 26 | {{end}} 27 | {{if eq .Type "string"}} 28 | var expected{{.NameWithUpperFirst}}2 {{.GoType}} = "{{index .TestValues 1}}" 29 | {{else}} 30 | var expected{{.NameWithUpperFirst}}2 {{.GoType}} = {{index .TestValues 1}} 31 | {{end}} 32 | {{end}} 33 | 34 | // Create a {{.NameWithLowerFirst}} in the database, read it back, test the contents. 35 | func TestIntCreate{{.NameWithUpperFirst}}StoreFetchBackAndCheckContents(t *testing.T) { 36 | log.SetPrefix("TestIntegrationegrationCreate{{.NameWithUpperFirst}}AndCheckContents") 37 | 38 | // Create a GORP {{.PluralNameWithLowerFirst}} repository 39 | repository, err := MakeRepository(false) 40 | if err != nil { 41 | log.Println(err.Error()) 42 | fmt.Fprintln(os.Stderr, err.Error()) 43 | os.Exit(-1) 44 | } 45 | defer repository.Close() 46 | 47 | clearDown(repository, t) 48 | 49 | o := gorp{{.NameWithUpperFirst}}.MakeInitialised{{.NameWithUpperFirst}}(0, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 50 | {{.NameWithLowerFirst}}, err := repository.Create(o) 51 | if err != nil { 52 | t.Errorf(err.Error()) 53 | } 54 | 55 | retrieved{{.NameWithUpperFirst}}, err := repository.FindByID({{.NameWithLowerFirst}}.ID()) 56 | if err != nil { 57 | t.Errorf(err.Error()) 58 | } 59 | 60 | if retrieved{{.NameWithUpperFirst}}.ID() != {{.NameWithLowerFirst}}.ID() { 61 | t.Errorf("expected ID to be %d actually %d", {{.NameWithLowerFirst}}.ID(), 62 | retrieved{{.NameWithUpperFirst}}.ID()) 63 | } 64 | {{range .Fields}} 65 | if retrieved{{$resourceNameUpper}}.{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}}1 { 66 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", expected{{.NameWithUpperFirst}}1, {{$resourceNameLower}}.{{.NameWithUpperFirst}}()) 67 | } 68 | {{end}} 69 | 70 | // Delete {{.NameWithLowerFirst}} and check response 71 | rows, err := repository.DeleteByID(retrieved{{.NameWithUpperFirst}}.ID()) 72 | if err != nil { 73 | t.Errorf(err.Error()) 74 | } 75 | if rows != 1 { 76 | t.Errorf("expected delete to return 1, actual %d", rows) 77 | } 78 | clearDown(repository, t) 79 | } 80 | 81 | // Create two {{.NameWithLowerFirst}} records in the DB, read them back and check the fields 82 | func TestIntCreateTwo{{.PluralNameWithUpperFirst}}AndReadBack(t *testing.T) { 83 | log.SetPrefix("TestCreate{{.NameWithUpperFirst}}AndReadBack") 84 | 85 | // Create a GORP {{.PluralNameWithLowerFirst}} repository 86 | repository, err := MakeRepository(false) 87 | if err != nil { 88 | log.Println(err.Error()) 89 | fmt.Fprintln(os.Stderr, err.Error()) 90 | os.Exit(-1) 91 | } 92 | defer repository.Close() 93 | 94 | clearDown(repository, t) 95 | 96 | //Create two {{.PluralNameWithLowerFirst}} 97 | 98 | o1 := gorp{{.NameWithUpperFirst}}.MakeInitialised{{.NameWithUpperFirst}}(0, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 99 | {{.NameWithLowerFirst}}1, err := repository.Create(o1) 100 | if err != nil { 101 | t.Errorf(err.Error()) 102 | } 103 | 104 | o2 := gorp{{.NameWithUpperFirst}}.MakeInitialised{{.NameWithUpperFirst}}(0, {{range .Fields}}expected{{.NameWithUpperFirst}}2{{if not .LastItem}}, {{end}}{{end}}) 105 | {{.NameWithLowerFirst}}2, err := repository.Create(o2) 106 | if err != nil { 107 | t.Errorf(err.Error()) 108 | } 109 | 110 | // read all the {{.PluralNameWithLowerFirst}} in the DB - expect just the two we created 111 | {{.PluralNameWithLowerFirst}}, err := repository.FindAll() 112 | if err != nil { 113 | t.Errorf(err.Error()) 114 | } 115 | 116 | if len({{.PluralNameWithLowerFirst}}) != 2 { 117 | t.Errorf("expected 2 rows, actual %d", len({{.PluralNameWithLowerFirst}})) 118 | } 119 | 120 | for _, {{.NameWithLowerFirst}} := range {{.PluralNameWithLowerFirst}} { 121 | 122 | matches := 1 123 | 124 | {{/* Check that the fields of each are consistent with the source object. 125 | (Note: we don't know what in order the two objects will come back.) */}} 126 | 127 | {{$firstField := index .Fields 0}} 128 | {{$firstFieldName := $firstField.NameWithUpperFirst}} 129 | switch {{.NameWithLowerFirst}}.{{$firstFieldName}}() { 130 | case expected{{$firstFieldName}}1: 131 | {{range .Fields}} 132 | {{if ne $firstFieldName .NameWithUpperFirst}} 133 | if {{$resourceNameLower}}.{{.NameWithUpperFirst}}() == expected{{.NameWithUpperFirst}}1 { 134 | matches++ 135 | } else { 136 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", 137 | expected{{.NameWithUpperFirst}}1, {{$resourceNameLower}}.{{.NameWithUpperFirst}}()) 138 | } 139 | {{end}} 140 | {{end}} 141 | case expected{{$firstFieldName}}2: 142 | {{range .Fields}} 143 | {{if ne $firstFieldName .NameWithUpperFirst}} 144 | if {{$resourceNameLower}}.{{.NameWithUpperFirst}}() == expected{{.NameWithUpperFirst}}2 { 145 | matches++ 146 | } else { 147 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", 148 | expected{{.NameWithUpperFirst}}2, {{$resourceNameLower}}.{{.NameWithUpperFirst}}()) 149 | } 150 | {{end}} 151 | {{end}} 152 | default: 153 | t.Errorf("unexpected {{.NameWithLowerFirst}} with name %s - expected %s or %s", 154 | {{$resourceNameLower}}.{{$firstFieldName}}(), expected{{$firstFieldName}}1, expected{{$firstFieldName}}2) 155 | } 156 | 157 | // We should have one match for each field 158 | if matches != {{len .Fields}} { 159 | t.Errorf("expected %d fields, actual %d", {{len .Fields}}, matches) 160 | } 161 | } 162 | 163 | 164 | // Find the first {{.NameWithLowerFirst}} by numeric ID and check the fields 165 | {{.NameWithLowerFirst}}1Returned, err := repository.FindByID({{.NameWithLowerFirst}}1.ID()) 166 | if err != nil { 167 | t.Errorf(err.Error()) 168 | } 169 | 170 | {{range .Fields}} 171 | if {{$resourceNameLower}}1Returned.{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}}1 { 172 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", 173 | expected{{.NameWithUpperFirst}}1, {{$resourceNameLower}}1Returned.{{.NameWithUpperFirst}}()) 174 | } 175 | {{end}} 176 | 177 | // Find the second {{.NameWithLowerFirst}} by string ID and check the fields 178 | IDStr := strconv.FormatUint({{.NameWithLowerFirst}}2.ID(), 10) 179 | {{.NameWithLowerFirst}}2Returned, err := repository.FindByIDStr(IDStr) 180 | if err != nil { 181 | t.Errorf(err.Error()) 182 | } 183 | 184 | {{range .Fields}} 185 | if {{$resourceNameLower}}2Returned.{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}}2 { 186 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", 187 | expected{{.NameWithUpperFirst}}2, {{$resourceNameLower}}2Returned.{{.NameWithUpperFirst}}()) 188 | } 189 | {{end}} 190 | 191 | clearDown(repository, t) 192 | } 193 | 194 | // Create two {{.PluralNameWithUpperFirst}}, remove one, check that we get back just the other 195 | func TestIntCreateTwo{{.PluralNameWithUpperFirst}}AndDeleteOneByIDStr(t *testing.T) { 196 | log.SetPrefix("TestIntegrationegrationCreateTwoPeopleAndDeleteOneByIDStr") 197 | 198 | // Create a GORP {{.PluralNameWithLowerFirst}} repository 199 | repository, err := MakeRepository(false) 200 | if err != nil { 201 | log.Println(err.Error()) 202 | fmt.Fprintln(os.Stderr, err.Error()) 203 | os.Exit(-1) 204 | } 205 | defer repository.Close() 206 | 207 | clearDown(repository, t) 208 | 209 | // Create two {{.PluralNameWithLowerFirst}} 210 | o1 := gorp{{.NameWithUpperFirst}}.MakeInitialised{{.NameWithUpperFirst}}(0, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 211 | {{.NameWithLowerFirst}}1, err := repository.Create(o1) 212 | if err != nil { 213 | t.Errorf(err.Error()) 214 | } 215 | 216 | o2 := gorp{{.NameWithUpperFirst}}.MakeInitialised{{.NameWithUpperFirst}}(0, {{range .Fields}}expected{{.NameWithUpperFirst}}2{{if not .LastItem}}, {{end}}{{end}}) 217 | {{.NameWithLowerFirst}}2, err := repository.Create(o2) 218 | if err != nil { 219 | t.Errorf(err.Error()) 220 | } 221 | 222 | var IDStr = fmt.Sprintf("%d", {{.NameWithLowerFirst}}1.ID()) 223 | rows, err := repository.DeleteByIDStr(IDStr) 224 | if err != nil { 225 | t.Errorf(err.Error()) 226 | } 227 | if rows != 1 { 228 | t.Errorf("expected one record to be deleted, actually %d", rows) 229 | } 230 | 231 | // We should have one record in the DB and it should match {{.NameWithLowerFirst}}2 232 | {{.PluralNameWithLowerFirst}}, err := repository.FindAll() 233 | if err != nil { 234 | t.Errorf(err.Error()) 235 | } 236 | 237 | if len({{.PluralNameWithLowerFirst}}) != 1 { 238 | t.Errorf("expected one record, actual %d", len({{.PluralNameWithLowerFirst}})) 239 | } 240 | 241 | if {{.PluralNameWithLowerFirst}}[0].ID() != {{.NameWithLowerFirst}}2.ID() { 242 | t.Errorf("expected id to be %d actually %d", 243 | {{.NameWithLowerFirst}}2.ID(), {{.PluralNameWithLowerFirst}}[0].ID()) 244 | } 245 | {{$name := .PluralNameWithLowerFirst}} 246 | {{range .Fields}} 247 | if {{$name}}[0].{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}}2 { 248 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", 249 | expected{{.NameWithUpperFirst}}2, {{$name}}[0].{{.NameWithUpperFirst}}()) 250 | } 251 | {{end}} 252 | 253 | clearDown(repository, t) 254 | } 255 | 256 | // Create a {{.NameWithLowerFirst}} record, update the record, read it back and check the updated values. 257 | func TestIntCreate{{.NameWithUpperFirst}}AndUpdate(t *testing.T) { 258 | log.SetPrefix("TestIntCreate{{.NameWithUpperFirst}}AndUpdate") 259 | 260 | // Create a GORP {{.PluralNameWithLowerFirst}} repository 261 | repository, err := MakeRepository(false) 262 | if err != nil { 263 | log.Println(err.Error()) 264 | fmt.Fprintln(os.Stderr, err.Error()) 265 | os.Exit(-1) 266 | } 267 | defer repository.Close() 268 | 269 | clearDown(repository, t) 270 | 271 | // Create a {{.NameWithLowerFirst}} in the DB. 272 | o := gorp{{.NameWithUpperFirst}}.MakeInitialised{{.NameWithUpperFirst}}(0, {{range .Fields}}expected{{.NameWithUpperFirst}}1{{if not .LastItem}}, {{end}}{{end}}) 273 | {{.NameWithLowerFirst}}, err := repository.Create(o) 274 | if err != nil { 275 | t.Errorf(err.Error()) 276 | } 277 | 278 | // Update the {{.NameWithLowerFirst}} in the DB. 279 | {{range .Fields}} 280 | {{$resourceNameLower}}.Set{{.NameWithUpperFirst}}(expected{{.NameWithUpperFirst}}2) 281 | {{end}} 282 | rows, err := repository.Update({{.NameWithLowerFirst}}) 283 | if err != nil { 284 | t.Errorf(err.Error()) 285 | } 286 | if rows != 1 { 287 | t.Errorf("expected 1 row to be updated, actually %d rows", rows) 288 | } 289 | 290 | // fetch the updated record back and check it. 291 | retrieved{{.NameWithUpperFirst}}, err := repository.FindByID({{.NameWithLowerFirst}}.ID()) 292 | if err != nil { 293 | t.Errorf(err.Error()) 294 | } 295 | 296 | {{range .Fields}} 297 | if retrieved{{$resourceNameUpper}}.{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}}2 { 298 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", 299 | expected{{.NameWithUpperFirst}}2, retrieved{{$resourceNameUpper}}.{{.NameWithUpperFirst}}()) 300 | } 301 | {{end}} 302 | 303 | clearDown(repository, t) 304 | } 305 | 306 | // clearDown() - helper function to remove all {{.PluralNameWithLowerFirst}} from the DB 307 | func clearDown(repository {{.NameWithLowerFirst}}.Repository, t *testing.T) { 308 | {{.PluralNameWithLowerFirst}}, err := repository.FindAll() 309 | if err != nil { 310 | t.Errorf(err.Error()) 311 | return 312 | } 313 | for _, {{.NameWithLowerFirst}} := range {{.PluralNameWithLowerFirst}} { 314 | rows, err := repository.DeleteByID({{.NameWithLowerFirst}}.ID()) 315 | if err != nil { 316 | t.Errorf(err.Error()) 317 | continue 318 | } 319 | if rows != 1 { 320 | t.Errorf("while clearing down, expected 1 row, actual %d", rows) 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /templates/repository.interface.go.template: -------------------------------------------------------------------------------- 1 | package {{.NameWithLowerFirst}} 2 | 3 | {{.Imports}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // This interface defines a repository (AKA a Data Access Object) for 12 | // the {{.TableName}} table. 13 | 14 | type Repository interface { 15 | 16 | // FindAll() returns a pointer to a slice of valid {{.PluralNameWithUpperFirst}} 17 | // records. Any invalid records are left out of the slice (so it may be empty). 18 | FindAll() ([]{{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) 19 | 20 | // FindByid fetches the row from the {{.TableName}} table with the given uint64 21 | // id and validates the data. If the data is valid, the method creates a new 22 | // {{.NameWithUpperFirst}} record and returns a pointer to the version in memory. 23 | // If the data is not valid the method returns an error message. 24 | FindByID(id uint64) ({{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) 25 | 26 | // FindByid fetches the row from the {{.TableName}} table with the given string 27 | // id and validates the data. If it's valid the method creates a {{.NameWithUpperFirst}} 28 | // object and returns a pointer to it. If the data is not valid the function 29 | // returns an error message. 30 | // 31 | // The ID in the database is always numeric so the method first checks that the 32 | // given ID is numeric before making the DB call, returning an error if it's not. 33 | FindByIDStr(idStr string) ({{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) 34 | 35 | // Create takes a {{.NameWithLowerFirst}} and creates a record in the {{.TableName}} 36 | // table containing the same data plus an auto-incremented ID. It returns a 37 | // pointer to the resulting {{.NameWithLowerFirst}} object, or any error that 38 | // the DB call supplies to it. 39 | Create({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) ({{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}, error) 40 | 41 | // Update takes a {{.NameWithLowerFirst}} object, validates it and, if it's 42 | // valid, searches the {{.TableName}} table for a record with a matching ID and 43 | // updates it. It returns the number of rows affected or any error from the 44 | // DB update call. On a successful update, it should return 1, having updated 45 | // one row. 46 | Update({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) (uint64, error) 47 | 48 | // DeleteById takes the given uint64 ID and deletes the record with that ID 49 | // from the {{.TableName}} table. It return the count of rows affected or any 50 | // error from the DB delete call. On a successful delete, it should return 1, 51 | // having deleted one row. 52 | DeleteByID(id uint64) (int64, error) 53 | 54 | // DeleteByIdStr takes the given String ID and deletes the record with that ID 55 | // from the {{.TableName}} table. The IDs in the database are numeric aso the 56 | // method checks that the given ID is also numeric before it makes the DB call 57 | // and returns an error if not. If the ID looks sensible, the method attempts 58 | // the delete and returns the number of rows affected or any error from the 59 | // DB delete call. On a successful delete, it should return 1, having deleted 60 | // one row. 61 | 62 | DeleteByIDStr(idStr string) (int64, error) 63 | 64 | // Close closes the repository, reclaiming any redundant resources, in 65 | // particular, any open database connection and transactions. Anything that 66 | // creates a repository MUST call this when it's finished, to avoid resource 67 | // leaks. 68 | Close() 69 | } 70 | -------------------------------------------------------------------------------- /templates/retrofit.template.go.template: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Generated by the goblimey scaffold generator. You are STRONGLY 8 | // recommended not to alter this file, as it will be overwritten next time the 9 | // scaffolder is run. For the same reason, do not commit this file to a 10 | // source code repository. Commit the json specification which was used to 11 | // produce it. 12 | 13 | // The Template interface mimics the methods of the html/Template API, 14 | // allowing templates to be mocked. 15 | 16 | type Template interface { 17 | // Execute executes the template 18 | Execute(wr io.Writer, data interface{}) error 19 | } 20 | -------------------------------------------------------------------------------- /templates/script.install.bat.template: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM batch file to build the {{.NameAllUpper}} web application server. 3 | REM 4 | REM The script is generated the first time you run the Goblimey scaffolder. If you 5 | REM need to recreate it, run the scaffolder with the -overwrite option. 6 | REM 7 | REM To buld the application, change directory to the one containing this file and 8 | REM run it, for example: 9 | REM 10 | REM cd goprojects/src/github.com/goblimey/{{.Name}} 11 | REM install.bat 12 | REM 13 | REM The script assumes that the scaffolder and the go tools are available via the 14 | REM PATH and that the GOPATH variable contains the name of the Go projects directory. 15 | 16 | goimports -w . 17 | 18 | gofmt -w . 19 | 20 | go install {{.SourceBase}} 21 | -------------------------------------------------------------------------------- /templates/script.install.sh.template: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Script to build the {{.NameAllUpper}} web application server. 4 | # 5 | # The script is generated the first time you run the Goblimey scaffolder. If you 6 | # need to recreate it, run the scaffolder with the -overwrite option. 7 | # 8 | # To buld the application, change directory to the one containing this file and 9 | # run it, for example: 10 | # 11 | # cd $HOME/goprojects/src/github.com/goblimey/{{.Name}} 12 | # ./install.sh 13 | # 14 | # The script assumes that the scaffolder and the go tools are available via the 15 | # PATH and that the GOPATH variable contains the name of the Go projects directory. 16 | 17 | goimports -w . 18 | 19 | gofmt -w . 20 | 21 | go install {{.SourceBase}} 22 | -------------------------------------------------------------------------------- /templates/script.test.bat.template: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Batch file to test the components of the {{.NameAllUpper}} web application server. 3 | REM 4 | REM This script creates mock objects and runs the tests. It's generated the first 5 | REM time you run the Goblimey scaffolder. If you need to recreate it, run the 6 | REM scaffolder with the -overwrite option. 7 | REM 8 | REM With no argument, run all tests. With the argument "unit" run just the unit 9 | REM tests. With argument "int" run just the integration tests. This is done by 10 | REM chhoosing the right names for the test methods - TestUnitIndexWithOnePerson() 11 | REM is assumed to be a unit test and TestIntIndexWithOnePerson() is assumed to 12 | REM be an integration test. 13 | REM 14 | REM The script must be run from the project root, which is where it is stored. It 15 | REM has the directories containing test code hard-wired. As you add your own modules, 16 | REM you need to keep it up to date. 17 | REM 18 | REM The script assumes that the go tools are available via the PATH and that the 19 | REM GOPATH variable contains the name of the Go projects directory. 20 | 21 | 22 | SET testcmd="go test -test.v" 23 | 24 | if [%1]==[] goto build 25 | 26 | if [%1]==["unit"] goto unit 27 | 28 | if [%1]==["int"] goto integration 29 | 30 | @echo "the first argument must be unit or int 31 | exit \B 1 32 | 33 | :unit 34 | SET testcmd="%testcmd% -run=^TestUnit" 35 | gotobuild 36 | 37 | :integration 38 | SET testcmd="%testcmd% -run=^TestInt" 39 | goto build 40 | 41 | :build 42 | 43 | SET startDir=%~dp0 44 | 45 | REM Build mocks 46 | if not exists mkdir %startDir%\src 47 | if not exists mkdir %startDir%\src\{{.SourceBase}} 48 | if not exists mkdir %startDir%\src\{{.SourceBase}}\generated 49 | if not exists mkdir %startDir%\src\{{.SourceBase}}\generated\crud 50 | if not exists mkdir %startDir%\src\{{.SourceBase}}\generated\crud\mocks 51 | if not exists mkdir %startDir%\src\{{.SourceBase}}\generated\crud\mocks\pegomock 52 | 53 | SET dir="{{.SourceBase}}\generated\crud\mocks\pegomock"" 54 | @echo ${dir} 55 | cd %startDir%\src\$dir 56 | pegomock generate --package pegomock --output=mock_template.go "{{.SourceBase}}\generated\crud\retrofit\template" Template 57 | pegomock generate --package pegomock --output=mock_services.go "{{.SourceBase}}\generated\crud\services Services" 58 | pegomock generate --package pegomock --output=mock_response_writer.go "net\http ResponseWriter" 59 | {{range .Resources}} 60 | if not exists mkdir {{.NameWithLowerFirst}} 61 | pegomock generate --package {{.NameWithLowerFirst}} --output="{{.NameWithLowerFirst}}"\mock_repository.go "{{.SourceBase}}\generated\crud\repositories\{{.NameWithLowerFirst}}" Repository 62 | {{end}} 63 | 64 | REM Build 65 | 66 | go build "github.com\goblimey\{{.NameWithLowerFirst}}" 67 | 68 | REM Test 69 | 70 | {{range .Resources}} 71 | dir="{{.SourceBase}}\generated\crud\models\{{.NameWithLowerFirst}}" 72 | @echo ${dir} 73 | cd %startDir%\src\$dir 74 | ${testcmd} 75 | 76 | dir="{{.SourceBase}}\generated\crud\models\{{.NameWithLowerFirst}}\gorp" 77 | @echo ${dir} 78 | cd %startDir%\src\$dir 79 | %testcmd% 80 | 81 | dir="{{.SourceBase}}\generated\crud\repositories\{{.NameWithLowerFirst}}\gorpmysql" 82 | @echo ${dir} 83 | cd %startDir%\src\$dir 84 | %testcmd% 85 | 86 | dir="{{.SourceBase}}\generated\crud\forms\{{.NameWithLowerFirst}}" 87 | @echo ${dir} 88 | cd %startDir%\src\$dir 89 | %testcmd% 90 | 91 | dir="{{.SourceBase}}\generated\crud\forms\{{.NameWithLowerFirst}}" 92 | @echo ${dir} 93 | cd %startDir%\src\$dir 94 | %testcmd% 95 | 96 | dir="{{.SourceBase}}\generated\crud\controllers\{{.NameWithLowerFirst}}" 97 | @echo ${dir} 98 | cd %startDir%\src\$dir 99 | %testcmd% 100 | 101 | {{end}} 102 | -------------------------------------------------------------------------------- /templates/script.test.sh.template: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Script to test the components of the {{.NameAllUpper}} web application server. 4 | # 5 | # This script creates mock objects and runs the tests. It's generated the first 6 | # time you run the Goblimey scaffolder. If you need to recreate it, run the 7 | # scaffolder with the -overwrite option. 8 | # 9 | # With no argument, run all tests. With the argument "unit" run just the unit 10 | # tests. With argument "int" run just the integration tests. This is done by 11 | # chhoosing the right names for the test methods - TestUnitIndexWithOnePerson() 12 | # is assumed to be a unit test and TestIntIndexWithOnePerson() is assumed to 13 | # be an integration test. 14 | # 15 | # The script must be run from the project root, which is where it is stored. It 16 | # has the directories containing test code hard-wired. As you add your own modules, 17 | # you need to keep it up to date. 18 | # 19 | # The script assumes that the go tools are available via the PATH and that the 20 | # GOPATH variable contains the name of the Go projects directory. 21 | 22 | 23 | # This should be set to your project directory 24 | homeDir={{.CurrentDir}} 25 | 26 | cd ${homeDir} 27 | 28 | testcmd='go test -test.v' 29 | if test ! -z $1 30 | then 31 | case $1 in 32 | unit ) 33 | testcmd="$testcmd -run='^TestUnit'";; 34 | int ) 35 | testcmd="$testcmd -run='^TestInt'";; 36 | * ) 37 | echo "first argument must be unit or int" >&2 38 | exit -1 39 | ;; 40 | esac 41 | fi 42 | 43 | # Build mocks 44 | mkdir -p ${homeDir}/generated/crud/mocks/pegomock 45 | dir='generated/crud/mocks/pegomock' 46 | echo ${dir} 47 | cd ${homeDir}/$dir 48 | pegomock generate --package pegomock --output=mock_template.go {{.SourceBase}}/generated/crud/retrofit/template Template 49 | pegomock generate --package pegomock --output=mock_services.go {{.SourceBase}}/generated/crud/services Services 50 | pegomock generate --package pegomock --output=mock_response_writer.go net/http ResponseWriter 51 | {{range .Resources}} 52 | mkdir -p {{.NameWithLowerFirst}} 53 | pegomock generate --package {{.NameWithLowerFirst}} --output={{.NameWithLowerFirst}}/mock_repository.go {{.SourceBase}}/generated/crud/repositories/{{.NameWithLowerFirst}} Repository 54 | {{end}} 55 | 56 | # Build 57 | 58 | go build {{.SourceBase}} 59 | 60 | # Test 61 | 62 | {{range .Resources}} 63 | dir='generated/crud/models/{{.NameWithLowerFirst}}' 64 | echo ${dir} 65 | cd ${homeDir}/$dir 66 | ${testcmd} 67 | 68 | dir='generated/crud/models/{{.NameWithLowerFirst}}/gorp' 69 | echo ${dir} 70 | cd ${homeDir}/$dir 71 | ${testcmd} 72 | 73 | dir='generated/crud/repositories/{{.NameWithLowerFirst}}/gorpmysql' 74 | echo ${dir} 75 | cd ${homeDir}/$dir 76 | ${testcmd} 77 | 78 | dir='generated/crud/forms/{{.NameWithLowerFirst}}' 79 | echo ${dir} 80 | cd ${homeDir}/$dir 81 | ${testcmd} 82 | 83 | dir='generated/crud/controllers/{{.NameWithLowerFirst}}' 84 | echo ${dir} 85 | cd ${homeDir}/$dir 86 | ${testcmd} 87 | 88 | {{end}} 89 | -------------------------------------------------------------------------------- /templates/services.concrete.go.template: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | {{.Imports}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // ConcreteServices satisfies the Services interface and provides services to 12 | // other modules. For example, it provides factory methods that create the basic 13 | // objects that represent database table rows, returning each as an interface 14 | // reference. This supports inversion of control. Rather than creating an object 15 | // itself (and therefore knowing its concrete type), a module use the service to 16 | // create the object. The module also only knows the service as an interface, so 17 | // during testing it can be given a different version of the service which returns 18 | // test objects such as pre-prepared mocks. 19 | 20 | type ConcreteServices struct { 21 | {{range .Resources}} 22 | {{.NameWithLowerFirst}}Repo {{.NameWithLowerFirst}}Repo.Repository 23 | {{end}} 24 | templateMap *map[string]map[string]retrofitTemplate.Template 25 | } 26 | 27 | // Template returns an HTML template, given a resource and a CRUD operation (Index, 28 | // Edit etc). 29 | func (cs ConcreteServices) Template(resource string, operation string) retrofitTemplate.Template { 30 | return (*cs.templateMap)[resource][operation] 31 | } 32 | 33 | // SetTemplates sets all HTML templates from the given map. 34 | func (cs *ConcreteServices) SetTemplates(templateMap *map[string]map[string]retrofitTemplate.Template) { 35 | cs.templateMap = templateMap 36 | } 37 | 38 | // SetTemplate sets the HTML template for the resource and operation. 39 | func (cs *ConcreteServices) SetTemplate(resource string, operation string, 40 | template retrofitTemplate.Template) { 41 | 42 | if (*cs.templateMap)[resource] == nil { 43 | // New row. 44 | (*cs.templateMap)[resource] = make(map[string]retrofitTemplate.Template) 45 | } 46 | 47 | (*cs.templateMap)[resource][operation] = template 48 | } 49 | 50 | {{range .Resources}} 51 | {{$resourceNameLower := .NameWithLowerFirst}} 52 | {{$resourceNameUpper := .NameWithUpperFirst}} 53 | // {{.NameWithUpperFirst}}Repository gets the {{.NameWithLowerFirst}} repository. 54 | func (cs ConcreteServices) {{.NameWithUpperFirst}}Repository() {{.NameWithLowerFirst}}Repo.Repository { 55 | return cs.{{.NameWithLowerFirst}}Repo 56 | } 57 | 58 | // Set{{.NameWithUpperFirst}}Repository sets the {{.NameWithLowerFirst}} repository. 59 | func (cs *ConcreteServices) Set{{.NameWithUpperFirst}}Repository(repo {{.NameWithLowerFirst}}Repo.Repository) { 60 | cs.{{.NameWithLowerFirst}}Repo = repo 61 | } 62 | 63 | // Make{{.NameWithUpperFirst}} creates and returns a new uninitialised {{.NameWithLowerFirst}} object, made by the 64 | // GORP Make{{.NameWithUpperFirst}}. 65 | func (cs *ConcreteServices) Make{{.NameWithUpperFirst}}() {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} { 66 | return gorp{{.NameWithUpperFirst}}.Make{{.NameWithUpperFirst}}() 67 | } 68 | 69 | // MakeInitialised{{.NameWithUpperFirst}} creates and returns a new {{.NameWithUpperFirst}} object initialised from 70 | // the arguments and created using the GORP MakeInitialised{{.NameWithUpperFirst}}. 71 | func (cs *ConcreteServices) MakeInitialised{{.NameWithUpperFirst}}(id uint64, {{range .Fields}}{{.NameWithLowerFirst}} {{.GoType}}{{if not .LastItem}}, {{end}}{{end}}) {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} { 72 | return gorp{{.NameWithUpperFirst}}.MakeInitialised{{.NameWithUpperFirst}}(id, {{range .Fields}}{{.NameWithLowerFirst}}{{if not .LastItem}}, {{end}}{{end}}) 73 | } 74 | 75 | // Clone{{.NameWithUpperFirst}} creates and returns a new {{.NameWithUpperFirst}} object initialised from a source {{.NameWithUpperFirst}}. 76 | // The copy is made using the GORP Clone. 77 | func (cs *ConcreteServices) Clone{{.NameWithUpperFirst}}(source {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} { 78 | return gorp{{.NameWithUpperFirst}}.Clone(source) 79 | } 80 | 81 | // Make{{.NameWithUpperFirst}}Form creates and returns an uninitialised {{.NameWithLowerFirst}} form. 82 | func (cs *ConcreteServices) Make{{.NameWithUpperFirst}}Form() {{.NameWithLowerFirst}}Forms.SingleItemForm { 83 | return {{.NameWithLowerFirst}}Forms.MakeSingleItemForm() 84 | } 85 | 86 | // MakeInitialised{{.NameWithUpperFirst}}Form creates a GORP {{.NameWithLowerFirst}} form containing the given 87 | // {{.NameWithLowerFirst}} and returns it as a {{.NameWithUpperFirst}}Form. 88 | func (cs *ConcreteServices) MakeInitialised{{.NameWithUpperFirst}}Form({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) {{.NameWithLowerFirst}}Forms.SingleItemForm { 89 | return {{.NameWithLowerFirst}}Forms.MakeInitialisedSingleItemForm({{.NameWithLowerFirst}}) 90 | } 91 | 92 | // MakeListForm creates and returns a new uninitialised {{.NameWithLowerFirst}} ListForm 93 | // object as a ListForm. 94 | func (cs *ConcreteServices) Make{{.NameWithUpperFirst}}ListForm() {{.NameWithLowerFirst}}Forms.ListForm { 95 | return {{.NameWithLowerFirst}}Forms.MakeListForm() 96 | } 97 | {{end}} 98 | -------------------------------------------------------------------------------- /templates/services.go.template: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | {{.Imports}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // The Services interface provides services to other modules. For example, it 12 | // provides factory methods that create the basic objects used by the system, 13 | // returning each as an interface reference. This supports inversion of control. 14 | // Rather than creating an object itself (and therefore knowing its concrete type), 15 | // a module should use the service to create the object. The module also only 16 | // knows the service as an interface, so during testing it can be given a different 17 | // version of the service which returns test objects such as pre-prepared mocks. 18 | 19 | type Services interface { 20 | 21 | // Template gets the HTML template named by the resource and operation. 22 | Template(resource string, operation string) retrofitTemplate.Template 23 | 24 | // SetTemplate sets the HTML template for the resource and operation. 25 | SetTemplate(resource string, operation string, template retrofitTemplate.Template) 26 | 27 | // SetTemplates sets all HTML templates from the given map 28 | SetTemplates(templateMap *map[string]map[string]retrofitTemplate.Template) 29 | 30 | {{range .Resources}} 31 | {{$resourceNameLower := .NameWithLowerFirst}} 32 | {{$resourceNameUpper := .NameWithUpperFirst}} 33 | // {{.NameWithUpperFirst}}Repository returns the {{.NameWithLowerFirst}} repository. 34 | {{.NameWithUpperFirst}}Repository() {{.NameWithLowerFirst}}Repo.Repository 35 | 36 | // Set{{.NameWithUpperFirst}}Repository sets the {{.NameWithLowerFirst}} repository. 37 | Set{{.NameWithUpperFirst}}Repository(repository {{.NameWithLowerFirst}}Repo.Repository) 38 | 39 | // Make{{.NameWithUpperFirst}} creates and returns a new uninitialised {{.NameWithUpperFirst}} object. 40 | Make{{.NameWithUpperFirst}}() {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} 41 | 42 | // MakeInitialised{{.NameWithUpperFirst}} creates and returns a new {{.NameWithUpperFirst}} object initialised from 43 | // the arguments. 44 | MakeInitialised{{.NameWithUpperFirst}}(id uint64, {{range .Fields}}{{.NameWithLowerFirst}} {{.GoType}}{{if not .LastItem}}, {{end}}{{end}}) {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} 45 | 46 | // Clone{{.NameWithUpperFirst}} creates and returns a new {{.NameWithUpperFirst}} object initialised from a source {{.NameWithUpperFirst}}. 47 | Clone{{.NameWithUpperFirst}}(source {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}} 48 | 49 | // Make{{.NameWithUpperFirst}}Form creates and returns an uninitialised {{.NameWithLowerFirst}} form. 50 | Make{{.NameWithUpperFirst}}Form() {{.NameWithLowerFirst}}Forms.SingleItemForm 51 | 52 | // MakeInitialised{{.NameWithUpperFirst}}Form creates and returns a {{.NameWithLowerFirst}} form containing the 53 | // given {{.NameWithLowerFirst}} object. 54 | MakeInitialised{{.NameWithUpperFirst}}Form({{.NameWithLowerFirst}} {{.NameWithLowerFirst}}.{{.NameWithUpperFirst}}) {{.NameWithLowerFirst}}Forms.SingleItemForm 55 | 56 | // Make{{.NameWithUpperFirst}}ListForm creates a new uninitialised {{.NameWithLowerFirst}} ConcreteListForm object and 57 | // returns it as a ListForm. 58 | Make{{.NameWithUpperFirst}}ListForm() {{.NameWithLowerFirst}}Forms.ListForm 59 | {{end}} 60 | } 61 | -------------------------------------------------------------------------------- /templates/sql.create.db.template: -------------------------------------------------------------------------------- 1 | -- Command to create the {{.Name}} database. 2 | -- Run these commands as the database admin user. 3 | 4 | -- Generated by the goblimey scaffold generator. You are STRONGLY 5 | -- recommended not to alter this file, as it will be overwritten next time the 6 | -- scaffolder is run. For the same reason, do not commit this file to a 7 | -- source code repository. Commit the json specification which was used to 8 | -- produce it. 9 | 10 | create database {{.Name}}; 11 | 12 | grant all on {{.Name}}.* to '{{.DBUser}}' identified by '{{.DBPassword}}'; 13 | 14 | quit -------------------------------------------------------------------------------- /templates/test.go.template: -------------------------------------------------------------------------------- 1 | {{$resourceNameLower := .NameWithLowerFirst}} 2 | {{$resourceNameUpper := .NameWithUpperFirst}} 3 | package {{$resourceNameLower}} 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | // Generated by the goblimey scaffold generator. You are STRONGLY 10 | // recommended not to alter this file, as it will be overwritten next time the 11 | // scaffolder is run. For the same reason, do not commit this file to a 12 | // source code repository. Commit the json specification which was used to 13 | // produce it. 14 | 15 | // Unit tests for the Concrete{{$resourceNameUpper}} object. 16 | 17 | func TestUnitCreateConcrete{{$resourceNameUpper}}Setters(t *testing.T) { 18 | var expectedID uint64 = 42 19 | /* 20 | This creates the expected values using the first test value from each field, 21 | something like: 22 | var expectedForename string = "s1" 23 | var expectedSurname string = "s3" 24 | */ 25 | {{range $index, $element := .Fields}} 26 | var expected{{.NameWithUpperFirst}} {{.Type}} = "{{index .TestValues 0}}" 27 | {{end}} 28 | {{$resourceNameLower}} := MakeInitialised{{$resourceNameUpper}}(expectedID, {{range .Fields}}expected{{.NameWithUpperFirst}}{{if not .LastItem}}, {{end}}{{end}}) 29 | if {{$resourceNameLower}}.ID() != expectedID { 30 | t.Errorf("expected ID to be %d actually %d", expectedID, {{$resourceNameLower}}.ID()) 31 | } 32 | {{range .Fields}} 33 | if {{$resourceNameLower}}.{{.NameWithUpperFirst}}() != expected{{.NameWithUpperFirst}} { 34 | t.Errorf("expected {{.NameWithLowerFirst}} to be %s actually %s", expected{{.NameWithUpperFirst}}, {{$resourceNameLower}}.{{.NameWithUpperFirst}}()) 35 | } 36 | {{end}} 37 | } 38 | -------------------------------------------------------------------------------- /templates/utilities.go.template: -------------------------------------------------------------------------------- 1 | package utilities 2 | 3 | {{.Imports}} 4 | 5 | // Generated by the goblimey scaffold generator. You are STRONGLY 6 | // recommended not to alter this file, as it will be overwritten next time the 7 | // scaffolder is run. For the same reason, do not commit this file to a 8 | // source code repository. Commit the json specification which was used to 9 | // produce it. 10 | 11 | // Helper methods for the brest of the system. 12 | 13 | func CreateTemplates() *map[string]map[string]retrofitTemplate.Template { 14 | 15 | templateMap := make(map[string]map[string]retrofitTemplate.Template) 16 | templateMap["html"] = make(map[string]retrofitTemplate.Template) 17 | templateMap["html"]["Index"] = template.Must(template.ParseFiles( 18 | "views/html/index.html", 19 | )) 20 | templateMap["html"]["Error"] = template.Must(template.ParseFiles( 21 | "views/html/error.html", 22 | )) 23 | {{range .Resources}} 24 | templateMap["{{.NameWithLowerFirst}}"] = make(map[string]retrofitTemplate.Template) 25 | 26 | templateMap["{{.NameWithLowerFirst}}"]["Index"] = template.Must(template.ParseFiles( 27 | "views/_base.ghtml", 28 | "views/generated/crud/templates/{{.NameWithLowerFirst}}/index.ghtml", 29 | )) 30 | 31 | templateMap["{{.NameWithLowerFirst}}"]["Create"] = template.Must(template.ParseFiles( 32 | "views/_base.ghtml", 33 | "views/generated/crud/templates/{{.NameWithLowerFirst}}/create.ghtml", 34 | )) 35 | 36 | templateMap["{{.NameWithLowerFirst}}"]["Edit"] = template.Must(template.ParseFiles( 37 | "views/_base.ghtml", 38 | "views/generated/crud/templates/{{.NameWithLowerFirst}}/edit.ghtml", 39 | )) 40 | 41 | templateMap["{{.NameWithLowerFirst}}"]["Show"] = template.Must(template.ParseFiles( 42 | "views/_base.ghtml", 43 | "views/generated/crud/templates/{{.NameWithLowerFirst}}/show.ghtml", 44 | )) 45 | 46 | {{end}} 47 | return &templateMap 48 | } 49 | 50 | // BadError handles difficult errors, for example, one that occurs before 51 | // a controller is created. 52 | func BadError(errorMessage string, response *restful.Response) { 53 | log.SetPrefix("BadError() ") 54 | log.Println() 55 | defer noPanic() 56 | fmt.Sprintf("foo", "1", "2") 57 | html := fmt.Sprintf("%s%s%s%s%s%s\n", 58 | "", 59 | "

", 60 | errorMessage, 61 | "

", 62 | "") 63 | 64 | _, err := fmt.Fprintln(response.ResponseWriter, html) 65 | if err != nil { 66 | log.Printf("error while attempting to display the error page of last resort - %s", err.Error()) 67 | http.Error(response.ResponseWriter, err.Error(), http.StatusInternalServerError) 68 | } 69 | return 70 | } 71 | 72 | // Dead displays a hand-crafted error page. It's the page of last resort. 73 | func Dead(response *restful.Response) { 74 | log.SetPrefix("Dead() ") 75 | log.Println() 76 | defer noPanic() 77 | fmt.Sprintf("foo", "1", "2") 78 | html := fmt.Sprintf("%s%s%s%s%s%s\n", 79 | "", 80 | "

", 81 | "This server is experiencing a Total Inability To Service Usual Processing (TITSUP).", 82 | "

", 83 | "

We will be restoring normality just as soon as we are sure what is normal anyway.

", 84 | "") 85 | 86 | _, err := fmt.Fprintln(response.ResponseWriter, html) 87 | if err != nil { 88 | log.Printf("error while attempting to display the error page of last resort - %s", err.Error()) 89 | http.Error(response.ResponseWriter, err.Error(), http.StatusInternalServerError) 90 | } 91 | } 92 | 93 | // Recover from any panic and log an error. 94 | func noPanic() { 95 | if p := recover(); p != nil { 96 | log.Printf("unrecoverable internal error %v\n", p) 97 | } 98 | } 99 | 100 | // Trim removes leading and trailing white space from a string. 101 | func Trim(str string) string { 102 | return strings.Trim(str, " \t\n") 103 | } 104 | 105 | // Map2String displays the contents of a map of strings with string values as a 106 | // single string.The field named "foo" with value "bar" becomes 'foo="bar",'. 107 | func Map2String(m map[string]string) string { 108 | // The result array has two entries for each map key plus leading and 109 | // trailing brackets. 110 | result := make([]string, 0, 2+len(m)*2) 111 | result = append(result, "[") 112 | for key, value := range m { 113 | result = append(result, key) 114 | result = append(result, "=\"") 115 | result = append(result, value) 116 | result = append(result, "\",") 117 | } 118 | result = append(result, "]") 119 | 120 | return strings.Join(result, "") 121 | } -------------------------------------------------------------------------------- /templates/view.base.ghtml.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | Generated by the goblimey scaffold generator the first time it is run. 3 | It's safe to edit this template. If you need to restore the original,run the 4 | scaffolder again with the -overwrite option. 5 | */}} 6 | 7 | 8 | 9 | {{"{{"}} template "PageTitle" .{{"}}"}} 10 | 11 | 12 | 13 |

{{.NameWithUpperFirst}}

14 |

{{"{{"}}template "PageTitle" .}}

15 |

{{"{{"}}.ErrorMessage{{"}}"}}

16 |

{{"{{"}}.Notice{{"}}"}}

17 |
18 | {{"{{"}}template "content" .{{"}}"}} 19 |
20 | 21 | -------------------------------------------------------------------------------- /templates/view.error.html.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | HTML template to create the template for the static error page. With go-restful, 3 | even simple HTML files like this need to be created on the fly from a template. 4 | Generated by the goblimey scaffold generator the first time it is run. 5 | It's safe to edit this template. If you need to restore the original,run the 6 | scaffolder again with the -overwrite option. 7 | */}} 8 | 9 | 10 | Internal error 11 | 12 | 13 |

{{.NameWithUpperFirst}}

14 |

15 | Internal Error - please try again later 16 |

17 | 18 | -------------------------------------------------------------------------------- /templates/view.index.ghtml.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | HTML template to create the template for the home page. With go-restful, even 3 | simple HTML files need to be created on the fly from a template. 4 | Generated by the goblimey scaffold generator the first time it is run. 5 | It's safe to edit this template. If you need to restore the original,run the 6 | scaffolder again with the -overwrite option. 7 | */}} 8 | {{$projectNameLower := .NameWithLowerFirst}} 9 | {{$projectNameUpper := .NameWithUpperFirst}} 10 | 11 | 12 | 13 | {{.NameWithUpperFirst}} list 14 | 15 | 16 | 17 |

{{.NameWithUpperFirst}}

18 | {{range .Resources}} 19 |

20 | Manage {{.PluralNameWithLowerFirst}} 21 |

22 | {{end}} 23 | 24 | -------------------------------------------------------------------------------- /templates/view.resource.create.ghtml.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | Text template to create the HTML template for the Create page. 3 | Generated by the goblimey scaffold generator. You are STRONGLY 4 | /recommended not to alter this file, as it will be overwritten next time the 5 | scaffolder is run. For the same reason, do not commit this file to a 6 | source code repository. Commit the json specification which was used to 7 | produce it. 8 | */}} 9 | {{$resourceNameLower := .NameWithLowerFirst}} 10 | {{$resourceNamePluralLower := .PluralNameWithLowerFirst}} 11 | {{$resourceNameUpper := .NameWithUpperFirst}} 12 | {{"{{"}} define "PageTitle"{{"}}"}}Create a {{.NameWithUpperFirst}} {{"{{end}}"}} 13 | {{"{{"}} define "content" {{"}}"}} 14 |

Items marked "*" are mandatory

15 |
16 | 17 | 18 | {{range .Fields}} 19 | 20 | 21 | 28 | {{end}} 29 | 30 | {{"{{if .FieldErrors."}}{{.NameWithUpperFirst}}{{"}}"}} 31 | 32 | {{"{{end}}"}} 33 | 34 | 35 | {{end}} 36 |
{{.NameWithUpperFirst}}: 22 | {{if eq .Type "bool"}} 23 | 24 | {{else}} 25 | 26 | {{end}} 27 | {{if .Mandatory}}*{{"{{.FieldErrors."}}{{.NameWithUpperFirst}}{{"}}"}}
37 | 38 |
39 |

40 | Home 41 | View All {{.PluralNameWithUpperFirst}} 42 |

43 | {{"{{end}}"}} 44 | -------------------------------------------------------------------------------- /templates/view.resource.edit.ghtml.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | Text template to create the HTML template for the Edit page. 3 | Generated by the goblimey scaffold generator. You are STRONGLY 4 | /recommended not to alter this file, as it will be overwritten next time the 5 | scaffolder is run. For the same reason, do not commit this file to a 6 | source code repository. Commit the json specification which was used to 7 | produce it. 8 | */}} 9 | {{$resourceNameLower := .NameWithLowerFirst}} 10 | {{$resourceNamePluralLower := .PluralNameWithLowerFirst}} 11 | {{$resourceNameUpper := .NameWithUpperFirst}} 12 | {{"{{"}} define "PageTitle"{{"}}"}}Edit {{.NameWithUpperFirst}} {{"{{."}}{{.NameWithUpperFirst}}.DisplayName{{"}}"}}{{"{{end}}"}} 13 | {{"{{"}} define "content" {{"}}"}} 14 |
15 | 16 | 17 | {{range .Fields}} 18 | 19 | 20 | 27 | {{end}} 28 | {{"{{"}}if .ErrorForField "{{.NameWithUpperFirst}}" {{"}}"}} 29 | 30 | {{"{{else}}"}} 31 | 32 | {{"{{end}}"}} 33 | 34 | {{end}} 35 |
{{.NameWithUpperFirst}}: 21 | {{if eq .Type "bool"}} 22 | 23 | {{else}} 24 | 25 | {{end}} 26 | {{if .Mandatory}}*{{"{{"}}.ErrorForField "{{.NameWithUpperFirst}}"{{"}}"}} 
36 | 37 |
38 |

39 |

40 | 41 | 42 |
43 |

44 |

45 | Home 46 | Show 47 | View All {{.PluralNameWithUpperFirst}} 48 | Create {{.NameWithUpperFirst}} 49 |

50 | {{"{{end}}"}} 51 | -------------------------------------------------------------------------------- /templates/view.resource.index.ghtml.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | Text template to create the HTML template for the Index page for a resource. 3 | Generated by the goblimey scaffold generator. You are STRONGLY 4 | /recommended not to alter this file, as it will be overwritten next time the 5 | scaffolder is run. For the same reason, do not commit this file to a 6 | source code repository. Commit the json specification which was used to 7 | produce it. 8 | */}} 9 | {{$resourceNameLower := .NameWithLowerFirst}} 10 | {{$resourceNamePluralLower := .PluralNameWithLowerFirst}} 11 | {{$resourceNameUpper := .NameWithUpperFirst}} 12 | {{"{{"}} define "PageTitle"{{"}}"}}{{.PluralNameWithUpperFirst}}{{"{{end}}"}} 13 | {{"{{"}} define "content" {{"}}"}} 14 | 15 | {{"{{range ."}}{{.PluralNameWithUpperFirst}} {{"}}"}} 16 | 17 | 20 | 23 | 29 | 30 | {{"{{end}}"}} 31 |
18 | {{"{{."}}DisplayName{{"}}"}} 19 | 21 | Edit 22 | 24 |
25 | 26 | 27 |
28 |
32 |

33 | Home 34 | Create {{.NameWithUpperFirst}} 35 |

36 | {{"{{end}}"}} -------------------------------------------------------------------------------- /templates/view.resource.show.ghtml.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | Text template to create the HTML template for the Show page for a resource. 3 | Generated by the goblimey scaffold generator. You are STRONGLY 4 | /recommended not to alter this file, as it will be overwritten next time the 5 | scaffolder is run. For the same reason, do not commit this file to a 6 | source code repository. Commit the json specification which was used to 7 | produce it. 8 | */}} 9 | {{$resourceNameLower := .NameWithLowerFirst}} 10 | {{$resourceNameUpper := .NameWithUpperFirst}} 11 | {{"{{define"}} "PageTitle"{{"}}"}}{{.NameWithUpperFirst}} {{"{{."}}{{.NameWithUpperFirst}}.DisplayName{{"}}"}}{{"{{end}}"}} 12 | {{"{{define"}} "content"{{"}}"}} 13 |

14 | id: {{"{{"}}.{{.NameWithUpperFirst}}.ID{{"}}"}} 15 |

16 | {{range .Fields}} 17 |

18 | {{.NameWithLowerFirst}}: {{"{{"}}.{{$resourceNameUpper}}.{{.NameWithUpperFirst}}{{"}}"}} 19 |

20 | {{end}} 21 |
22 |
23 | 24 | 25 |
26 |
27 |

28 | Home 29 | Edit 30 | View All {{.PluralNameWithUpperFirst}} 31 |

32 | {{"{{end}}"}} 33 | -------------------------------------------------------------------------------- /templates/view.stylesheets.scaffold.css.template: -------------------------------------------------------------------------------- 1 | {{/* 2 | HTML template to create the template for the stylesheet. Every HTML page 3 | generated by the scaffolder references this. 4 | Generated by the goblimey scaffold generator the first time it is run. 5 | It's safe to edit this file and add extra css directives. If you need to restore 6 | the original,run the scaffolder again with the -overwrite option. 7 | */}} 8 | div.notice { 9 | color: green; 10 | font-weight: bold; 11 | } 12 | 13 | div.ErrorMessage { 14 | color: red; 15 | font-weight: bold; 16 | } --------------------------------------------------------------------------------