-
Notifications
You must be signed in to change notification settings - Fork 1
How to add a feature
Adding a feature to Harmony consists on modifying some or all the parts of the execution pipeline and adapting all the existing templates in order to support it. This section explains step by step the addition of a new feature.
First, let us assume that we want to add a new feature that allows the user to deprecate Resources (i.e. while still serving the same URL, have the server respond always with a 410 GONE HTTP code). Note that this feature is probably not the most interesting for prototyping purposes (Harmony's goal), but it is a good candidate for explaining this process.
The first step is to make the grammar support the new feature, so the lexer and parser are able to interpret it. At this step it is really important to pay attention to backwards compatibility (i.e. all the specifications that worked before should keep working after the changes). Ideally, the test suite should catch any violation of this constraint, but the developer should pay attention anyway.
In order to add support for the feature, language-spec/Language.cf needs to be modified. For this concrete case, we modify the Resource node to add a deprecation option.
Res. Resource ::=
"resource" Ident "(" "\"" RouteIdent "\"" ")" Mode;Res. Resource ::=
DepOpt "resource" Ident "(" "\"" RouteIdent "\"" ")" Mode;
NoDep. DepOpt ::= ;
Dep. DepOpt ::= deprecated;Now, the user can preprend the deprecated keyword to a resource, marking it as deprecated. One can easily realise that this specification does not break previous projects, as the NoDep node covers all previous cases.
The next step is to modify StaticCheck.hs to handle the new AST node type (DepOpt). Our recommendation is to first run cabal build and fix any errors shown by the compiler (Haskell's type system is powerful, and this will solve most if not all compatibilities).
Moreover, it is necessary to add the new logic to this class. For the static checking phase, we only need to make sure that there is at least one non-deprecated resource. We also need to modify ApiSpec.hs in order to support the deprecation option (and adapt StaticCheck.hs to make it mark the resources as deprecated).
Let us make the change to ApiSpec.hs. ApiSpec.hs contains the ApiSpec record definition, being resources one of its fields. resources contains all the resources specified by the user in a Map Id ResourceInfo. ResourceInfo needs to be modified in order to mark the resource as deprecated.
Before:
type ResourceInfo = (Route, Writable)After:
-- Deprecated is a boolean type.
type Deprecated = Bool
type ResourceInfo = (Route, Writable, Deprecated)Now it is time to add a new phase to StaticCheck.hs so it makes sure at least one resource is non deprecated:
Before:
staticCheck :: Specification -> Err AS.ApiSpec
staticCheck spec@(Spec _ _ modules enums structs resources) = do
...
readAndCheckEnums enums
addStructNames structs
processModules modules
readAndCheckStructs structs
checkResources resources
sortStructsByDependencyOrder After:
staticCheck :: Specification -> Err AS.ApiSpec
staticCheck spec@(Spec _ _ modules enums structs resources) = do
...
readAndCheckEnums enums
addStructNames structs
processModules modules
readAndCheckStructs structs
checkResources resources
-- New phase to check that not all resources are deprecated
checkResourcesNotAllDeprecated resources
sortStructsByDependencyOrder
-- Makes sure that not all the resources are deprecated,
-- otherwise fails.
checkResourcesNotAllDeprecated :: [Resource] -> StaticCheck ()
checkResourcesNotAllDeprecated resources = do
-- Check if all resources are deprecated
allDeprecated <-
allM (\(_, _, deprecated) -> not deprecated) resources
if allDeprecated
then fail "Incorrect specification: all resources deprecated"
else return ()Finally, the ApiSpec generation needs to add the deprecation option to its output.
Before:
addResource res =
CMS.modify (\textbackslash(names, as) ->
(names, as { AS.resources =
M.insert (resName res)
(resRoute res, resIsWritable res)
(AS.resources as)}))After:
-- Returns true if a resource is deprecated
resIsDeprecated :: Resource -> Bool
resIsDeprecated (_, _, deprecated) = deprecated
...
addResource res =
CMS.modify (\(names, as) ->
(names, as { AS.resources =
M.insert (resName res)
( resRoute res
, resIsWritable res
, resIsDeprecated res
)
(AS.resources as)}))Now it is time to modify the Service record (specifically, the Schema type in TemplateCompiler.hs).
Before:
data Schema = Schema { schemaName :: String
, schemaRoute :: Maybe StrValue
, writable :: Bool
, hasKeyField :: Bool
, keyField :: String
, schemaVars :: [SchemaVar] }
deriving (Show, Data, Typeable)After:
data Schema = Schema { schemaName :: String
, schemaRoute :: Maybe StrValue
, writable :: Bool
-- New field that can mark it as deprecated
, deprecated :: Bool
, hasKeyField :: Bool
, keyField :: String
, schemaVars :: [SchemaVar] }
deriving (Show, Data, Typeable)After this change, one should also modify ServiceGenerator.hs to set this field.
Before:
generateSchema :: (AS.Type -> String)
-> AS.ApiSpec
-> AS.Id
-> TC.Schema
generateSchema fieldMapping apiSpec strId =
TC.Schema { TC.schemaName = strId
, TC.schemaRoute = schemaRoute'
, TC.writable = writable'
, TC.hasKeyField = hasKeyField
, TC.keyField = keyField
, TC.schemaVars =
generateVars fieldMapping apiSpec structInfo }
where
...After:
generateSchema :: (AS.Type -> String)
-> AS.ApiSpec
-> AS.Id
-> TC.Schema
generateSchema fieldMapping apiSpec strId =
TC.Schema { TC.schemaName = strId
, TC.schemaRoute = schemaRoute'
, TC.writable = writable'
-- Can be used as {{#deprecated}} in the templates
, TC.deprecated = deprecated'
, TC.hasKeyField = hasKeyField
, TC.keyField = keyField
, TC.schemaVars =
generateVars fieldMapping apiSpec structInfo }
where
-- This should check whether the resource obtained using strId
-- is deprecated or not.
deprecated' = ...
...This step can be one or the most complex in some cases (not for this one though). In this case, since this only affects server and tests (we need to write tests that check a deprecated resource), we need to modify them.
As an example, for the server, we have to modify templates/server/js/server.js to check the new condition:
{{#schema}}
{{#deprecated}
app.get(..., function(req, res) {
// Resource is deprecated, so we send a 410 code.
res.status(410).send();
});
// Repeat for post, put, delete
{{/deprecated}}
{{^deprecated}}
// Previous behaviour
{{/deprecated}}
{{/schema}}
At this point the process is almost finished, but the developer still needs to follow the testing process and to write tests for the new functionality.
The process is quite simple and automated:
- Run
cabal test: this will run style, unit and property based tests. - Run
sh tests.sh: this will run the end to end testing (it is necessary to have a runningMongoDBinstance in the development machine).
If these two commands succeed, then it means the changes do not break backwards compatibility (ideally, as some corner cases might be missed by the current tests).
### Writing tests for the changes
Now that the developer made sure the changes do not break anything, she should make sure that the new code behaves correctly and it is tested. Ideally, unit and property based tests were written while developing the feature (otherwise, that should be done at this point). In addition, end to end tests should be created (at least one in examples/good and one in examples/bad).
For the example, we can create a test in example/bad/all_deprecated.hmy with a minimal example that fails:
service_name: AllDeprecated
service_version: 1.0.0
struct Str1 {
field : String
}
deprecated resource Str1 ("/deprecated");
For the correct case, in example/good/deprecation.hmy:
service_name: Deprecation
service_version: 1.0.0
struct Str1 {
field : String
}
struct Str2 {
field : String
}
resource Str1 ("/nondeprecated");
deprecated resource Str2 ("/deprecated");
If the test templates were adapted, then the output generated by the tool should now be able to test this new specification and make sure it works correctly (or warn the developer if it does not).