├── 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 |
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 |
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 |
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 |
38 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------