diff --git a/.gitignore b/.gitignore index c53038e..0c08a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ pom.xml.asc /.nrepl-port .hgignore .hg/ + +resources/auto/ diff --git a/doc/intro.md b/doc/intro.md index 90828b1..2e730b8 100644 --- a/doc/intro.md +++ b/doc/intro.md @@ -1,11 +1,11 @@ - Application Description Language framework +# Introduction **NOTE**: *this markdown was automatically generated from `adl_user_doc.html`, which in turn was taken from the Wiki page on which this documentation was originally written.* Application Description Language framework ========================================== -Contents +## Contents -------- * [1 What is Application Description Language?](#What_is_Application_Description_Language.3F) @@ -55,16 +55,14 @@ Contents * [9.2.6 Generate Monorail controller classes](#Generate_Monorail_controller_classes) * [9.2.7 Generate Velocity views for use with Monorail](#Generate_Velocity_views_for_use_with_Monorail) -// - -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=1 "Edit section: What is Application Description Language?")\] What is Application Description Language? +## What is Application Description Language? -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Application Description Language is an XML vocabulary, defined in a [Document Type Definition](http://en.wikipedia.org/wiki/Document_Type_Definition "http://en.wikipedia.org/wiki/Document_Type_Definition"), which declaratively describes the entities in an application domain, their relationships, and their properties. Because ADL is defined in a formal definition which can be parsed by XML editors, any DTD-aware XML editor (such as that built into Visual studio) can provide context-sensitive auto-completion for ADL, making the vocabulary easy to learn and to edit. It would perhaps be desirable to replace this DTD at some future stage with an XML Schema, since it is desirable to be able to mix HTML in with ADL in the same document. ADL is thus a '[Fourth Generation Language](http://en.wikipedia.org/wiki/Fourth-generation_programming_language "http://en.wikipedia.org/wiki/Fourth-generation_programming_language")' as understood in the 1980s - an ultra-high level language for a specific problem domain; but it is a purely declarative 4GL. -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=2 "Edit section: Current versions")\] Current versions +## Current versions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ * The current STABLE version of ADL is 1.1. @@ -76,12 +74,12 @@ ADL is thus a '[Fourth Generation Language](http://en.wikipedia.org/wiki/Fourth- * Transforms for ADL 1.2 can be found at [http://libs.cygnets.co.uk/adl/1.2/ADL/transforms/](http://libs.cygnets.co.uk/adl/1.2/ADL/transforms/ "http://libs.cygnets.co.uk/adl/1.2/ADL/transforms/") * The document type definition for ADL 1.2 can be found at [http://libs.cygnets.co.uk/adl/1.2/ADL/schemas/adl-1.2.dtd](http://libs.cygnets.co.uk/adl/1.2/ADL/schemas/adl-1.2.dtd "http://libs.cygnets.co.uk/adl/1.2/ADL/schemas/adl-1.2.dtd") -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=3 "Edit section: What is the Application Description Language Framework?")\] What is the Application Description Language Framework? +\ What is the Application Description Language Framework? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ The Application Description Language Framework is principally a set of XSL transforms which transform a single ADL file into all the various source files required to build an application. -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=4 "Edit section: Why does it matter?")\] Why does it matter? +## Why does it matter? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ The average data driven web application comprises pages (lists) which show lists of entities, pages (forms) that edit instances of entities, and pages (inspectors) that show details of instances of entities. That comprises 100% of many applications and 90% of others; traditionally, even with modern tools like Monorail, coding these lists, forms and inspectors has taken 90% of the development effort. @@ -90,15 +88,15 @@ I realised about three years ago that I was doing essentially the same job over The whole purpose of ADL is to increase productivity - mine, and that of anyone else who chooses to follow me down this path. It is pragmatic technology - it is designed to be an 80/20 or 90/10 solution, taking the repetitious grunt-work out of application development so that we can devote more time to the fun, interesting and novel bits. It is not intended to be an academic, perfect, 100% solution - although for many applications it may in practice be a 100% solution. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=5 "Edit section: Automated Application Generation")\] Automated Application Generation +### Automated Application Generation Thus to create a new application, all that should be necessary is to create a new ADL file, and to compile it using a single, standardised \[[NAnt](http://nant.sourceforge.net/ "http://nant.sourceforge.net/")\] (or \[[Ant](http://ant.apache.org/ "http://ant.apache.org/")\]) build file using scripts already created as part of the framework. All these scripts (with the exception of the PSQL one, which was pre-existing) have been created as part of the [C1873 - SRU - Hospitality](http://wiki.cygnets.co.uk/index.php/C1873_-_SRU_-_Hospitality "C1873 - SRU - Hospitality") contract, but they contain almost no SRU specific material (and what does exist has been designed to be factored out). Prototype 1 of the SRU Hospitality Application contains no hand-written code whatever - all the application code is automatically generated from the single ADL file. The one exception to this rule is the CSS stylesheet which provides look-and-feel and branding. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=6 "Edit section: Integration with hand-written code")\] Integration with hand-written code +### Integration with hand-written code Application-specific procedural code, covering specific business procedures, may still need to be hand written; the code generated by the ADL framework is specifically designed to make it easy to integrate hand-written code. Thus for example the C# entity controller classes generated are intentionally generated as _partial_ classes, so that they may be complemented by other partial classes which may be manually maintained and held in a version control system. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=7 "Edit section: High quality auto-generated code")\] High quality auto-generated code +### High quality auto-generated code One key objective of the framework is that the code which is generated should be as clear and readable - and as well commented - as the best hand-written code. Consider this example: @@ -181,36 +179,36 @@ One key objective of the framework is that the code which is generated should be This means that it should be trivial to decide at some point in development of a project to manually modify and maintain auto-generated code. -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=8 "Edit section: What can the Application Description Language framework now do?")\] What can the Application Description Language framework now do? +## What can the Application Description Language framework now do? ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Currently the framework includes: -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=9 "Edit section: adl2entityclass.xsl")\] adl2entityclass.xsl +### adl2entityclass.xsl Transforms the ADL file into C# source files for classes which describe the entities in a manner acceptable to [NHibernate](http://www.hibernate.org/ "http://www.hibernate.org/"), a widely used Object/Relational mapping layer. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=10 "Edit section: adl2mssql.xsl")\] adl2mssql.xsl +### adl2mssql.xsl Transforms the ADL file into an SQL script in Microsoft SQL Server 2000 syntax which initialises the database required by the application, with all relationships, permissions, referential integrity constraints and so on. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=11 "Edit section: adl2views.xsl")\] adl2views.xsl +### adl2views.xsl Transforms the ADL file into [Velocity](http://velocity.apache.org/ "http://velocity.apache.org/") template files as used by the [Monorail](http://www.castleproject.org/monorail/index.html "http://www.castleproject.org/monorail/index.html") framework, one template each for all the lists, forms and inspectors described in the ADL. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=12 "Edit section: adl2controllerclasses.xsl")\] adl2controllerclasses.xsl +### adl2controllerclasses.xsl Transforms the ADL file into a series of C# source files for classes which are controllers as used by the Monorail framework. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=13 "Edit section: adl2hibernate.xsl")\] adl2hibernate.xsl +### adl2hibernate.xsl Transforms the ADL file into a Hibernate mapping file, used by the [Hibernate](http://www.hibernate.org/ "http://www.hibernate.org/") ([Java](http://java.sun.com/ "http://java.sun.com")) and [NHibernate](http://www.hibernate.org/ "http://www.hibernate.org/") (C#) Object/Relational mapping layers. This transform is relatively trivial, since ADL is not greatly different from being a superset of the Hibernate vocabulary - it describes the same sorts of things but in more detail. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=14 "Edit section: adl2pgsql.xsl")\] adl2pgsql.xsl +### adl2pgsql.xsl Transforms the ADL file into an SQL script in [Postgres](http://www.postgresql.org/ "http://www.postgresql.org/") 7 syntax which initialises the database required by the application, with all relationships, permissions, referential integrity constraints and so on. -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=15 "Edit section: So is ADL a quick way to build Monorail applications?")\] So is ADL a quick way to build Monorail applications? +## So is ADL a quick way to build Monorail applications? --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Yes and no. @@ -219,35 +217,35 @@ ADL _is_ a quick way to build Monorail applications, because it seemed to me tha However ADL wasn't originally conceived with Monorail in mind. It was originally intended to generated LISP for [CLHTTPD](http://www.cl-http.org:8001/cl-http/ "http://www.cl-http.org:8001/cl-http/"), and I have a half-finished set of scripts to generate Java as part of the Jacquard2 project which I never finished. Because ADL is at a level of abstraction considerably above any [3GL](http://en.wikipedia.org/wiki/Third-generation_programming_language "http://en.wikipedia.org/wiki/Third-generation_programming_language"), it is inherently agnostic to what 3GL it is compiled down to - so that it would be as easy to write transforms that compiled ADL to [Struts](http://struts.apache.org/ "http://struts.apache.org/") or [Ruby on Rails](http://www.rubyonrails.org/ "http://www.rubyonrails.org/") as to C#/Monorail. More importantly, ADL isn't inherently limited to Web applications - it doesn't actually know anything about the Web. It should be possible to write transforms which compile ADL down to Windows native applications or to native applications for mobile phones (and, indeed, if we did have those transforms then we could make all our applications platform agnostic). -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=16 "Edit section: Limitations on ADL")\] Limitations on ADL +## Limitations on ADL ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=17 "Edit section: Current limitations")\] Current limitations +### Current limitations Although I've built experimental systems before using ADL, the SRU project is the first time I've really used it in anger. There are some features I need which it can't yet represent. -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=18 "Edit section: Authentication model")\] Authentication model +#### Authentication model For SRU, I have implemented an authentication model which authenticates the user against real database user accounts. I've done this because I think, in general, this is the correct solution, and because without this sort of authentication you cannot implement table-layer security. However most web applications use application layer authentication rather than database layer authentication, and I have not yet written controller-layer code to deal with this. So unless you do so, ADL applications can currently only authenticate at database layer. ADL defines field-level permissions, but the current controller generator does not implement this. -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=19 "Edit section: Alternative Verbs")\] Alternative Verbs +#### Alternative Verbs Generically, with an entity form, one needs to be able to save the record being edited, and one (often) needs to be able to delete it. But sometimes one needs to be able to do other things. With SRU, for example, there is a need to be able to export event data to [Perfect Table Plan](http://www.perfecttableplan.com/ "http://www.perfecttableplan.com/"), and to reimport data from Perfect Table Plan. This will need custom buttons on the event entity form, and will also need hand-written code at the controller layer to respond to those buttons. Also, a person will have, over the course of their interaction with the SRU, potentially many invitations. In order to access those invitations it will be necessary to associate lists of dependent records with forms. Currently ADL cannot represent these. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=20 "Edit section: Inherent limitations")\] Inherent limitations +### Inherent limitations At this stage I doubt whether there is much point in extending ADL to include a vocabulary to describe business processes. It would make the language much more complicated, and would be unlikely to be able to offer a significantly higher level of abstraction than current 3GLs. If using ADL does not save work, it isn't worth doing it in ADL; remember this is conceived as an 80/20 solution, and you need to be prepared to write the 20 in something else. -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=21 "Edit section: ADL Vocabulary")\] ADL Vocabulary +## ADL Vocabulary --------------------------------------------------------------------------------------------------------------------------------------------------------------------- This section of this document presents and comments on the existing ADL document type definition (DTD). -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=22 "Edit section: Basic definitions")\] Basic definitions +### Basic definitions The DTD starts with some basic definitions @@ -266,7 +264,7 @@ The DTD starts with some basic definitions --> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=23 "Edit section: Permissions")\] Permissions +#### Permissions Key to any data driven application is who has authority to do what to what: 'permissions'. @@ -285,7 +283,7 @@ Key to any data driven application is who has authority to do what to what: 'per --> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=24 "Edit section: Data types")\] Data types +#### Data types ADL needs to know what type of data can be stored on different properties of different entities. The data types were originally based on JDBC data types: @@ -302,7 +300,7 @@ ADL needs to know what type of data can be stored on different properties of dif timestamp: timestamp java.sql.Types.TIMESTAMP --> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=25 "Edit section: Definable data types")\] Definable data types +#### Definable data types However, in order to be able to do data validation, it's useful to associate rules with data types. ADL has the concept of definable data types, to allow data validation code to be generated from the declarative description. These definable data types are used in the ADL application, for example, to define derived types for phone numbers, email addresses, postcodes, and range types. @@ -328,7 +326,7 @@ However, in order to be able to do data validation, it's useful to associate rul -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=26 "Edit section: Page content")\] Page content +#### Page content Pages in applications typically have common, often largely static, sections above, below, to the left or right of the main content which incorporates things like branding, navigation, and so on. This can be defined globally or per page. The intention is that the `head`, `top` and `foot` elements in ADL should be allowed to contain arbitrary HTML, but currently I don't have enough skill with DTD design to know how to specify this. @@ -343,9 +341,9 @@ Pages in applications typically have common, often largely static, sections abov "name CDATA #REQUIRED properties (all|listed) #REQUIRED" > -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=27 "Edit section: The Elements")\] The Elements +### The Elements -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=28 "Edit section: Application")\] Application +#### Application The top level element of an Application Description Language file is the application element: @@ -355,7 +353,7 @@ The top level element of an Application Description Language file is the applica name CDATA #REQUIRED version CDATA #IMPLIED> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=29 "Edit section: Definition")\] Definition +#### Definition In order to be able to use defined types, you need to be able to provide definitions of these types: @@ -377,7 +375,7 @@ In order to be able to use defined types, you need to be able to provide definit minimum CDATA #IMPLIED maximum CDATA #IMPLIED> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=30 "Edit section: Groups")\] Groups +#### Groups In order to be able to user permissions, we need to define who has those permissions. Groups in ADL map directly onto groups/roles at SQL level, but the intention with ADL is that groups should be defined hierarchically. @@ -388,7 +386,7 @@ In order to be able to user permissions, we need to define who has those permiss -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=31 "Edit section: Enities and Properties")\] Enities and Properties +#### Enities and Properties A thing-in-the-domain has properties. Things in the domain fall into regularities, groups of things which share similar collections of properties, such that the values of these properties may have are constrained. This is a representation of the world which is not perfect, but which is sufficiently useful to be recognised by the software technologies which ADL abstracts, so we need to be able to define these. Hence we have entities and properties/ @@ -432,7 +430,7 @@ A thing-in-the-domain has properties. Things in the domain fall into regularitie required %Boolean; #IMPLIED size CDATA #IMPLIED> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=32 "Edit section: Options")\] Options +#### Options Sometimes a property has a constrained list of specific values; this is represented for example in the enumerated types supported by many programming languages. Again, we need to be able to represent this. @@ -447,7 +445,7 @@ Sometimes a property has a constrained list of specific values; this is represen -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=33 "Edit section: Permissions")\] Permissions +#### Permissions Permissions define policies to allow groups of users to access forms, pages, fields (not yet implemented) or entities. Only entity permissions are enforced at database layer, and field protection is not yet implemented at controller layer. But the ADL allows it to be described, and future implementations of the controller generating transform will do this. @@ -462,7 +460,7 @@ Permissions define policies to allow groups of users to access forms, pages, fie group CDATA #REQUIRED permission (%Permissions;) #REQUIRED> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=34 "Edit section: Pragmas")\] Pragmas +#### Pragmas Pragmas are currently not used at all. They are there as a possible means to provide additional controls on forms, but may not be the correct solutions for that. @@ -477,7 +475,7 @@ Pragmas are currently not used at all. They are there as a possible means to pro name CDATA #REQUIRED value CDATA #REQUIRED> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=35 "Edit section: Prompts, helptexts and error texts")\] Prompts, helptexts and error texts +#### Prompts, helptexts and error texts When soliciting a value for a property from the user, we need to be able to offer the user a prompt to describe what we're asking for, and we need to be able to offer that in the user's preferred natural language. Prompts are typically brief. Sometimes, however, we need to give the user a more extensive description of what is being solicited - 'help text'. Finally, if the data offered by the user isn't adequate for some reason, we need ways of feeding that back. Currently the only error text which is carried in the ADL is 'ifmissing', text to be shown if the value for a required property is missing. All prompts, helptexts and error texts have locale information, so that it should be possible to generate variants of all pages for different natural languages from the same ADL. @@ -519,7 +517,7 @@ When soliciting a value for a property from the user, we need to be able to offe -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=36 "Edit section: Forms, Pages and Lists")\] Forms, Pages and Lists +#### Forms, Pages and Lists The basic pages of the user interface. Pages and Forms by default show fields for all the properties of the entity they describe, or they may show only a listed subset. Currently lists show fields for only those properties which are 'user distinct'. Forms, pages and lists may each have their own head, top and foot content, or they may inherit the content defined for the application. @@ -576,10 +574,10 @@ The basic pages of the user interface. Pages and Forms by default show fields fo --> -\[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=37 "Edit section: Using ADL in your project")\] Using ADL in your project +## Using ADL in your project ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=38 "Edit section: Selecting the version")\] Selecting the version +### Selecting the version Current versions of ADL are given at the top of this document. Historical versions are as follows: @@ -605,11 +603,11 @@ Current versions of ADL are given at the top of this document. Historical versio * The version at that location is automatically updated from CVS every night * Projects using the **b_development** branch of ADL should be built against the **b_development** branch of CygnetToolkit. -### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=39 "Edit section: Integrating into your build")\] Integrating into your build +### Integrating into your build To use ADL, it is currently most convenient to use NAnt. It is probably possible to do this with MSBuild, but as of yet I don't know how. -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=40 "Edit section: Properties")\] Properties +#### Properties For the examples given here to work, you will need to set up at least the following properties in your NAnt `.build` file: @@ -630,7 +628,7 @@ For the examples given here to work, you will need to set up at least the follow where, obviously, **YourProjectName**, **YourSourceDir** and **YourADL.adl.xml** stand in for the actual names of your project, your source directory (relative to your solution directory, where the .build file is) and your ADL file, respectively. Note that if it is to be used as an assembly name, the project name should include neither spaces, hyphens nor periods. If it must do so, you should give an assembly name which does not, explicitly. -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=41 "Edit section: Canonicalisation")\] Canonicalisation +#### Canonicalisation The first thing you need to do with your ADL file is canonicalise it. You should generally not need to alter this, you should copy and paste it verbatim: @@ -644,7 +642,7 @@ The first thing you need to do with your ADL file is canonicalise it. You should -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=42 "Edit section: Generate NHibernate mapping")\] Generate NHibernate mapping +#### Generate NHibernate mapping You should generally not need to alter this at all, just copy and paste it verbatim: @@ -660,7 +658,7 @@ You should generally not need to alter this at all, just copy and paste it verba -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=43 "Edit section: Generate SQL")\] Generate SQL +#### Generate SQL @@ -674,7 +672,7 @@ You should generally not need to alter this at all, just copy and paste it verba -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=44 "Edit section: Generate C# entity classes ('POCOs')")\] Generate C# entity classes ('POCOs') +#### Generate C# entity classes ('POCOs') Note that for this to work you must have the following: @@ -707,7 +705,7 @@ Note that for this to work you must have the following: pattern="cut here: next file '(\[a-zA-Z0-9_.\]*)'"/> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=45 "Edit section: Generate Monorail controller classes")\] Generate Monorail controller classes +#### Generate Monorail controller classes Note that for this to work you must have @@ -737,7 +735,7 @@ Note that for this to work you must have destdir="${controllers}/Auto" pattern="cut here: next file '(\[a-zA-Z0-9_.\]*)'"/> -#### \[[edit](http://wiki.cygnets.co.uk/index.php?title=Application_Description_Language_framework&action=edit§ion=46 "Edit section: Generate Velocity views for use with Monorail")\] Generate Velocity views for use with Monorail +#### Generate Velocity views for use with Monorail Note that for this to work you must have diff --git a/project.clj b/project.clj index e4673d3..945cda4 100644 --- a/project.clj +++ b/project.clj @@ -5,4 +5,6 @@ :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.8.0"] [org.clojure/math.combinatorics "0.1.4"] - [bouncer "1.0.1"]]) + [bouncer "1.0.1"] + [hiccup "1.0.5"]] + :plugins [[lein-codox "0.10.3"]]) diff --git a/resources/transforms/adl2canonical.xslt b/resources/transforms/adl2canonical.xslt index 1f381be..c157874 100755 --- a/resources/transforms/adl2canonical.xslt +++ b/resources/transforms/adl2canonical.xslt @@ -2,23 +2,23 @@ @@ -75,8 +75,8 @@ * BE MANUALLY EDITED. * * Generated using adl2canonical.xslt - * - *************************************************************************** + * + *************************************************************************** @@ -84,10 +84,10 @@ - + entity already has a key - not generating one @@ -97,7 +97,7 @@ - @@ -139,7 +139,7 @@ entity has no key - generating one - + @@ -214,8 +214,8 @@ -
@@ -231,8 +231,8 @@
- @@ -248,8 +248,8 @@ - @@ -265,7 +265,7 @@ -
@@ -281,7 +281,7 @@
- @@ -298,7 +298,7 @@ - @@ -399,7 +399,7 @@ - @@ -434,7 +434,7 @@ - @@ -450,4 +450,4 @@ - \ No newline at end of file + diff --git a/resources/transforms/adl2views.xslt b/resources/transforms/adl2views.xslt index 53ace20..a067b55 100755 --- a/resources/transforms/adl2views.xslt +++ b/resources/transforms/adl2views.xslt @@ -1,7 +1,7 @@ % :attrs :name))] - (str - (name target) " = " - (if - (quoted-type? %) - (str "'" target "'") - target))) - (key-properties entity-map)))))) + "WHERE " entity-name "." + (s/join + (str " AND\n\t" entity-name ".") + (map #(str % " = " (keyword %)) (key-names entity)))))) -(defn order-by-clause [entity-map] +(defn order-by-clause + "Generate an appropriate `order by` clause for queries on this `entity`" + [entity] (let - [entity-name (:name (:attrs entity-map)) + [entity-name (:name (:attrs entity)) preferred (map - #(:name (:attrs %)) - (filter #(and - (#{"all", "user"} (-> % :attrs :distinct)) - (= (-> % :tag) :property)) - (concat (properties entity-map)(key-properties entity-map))))] - (str - "ORDER BY " entity-name "." - (s/join - (str ",\n\t" entity-name ".") - (doall - (flatten - (cons - preferred - (map - #(-> % :attrs :name) - (filter - #(not (#{"all", "user"} (-> % :attrs :distinct))) - (key-properties entity-map)))))))))) + #(:name (:attrs %)) + (filter #(#{"user" "all"} (-> % :attrs :distinct)) + (children entity #(= (:tag %) :property))))] + (if + (empty? preferred) + "" + (str + "ORDER BY " entity-name "." + (s/join + (str ",\n\t" entity-name ".") + (flatten (cons preferred (key-names entity)))))))) -(defn insert-query [entity-map] - (let [entity-name (:name (:attrs entity-map)) +(defn insert-query + "Generate an appropriate `insert` query for this `entity`. + TODO: this depends on the idea that system-unique properties + are not insertable, which is... dodgy." + [entity] + (let [entity-name (:name (:attrs entity)) pretty-name (singularise entity-name) - props (concat (properties entity-map) (insertable-key-properties entity-map)) - pnames (map #(-> % :attrs :name) props) + insertable-property-names (map + #(:name (:attrs %)) + (filter + #(not (= (:distinct (:attrs %)) "system")) + (all-properties entity))) query-name (str "create-" pretty-name "!") signature ":! :n"] (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :type :insert-1 - :query - (str "-- :name " query-name " " signature "\n" - "-- :doc creates a new " pretty-name " record\n" - "INSERT INTO " entity-name " (" - (s/join ",\n\t" pnames) - ")\nVALUES (" - (s/join ",\n\t" - (map - #(let [target (keyword (-> % :attrs :name))] - (if - (quoted-type? %) - (str "'" target "'") - target)) - props)) - ")" - (if - (has-primary-key? entity-map) - (str "\nreturning " (s/join ",\n\t" (key-names entity-map)))) - "\n\n")}))) + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :insert-1 + :query + (str "-- :name " query-name " " signature "\n" + "-- :doc creates a new " pretty-name " record\n" + "INSERT INTO " entity-name " (" + (s/join ",\n\t" insertable-property-names) + ")\nVALUES (" + (s/join ",\n\t" (map keyword insertable-property-names)) + ")" + (if + (has-primary-key? entity) + (str "\nreturning " (s/join ",\n\t" (key-names entity)))))}))) -(defn update-query [entity-map] +(defn update-query + "Generate an appropriate `update` query for this `entity`" + [entity] (if (and - (has-primary-key? entity-map) - (has-non-key-properties? entity-map)) - (let [entity-name (:name (:attrs entity-map)) + (has-primary-key? entity) + (has-non-key-properties? entity)) + (let [entity-name (:name (:attrs entity)) pretty-name (singularise entity-name) - property-names (property-names entity-map) + property-names (remove + nil? + (map + #(if (= (:tag %) :property) (:name (:attrs %))) + (vals (:properties (:content entity))))) query-name (str "update-" pretty-name "!") signature ":! :n"] (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :type :update-1 - :query - (str "-- :name " query-name " " signature "\n" - "-- :doc updates an existing " pretty-name " record\n" - "UPDATE " entity-name "\n" - "SET " - (s/join ",\n\t" (map #(str % " = " (keyword %)) property-names)) - "\n" - (where-clause entity-map) - "\n\n")})) + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :update-1 + :query + (str "-- :name " query-name " " signature "\n" + "-- :doc updates an existing " pretty-name " record\n" + "UPDATE " entity-name "\n" + "SET " + (s/join ",\n\t" (map #(str % " = " (keyword %)) property-names)) + "\n" + (where-clause entity))})) {})) -(defn search-query [entity-map] - (let [entity-name (:name (:attrs entity-map)) +(defn search-query [entity] + "Generate an appropriate search query for this `entity`" + (let [entity-name (:name (:attrs entity)) pretty-name (singularise entity-name) query-name (str "search-strings-" pretty-name) signature ":? :1" props (concat (properties entity-map) (insertable-key-properties entity-map)) string-fields (filter - ;; TODO: should also allow typdefed fields which typedef to string. - #(= (-> % :attrs :type) "string") - props)] + #(= (-> % :attrs :type) "string") + (children entity #(= (:tag %) :property)))] (if (empty? string-fields) {} (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :type :text-search - :query - (str "-- :name " query-name " " signature "\n" - "-- :doc selects existing " entity-name " records having any string field matching `:pattern` by substring match\n" - "SELECT * FROM " entity-name "\n" - "WHERE " - (s/join - "\n\tOR " - (map - #(str (-> % :attrs :name) " LIKE '%:pattern%'") - string-fields)) - "\n" - (order-by-clause entity-map) - "\n" - "--~ (if (:offset params) \"OFFSET :offset \") \n" - "--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")" - "\n\n")})))) + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :text-search + :query + (s/join + "\n" + (remove + empty? + (list + (str "-- :name " query-name " " signature) + (str + "-- :doc selects existing " + pretty-name + " records having any string field matching `:pattern` by substring match") + (str "SELECT * FROM " entity-name) + "WHERE " + (s/join + "\n\tOR " + (map + #(str (-> % :attrs :name) " LIKE '%:pattern%'") + string-fields)) + (order-by-clause entity) + "--~ (if (:offset params) \"OFFSET :offset \")" + "--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))})))) -(defn select-query [entity-map] +(defn select-query [entity] + "Generate an appropriate `select` query for this `entity`" (if - (has-primary-key? entity-map) - (let [entity-name (:name (:attrs entity-map)) + (has-primary-key? entity) + (let [entity-name (:name (:attrs entity)) pretty-name (singularise entity-name) query-name (str "get-" pretty-name) signature ":? :1"] (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :type :select-1 - :query - (str "-- :name " query-name " " signature "\n" - "-- :doc selects an existing " pretty-name " record\n" - "SELECT * FROM " entity-name "\n" - (where-clause entity-map) - "\n\n")})) + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :select-1 + :query + (s/join + "\n" + (remove + empty? + (list + (str "-- :name " query-name " " signature) + (str "-- :doc selects an existing " pretty-name " record") + (str "SELECT * FROM " entity-name) + (where-clause entity) + (order-by-clause entity))))})) {})) (defn list-query - "Generate a query to list records in the table represented by this `entity-map`. + "Generate a query to list records in the table represented by this `entity`. Parameters `:limit` and `:offset` may be supplied. If not present limit defaults to 100 and offset to 0." - [entity-map] - (let [entity-name (:name (:attrs entity-map)) + [entity] + (let [entity-name (:name (:attrs entity)) pretty-name (singularise entity-name) query-name (str "list-" entity-name) signature ":? :*"] (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :type :select-many - :query - (str "-- :name " query-name " " signature "\n" - "-- :doc lists all existing " pretty-name " records\n" - "SELECT * FROM " entity-name "\n" - (order-by-clause entity-map) "\n" - "--~ (if (:offset params) \"OFFSET :offset \") \n" - "--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")" - "\n\n")}))) + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :select-many + :query + (s/join + "\n" + (remove + empty? + (list + (str "-- :name " query-name " " signature) + (str "-- :doc lists all existing " pretty-name " records") + (str "SELECT * FROM " entity-name) + (order-by-clause entity) + "--~ (if (:offset params) \"OFFSET :offset \")" + "--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))}))) -(defn foreign-queries [entity-map entities-map] - (let [entity-name (:name (:attrs entity-map)) +(defn foreign-queries + + [entity application] + (let [entity-name (:name (:attrs entity)) pretty-name (singularise entity-name) - links (filter #(-> % :attrs :entity) (-> entity-map :content :properties vals))] + links (filter #(-> % :attrs :entity) (children entity #(= (:tag %) :property)))] (apply - merge - (map - #(let [far-name (-> % :attrs :entity) - far-entity ((keyword far-name) entities-map) - pretty-far (s/replace (s/replace far-name #"_" "-") #"s$" "") - farkey (-> % :attrs :farkey) - link-field (-> % :attrs :name) - query-name (str "list-" entity-name "-by-" pretty-far) - signature ":? :*"] - (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :type :select-one-to-many - :far-entity far-entity - :query - (str "-- :name " query-name " " signature "\n" - "-- :doc lists all existing " pretty-name " records related to a given " pretty-far "\n" - "SELECT * \nFROM " entity-name "\n" - "WHERE " entity-name "." link-field " = :id\n" - (order-by-clause entity-map) - "\n\n")})) - links)))) + merge + (map + #(let [far-name (-> % :attrs :entity) + far-entity (first + (children + application + (fn [x] + (and + (= (:tag x) :entity) + (= (:name (:attrs x)) far-name))))) + pretty-far (singularise far-name) + farkey (-> % :attrs :farkey) + link-field (-> % :attrs :name) + query-name (str "list-" entity-name "-by-" pretty-far) + signature ":? :*"] + (hash-map + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :select-one-to-many + :far-entity far-entity + :query + (s/join + "\n" + (remove + empty? + (list + (str "-- :name " query-name " " signature) + (str "-- :doc lists all existing " pretty-name " records related to a given " pretty-far) + (str "SELECT * \nFROM " entity-name) + (str "WHERE " entity-name "." link-field " = :id") + (order-by-clause entity))))})) + links)))) -(defn link-table-query [near link far] - (let [properties (-> link :content :properties vals) - links (apply - merge - (map - #(hash-map (keyword (-> % :attrs :entity)) %) - (filter #(-> % :attrs :entity) properties))) - near-name (-> near :attrs :name) - link-name (-> link :attrs :name) - far-name (-> far :attrs :name) - pretty-far (singularise far-name) - query-name (str "list-" link-name "-" near-name "-by-" pretty-far) - signature ":? :*"] - (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity link - :type :select-many-to-many - :near-entity near - :far-entity far - :query - (str "-- :name " query-name " " signature " \n" - "-- :doc lists all existing " near-name " records related through " link-name " to a given " pretty-far "\n" - "SELECT "near-name ".*\n" - "FROM " near-name ", " link-name "\n" - "WHERE " near-name "." (first (key-names near)) " = " link-name "." (-> (links (keyword near-name)) :attrs :name) "\n\t" - "AND " link-name "." (-> (links (keyword far-name)) :attrs :name) " = :id\n" - (order-by-clause near) - "\n\n")}))) +(defn link-table-query + "Generate a query which links across the entity passed as `link` + from the entity passed as `near` to the entity passed as `far`. + TODO: not working?" + [near link far] + (if + (and + (entity? near) + (entity? link) + (entity? far)) + (let [properties (-> link :content :properties vals) + links (apply + merge + (map + #(hash-map (keyword (-> % :attrs :entity)) %) + (filter #(-> % :attrs :entity) properties))) + near-name (-> near :attrs :name) + link-name (-> link :attrs :name) + far-name (-> far :attrs :name) + pretty-far (singularise far-name) + query-name (str "list-" link-name "-" near-name "-by-" pretty-far) + signature ":? :*"] + (hash-map + (keyword query-name) + {:name query-name + :signature signature + :entity link + :type :select-many-to-many + :near-entity near + :far-entity far + :query + (s/join + "\n" + (remove + empty? + (list + (str "-- :name " query-name " " signature) + (str "-- :doc lists all existing " near-name " records related through " link-name " to a given " pretty-far ) + (str "SELECT "near-name ".*") + (str "FROM " near-name ", " link-name ) + (str "WHERE " near-name "." (first (key-names near)) " = " link-name "." (-> (links (keyword near-name)) :attrs :name) ) + ("\tAND " link-name "." (-> (links (keyword far-name)) :attrs :name) " = :id") + (order-by-clause near))))})))) -(defn link-table-queries [entity-map entities-map] +(defn link-table-queries [entity application] + "Generate all the link queries in this `application` which link via this `entity`." (let [entities (map - #((keyword %) entities-map) - (remove nil? (map #(-> % :attrs :entity) (-> entity-map :content :properties vals)))) + ;; find the far-side entities + (fn + [far-name] + (children + application + (fn [x] + (and + (= (:tag x) :entity) + (= (:name (:attrs x)) far-name))))) + ;; of those properties of this `entity` which are of type `entity` + (remove + nil? + (map + #(-> % :attrs :entity) + (children entity #(= (:tag %) :property))))) pairs (combinations entities 2)] (apply - merge - (map - #(merge - (link-table-query (nth % 0) entity-map (nth % 1)) - (link-table-query (nth % 1) entity-map (nth % 0))) - pairs)))) + merge + (map + #(merge + (link-table-query (nth % 0) entity (nth % 1)) + (link-table-query (nth % 1) entity (nth % 0))) + pairs)))) -(defn delete-query [entity-map] +(defn delete-query [entity] + "Generate an appropriate `delete` query for this `entity`" (if - (has-primary-key? entity-map) - (let [entity-name (:name (:attrs entity-map)) + (has-primary-key? entity) + (let [entity-name (:name (:attrs entity)) pretty-name (singularise entity-name) query-name (str "delete-" pretty-name "!") signature ":! :n"] (hash-map - (keyword query-name) - {:name query-name - :signature signature - :entity entity-map - :type :delete-1 - :query - (str "-- :name " query-name " " signature "\n" - "-- :doc updates an existing " pretty-name " record\n" - "DELETE FROM " entity-name "\n" - (where-clause entity-map) - "\n\n")})))) + (keyword query-name) + {:name query-name + :signature signature + :entity entity + :type :delete-1 + :query + (str "-- :name " query-name " " signature "\n" + "-- :doc updates an existing " pretty-name " record\n" + "DELETE FROM " entity-name "\n" + (where-clause entity))})))) (defn queries - [entity-map entities-map] + "Generate all standard queries for this `entity` in this `application`." + [entity application] (merge - {} - (insert-query entity-map) - (update-query entity-map) - (delete-query entity-map) - (if - (is-link-table? entity-map) - (link-table-queries entity-map entities-map) - (merge - (select-query entity-map) - (list-query entity-map) - (search-query entity-map) - (foreign-queries entity-map entities-map))))) + {} + (insert-query entity) + (update-query entity) + (delete-query entity) + (if + (link-table? entity) + (link-table-queries entity application) + (merge + (select-query entity) + (list-query entity) + (search-query entity) + (foreign-queries entity application))))) -;; (defn migrations-to-queries-sql -;; ([migrations-path] -;; (migrations-to-queries-sql migrations-path "queries.auto.sql")) -;; ([migrations-path output] -;; (let -;; [adl-struct (migrations-to-xml migrations-path "Ignored") -;; file-content (apply -;; str -;; (cons -;; (str "-- " -;; output -;; " autogenerated by \n-- [squirrel-parse](https://github.com/simon-brooke/squirrel-parse)\n-- at " -;; (f/unparse (f/formatters :basic-date-time) (t/now)) -;; "\n\n") -;; (doall -;; (map -;; #(:query %) -;; (sort -;; #(compare (:name %1) (:name %2)) -;; (vals -;; (apply -;; merge -;; (map -;; #(queries % adl-struct) -;; (vals adl-struct)))))))))] -;; (spit output file-content) -;; file-content))) +(defn to-hugsql-queries + "Generate all [HugSQL](https://www.hugsql.org/) queries implied by this ADL `application` spec." + [application] + (spit + (str *output-path* "queries.sql") + (s/join + "\n\n" + (cons + (s/join + "\n-- " + (list + "-- File queries.sql" + "autogenerated by adl.to-hugsql-queries at" + (t/now) + "See [Application Description Language](https://github.com/simon-brooke/adl).\n\n")) + (map + #(:query %) + (sort + #(compare (:name %1) (:name %2)) + (vals + (apply + merge + (map + #(queries % application) + (children + application + (fn [child] (= (:tag child) :entity)))))))))))) + diff --git a/src/adl/to_json_routes.clj b/src/adl/to_json_routes.clj index f6bac58..01a8ddc 100644 --- a/src/adl/to_json_routes.clj +++ b/src/adl/to_json_routes.clj @@ -39,10 +39,10 @@ 'ns (symbol (str parent-name ".routes." this-name)) (str "JSON routes for " parent-name - " auto-generated by [squirrel-parse](https://github.com/simon-brooke/squirrel-parse) at " + " auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at " (f/unparse (f/formatters :basic-date-time) (t/now))) (list - 'require + :require '[noir.response :as nresponse] '[noir.util.route :as route] '[compojure.core :refer [defroutes GET POST]] diff --git a/src/adl/to_reframe.clj b/src/adl/to_reframe.clj new file mode 100644 index 0000000..c23536e --- /dev/null +++ b/src/adl/to_reframe.clj @@ -0,0 +1,87 @@ +(ns adl.to-reframe + (:require [adl.utils :refer :all] + [clojure.string :as s] + [clj-time.core :as t] + [clj-time.format :as f])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; adl.to-hugsql-queries: generate re-frame/re-com views. +;;;; +;;;; This program is free software; you can redistribute it and/or +;;;; modify it under the terms of the GNU General Public License +;;;; as published by the Free Software Foundation; either version 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2018 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn file-header + ([parent-name this-name extra-requires] + (list 'ns (symbol (str parent-name ".views." this-name)) + (str "Re-frame views for " parent-name + " auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at " + (f/unparse (f/formatters :basic-date-time) (t/now))) + (concat + (list :require + '[re-frame.core :refer [reg-sub subscribe dispatch]]) + extra-requires))) + ([parent-name this-name] + (file-header parent-name this-name '()))) + + + + +(defn generate-form + "Generate as re-frame this `form` taken from this `entity` of this `document`." + [form entity application] + (let [record @(subscribe [:record]) + errors @(subscribe [:errors]) + messages @(subscribe [:messages]) + properties (required-properties entity form)] + (list + 'defn + (symbol + (s/join + "-" + (:name (:attrs entity)) + (:name (:attrs form)) + "-form-panel")) + [] + (apply + vector + (remove + nil? + (list + :div + (or + (:top (:content form)) + (:top (:content application))) + (map #(list 'ui/error-panel %) errors) + (map #(list 'ui/message-panel %) messages) + [:h1 (:name (:attrs form))] + [:div.container {:id "main-container"} + (apply + vector + (list + :div + {} + (map + #(generate-widget % form entity) + properties)))] + (or + (:foot (:content form)) + (:foot (:content application)))))) + ))) + diff --git a/src/adl/to_selmer_templates.clj b/src/adl/to_selmer_templates.clj new file mode 100644 index 0000000..266589e --- /dev/null +++ b/src/adl/to_selmer_templates.clj @@ -0,0 +1,351 @@ +(ns ^{;; :doc "Application Description Language - generate RING routes for REST requests." + :author "Simon Brooke"} + adl.to-selmer-templates + (:require [adl.utils :refer :all] + [clojure.java.io :refer [file]] + [clojure.math.combinatorics :refer [combinations]] + [clojure.pprint :as p] + [clojure.string :as s] + [clojure.xml :as x] + [clj-time.core :as t] + [clj-time.format :as f] + [hiccup.core :as h])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; adl.to-json-routes: generate RING routes for REST requests. +;;;; +;;;; This program is free software; you can redistribute it and/or +;;;; modify it under the terms of the GNU General Public License +;;;; as published by the Free Software Foundation; either version 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2018 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(def ^:dynamic *locale* + "The locale for which files will be generated." + "en-GB") + +(def ^:dynamic *output-path* + "The path to which generated files will be written." + "resources/auto/") + +(defn file-header + "Generate a header for a template file." + [filename] + (str + "{% extends \"templates/base.html\" %}\n\n" + "\n\n" + "{% block content %}")) + +(defn file-footer + "Generate a header for a template file." + [filename] + "{% endblock %}\n") + + +(defn prompt + "Return an appropriate prompt for the given `field-or-property` taken from this + `form` of this `entity` of this `application`, in the context of the current + binding of `*locale*`. TODO: something more sophisticated about i18n" + [field-or-property form entity application] + (or + (first + (children + field-or-property + #(and + (= (:tag %) :prompt) + (= (:locale :attrs %) *locale*)))) + (:name (:attrs field-or-property)))) + + +(defn csrf-widget + "For the present, just return the standard cross site scripting protection field statement" + [] + "{% csrf-field %}") + + +(defn save-widget + "Return an appropriate 'save' widget for this `form` operating on this `entity` taken + from this `application`." + [form entity application] + {:tag :p + :attrs {:class "widget action-safe"} + :content [{:tag :label + :attrs {:for "save-button" :class "action-safe"} + :content [(str "To save this " (:name (:attrs entity)) " record")]} + {:tag :input + :attrs {:id "save-button" + :name "save-button" + :class "action-safe" + :type :submit + :value (str "Save!")}}]}) + + +(defn delete-widget + "Return an appropriate 'save' widget for this `form` operating on this `entity` taken + from this `application`." + [form entity application] + {:tag :p + :attrs {:class "widget action-dangerous"} + :content [{:tag :label + :attrs {:for "delete-button" :class "action-dangerous"} + :content [(str "To delete this " (:name (:attrs entity)) " record")]} + {:tag :input + :attrs {:id "delete-button" + :name "delete-button" + :class "action-dangerous" + :type :submit + :value (str "Delete!")}}]}) + + +(defn get-options + "Produce template code to get options for this `property` of this `entity` taken from + this `application`." + [property form entity application] + (let + [type (:type (:attrs property)) + farname (:entity (:attrs property)) + farside (first + (children + application + #(and + (= (:tag %) :entity) + (= (:name (:attrs %)) farname)))) + fs-distinct (flatten + (list + (children farside #(#{"user" "all"} (:distinct (:attrs %)))) + (children + (first + (children farside #(= (:tag %) :key))) + #(#{"user" "all"} (:distinct (:attrs %)))))) + farkey (or + (:farkey (:attrs property)) + (:name (:attrs (first (children (children farside #(= (:tag %) :key)))))) + "id")] + [(str "{% for record in " farname " %}{% endfor %}")])) + + +(defn widget-type + "Return an appropriate HTML5 input type for this property." + ([property application] + (widget-type property application (typedef property application))) + ([property application typedef] + (let [t (if + typedef + (:type (:attrs typedef)) + (:type (:attrs property)))] + (case t + ("integer" "real" "money") "number" + ("uploadable" "image") "file" + "boolean" "checkbox" + "date" "date" + "time" "time" + "text" ;; default + )))) + + +(defn widget + "Generate a widget for this `field-or-property` of this `form` for this `entity` + taken from within this `application`." + [field-or-property form entity application] + (let + [widget-name (:name (:attrs field-or-property)) + property (if + (= (:tag field-or-property) :property) + field-or-property + (first + (children + entity + #(and + (= (:tag %) :property) + (= (:name (:attrs %)) (:property (:attrs field-or-property))))))) + permissions (permissions property form entity application) + typedef (typedef property application) + visible-to (visible-to permissions) + ;; if the form isn't actually a form, no widget is writable. + writable-by (if (= (:tag form) :form) (writable-by permissions)) + select? (#{"entity" "list" "link"} (:type (:attrs property)))] + (if + (formal-primary-key? property entity) + {:tag :input + :attrs {:id widget-name + :name widget-name + :type "hidden" + :value (str "{{record." widget-name "}}")}} + {:tag :p + :attrs {:class "widget"} + :content [{:tag :label + :attrs {:for widget-name} + :content [(prompt field-or-property form entity application)]} + "TODO: selmer command to hide for all groups except for those for which it is writable" + (if + select? + {:tag :select + :attrs {:id widget-name + :name widget-name} + :content (get-options property form entity application)} + {:tag :input + :attrs (merge + {:id widget-name + :name widget-name + :type (widget-type property application typedef) + :value (str "{{record." widget-name "}}")} + (if + (:minimum (:attrs typedef)) + {:min (:minimum (:attrs typedef))}) + (if + (:maximum (:attrs typedef)) + {:max (:maximum (:attrs typedef))}))}) + "{% else %}" + "TODO: selmer if command to hide for all groups except to those for which it is readable" + {:tag :span + :attrs {:id widget-name + :name widget-name + :class "pseudo-widget disabled"} + :content [(str "{{record." widget-name "}}")]} + "{% endif %}" + "{% endif %}"]}))) + + +(defn form-to-template + "Generate a template as specified by this `form` element for this `entity`, + taken from this `application`. If `form` is nill, generate a default form + template for the entity." + [form entity application] + (let + [name (str (if form (:name (:attrs form)) "edit") "-" (:name (:attrs entity))) + keyfields (children + ;; there should only be one key; its keys are properties + (first (children entity #(= (:tag %) :key)))) + fields (if + (and form (= "listed" (:properties (:attrs form)))) + ;; if we've got a form, collect its fields, fieldgroups and verbs + (flatten + (map #(if (#{:field :fieldgroup :verb} (:tag %)) %) + (children form))) + (children entity #(= (:tag %) :property)))] + {:tag :div + :attrs {:id "content" :class "edit"} + :content + [{:tag :form + :attrs {:action (str "{{servlet-context}}/" name) + :method "POST"} + :content (flatten + (list + (csrf-widget) + (map + #(widget % form entity application) + keyfields) + (map + #(widget % form entity application) + fields) + (save-widget form entity application) + (delete-widget form entity application)))}]})) + + + +(defn page-to-template + "Generate a template as specified by this `page` element for this `entity`, + taken from this `application`. If `page` is nill, generate a default page + template for the entity." + [page entity application] + ) + +(defn list-to-template + "Generate a template as specified by this `list` element for this `entity`, + taken from this `application`. If `list` is nill, generate a default list + template for the entity." + [list entity application] + ) + + +(defn entity-to-templates + "Generate one or more templates for editing instances of this + `entity` in this `application`" + [entity application] + (let + [forms (children entity #(= (:tag %) :form)) + pages (children entity #(= (:tag %) :page)) + lists (children entity #(= (:tag %) :list))] + (if + (and + (= (:tag entity) :entity) ;; it seems to be an ADL entity + (not (link-table? entity))) + (merge + (if + forms + (apply merge (map #(assoc {} (keyword (str "form-" (:name (:attrs entity)) "-" (:name (:attrs %)))) + (form-to-template % entity application)) + forms)) + {(keyword (str "form-" (:name (:attrs entity)))) + (form-to-template nil entity application)}) + (if + pages + (apply merge (map #(assoc {} (keyword (str "page-" (:name (:attrs entity)) "-" (:name (:attrs %)))) + (page-to-template % entity application)) + pages)) + {(keyword (str "page-" (:name (:attrs entity)))) + (page-to-template nil entity application)}) + (if + lists + (apply merge (map #(assoc {} (keyword (str "list-" (:name (:attrs entity)) "-" (:name (:attrs %)))) + (list-to-template % entity application)) + lists)) + {(keyword (str "list-" (:name (:attrs entity)))) + (form-to-template nil entity application)}))))) + + +(defn write-template-file + [filename template] + (spit + (str *output-path* filename) + (s/join + "\n" + (list + (file-header filename) + (with-out-str (x/emit-element template)) + (file-footer filename))))) + + +(defn to-selmer-templates + "Generate all [Selmer](https://github.com/yogthos/Selmer) templates implied by this ADL `application` spec." + [application] + (let + [templates-map (reduce + merge + {} + (map + #(entity-to-templates % application) + (children application #(= (:tag %) :entity))))] + (doall + (map + #(if + (templates-map %) + (let [filename (str (name %) ".html")] + (write-template-file filename (templates-map %)))) + (keys templates-map))) + templates-map)) + + diff --git a/src/adl/utils.clj b/src/adl/utils.clj index 5b9ff16..de0cc44 100644 --- a/src/adl/utils.clj +++ b/src/adl/utils.clj @@ -1,9 +1,13 @@ -(ns adl.utils - (:require [clojure.string :as s])) +(ns ^{:doc "Application Description Language - utility functions." + :author "Simon Brooke"} + adl.utils + (:require [clojure.string :as s] + [clojure.xml :as x] + [adl.validator :refer [valid-adl? validate-adl]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; -;;;; adl.utils: utility functions generally useful to generators. +;;;; adl.utils: utility functions. ;;;; ;;;; This program is free software; you can redistribute it and/or ;;;; modify it under the terms of the GNU General Public License @@ -22,105 +26,181 @@ ;;;; ;;;; Copyright (C) 2018 Simon Brooke ;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; **Argument name conventions**: arguments with names of the form `*-map` -;;; represent elements extracted from an ADL XML file as parsed by -;;; `clojure.xml/parse`. Thus `entity-map` represents an ADL entity, -;;; `property-map` a property, and so on. -;;; -;;; Generally, `(:tag x-map) => "x"`, and for every such object -;;; `(:attrs x-map)` should return a map of attributes whose keys -;;; are keywords and whose values are strings. + +(defn children + "Return the children of this `element`; if `predicate` is passed, return only those + children satisfying the predicate." + ([element] + (if + (keyword? (:tag element)) ;; it has a tag; it seems to be an XML element + (:content element))) + ([element predicate] + (remove ;; there's a more idionatic way of doing remove-nil-map, but for the moment I can't recall it. + nil? + (map + #(if (predicate %) %) + (children element))))) + + +(defn attributes + "Return the attributes of this `element`; if `predicate` is passed, return only those + attributes satisfying the predicate." + ([element] + (if + (keyword? (:tag element)) ;; it has a tag; it seems to be an XML element + (:attrs element))) + ([element predicate] + (remove ;; there's a more idionatic way of doing remove-nil-map, but for the moment I can't recall it. + nil? + (map + #(if (predicate %) %) + (:attrs element))))) + + +(defn typedef + "If this `property` is of type `defined`, return its type definition from + this `application`, else nil." + [property application] + (if + (= (:type (:attrs property)) "defined") + (first + (children + application + #(and + (= (:tag %) :typedef) + (= (:name (:attrs %)) (:typedef (:attrs property)))))))) + + +(defn permissions + "Return appropriate permissions of this `property`, taken from this `entity` of this + `application`, in the context of this `page`." + [property page entity application] + (first + (remove + empty? + (list + (children page #(= (:tag %) :permission)) + (children property #(= (:tag %) :permission)) + (children entity #(= (:tag %) :permission)) + (children application #(= (:tag %) :permission)))))) + + +(defn permission-groups + "Return a list of names of groups to which this `predicate` is true of + some permission taken from these `permissions`, else nil." + [permissions predicate] + (let [groups (remove + nil? + (map + #(if + (apply predicate (list %)) + (:group (:attrs %))) + permissions))] + (if groups groups))) + + +(defn formal-primary-key? + "Does this `prop-or-name` appear to be a property (or the name of a property) + which is a formal primary key of this entity?" + [prop-or-name entity] + (if + (map? prop-or-name) + (formal-primary-key? (:name (:attrs prop-or-name)) entity) + (let [primary-key (first (children entity #(= (:tag %) :key))) + property (first + (children + primary-key + #(and + (= (:tag %) :property) + (= (:name (:attrs %)) prop-or-name))))] + (= (:distinct (:attrs property)) "system")))) + + +(defn entity? + "Return true if `x` is an ADL entity." + [x] + (= (:tag x) :entity)) + + +(defn visible-to + "Return a list of names of groups to which are granted read access, + given these `permissions`, else nil." + [permissions] + (permission-groups permissions #(#{"read" "insert" "noedit" "edit" "all"} (:permission (:attrs %))))) + + +(defn writable-by + "Return a list of names of groups to which are granted read access, + given these `permissions`, else nil." + [permissions] + (permission-groups permissions #(#{"edit" "all"} (:permission (:attrs %))))) + (defn singularise - "Assuming this string represents an English language plural noun, - construct a Clojure symbol name which represents the singular." + "Attempt to construct an idiomatic English-language singular of this string." [string] - (s/replace (s/replace (s/replace string #"_" "-") #"s$" "") #"ie$" "y")) + (s/replace + (s/replace + (s/replace + (s/replace string #"_" "-") + #"s$" "") + #"se$" "s") + #"ie$" "y")) -(defn entities - [application-map] - (filter #(= (-> % :tag) :entity) (:content application-map))) -(defn is-link-table? - "Does this `entity-map` represent a pure link table?" - [entity-map] - (let [properties (-> entity-map :content :properties vals) +(defn link-table? + "Return true if this `entity` represents a link table." + [entity] + (let [properties (children entity #(= (:tag %) :property)) links (filter #(-> % :attrs :entity) properties)] (= (count properties) (count links)))) +(defn read-adl [url] + (let [adl (x/parse url) + valid? (valid-adl? adl)] + adl)) +;; (if valid? adl +;; (throw (Exception. (str (validate-adl adl))))))) -(defn key-properties - "Return a list of all properties in the primary key of this `entity-map`." - [entity-map] - (filter - #(= (:tag %) :property) - (:content - ;; there's required to be only one key element in and entity element - (first - (filter - #(= (:tag %) :key) - (:content entity-map)))))) - - -(defn insertable-key-properties - "List properties in the key of the entity indicated by this `entity-map` - which should be inserted. - A key property is insertable it it is not `system` (database) generated. - But note that `system` is the default." - [entity-map] - (filter - #(let - [generator (-> % :attrs :generator)] - (not - (or (nil? generator) - (= generator "system")))) - (key-properties entity-map))) - - -(defn key-names - "List the names of all properties in the primary key of this `entity-map`." - [entity-map] +(defn key-names [entity-map] (remove nil? (map #(:name (:attrs %)) - (key-properties entity-map)))) + (vals (:content (:key (:content entity-map))))))) -(defn has-primary-key? - "True if this `entity-map` has a primary key." - [entity-map] - (not (empty? (key-names entity-map)))) +(defn has-primary-key? [entity-map] + (> (count (key-names entity-map)) 0)) -(defn properties - "List the non-primary-key properties of this `entity-map`." - [entity-map] - (filter #(= (-> % :tag) :property) (:content entity-map))) +(defn has-non-key-properties? [entity-map] + (> + (count (vals (:properties (:content entity-map)))) + (count (key-names entity-map)))) -(defn has-non-key-properties? - "True if this `entity-map` has properties which do not form part of the - primary key." - [entity-map] - (not - (empty? (properties entity-map)))) +(defn children-with-tag + "Return all children of this `element` which have this `tag`." + [element tag] + (children element #(= (:tag %) tag))) + +(defn descendants-with-tag + "Return all descendants of this `element`, recursively, which have this `tag`." + [element tag] + (flatten + (remove + empty? + (cons + (children element #(= (:tag %) tag)) + (map + #(descendants-with-tag % tag) + (children element)))))) -(defn property-names - "List the names of non-primary-key properties of this `entity-map`." - [entity-map] - (map #(:name (:attrs %)) (properties entity-map))) - - -(defn quoted-type? - "Is the type of the property represented by this `property-map` one whose - values should be quoted in SQL queries? - TODO: this won't work for typedef types, which means we need to pass the - entire parsed ADL down the chain to here (and probably, generally) so that - we can resolve issues like that." - [property-map] - (#{"string", "text", "date", "time", "timestamp"} (-> property-map :attrs :type))) +(defn all-properties + "Return all properties of this entity (including key properties)." + [entity] + (descendants-with-tag entity :property)) diff --git a/src/adl/validator.clj b/src/adl/validator.clj index 13a547f..9513542 100644 --- a/src/adl/validator.clj +++ b/src/adl/validator.clj @@ -652,3 +652,8 @@ entity-validations)]]}) +(defn valid-adl? [src] + (b/valid? src application-validations)) + +(defn validate-adl [src] + (b/validate src application-validations)) diff --git a/test/adl/to_hugsql_queries_test.clj b/test/adl/to_hugsql_queries_test.clj index 29f87bd..8b4c449 100644 --- a/test/adl/to_hugsql_queries_test.clj +++ b/test/adl/to_hugsql_queries_test.clj @@ -83,7 +83,8 @@ (testing "update query generation" (let [expected "-- :name update-addres! :! :n -- :doc updates an existing addres record - UPDATE address\nSET street = :street, + UPDATE address + SET street = :street, town = :town, postcode = :postcode WHERE address.id = :id\n\n" diff --git a/yyy.adl.clj b/yyy.adl.clj new file mode 100644 index 0000000..539687b --- /dev/null +++ b/yyy.adl.clj @@ -0,0 +1,630 @@ +{:tag :application, + :attrs {:version "0.1.1", :name "youyesyet"}, + :content + [{:tag :entity, + :attrs {:name "electors"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "integer", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:column "name", :name "name", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "name"}, + :content ["\nname\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "addresses", + :column "address_id", + :name "address_id", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "address_id"}, + :content ["\naddress_id\n"]}]} + {:tag :property, + :attrs {:column "phone", :name "phone", :type "string"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "phone"}, + :content ["\nphone\n"]}]} + {:tag :property, + :attrs {:column "email", :name "email", :type "string"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "email"}, + :content ["\nemail\n"]}]}]} + {:tag :entity, + :attrs {:name "addresses"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "integer", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:column "address", + :name "address", + :type "string", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "address"}, + :content ["\naddress\n"]}]} + {:tag :property, + :attrs {:column "postcode", :name "postcode", :type "string"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "postcode"}, + :content ["\npostcode\n"]}]} + {:tag :property, + :attrs {:column "phone", :name "phone", :type "string"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "phone"}, + :content ["\nphone\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "districts", + :column "district_id", + :name "district_id", + :type "entity"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "district_id"}, + :content ["\ndistrict_id\n"]}]} + {:tag :property, + :attrs {:column "latitude", :name "latitude", :type "real"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "latitude"}, + :content ["\nlatitude\n"]}]} + {:tag :property, + :attrs {:column "longitude", :name "longitude", :type "real"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "longitude"}, + :content ["\nlongitude\n"]}]}]} + {:tag :entity, + :attrs {:name "visits"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "integer", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "addresses", + :column "address_id", + :name "address_id", + :type "integer", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "address_id"}, + :content ["\naddress_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "canvassers", + :column "canvasser_id", + :name "canvasser_id", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "canvasser_id"}, + :content ["\ncanvasser_id\n"]}]} + {:tag :property, + :attrs + {:column "date", + :name "date", + :type "timestamp", + :default "", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "date"}, + :content ["\ndate\n"]}]}]} + {:tag :entity, + :attrs {:name "authorities"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]}]} + {:tag :entity, + :attrs {:name "issues"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs {:column "url", :name "url", :type "string"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "url"}, + :content ["\nurl\n"]}]}]} + {:tag :entity, + :attrs {:name "schema_migrations"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "integer", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]}]} + {:tag :entity, + :attrs {:name "intentions"}, + :content + [{:tag :property, + :attrs + {:column "visit_id", + :name "visit_id", + :farkey "id", + :entity "visits", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "visit_id"}, + :content ["\nvisit_id\n"]}]} + {:tag :property, + :attrs + {:column "elector_id", + :name "elector_id", + :farkey "id", + :entity "electors", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "elector_id"}, + :content ["\nelector_id\n"]}]} + {:tag :property, + :attrs + {:column "option_id", + :name "option_id", + :farkey "id", + :entity "options", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "option_id"}, + :content ["\noption_id\n"]}]}]} + {:tag :entity, + :attrs {:name "canvassers"}, + :content + [{:tag :property, + :attrs {:column "id", :name "id", :type "integer"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:column "username", + :name "username", + :type "string", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "username"}, + :content ["\nusername\n"]}]} + {:tag :property, + :attrs + {:column "fullname", + :name "fullname", + :type "string", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "fullname"}, + :content ["\nfullname\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "electors", + :column "elector_id", + :name "elector_id", + :type "entity"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "elector_id"}, + :content ["\nelector_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "addresses", + :column "address_id", + :name "address_id", + :type "integer", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "address_id"}, + :content ["\naddress_id\n"]}]} + {:tag :property, + :attrs {:column "phone", :name "phone", :type "string"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "phone"}, + :content ["\nphone\n"]}]} + {:tag :property, + :attrs {:column "email", :name "email", :type "string"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "email"}, + :content ["\nemail\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "authorities", + :column "authority_id", + :name "authority_id", + :type "string", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "authority_id"}, + :content ["\nauthority_id\n"]}]} + {:tag :property, + :attrs + {:column "authorised", :name "authorised", :type "boolean"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "authorised"}, + :content ["\nauthorised\n"]}]}]} + {:tag :entity, + :attrs {:name "followuprequests"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "integer", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "electors", + :column "elector_id", + :name "elector_id", + :type "integer", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "elector_id"}, + :content ["\nelector_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "visits", + :column "visit_id", + :name "visit_id", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "visit_id"}, + :content ["\nvisit_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "issues", + :column "issue_id", + :name "issue_id", + :type "string", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "issue_id"}, + :content ["\nissue_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "followupmethods", + :column "method_id", + :name "method_id", + :type "string", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "method_id"}, + :content ["\nmethod_id\n"]}]}]} + {:tag :entity, + :attrs {:name "rolememberships"}, + :content + [{:tag :property, + :attrs + {:column "role_id", + :name "role_id", + :farkey "id", + :entity "roles", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "role_id"}, + :content ["\nrole_id\n"]}]} + {:tag :property, + :attrs + {:column "canvasser_id", + :name "canvasser_id", + :farkey "id", + :entity "canvassers", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "canvasser_id"}, + :content ["\ncanvasser_id\n"]}]}]} + {:tag :entity, + :attrs {:name "roles"}, + :content + [{:tag :property, + :attrs {:column "id", :name "id", :type "integer"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:column "name", :name "name", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "name"}, + :content ["\nname\n"]}]}]} + {:tag :entity, + :attrs {:name "teams"}, + :content + [{:tag :property, + :attrs {:column "id", :name "id", :type "integer"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:column "name", :name "name", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "name"}, + :content ["\nname\n"]}]} + {:tag :property, + :attrs + {:column "district_id", + :name "district_id", + :farkey "id", + :entity "districts", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "district_id"}, + :content ["\ndistrict_id\n"]}]} + {:tag :property, + :attrs {:column "latitude", :name "latitude", :type "real"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "latitude"}, + :content ["\nlatitude\n"]}]} + {:tag :property, + :attrs {:column "longitude", :name "longitude", :type "real"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "longitude"}, + :content ["\nlongitude\n"]}]}]} + {:tag :entity, + :attrs {:name "districts"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "integer", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:column "name", :name "name", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "name"}, + :content ["\nname\n"]}]}]} + {:tag :entity, + :attrs {:name "teamorganiserships"}, + :content + [{:tag :property, + :attrs + {:column "team_id", + :name "team_id", + :farkey "id", + :entity "teams", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "team_id"}, + :content ["\nteam_id\n"]}]} + {:tag :property, + :attrs + {:column "canvasser_id", + :name "canvasser_id", + :farkey "id", + :entity "canvassers", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "canvasser_id"}, + :content ["\ncanvasser_id\n"]}]}]} + {:tag :entity, + :attrs {:name "followupactions"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "integer", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "followuprequests", + :column "request_id", + :name "request_id", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "request_id"}, + :content ["\nrequest_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "canvassers", + :column "actor", + :name "actor", + :type "integer", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "actor"}, + :content ["\nactor\n"]}]} + {:tag :property, + :attrs + {:column "date", + :name "date", + :type "timestamp", + :default "", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "date"}, + :content ["\ndate\n"]}]} + {:tag :property, + :attrs {:column "notes", :name "notes", :type "text"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "notes"}, + :content ["\nnotes\n"]}]} + {:tag :property, + :attrs {:column "closed", :name "closed", :type "boolean"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "closed"}, + :content ["\nclosed\n"]}]}]} + {:tag :entity, + :attrs {:name "issueexpertise"}, + :content + [{:tag :property, + :attrs + {:farkey "id", + :entity "canvassers", + :column "canvasser_id", + :name "canvasser_id", + :type "integer", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "canvasser_id"}, + :content ["\ncanvasser_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "issues", + :column "issue_id", + :name "issue_id", + :type "string", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "issue_id"}, + :content ["\nissue_id\n"]}]} + {:tag :property, + :attrs + {:farkey "id", + :entity "followupmethods", + :column "method_id", + :name "method_id", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "method_id"}, + :content ["\nmethod_id\n"]}]}]} + {:tag :entity, + :attrs {:name "options"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]}]} + {:tag :entity, + :attrs {:name "teammemberships"}, + :content + [{:tag :property, + :attrs + {:column "team_id", + :name "team_id", + :farkey "id", + :entity "teams", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "team_id"}, + :content ["\nteam_id\n"]}]} + {:tag :property, + :attrs + {:column "canvasser_id", + :name "canvasser_id", + :farkey "id", + :entity "canvassers", + :type "entity", + :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "canvasser_id"}, + :content ["\ncanvasser_id\n"]}]}]} + {:tag :entity, + :attrs {:name "followupmethods"}, + :content + [{:tag :property, + :attrs + {:column "id", :name "id", :type "string", :required "true"}, + :content + [{:tag :prompt, + :attrs {:locale "en-GB", :prompt "id"}, + :content ["\nid\n"]}]}]}]}