diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index dcf374d3..00000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "travis"]
- path = travis
- url = https://github.com/nemgrouplimited/travis-functions.git
diff --git a/build.gradle b/build.gradle
index b0b2da94..7a5a64cd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -34,6 +34,7 @@ plugins {
id 'jacoco'
id 'signing'
id 'java-library'
+ id 'java'
}
apply plugin: 'nebula-aggregate-javadocs'
@@ -43,8 +44,8 @@ apply plugin: 'nebula-aggregate-javadocs'
ext {
vertxVersion = "3.5.0"
rxjavaVersion = "2.1.7"
- junitVersion = "5.4.0"
- catbufferVersion = "0.1.2"
+ junitVersion = "5.9.0"
+ catbufferVersion = "0.1.3-SNAPSHOT"
restApiVersion = "1.0.0"
jackson_version = "2.9.9"
jackson_databind_version = "2.9.9"
diff --git a/build_serializers.sh b/build_serializers.sh
new file mode 100644
index 00000000..da06cb17
--- /dev/null
+++ b/build_serializers.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# deliberate order as generators seem to be using some ancient pyyaml
+
+cd catbuffer-generators
+pip install -r requirements.txt
+cd ..
+
+cd catbuffer-parser
+pip install -r requirements.txt
+cd ..
+
+cd catbuffer-generators
+
+PYTHONPATH=. python3 ../catbuffer-parser/main.py \
+ --schema ../catbuffer-schemas/schemas/all.cats \
+ --include ../catbuffer-schemas/schemas/ \
+ --generator java \
+ --copyright HEADER.inc
diff --git a/catbuffer-generators/.gitignore b/catbuffer-generators/.gitignore
new file mode 100644
index 00000000..97a42225
--- /dev/null
+++ b/catbuffer-generators/.gitignore
@@ -0,0 +1,15 @@
+*~
+*.pch
+*.pyc
+__pycache__/
+.idea
+.vscode/
+.DS_Store
+_generated/
+.python-version
+node_modules/
+.gradle
+build
+.idea
+/catbuffer-generators.iml
+/build-old/
diff --git a/catbuffer-generators/.gitmodules b/catbuffer-generators/.gitmodules
new file mode 100644
index 00000000..909e2ac5
--- /dev/null
+++ b/catbuffer-generators/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "catbuffer"]
+ path = catbuffer
+ url = https://github.com/nemtech/catbuffer.git
diff --git a/catbuffer-generators/.pycodestyle b/catbuffer-generators/.pycodestyle
new file mode 100644
index 00000000..caa456f8
--- /dev/null
+++ b/catbuffer-generators/.pycodestyle
@@ -0,0 +1,2 @@
+[pycodestyle]
+max-line-length = 140
diff --git a/catbuffer-generators/.pylintrc b/catbuffer-generators/.pylintrc
new file mode 100644
index 00000000..f4817710
--- /dev/null
+++ b/catbuffer-generators/.pylintrc
@@ -0,0 +1,438 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+#disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call
+#disable=missing-docstring,misplaced-comparison-constant
+
+# Disable the message(s) with the given id(s).
+# C0111 = Missing docstring
+# C0122 = Misplaced comparison constant
+# C0103 = Invalid name
+# R0201 = Method could be a function
+# R0902 = Too many instance attributes
+# R0903 = Too few public methods
+# R0913 = Too many arguments
+# R0914 = Too many local variables
+# W0105 = String statement has no effect
+disable=C0111,C0122,C0103,R0201,R0902,R0903,R0913,R0914,W0105
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[BASIC]
+
+# Naming hint for argument names
+argument-name-hint=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct argument names
+argument-rgx=(([a-z][a-zA-Z0-9_]{2,50})|(_[a-z0-9_]*))$
+
+# Naming hint for attribute names
+attr-name-hint=(([a-z][a-zA-Z0-9_]{2,50})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct attribute names
+attr-rgx=(([a-z][a-zA-Z0-9_]{2,50})|(_[a-z0-9_]*))$
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Naming hint for class attribute names
+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Naming hint for class names
+class-name-hint=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Naming hint for constant names
+const-name-hint=(([A-Z_][A-Z0-9_]*)|(t_[A-Z0-9_]+)|(__.*__))$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(t_[A-Z0-9_]+)|(__.*__))$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=100
+
+# Naming hint for function names
+function-name-hint=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct function names
+function-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming hint for inline iteration names
+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Naming hint for method names
+method-name-hint=(([a-z][a-zA-Z0-9_]{2,50})|(_[a-zA-Z0-9_]*))$
+
+# Regular expression matching correct method names
+method-rgx=(([a-z][a-zA-Z0-9_]{2,50})|(_[a-zA-Z0-9_]*))$
+
+# Naming hint for module names
+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct module names
+module-rgx=([A-Za-z][a-zA-Z0-9]+)$
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=.
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming hint for variable names
+variable-name-hint=(([a-z][a-zA-Z0-9_]{2,50})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=(([a-z][a-zA-Z0-9_]{2,50})|(_[a-z0-9_]*))$
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=140
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=100
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,future.builtins
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=8
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/catbuffer-generators/.travis.yml b/catbuffer-generators/.travis.yml
new file mode 100644
index 00000000..dc920493
--- /dev/null
+++ b/catbuffer-generators/.travis.yml
@@ -0,0 +1,81 @@
+language: python
+python:
+ - '3.7'
+addons:
+ apt:
+ packages:
+ - openjdk-8-jdk
+before_cache:
+ - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
+ - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
+cache:
+ directories:
+ - "$HOME/.gradle/caches/"
+ - "$HOME/.gradle/wrapper/"
+ - "$HOME/.cache/pip"
+ - "$HOME/.npm"
+install:
+ - pip install -r requirements.txt
+env:
+ global:
+ - DEV_BRANCH=dev
+ - RELEASE_BRANCH=main
+ - POST_RELEASE_BRANCH=main
+ - RELEASE_MESSAGE=release
+before_script:
+ - export PYTHONPATH=$PYTHONPATH:./catbuffer
+ - nvm install --lts
+ - node --version
+script:
+ - . ./travis/travis-functions.sh
+ - validate_env_variables
+jobs:
+ include:
+ - stage: test
+ name: pylint
+ script: pylint --load-plugins pylint_quotes generators
+ - name: pycodestyle
+ script: pycodestyle --config=.pycodestyle .
+
+ - name: java
+ script: ./scripts/generate_java.sh
+ - name: typescript
+ script: ./scripts/generate_typescript.sh
+ - name: python
+ script: ./scripts/generate_python.sh
+
+ - stage: alpha
+ name: java publish alpha
+ script: ./scripts/generate_java.sh publish
+ if: branch = env(DEV_BRANCH) AND type = push
+ - name: typescript publish alpha
+ script: ./scripts/generate_typescript.sh publish
+ if: branch = env(DEV_BRANCH) AND type = push
+ - name: python publish alpha
+ script: ./scripts/generate_python.sh publish
+ if: branch = env(DEV_BRANCH) AND type = push
+
+ - stage: release
+ name: java publish release
+ script: ./scripts/generate_java.sh release
+ if: branch = env(RELEASE_BRANCH) AND type = api AND commit_message = env(RELEASE_MESSAGE)
+ - name: typescript publish release
+ script: ./scripts/generate_typescript.sh release
+ if: branch = env(RELEASE_BRANCH) AND type = api AND commit_message = env(RELEASE_MESSAGE)
+ - name: python publish release
+ script: ./scripts/generate_python.sh release
+ if: branch = env(RELEASE_BRANCH) AND type = api AND commit_message = env(RELEASE_MESSAGE)
+
+ - stage: post release
+ name: tag and version upgrade
+ script: /bin/bash travis/travis-functions.sh post_release_version_file
+ if: branch = env(RELEASE_BRANCH) AND type = api AND commit_message = env(RELEASE_MESSAGE)
+
+before_install:
+ - |
+ if [ -z "${signingKeyId}" ]; then
+ echo "No signing the artifacts"
+ else
+ echo "Signing artifacts"
+ openssl aes-256-cbc -K $encrypted_37d6c1a7ee80_key -iv $encrypted_37d6c1a7ee80_iv -in travis/symbol-sdk-java.gpg.enc -out symbol-sdk-java.gpg -d
+ fi
diff --git a/catbuffer-generators/CONTRIBUTING.md b/catbuffer-generators/CONTRIBUTING.md
new file mode 100644
index 00000000..31c4785b
--- /dev/null
+++ b/catbuffer-generators/CONTRIBUTING.md
@@ -0,0 +1,49 @@
+# Contributing to catbuffer-generators
+
+As explained in the [README](README.md) file, this project consists of a set of code generators that serialize and deserialize Catapult entities (Transactions, Types, ...). You can use this project to obtain classes that will help you deal with Catapult entities in their binary form from your language of choice.
+
+If the language you are interested in is not covered by the project, you can add a new generator by following this guide.
+
+## The Generators
+
+The [Catapult Server](https://github.com/nemtech/catapult-server) manages a number of entities (e.g. transactions) which need to be stored in binary form when communicated over a network. The binary layout of these entities is described using **catbuffer** files (Catapult Buffer files with ``.cat`` extension), stored in the [catbuffer](https://github.com/nemtech/catbuffer) repository, inside the ``schemas`` folder.
+
+The generators in this project read catbuffer schema files and produce the necessary files in the target language to serialize and deserialize them.
+
+Each generator is a Python class residing in the ``generators`` folder and listed in ``generators/All.py``. Most of them use [Mako templates](https://www.makotemplates.org/) so the boilerplate code is abstracted to common classes.
+
+> **NOTE:**
+> Some generators like ``javascript`` and ``cpp_builder`` are still manually built and do not use templates. They are in the process of being adapted to the new mechanism. **Do not use them as examples to build your own generators!**
+
+Generators are invoked by name from the ``catbuffer/main.py``, after pointing the ``PYTHONPATH`` environment variable to the ``catbuffer-generators`` folder.
+
+For instance, if you're already in the ``catbuffer-generators`` folder, you can see the list of available generators by running:
+
+```bash
+PYTHONPATH=. python3 catbuffer/main.py
+```
+
+So, to use the java generator:
+
+```bash
+PYTHONPATH=. python3 catbuffer/main.py -g java
+```
+
+You can take a look at the ``scripts`` folder to see more invocation details of ``main.py``.
+
+## Adding a New Generator
+
+Unfortunately, the process to add a new generator is not automated yet:
+
+1. Copy the ``java`` folder inside ``generators`` and rename the folder and the files inside to the desired language.
+
+2. Edit ``JavaFileGenerator.py`` and ``JavaHelper.py`` (now renamed) to use the proper language name and file extension.
+
+3. Edit all files inside the ``templates`` folder to adapt to the selected language. Use the Java version for inspiration and make sure you know how [Mako templates](https://www.makotemplates.org/) work.
+
+4. Add your new generator to the global register in ``generators/All.py``.
+
+Once the generator is ready you should be able to invoke it using ``catbuffer/main.py`` as shown above. Create a helper script like the ones in the ``scripts`` folder to automate building and deploying the new serializer classes!
+
+> **NOTE:**
+> The ``VectorTest`` file contains unit tests for the generator. Add your own tests to the new generator and run them from a script in the ``scripts`` folder. See how ``generate_typescrpt.sh`` executes ``npm run test``, for example.
diff --git a/catbuffer-generators/HEADER.inc b/catbuffer-generators/HEADER.inc
new file mode 100644
index 00000000..243a6f9a
--- /dev/null
+++ b/catbuffer-generators/HEADER.inc
@@ -0,0 +1,20 @@
+/**
+*** Copyright (c) 2016-2019, Jaguar0625, gimre, BloodyRookie, Tech Bureau, Corp.
+*** Copyright (c) 2020-present, Jaguar0625, gimre, BloodyRookie.
+***
+*** This file is part of Catapult.
+***
+*** Catapult is free software: you can redistribute it and/or modify
+*** it under the terms of the GNU Lesser General Public License as published by
+*** the Free Software Foundation, either version 3 of the License, or
+*** (at your option) any later version.
+***
+*** Catapult 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 Lesser General Public License for more details.
+***
+*** You should have received a copy of the GNU Lesser General Public License
+*** along with Catapult. If not, see .
+**/
+
diff --git a/catbuffer-generators/LICENSE b/catbuffer-generators/LICENSE
new file mode 100644
index 00000000..ab602974
--- /dev/null
+++ b/catbuffer-generators/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/catbuffer-generators/README.md b/catbuffer-generators/README.md
new file mode 100644
index 00000000..8c83c6ce
--- /dev/null
+++ b/catbuffer-generators/README.md
@@ -0,0 +1,63 @@
+# catbuffer-generators
+
+[![Build Status](https://api.travis-ci.com/nemtech/catbuffer-generators.svg?branch=main)](https://travis-ci.com/nemtech/catbuffer-generators)
+
+Set of code generators to serialize and deserialize Catapult entities in different programming languages.
+
+In combination with the [catbuffer](https://github.com/nemtech/catbuffer) project, developers can generate builder classes for a given set of programming languages. For example, the [Symbol SDKs](https://nemtech.github.io/sdk) use the generated code to operate with the entities in binary form before announcing them to the network.
+
+## Supported programming languages
+
+- C++
+- Java
+- TypeScript/JavaScript
+- Python
+
+## Requirements
+
+- Python >= 3.4
+
+## Installation
+
+1. Clone the ``catbuffer-generators`` repository:
+
+```bash
+git clone --recurse-submodules https://github.com/nemtech/catbuffer-generators
+```
+
+This will also clone the ``catbuffer`` repository as a submodule.
+
+2. Install the package requirements:
+
+```bash
+cd catbuffer-generators
+pip install -r requirements.txt
+```
+
+## Usage
+
+Use the ``scripts/generate_all.sh`` script to create code for the different languages. For example:
+
+```bash
+scripts/generate_all.sh cpp_builder
+```
+
+This processes every schema and writes the output files in the ``catbuffer/_generated/`` folder.
+
+Alternatively, you can use any of the language-specific scripts like ``scripts/generate_typescript.sh``. Most of these scripts, after producing the code will compile it into an output artifact in the ``build`` folder.
+
+> **NOTE:**
+> These scripts require Bash 4 or higher.
+
+### Run the linter
+
+```bash
+pylint --load-plugins pylint_quotes generators test/python
+pycodestyle --config=.pycodestyle .
+```
+
+> **NOTE:**
+> This requires Python 3.7 or higher.
+
+Copyright (c) 2016-2019, Jaguar0625, gimre, BloodyRookie, Tech Bureau, Corp.
+Copyright (c) 2020-present, Jaguar0625, gimre, BloodyRookie.
diff --git a/catbuffer-generators/generators/All.py b/catbuffer-generators/generators/All.py
new file mode 100644
index 00000000..68c761fa
--- /dev/null
+++ b/catbuffer-generators/generators/All.py
@@ -0,0 +1,11 @@
+from generators.cpp_builder.BuilderGenerator import BuilderGenerator
+from generators.java.JavaFileGenerator import JavaFileGenerator
+from generators.typescript.TypescriptFileGenerator import TypescriptFileGenerator
+from generators.python.PythonFileGenerator import PythonFileGenerator
+
+AVAILABLE_GENERATORS = {
+ 'cpp_builder': BuilderGenerator,
+ 'java': JavaFileGenerator,
+ 'typescript': TypescriptFileGenerator,
+ 'python': PythonFileGenerator
+}
diff --git a/catbuffer-generators/generators/Descriptor.py b/catbuffer-generators/generators/Descriptor.py
new file mode 100644
index 00000000..b6bc5ab0
--- /dev/null
+++ b/catbuffer-generators/generators/Descriptor.py
@@ -0,0 +1,3 @@
+from collections import namedtuple
+
+Descriptor = namedtuple('Descriptor', ['filename', 'code'])
diff --git a/catbuffer-generators/generators/common/FileGenerator.py b/catbuffer-generators/generators/common/FileGenerator.py
new file mode 100644
index 00000000..fc6272d8
--- /dev/null
+++ b/catbuffer-generators/generators/common/FileGenerator.py
@@ -0,0 +1,145 @@
+import os
+from abc import ABC, abstractmethod
+
+from generators.Descriptor import Descriptor
+from generators.common.MakoClassGenerator import MakoClassGenerator
+from generators.common.MakoEnumGenerator import MakoEnumGenerator
+from generators.common.MakoStaticClassGenerator import MakoStaticClassGenerator
+from generators.common.MakoTypeGenerator import MakoTypeGenerator
+
+
+class FileGenerator(ABC):
+ """
+ Generic top level file generator. A language will extend this class defining how to create the different generators.
+ """
+
+ def __init__(self, schema, options):
+ self.schema = schema
+ self.current = None
+ self.options = options
+
+ def __iter__(self):
+ self.current = self.generate()
+ return self
+
+ def __next__(self):
+ return next(self.current)
+
+ def generate(self):
+ """
+ Main entry point for the generator. It collects all the possible file generators and execute
+ them producing different files.
+ :return: multiple Descriptors using yield.
+ """
+ helper = self.create_helper()
+ generators = []
+ for type_name, class_schema in self.schema.items():
+ attribute_type = class_schema['type']
+ if helper.is_byte_type(attribute_type):
+ generators.extend(self.create_type_generators(helper, type_name, class_schema))
+ elif helper.is_enum_type(attribute_type):
+ generators.extend(self.create_enum_generators(helper, type_name, class_schema))
+ elif helper.is_struct_type(attribute_type) and helper.should_generate_class(type_name):
+ generators.extend(self.create_class_generators(helper, type_name, class_schema))
+ # write all the helper files
+ for filename in self.get_static_templates_file_names():
+ generators.extend(self.create_static_class_generators(filename, helper))
+ for generator in generators:
+ code = self.init_code()
+ code += generator.generate()
+ yield Descriptor(generator.get_generated_file_name(), code)
+
+ def init_code(self):
+ """
+ :return: a brand new memory file with the license if provided.
+ """
+ copyright_file = self.options['copyright']
+ code = []
+ if os.path.isfile(copyright_file):
+ with open(copyright_file) as header:
+ code = [line.strip() for line in header]
+ return code
+
+ def create_static_class_generators(self, filename, helper):
+ """
+ It creates the generators for a static generator. By default creates one generator by file (like .java or .ts)
+ Note that other languages may need more than one (like .cpp and .h)
+ :param filename: the filename
+ :param helper: the language helper
+ :return: a list of generator, one by default using mako templates
+ """
+ return [MakoStaticClassGenerator(self.get_template_path() + filename + '.mako',
+ filename + self.get_main_file_extension(), helper,
+ self.schema, None)]
+
+ def create_class_generators(self, helper, type_name, class_schema):
+ """
+ Creates the generators for given class type. By default creates one generator by file (like .java or .ts)
+ Note that other languages may need more than one (like .cpp and .h)
+
+ :param helper: the language helper
+ :param type_name: the type name.
+ :param class_schema: the schema of the currency class
+ :return: a list of generator, one by default using mako templates
+ """
+ return [MakoClassGenerator(helper, type_name, self.schema, class_schema, self.get_template_path(),
+ self.get_main_file_extension())]
+
+ def create_enum_generators(self, helper, type_name, class_schema):
+ """
+
+ Creates the generators for given enum type. By default creates one generator by file (like .java or .ts)
+ Note that other languages may need more than one (like .cpp and .h)
+
+ :param helper: the language helper
+ :param type_name: the type name.
+ :param class_schema: the schema of the currency class
+ :return: a list of generator, one by default using mako templates
+ """
+ return [MakoEnumGenerator(helper, type_name, self.schema, class_schema, self.get_template_path(),
+ self.get_main_file_extension())]
+
+ def create_type_generators(self, helper, type_name, class_schema):
+ """
+
+ Creates the generators for given atomic type. By default creates one generator by file (like .java or .ts)
+ Note that other languages may need more than one (like .cpp and .h)
+
+ :param helper: the language helper
+ :param type_name: the type name.
+ :param class_schema: the schema of the currency class
+ :return: a list of generator, one by default using mako templates
+ """
+ return [MakoTypeGenerator(helper, type_name, self.schema, class_schema, self.get_template_path(),
+ self.get_main_file_extension())]
+
+ @abstractmethod
+ def get_template_path(self):
+ """
+
+ :return: the path where the language templates will be find. It needs to be redefined if Mako genertors are used.
+ """
+ raise NotImplementedError('get_template_path must be defined in subclass')
+
+ @abstractmethod
+ def get_main_file_extension(self):
+ """
+
+ :return: the extension of the generated files. Example: '.java'
+ """
+ raise NotImplementedError('get_main_file_extension must be defined in subclass')
+
+ @abstractmethod
+ def create_helper(self):
+ """
+
+ :return: the language helper. Subclasses would override this method returning a subclass of Helper.
+ """
+ raise NotImplementedError('create_helper must be defined in subclass')
+
+ def get_static_templates_file_names(self):
+ """
+
+ :return: a list of known static (GeneratorUtils for example) to be generated. Most languages would override this.
+ """
+ return []
diff --git a/catbuffer-generators/generators/common/Helper.py b/catbuffer-generators/generators/common/Helper.py
new file mode 100644
index 00000000..7f87242e
--- /dev/null
+++ b/catbuffer-generators/generators/common/Helper.py
@@ -0,0 +1,252 @@
+from enum import Enum
+
+from abc import ABC, abstractmethod
+
+
+# pylint: disable=too-many-public-methods
+
+
+class TypeDescriptorType(Enum):
+ """Type descriptor enum"""
+ Byte = 'byte'
+ Struct = 'struct'
+ Enum = 'enum'
+
+
+class TypeDescriptorDisposition(Enum):
+ Inline = 'inline'
+ Const = 'const'
+ Fill = 'fill'
+ Var = 'var'
+
+
+class AttributeKind(Enum):
+ """Attribute type enum"""
+ SIMPLE = 1
+ BUFFER = 2
+ ARRAY = 3
+ CUSTOM = 4
+ FLAGS = 5
+ SIZE_FIELD = 6
+ FILL_ARRAY = 7
+ VAR_ARRAY = 8
+ UNKNOWN = 100
+
+
+class Helper(ABC):
+ """
+ Helper stateless methods used when generating templates. Most languages would extend this object.
+ """
+
+ def __init__(self):
+ # a shortcut for the templates to access the AttributeKind type.
+ self.AttributeKind = AttributeKind
+
+ @staticmethod
+ def is_struct_type(typename):
+ return typename == TypeDescriptorType.Struct.value
+
+ @staticmethod
+ def is_enum_type(typename):
+ return typename == TypeDescriptorType.Enum.value
+
+ @staticmethod
+ def is_byte_type(typename):
+ return typename == TypeDescriptorType.Byte.value
+
+ @staticmethod
+ def resolve_alignment(a):
+ embedded = a.attribute is not None and 'type' in a.attribute and a.attribute[
+ 'type'] == 'EmbeddedTransaction'
+ parent_embedded = a.parent_attribute is not None and 'type' in a.parent_attribute and a.parent_attribute[
+ 'type'] == 'EmbeddedTransaction'
+ if embedded or parent_embedded:
+ return 8
+ return 0
+
+ @staticmethod
+ def is_inline_type(attribute):
+ return 'disposition' in attribute and attribute['disposition'] == TypeDescriptorDisposition.Inline.value
+
+ @staticmethod
+ def is_const_type(attribute):
+ return 'disposition' in attribute and attribute['disposition'] == TypeDescriptorDisposition.Const.value
+
+ @staticmethod
+ def is_fill_array_type(attribute):
+ return 'disposition' in attribute and attribute['disposition'] == TypeDescriptorDisposition.Fill.value
+
+ @staticmethod
+ def is_var_array_type(attribute):
+ return 'disposition' in attribute and attribute['disposition'] == TypeDescriptorDisposition.Var.value
+
+ @staticmethod
+ def is_any_array_kind(attribute_kind):
+ return attribute_kind in (AttributeKind.ARRAY, AttributeKind.VAR_ARRAY, AttributeKind.FILL_ARRAY)
+
+ @staticmethod
+ def is_sorted_array(attribute):
+ return 'sort_key' in attribute
+
+ @staticmethod
+ def is_reserved_field(attribute):
+ return 'name' in attribute and '_Reserved' in attribute['name'] and 'size' in attribute
+
+ @staticmethod
+ def is_conditional_attribute(attribute):
+ return 'condition' in attribute
+
+ @staticmethod
+ def is_attribute_count_size_field(attribute, class_attributes):
+ if class_attributes is None:
+ return False
+ attribute_name = attribute['name']
+ is_size_of_class_attributes = list(
+ filter(lambda a: 'size' in a and a['size'] == attribute_name, class_attributes))
+ return len(is_size_of_class_attributes) == 1
+
+ @staticmethod
+ def should_generate_class(name):
+ # subclassees may override this method if the language is not ready to generate all the classes
+ # I need to exclude due to the ReceiptBuilder hack of not serializing the size
+ # Also, SizePrefixedEntity needs to go first, not VerifiableEntity or EntityBody the way we handle super classes.
+ return name not in ('SizePrefixedEntity', 'VerifiableEntity', 'EntityBody', 'EmbeddedTransactionHeader',
+ 'TransactionHeader')
+ # return True
+
+ @staticmethod
+ def should_use_super_class():
+ # if true, first inline is super class, the rest are inline builders
+ # if false, there is no super class, all inline attributes use inline builders.
+ return True
+
+ @staticmethod
+ def add_required_import(required_import: set,
+ import_type,
+ class_name,
+ base_class_name # pylint: disable=unused-argument
+ ):
+ if not import_type == class_name:
+ required_import.add(import_type)
+ return required_import
+
+ @staticmethod
+ def get_all_constructor_params(attributes):
+ return [a for a in attributes if not a.kind == AttributeKind.SIZE_FIELD and a.attribute_name != 'size']
+
+ def get_generated_class_name(self, typename, class_schema, schema):
+ class_type = class_schema['type']
+ default_name = typename + 'Dto'
+ if self.is_byte_type(class_type) or self.is_enum_type(class_type) or typename not in schema:
+ return default_name
+ return typename + 'Builder' if self.is_struct_type(schema[typename]['type']) else default_name
+
+ def is_builtin_type(self, typename, size):
+ # byte up to long are passed as 'byte' with size set to proper value
+ return not isinstance(size, str) and self.is_byte_type(typename) and size <= 8
+
+ def get_attribute_size(self, schema, attribute):
+ if 'size' not in attribute and not self.is_byte_type(attribute['type']) and not self.is_enum_type(
+ attribute['type']):
+ attr = schema[attribute['type']]
+ if 'size' in attr:
+ return attr['size']
+ return 1
+ return attribute['size']
+
+ @staticmethod
+ def get_base_type(schema: dict, attribute_type):
+ attribute: dict = schema.get(attribute_type)
+ if attribute is not None:
+ return attribute.get('type')
+ return None
+
+ @staticmethod
+ def is_flags_enum(attribute_type):
+ return attribute_type.endswith('Flags')
+
+ @staticmethod
+ def is_inline_class(attribute):
+ return 'disposition' in attribute and attribute['disposition'] == TypeDescriptorDisposition.Inline.value
+
+ @staticmethod
+ def capitalize_first_character(string):
+ return string if not string else string[0].upper() + string[1:]
+
+ @staticmethod
+ def decapitalize_first_character(string):
+ return string if not string else string[0].lower() + string[1:]
+
+ @staticmethod
+ def snake_case(string: str):
+ return string if not string else string[0] + ''.join('_' + x if x.isupper() else x for x in string[1:])
+
+ # pylint: disable=R0911
+ def get_attribute_kind(self, attribute, class_attributes):
+ if self.is_var_array_type(attribute):
+ return AttributeKind.VAR_ARRAY
+ if self.is_fill_array_type(attribute):
+ return AttributeKind.FILL_ARRAY
+ if self.is_inline_class(attribute):
+ return AttributeKind.CUSTOM
+ if self.is_attribute_count_size_field(attribute, class_attributes):
+ return AttributeKind.SIZE_FIELD
+
+ attribute_type = attribute['type']
+
+ if self.is_flags_enum(attribute_type):
+ return AttributeKind.FLAGS
+
+ if self.is_struct_type(attribute_type) or self.is_enum_type(attribute_type) or 'size' not in attribute:
+ return AttributeKind.CUSTOM
+
+ attribute_size = attribute['size']
+
+ if isinstance(attribute_size, str):
+ if attribute_type == 'byte':
+ return AttributeKind.BUFFER
+ return AttributeKind.ARRAY
+
+ if isinstance(attribute_size, int) and not attribute_type == 'byte':
+ return AttributeKind.ARRAY
+
+ if self.is_builtin_type(attribute_type, attribute_size):
+ return AttributeKind.SIMPLE
+
+ return AttributeKind.BUFFER
+
+ def get_attribute_property_equal(self, schema, attributes, attribute_name, attribute_value, recurse=True):
+ for attribute in attributes:
+ if attribute_name in attribute and attribute[attribute_name] == attribute_value:
+ return attribute
+ if (recurse and 'disposition' in attribute and
+ attribute['disposition'] == TypeDescriptorDisposition.Inline.value):
+ value = self.get_attribute_property_equal(schema, schema[attribute['type']]['layout'], attribute_name,
+ attribute_value)
+ if value is not None:
+ return value
+ return None
+
+ def get_name_from_type(self, type_name: str):
+ return self.decapitalize_first_character(type_name)
+
+ @staticmethod
+ def get_comment_from_name(name):
+ return name[0].upper() + ''.join(' ' + x.lower() if x.isupper() else x for x in name[1:])
+
+ def get_comments_from_attribute(self, attribute):
+ comment = attribute['comments'].strip() if 'comments' in attribute else ''
+ if not comment and 'name' in attribute:
+ comment = self.get_comment_from_name(attribute['name'])
+ return comment
+
+ def create_enum_name(self, name: str):
+ return self.snake_case(name).upper()
+
+ @abstractmethod
+ def get_builtin_type(self, size):
+ raise NotImplementedError('get_builtin_type must be overridden')
+
+ @abstractmethod
+ def get_generated_type(self, schema, attribute, attribute_kind):
+ raise NotImplementedError('get_generated_type must be overridden')
diff --git a/catbuffer-generators/generators/common/MakoClassGenerator.py b/catbuffer-generators/generators/common/MakoClassGenerator.py
new file mode 100644
index 00000000..377b19e6
--- /dev/null
+++ b/catbuffer-generators/generators/common/MakoClassGenerator.py
@@ -0,0 +1,187 @@
+from collections import namedtuple
+from itertools import chain
+from typing import List
+
+from generators.common.Helper import TypeDescriptorDisposition
+from .MakoStaticClassGenerator import MakoStaticClassGenerator
+
+AttributeData = namedtuple('AttributeData',
+ ['attribute', 'kind', 'attribute_name', 'attribute_comment', 'attribute_base_type',
+ 'attribute_var_type', 'attribute_is_final', 'attribute_class_name', 'attribute_is_super',
+ 'attribute_size', 'attribute_is_conditional', 'attribute_aggregate_attribute_name',
+ 'attribute_is_reserved', 'attribute_aggregate_class', 'attribute_is_inline',
+ 'attribute_is_aggregate', 'parent_attribute', 'condition_type_attribute',
+ 'attribute_condition_value', 'attribute_condition_provide',
+ 'conditional_read_before'])
+
+
+class MakoClassGenerator(MakoStaticClassGenerator):
+ """
+ Generic Mako generator for class type schemas.
+ """
+
+ def __init__(self, helper, name, schema, class_schema, template_path, file_extension):
+ super().__init__(template_path + 'Class.mako',
+ helper.get_generated_class_name(name, class_schema, schema) + file_extension,
+ helper,
+ schema,
+ class_schema)
+ class_schema['name'] = name[0].lower() + name[1:]
+ self.required_import = set()
+ self.name = name
+ self.attributes = []
+ self.generated_class_name = helper.get_generated_class_name(name, class_schema, schema)
+ self.base_class_name = None
+ self.generated_base_class_name = None
+ if self.helper.should_use_super_class():
+ self.foreach_attributes(self.class_schema['layout'], self._find_base_callback)
+ self.comments = helper.get_comments_from_attribute(self.class_schema)
+ self._recurse_foreach_attribute(self.name, self._add_attribute)
+ self.body_class_name = helper.get_body_class_name(self.name)
+
+ condition_types = [(a, schema[a.condition_type_attribute['type']]) for a in self.attributes if
+ a.attribute_is_conditional and a.attribute['condition_operation'] != 'has']
+ condition_types_values = self._calculate_constructor_options(condition_types)
+
+ self.all_constructor_params = helper.get_all_constructor_params(self.attributes)
+ # not a.attribute_is_aggregate
+ self.constructor_attributes = [self.all_constructor_params] if not condition_types else [
+ self.constructor_arguments(self.all_constructor_params, condition_type) for condition_type in
+ condition_types_values]
+
+ @staticmethod
+ def _calculate_constructor_options(condition_types):
+ if not condition_types:
+ return []
+ condition_types_values = [[(a, value['name'])
+ for value in schema_type['values']] for (a, schema_type) in condition_types]
+ condition_types_values = list(chain.from_iterable(condition_types_values))
+ condition_types_values = [
+ (a.condition_type_attribute['name'], a.attribute['condition_value'], value) for
+ (a, value) in condition_types_values]
+
+ with_values = {a_condition_value for (a_condition_name, a_condition_value, conditional_value) in
+ condition_types_values}
+
+ condition_types_values = {(a_condition_name,
+ conditional_value if conditional_value in with_values else None,
+ conditional_value) for
+ (a_condition_name, a_condition_value, conditional_value) in
+ condition_types_values}
+ return condition_types_values
+
+ def _recurse_foreach_attribute(self, class_name: str, callback, aggregate_attribute=None, deep=0):
+ print(str('\t' * deep) + '- ' + class_name)
+ class_generated = (class_name != self.name and self.helper.should_generate_class(class_name))
+ class_attributes = self.schema[class_name]['layout']
+ for attribute in class_attributes:
+ if class_generated:
+ attribute['aggregate_class'] = class_name
+ if 'disposition' in attribute:
+ if attribute['disposition'] == TypeDescriptorDisposition.Inline.value:
+ attribute['name'] = self.helper.decapitalize_first_character(attribute['type'])
+ aggregate_class_is_generated = self.helper.should_generate_class(attribute['type'])
+ # Is the aggregate class generated?
+ if aggregate_class_is_generated:
+ print(str('\t ' * (deep + 1)) + ' ' + attribute['name'])
+ callback(attribute, class_attributes, aggregate_attribute)
+ new_aggregate_attribute = attribute if aggregate_attribute is None and aggregate_class_is_generated \
+ else aggregate_attribute
+ self._recurse_foreach_attribute(attribute['type'], self._add_attribute,
+ new_aggregate_attribute,
+ deep + 1)
+ elif attribute['disposition'] == TypeDescriptorDisposition.Const.value:
+ continue
+ elif self.helper.is_var_array_type(attribute) or self.helper.is_fill_array_type(attribute):
+ print(str('\t ' * (deep + 1)) + ' ' + attribute['name'])
+ callback(attribute, class_attributes, aggregate_attribute)
+ continue
+ else:
+ print(str('\t ' * (deep + 1)) + ' ' + attribute['name'])
+ callback(attribute, class_attributes, aggregate_attribute)
+
+ def _add_attribute(self, attribute, class_attributes, aggregate_attribute):
+ aggregate_attribute_name = aggregate_attribute['name'] if aggregate_attribute else None
+ aggregate_attribute_type = aggregate_attribute['type'] if aggregate_attribute else None
+ kind = self.helper.get_attribute_kind(attribute, class_attributes)
+ attribute_is_conditional = self.helper.is_conditional_attribute(attribute)
+ attribute_comment = self.helper.get_comments_from_attribute(attribute)
+ attribute_name = attribute['name']
+ attribute_size = self.helper.get_attribute_size(self.schema, attribute)
+ attribute_var_type = self.helper.get_generated_type(self.schema, attribute, kind)
+ attribute_is_final = attribute_name != 'size' and not attribute_is_conditional
+ attribute_type = attribute.get('type', None)
+ attribute_base_type = self.helper.get_base_type(self.schema, attribute_type)
+ attribute_class_name = self.helper.get_generated_class_name(attribute_type, attribute, self.schema)
+ attribute_aggregate_attribute_name = aggregate_attribute_name
+ attribute_is_aggregate = self.helper.is_inline_class(attribute)
+ attribute_is_super = self.base_class_name is not None and self.base_class_name == aggregate_attribute_type
+ if attribute_is_aggregate and self.base_class_name is not None:
+ attribute_is_super = attribute_type == self.base_class_name
+ attribute_is_reserved = self.helper.is_reserved_field(attribute)
+ attribute_is_inline = not attribute_is_super and aggregate_attribute_name is not None
+ attribute_aggregate_class = attribute.get('aggregate_class', None)
+ self.required_import = self.helper.add_required_import(self.required_import,
+ attribute_var_type,
+ self.generated_class_name,
+ self.generated_base_class_name)
+ if attribute_is_conditional:
+ condition_type_attribute = self.helper.get_attribute_property_equal(self.schema,
+ self.class_schema['layout'], 'name',
+ attribute['condition'])
+ else:
+ condition_type_attribute = None
+
+ parent_attribute = self.helper.get_attribute_property_equal(self.schema, self.class_schema['layout'], 'size',
+ attribute_name)
+ conditional_read_before: bool = False
+ if 'condition' in attribute:
+ conditional_read_before = len(
+ [a1 for a1 in self.attributes if a1.attribute_name == attribute['condition']]) == 0
+
+ attribute_tuple = AttributeData(attribute, kind, attribute_name,
+ attribute_comment, attribute_base_type, attribute_var_type,
+ attribute_is_final, attribute_class_name,
+ attribute_is_super, attribute_size, attribute_is_conditional,
+ attribute_aggregate_attribute_name, attribute_is_reserved,
+ attribute_aggregate_class,
+ attribute_is_inline, attribute_is_aggregate, parent_attribute,
+ condition_type_attribute, None, True, conditional_read_before)
+ self.attributes.append(attribute_tuple)
+
+ def _find_base_callback(self, attribute):
+ if self.helper.is_inline_class(attribute) and self.helper.should_generate_class(attribute['type']):
+ self.base_class_name = attribute['type']
+ self.generated_base_class_name = self.helper.get_generated_class_name(self.base_class_name,
+ self.schema[self.base_class_name],
+ self.schema)
+ return True
+ return False
+
+ def foreach_attributes(self, attributes, callback):
+ for attribute in attributes:
+ if callback(attribute):
+ break
+
+ def constructor_arguments(self, constructor_params: List[AttributeData], condition_type_and_value):
+ return [self.set_default_argument(a, condition_type_and_value) for a in constructor_params]
+
+ def set_default_argument(self, a: AttributeData, condition_type_and_value) -> AttributeData:
+
+ (a_condition_name, a_condition_value, conditional_value) = condition_type_and_value
+
+ if a.attribute_name == a_condition_name:
+ return a._replace(attribute_condition_value=conditional_value)
+
+ if self.should_not_provide_argument(a, a_condition_name, a_condition_value):
+ return a._replace(attribute_condition_provide=False)
+ return a
+
+ def should_not_provide_argument(self, a: AttributeData, a_condition, a_condition_value):
+ if 'condition' not in a.attribute:
+ return False
+ if a.attribute['condition'] != a_condition:
+ return False
+ if a.attribute['condition_value'] == a_condition_value:
+ return False
+ return True
diff --git a/catbuffer-generators/generators/common/MakoEnumGenerator.py b/catbuffer-generators/generators/common/MakoEnumGenerator.py
new file mode 100644
index 00000000..364c3371
--- /dev/null
+++ b/catbuffer-generators/generators/common/MakoEnumGenerator.py
@@ -0,0 +1,41 @@
+from generators.common.Helper import TypeDescriptorDisposition
+from .MakoStaticClassGenerator import MakoStaticClassGenerator
+
+
+class MakoEnumGenerator(MakoStaticClassGenerator):
+ """
+ Generic Mako generator for enum type schemas.
+ """
+
+ def __init__(self, helper, name: str, schema, class_schema, template_path: str, file_extension: str):
+ super().__init__(template_path + 'Enum.mako',
+ helper.get_generated_class_name(name, class_schema, schema) + file_extension,
+ helper,
+ schema,
+ class_schema)
+ self.name = name
+ self.enum_values = {}
+ self.size = self.class_schema['size']
+ self.enum_type = helper.get_builtin_type(self.size)
+ self.generated_class_name = helper.get_generated_class_name(name, class_schema, schema)
+ self._add_enum_values(self.class_schema)
+ self.comments = helper.get_comments_from_attribute(self.class_schema)
+ self.is_flag = helper.is_flags_enum(self.name)
+ for type_descriptor, entity_schema in self.schema.items():
+ if 'layout' in entity_schema:
+ for attribute in entity_schema['layout']:
+ if attribute.get('disposition', None) == TypeDescriptorDisposition.Const.value and attribute.get(
+ 'type', None) == self.name:
+ enum_name = type_descriptor
+ enum_comment = self.helper.get_comment_from_name(enum_name)
+ enum_value = attribute['value']
+ self._add_enum_value(enum_name, enum_value, enum_comment)
+
+ def _add_enum_values(self, enum_attribute):
+ enum_attribute_values = enum_attribute['values']
+ for current_attribute in enum_attribute_values:
+ self._add_enum_value(current_attribute['name'], current_attribute['value'],
+ self.helper.get_comments_from_attribute(current_attribute))
+
+ def _add_enum_value(self, name, value, comments):
+ self.enum_values[self.helper.create_enum_name(name)] = [value, comments]
diff --git a/catbuffer-generators/generators/common/MakoStaticClassGenerator.py b/catbuffer-generators/generators/common/MakoStaticClassGenerator.py
new file mode 100644
index 00000000..efabe7fa
--- /dev/null
+++ b/catbuffer-generators/generators/common/MakoStaticClassGenerator.py
@@ -0,0 +1,44 @@
+from inspect import getframeinfo, currentframe
+from os.path import dirname, abspath, realpath, join
+
+from mako.template import Template
+
+
+class MakoStaticClassGenerator:
+ """
+ Generic Mako generator.
+ Note that the mako context has 2 main objects.
+ - "genertor" with this object keeping all the known state
+ - "helper" with the language helper methods.
+ """
+
+ def __init__(self, template_file_name, generated_file_name, helper, schema, class_schema):
+ self.template_file_name = template_file_name
+ self.generated_file_name = generated_file_name
+ self.class_output = []
+ self.schema = schema
+ self.class_schema = class_schema
+ self.helper = helper
+
+ def _get_full_file_name(self):
+ filename = getframeinfo(currentframe()).filename
+ path = dirname(realpath(abspath(filename)))
+ return join(path, self.template_file_name)
+
+ def _read_file(self):
+ full_file_name = self._get_full_file_name()
+ fileTemplate = Template(filename=full_file_name)
+ self.class_output += [fileTemplate.render(generator=self, helper=self.helper)]
+
+ def generate(self):
+ self._read_file()
+ return self.class_output
+
+ def log_context(self):
+ description = ''
+ for key in filter(lambda a: not a.startswith('_'), dir(self)):
+ description = description + key + ' = \'' + str(getattr(self, key)) + '\'\n'
+ return description
+
+ def get_generated_file_name(self):
+ return self.generated_file_name
diff --git a/catbuffer-generators/generators/common/MakoTypeGenerator.py b/catbuffer-generators/generators/common/MakoTypeGenerator.py
new file mode 100644
index 00000000..34b6cb07
--- /dev/null
+++ b/catbuffer-generators/generators/common/MakoTypeGenerator.py
@@ -0,0 +1,22 @@
+from generators.common.Helper import AttributeKind
+from .MakoStaticClassGenerator import MakoStaticClassGenerator
+
+
+class MakoTypeGenerator(MakoStaticClassGenerator):
+ """
+ Generic Mako generator for atomic type schemas.
+ """
+
+ def __init__(self, helper, name: str, schema, class_schema, template_path: str, file_extension: str):
+ super().__init__(template_path + 'Type.mako',
+ helper.get_generated_class_name(name, class_schema, schema) + file_extension, helper, schema,
+ class_schema)
+ class_schema['name'] = name[0].lower() + name[1:]
+ self.name = name
+ self.attribute_name = self.class_schema['name']
+ self.size = self.class_schema['size']
+ self.generated_class_name = helper.get_generated_class_name(name, class_schema, schema)
+ self.attribute_kind = helper.get_attribute_kind(self.class_schema, None)
+ self.attribute_type = helper.get_generated_type(self.schema, self.class_schema, self.attribute_kind)
+ self.comments = helper.get_comments_from_attribute(self.class_schema)
+ self.AttributeKind = AttributeKind
diff --git a/catbuffer-generators/generators/cpp_builder/BuilderGenerator.py b/catbuffer-generators/generators/cpp_builder/BuilderGenerator.py
new file mode 100644
index 00000000..3cc1362e
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/BuilderGenerator.py
@@ -0,0 +1,43 @@
+# pylint: disable=too-few-public-methods
+from generators.Descriptor import Descriptor
+from .HeaderGenerator import HeaderGenerator
+from .ImplementationGenerator import ImplementationGenerator
+
+
+class BuilderGenerator:
+ """Cpp transaction builder generator, creates both header and implementation file"""
+ def __init__(self, schema, options):
+ self.schema = schema
+ self.options = options
+ self.current = None
+ self.generated_header = False
+ self.current_name = None
+
+ def __iter__(self):
+ """Creates an iterator around this generator"""
+ self.current = iter(self.schema)
+ self.generated_header = False
+ return self
+
+ def _iterate_until_next_transaction(self):
+ if self.generated_header:
+ return None
+
+ name = next(self.current)
+ while name == 'Transaction' or name.startswith('Embedded') or not name.endswith('Transaction'):
+ name = next(self.current)
+ return name
+
+ def __next__(self):
+ """Returns Descriptor with desired filename and generated file content"""
+ if not self.generated_header:
+ self.current_name = self._iterate_until_next_transaction()
+ generator = HeaderGenerator(self.schema, self.options, self.current_name)
+ code = generator.generate()
+ self.generated_header = True
+ return Descriptor('{}.h'.format(generator.builder_name()), code)
+
+ generator = ImplementationGenerator(self.schema, self.options, self.current_name)
+ code = generator.generate()
+ self.generated_header = False
+ return Descriptor('{}.cpp'.format(generator.builder_name()), code)
diff --git a/catbuffer-generators/generators/cpp_builder/CppGenerator.py b/catbuffer-generators/generators/cpp_builder/CppGenerator.py
new file mode 100644
index 00000000..ebadd866
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/CppGenerator.py
@@ -0,0 +1,310 @@
+# pylint: disable=too-few-public-methods
+from abc import ABC, abstractmethod
+from enum import Enum
+import os
+import re
+import yaml
+
+SUFFIX = 'Transaction'
+
+
+class FieldKind(Enum):
+ SIMPLE = 1
+ BUFFER = 2
+ VECTOR = 3
+ UNKNOWN = 100
+
+
+def tokenize(string):
+ return re.findall('[A-Z][^A-Z]*', string)
+
+
+def join_lower(strings):
+ return ' '.join([string.lower() for string in strings])
+
+
+def uncapitalize(string):
+ return string[0].lower() + string[1:] if string else string
+
+
+# note that string.capitalize also lowers [1:]
+def capitalize(string):
+ return string[0].upper() + string[1:] if string else string
+
+
+def singularize(string):
+ if string.endswith('ies'):
+ return string[:-3] + 'y'
+
+ if string.endswith('es'):
+ return string[:-2]
+
+ if string.endswith('s'):
+ return string[:-1]
+
+ return string
+
+
+class GeneratorInterface(ABC):
+ @abstractmethod
+ def _add_includes(self):
+ raise NotImplementedError('need to override method')
+
+ @abstractmethod
+ def _class_header(self):
+ raise NotImplementedError('need to override method')
+
+ @abstractmethod
+ def _generate_setter(self, field_kind, field, full_setter_name, param_name):
+ raise NotImplementedError('need to override method')
+
+ @abstractmethod
+ def _generate_field(self, field_kind, field, builder_field_typename):
+ raise NotImplementedError('need to override method')
+
+ @abstractmethod
+ def _builds(self):
+ raise NotImplementedError('need to override method')
+
+ @abstractmethod
+ def _class_footer(self):
+ raise NotImplementedError('need to override method')
+
+
+# FP from pylint, this is semi-abstract class
+# pylint: disable=abstract-method
+class CppGenerator(GeneratorInterface):
+ def __init__(self, schema, options, name):
+ super(CppGenerator, self).__init__()
+ self.schema = schema
+ self.code = []
+ self.transaction_name = name
+ self.replacements = {
+ 'TRANSACTION_NAME': self.transaction_name,
+ 'BUILDER_NAME': self.builder_name(),
+ 'COMMENT_NAME': self.written_name(),
+ 'COMMENT_NAME_A_OR_AN': 'an' if self.written_name().startswith(('a', 'e', 'i', 'o', 'u')) else 'a'
+ }
+
+ self.indent = 0
+ self.hints = CppGenerator._load_hints(['includes', 'namespaces', 'plugin', 'rewrites', 'setters'])[self.transaction_name]
+ self.prepend_copyright(options['copyright'])
+
+ @staticmethod
+ def _load_hints(filenames):
+ all_hints = {}
+ for filename in filenames:
+ with open('generators/cpp_builder/hints/{0}.yaml'.format(filename)) as input_file:
+ hints = yaml.load(input_file, Loader=yaml.SafeLoader)
+ for hint_key in hints:
+ if hint_key not in all_hints:
+ all_hints[hint_key] = {}
+
+ all_hints[hint_key][filename] = hints.get(hint_key)
+
+ return all_hints
+
+ def transaction_body_name(self):
+ return '{}Body'.format(self.transaction_name)
+
+ def builder_name(self):
+ return '{}Builder'.format(self.transaction_name[:-len(SUFFIX)])
+
+ def written_name(self):
+ return join_lower(tokenize(self.transaction_name[:-len(SUFFIX)]))
+
+ def prepend_copyright(self, copyright_file):
+ if os.path.isfile(copyright_file):
+ with open(copyright_file) as header:
+ self.code = [line.strip() for line in header]
+
+ def generate(self):
+ self._add_includes()
+ self._namespace_start()
+ self.indent = 1
+ self._class_header()
+ self._setters()
+ self._builds()
+ self._privates()
+ self._class_footer()
+ self.indent = 0
+ self._namespace_end()
+
+ return self.code
+
+ # region helpers
+
+ def _get_namespace(self, typename):
+ namespace = self.hints['namespaces'].get(typename, '') if 'namespaces' in self.hints else ''
+ if namespace:
+ namespace += '::'
+
+ return namespace
+
+ def append(self, multiline_string, additional_replacements=None):
+ for line in re.split(r'\n', multiline_string):
+ # indent non-empty lines
+ if line:
+ replacements = {**self.replacements, **additional_replacements} if additional_replacements else self.replacements
+ self.code.append('\t' * self.indent + line.format(**replacements))
+ else:
+ self.code.append('')
+
+ def qualified_type(self, typename):
+ namespace = self._get_namespace(typename)
+ return namespace + typename
+
+ @staticmethod
+ def _is_builtin_type(typename, size):
+ # uint8_t up to uint64_t are passed as 'byte' with size set to proper value
+ return 'byte' == typename and size <= 8
+
+ @staticmethod
+ def _builtin_type(size, signedness):
+ builtin_types = {1: 'int8_t', 2: 'int16_t', 4: 'int32_t', 8: 'int64_t'}
+ builtin_type = builtin_types[size]
+ return builtin_type if signedness == 'signed' else 'u' + builtin_type
+
+ def param_type(self, typename, size, signedness):
+ if not isinstance(size, str) and size > 0 and self._is_builtin_type(typename, size):
+ return self._builtin_type(size, signedness)
+
+ # if type is simple pass by value, otherwise pass by reference
+ type_descriptor = self.schema[typename]
+ qualified_typename = self.qualified_type(typename)
+
+ if 'byte' == type_descriptor['type'] and type_descriptor['size'] <= 8:
+ return qualified_typename
+
+ if 'enum' == type_descriptor['type']:
+ return qualified_typename
+
+ return 'const {}&'.format(qualified_typename)
+
+ def _get_schema_field(self, field_name):
+ return next(field for field in self.schema[self.transaction_body_name()]['layout'] if field['name'] == field_name)
+
+ @staticmethod
+ def method_name(prefix, param_name):
+ return '{PREFIX}{CAPITALIZED_PARAM_NAME}'.format(PREFIX=prefix, CAPITALIZED_PARAM_NAME=capitalize(param_name))
+
+ @staticmethod
+ def full_method_name(prefix, typename, param_name):
+ method_name = CppGenerator.method_name(prefix, param_name)
+ return '{METHOD_NAME}({TYPE_NAME} {PARAM_NAME})'.format(METHOD_NAME=method_name, TYPE_NAME=typename, PARAM_NAME=param_name)
+
+ # endregion
+
+ # region generate sub-methods
+
+ def _namespace_start(self):
+ self.append('namespace catapult {{ namespace builders {{')
+ self.append('')
+
+ def _setters(self):
+ self._foreach_builder_field(self._generate_setter_proxy)
+
+ def _privates(self):
+ self._foreach_builder_field(self._generate_field_proxy)
+
+ def _namespace_end(self):
+ self.append('}}}}')
+
+ # endregion
+
+ # region internals
+
+ def _foreach_builder_field(self, callback):
+ for field in self.schema[self.transaction_body_name()]['layout']:
+ # for builder fields, skip Size or count fields, they are always used for variable data
+ name = field['name']
+ if name.endswith('Size') or name.endswith('Count') or '_Reserved' in name:
+ continue
+
+ callback(field)
+
+ def _get_simple_setter_name_desc(self, field):
+ """sample: void setRemoteAccountKey(const Key& remoteAccountKey)"""
+ param_type = self.param_type(field['type'], field.get('size', 0), field.get('signedness', ''))
+ param_name = field['name']
+ return 'set', param_type, param_name
+
+ @staticmethod
+ def _get_buffer_setter_name_desc(field):
+ """sample: void setMessage(const RawBuffer& message)"""
+ assert 'byte' == field['type']
+ param_type = 'const RawBuffer&'
+ param_name = field['name']
+ return 'set', param_type, param_name
+
+ def _get_vector_setter_name_desc(self, field):
+ """sample: void addMosaic(const Mosaic& mosaic)"""
+ param_type = self.param_type(field['type'], field.get('size', 0), field.get('signedness', ''))
+ param_name = singularize(field['name'])
+ return 'add', param_type, param_name
+
+ def _get_setter_name_desc(self, field_kind, field):
+ getters = {
+ FieldKind.SIMPLE: self._get_simple_setter_name_desc,
+ FieldKind.BUFFER: self._get_buffer_setter_name_desc,
+ FieldKind.VECTOR: self._get_vector_setter_name_desc
+ }
+ return getters[field_kind](field)
+
+ @staticmethod
+ def _get_field_kind(field):
+ if 'size' not in field:
+ return FieldKind.SIMPLE
+
+ # if raw uint type treat as SIMPLE (uint8_t - uint64_t)
+ if not isinstance(field['size'], str) and 'byte' == field['type'] and field['size'] <= 8:
+ return FieldKind.SIMPLE
+
+ if field['size'].endswith('Size'):
+ return FieldKind.BUFFER
+
+ if field['size'].endswith('Count'):
+ return FieldKind.VECTOR
+
+ return FieldKind.UNKNOWN
+
+ def _contains_any_field_kind(self, field_kind):
+ for field in self.schema[self.transaction_body_name()]['layout']:
+ if field_kind == CppGenerator._get_field_kind(field):
+ return True
+
+ return False
+
+ def _contains_any_other_field_kind(self, field_kind):
+ for field in self.schema[self.transaction_body_name()]['layout']:
+ if field_kind != CppGenerator._get_field_kind(field):
+ return True
+
+ return False
+
+ def _generate_setter_proxy(self, field):
+ suppress_setter = self.hints['setters'].get(field['name'], '') if 'setters' in self.hints else ''
+ if suppress_setter:
+ return
+
+ field_kind = CppGenerator._get_field_kind(field)
+ prefix, param_type, param_name = self._get_setter_name_desc(field_kind, field)
+ full_setter_name = CppGenerator.full_method_name(prefix, param_type, param_name)
+ self._generate_setter(field_kind, field, full_setter_name, param_name)
+
+ def _generate_field_proxy(self, field):
+ field_kind = CppGenerator._get_field_kind(field)
+ field_type = field['type']
+ if 'size' in field and not isinstance(field['size'], str) and self._is_builtin_type(field['type'], field['size']):
+ field_type = self._builtin_type(field['size'], field['signedness'])
+
+ qualified_typename = self.qualified_type(field_type)
+ types = {
+ FieldKind.SIMPLE: '{TYPE}',
+ FieldKind.BUFFER: 'std::vector',
+ FieldKind.VECTOR: 'std::vector<{TYPE}>'
+ }
+ builder_field_typename = types[field_kind].format(TYPE=qualified_typename)
+ self._generate_field(field_kind, field, builder_field_typename)
+
+ # endregion
diff --git a/catbuffer-generators/generators/cpp_builder/HeaderGenerator.py b/catbuffer-generators/generators/cpp_builder/HeaderGenerator.py
new file mode 100644
index 00000000..d1ae468b
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/HeaderGenerator.py
@@ -0,0 +1,104 @@
+from .CppGenerator import CppGenerator, FieldKind, capitalize
+
+# note: part of formatting happens in CppGenerator, so whenever literal brace needs
+# to be produced, it needs to be doubled here
+
+
+class HeaderGenerator(CppGenerator):
+ def _add_includes(self):
+ self.append('''#pragma once
+#include "TransactionBuilder.h"
+#include "plugins/txes/{PLUGIN}/src/model/{{TRANSACTION_NAME}}.h"'''.format(PLUGIN=self.hints['plugin']))
+
+ if self._contains_any_field_kind(FieldKind.VECTOR):
+ self.append('#include ')
+
+ self.append('')
+
+ def _class_header(self):
+ self.append('/// Builder for {COMMENT_NAME_A_OR_AN} {COMMENT_NAME} transaction.')
+ self.append('class {BUILDER_NAME} : public TransactionBuilder {{')
+ self.append('public:')
+
+ self.indent += 1
+ self.append('using Transaction = model::{TRANSACTION_NAME};')
+ self.append('using EmbeddedTransaction = model::Embedded{TRANSACTION_NAME};')
+ self.append('')
+
+ self.indent -= 1
+ self.append('public:')
+
+ self.indent += 1
+ self.append('/// Creates {COMMENT_NAME_A_OR_AN} {COMMENT_NAME} builder for building'
+ + ' {COMMENT_NAME_A_OR_AN} {COMMENT_NAME} transaction from \\a signer')
+ self.append('/// for the network specified by \\a networkIdentifier.')
+ self.append('{BUILDER_NAME}(model::NetworkIdentifier networkIdentifier, const Key& signer);')
+ self.append('')
+
+ self.indent -= 1
+
+ @staticmethod
+ def _format_bound(field):
+ return ' and {} to `{}`'.format(field['condition'], field['condition_value'])
+
+ def _add_comment(self, field_kind, field, param_name):
+ comments = {
+ FieldKind.SIMPLE: 'Sets the {COMMENT} to \\a {NAME}{BOUND}.',
+ FieldKind.BUFFER: 'Sets the {COMMENT} to \\a {NAME}.',
+ FieldKind.VECTOR: 'Adds \\a {NAME} to {COMMENT}.'
+ }
+ bound_msg = ''
+ if 'condition' in field:
+ bound_msg = HeaderGenerator._format_bound(field)
+
+ comment_parts = field['comments'].split(' \\note ')
+ self.append('/// ' + comments[field_kind].format(COMMENT=comment_parts[0], NAME=param_name, BOUND=bound_msg))
+ for comment_note in comment_parts[1:]:
+ self.append('/// \\note {0}.'.format(capitalize(comment_note)))
+
+ def _generate_setter(self, field_kind, field, full_setter_name, param_name):
+ self._add_comment(field_kind, field, param_name)
+ self.append('void {};\n'.format(full_setter_name))
+
+ def _setters(self):
+ self.append('public:')
+ self.indent += 1
+ super(HeaderGenerator, self)._setters()
+ self.indent -= 1
+
+ def _builds(self):
+ self.append('public:')
+ self.indent += 1
+ self.append('''/// Gets the size of {COMMENT_NAME} transaction.
+/// \\note This returns size of a normal transaction not embedded transaction.
+size_t size() const;
+
+/// Builds a new {COMMENT_NAME} transaction.
+std::unique_ptr build() const;
+
+/// Builds a new embedded {COMMENT_NAME} transaction.
+std::unique_ptr buildEmbedded() const;
+''')
+ self.indent -= 1
+
+ self.append('private:')
+ self.indent += 1
+ self.append('''template
+size_t sizeImpl() const;
+
+template
+std::unique_ptr buildImpl() const;
+''')
+ self.indent -= 1
+
+ def _generate_field(self, field_kind, field, builder_field_typename):
+ self.append('{TYPE} m_{NAME};'.format(TYPE=builder_field_typename, NAME=field['name']))
+
+ def _privates(self):
+ self.append('private:')
+ self.indent += 1
+ super(HeaderGenerator, self)._privates()
+ self.indent -= 1
+
+ def _class_footer(self):
+ self.append('}};')
diff --git a/catbuffer-generators/generators/cpp_builder/ImplementationGenerator.py b/catbuffer-generators/generators/cpp_builder/ImplementationGenerator.py
new file mode 100644
index 00000000..c3208078
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/ImplementationGenerator.py
@@ -0,0 +1,196 @@
+from .CppGenerator import CppGenerator, FieldKind, capitalize
+
+SUFFIX = 'Transaction'
+
+
+class ImplementationGenerator(CppGenerator):
+ def _add_includes(self):
+ self.append('#include "{BUILDER_NAME}.h"')
+
+ if 'includes' in self.hints:
+ for include in self.hints['includes']:
+ self.append('#include "{0}"'.format(include))
+
+ self.append('')
+
+ def _class_header(self):
+ self.append('{BUILDER_NAME}::{BUILDER_NAME}(model::NetworkIdentifier networkIdentifier, const Key& signer)')
+ self.indent += 2
+ self.append(': TransactionBuilder(networkIdentifier, signer)')
+ self._foreach_builder_field(self._generate_field_initializer_list_entry)
+ self.indent -= 2
+ self.append('{{}}')
+ self.append('')
+
+ def _generate_call_to_setter_for_bound_field(self, condition_field_name, condition_value):
+ field = self._get_schema_field(condition_field_name)
+ field_kind = CppGenerator._get_field_kind(field)
+ _, param_type, param_name = self._get_setter_name_desc(field_kind, field)
+ return 'm_{NAME} = {TYPE_NAME}::{VALUE};'.format(NAME=param_name, TYPE_NAME=param_type, VALUE=condition_value)
+
+ def _generate_setter(self, field_kind, field, full_setter_name, param_name):
+ self.append('void {BUILDER_NAME}::' + full_setter_name + ' {{')
+ self.indent += 1
+ if field_kind == FieldKind.SIMPLE:
+ self.append('m_{NAME} = {NAME};'.format(NAME=param_name))
+ if 'condition' in field:
+ call_line = self._generate_call_to_setter_for_bound_field(field['condition'], capitalize(field['condition_value']))
+ self.append(call_line)
+ elif field_kind == FieldKind.BUFFER:
+ self.append('''if (0 == {NAME}.Size)
+\tCATAPULT_THROW_INVALID_ARGUMENT("argument `{NAME}` cannot be empty");
+
+if (!m_{NAME}.empty())
+\tCATAPULT_THROW_RUNTIME_ERROR("`{NAME}` field already set");
+
+m_{NAME}.resize({NAME}.Size);
+m_{NAME}.assign({NAME}.pData, {NAME}.pData + {NAME}.Size);'''.format(NAME=param_name))
+ else:
+ if 'sort_key' in field:
+ format_string = 'InsertSorted(m_{FIELD}, {PARAM}, [](const auto& lhs, const auto& rhs) {{{{'
+ self.append(format_string.format(FIELD=field['name'], PARAM=param_name))
+ self.indent += 1
+ self.append('return lhs.{SORT_KEY} < rhs.{SORT_KEY};'.format(SORT_KEY=capitalize(field['sort_key'])))
+ self.indent -= 1
+ self.append('}});')
+ else:
+ self.append('m_{FIELD}.push_back({PARAM});'.format(FIELD=field['name'], PARAM=param_name))
+ self.indent -= 1
+ self.append('}}\n')
+
+ def _generate_field(self, field_kind, field, builder_field_typename):
+ pass
+
+ def _generate_field_initializer_list_entry(self, field):
+ self.append(', m_{NAME}()'.format(NAME=field['name']))
+
+ def _generate_build_variable_fields_size(self, variable_sizes, field):
+ field_kind = CppGenerator._get_field_kind(field)
+ formatted_vector_size = 'm_{NAME}.size()'.format(NAME=field['name'])
+ if field_kind == FieldKind.BUFFER:
+ self.append('size += {};'.format(formatted_vector_size))
+ elif field_kind == FieldKind.VECTOR:
+ qualified_typename = self.qualified_type(field['type'])
+ formatted_size = '{ARRAY_SIZE} * sizeof({TYPE})'.format(ARRAY_SIZE=formatted_vector_size, TYPE=qualified_typename)
+ self.append('size += {};'.format(formatted_size))
+
+ if field_kind != FieldKind.SIMPLE:
+ variable_sizes[field['size']] = formatted_vector_size
+
+ def _generate_transaction_field_name(self, name):
+ field_name = capitalize(name)
+ rewritten = self.hints['rewrites'].get(field_name, '') if 'rewrites' in self.hints else ''
+ return rewritten or field_name
+
+ def _generate_build_variable_fields(self, field):
+ field_kind = CppGenerator._get_field_kind(field)
+ if field_kind == FieldKind.SIMPLE:
+ return
+
+ template = {'NAME': field['name'], 'TX_FIELD_NAME': self._generate_transaction_field_name(field['name'])}
+ if field_kind in (FieldKind.BUFFER, FieldKind.VECTOR):
+ self.append('std::copy(m_{NAME}.cbegin(), m_{NAME}.cend(), pTransaction->{TX_FIELD_NAME}Ptr());'.format(**template))
+
+ @staticmethod
+ def byte_size_to_type_name(size):
+ return {1: 'uint8_t', 2: 'uint16_t', 4: 'uint32_t', '8': 'uint64_t'}[size]
+
+ def _generate_condition(self, condition_field_name, condition_value):
+ field = self._get_schema_field(condition_field_name)
+ field_kind = CppGenerator._get_field_kind(field)
+ _, param_type, _ = self._get_setter_name_desc(field_kind, field)
+ return 'if ({TYPE_NAME}::{VALUE} == m_{NAME})'.format(TYPE_NAME=param_type, VALUE=capitalize(condition_value), NAME=field['name'])
+
+ def _generate_build(self, variable_sizes):
+ self.append('template')
+ self.append('std::unique_ptr {BUILDER_NAME}::buildImpl() const {{')
+ self.indent += 1
+
+ self.append('// 1. allocate, zero (header), set model::Transaction fields')
+ self.append('auto pTransaction = createTransaction(sizeImpl());')
+ self.append('')
+
+ self.append('// 2. set fixed transaction fields')
+
+ # set non-variadic fields
+ for field in self.schema[self.transaction_body_name()]['layout']:
+ template = {'NAME': field['name'], 'TX_FIELD_NAME': self._generate_transaction_field_name(field['name'])}
+ if field['name'].endswith('Size') or field['name'].endswith('Count'):
+ size = variable_sizes[field['name']]
+ size_type = ImplementationGenerator.byte_size_to_type_name(field['size'])
+ format_string = 'pTransaction->{TX_FIELD_NAME} = utils::checked_cast({SIZE});'
+ self.append(format_string.format(**template, SIZE_TYPE=size_type, SIZE=size))
+ else:
+ field_kind = CppGenerator._get_field_kind(field)
+ if field_kind == FieldKind.SIMPLE:
+ if 'condition' in field:
+ condition = self._generate_condition(field['condition'], field['condition_value'])
+ self.append(condition)
+ self.indent += 1
+
+ # if setter has been suppressed, fill in with what is defined in setters.yaml hint file
+ setter = self.hints['setters'].get(field['name'], '') if 'setters' in self.hints else ''
+ if setter:
+ self.append('pTransaction->{TX_FIELD_NAME} = {SETTER};'.format(**template, SETTER=setter))
+ elif '_Reserved' in field['name']:
+ self.append('pTransaction->{TX_FIELD_NAME} = 0;'.format(**template))
+ else:
+ self.append('pTransaction->{TX_FIELD_NAME} = m_{NAME};'.format(**template))
+
+ if 'condition' in field:
+ self.indent -= 1
+ self.append('')
+
+ # variadic fields are defined at the end of schema,
+ # so break if loop reached any of them
+ else:
+ break
+
+ self.append('')
+
+ if self._contains_any_other_field_kind(FieldKind.SIMPLE):
+ self.append('// 3. set transaction attachments')
+ self._foreach_builder_field(self._generate_build_variable_fields)
+
+ # variable fields that expand to conditional statement will append a blank line, so, if one is present, don't add another
+ if '' != self.code[-1]:
+ self.append('')
+
+ self.append('return pTransaction;')
+ self.indent -= 1
+ self.append('}}')
+
+ def _generate_size(self):
+ self.append('template')
+ self.append('size_t {BUILDER_NAME}::sizeImpl() const {{')
+ self.indent += 1
+ self.append('// calculate transaction size')
+ self.append('auto size = sizeof(TransactionType);')
+
+ # go through variable data and add it to size, collect sizes
+ variable_sizes = {}
+ self._foreach_builder_field(lambda field: self._generate_build_variable_fields_size(variable_sizes, field))
+
+ self.append('return size;')
+ self.indent -= 1
+ self.append('}}\n')
+ return variable_sizes
+
+ def _builds(self):
+ self.append('''size_t {BUILDER_NAME}::size() const {{
+\treturn sizeImpl();
+}}
+
+std::unique_ptr<{BUILDER_NAME}::Transaction> {BUILDER_NAME}::build() const {{
+\treturn buildImpl();
+}}
+
+std::unique_ptr<{BUILDER_NAME}::EmbeddedTransaction> {BUILDER_NAME}::buildEmbedded() const {{
+\treturn buildImpl();
+}}
+''')
+ variable_sizes = self._generate_size()
+ self._generate_build(variable_sizes)
+
+ def _class_footer(self):
+ pass
diff --git a/catbuffer-generators/generators/cpp_builder/hints/includes.yaml b/catbuffer-generators/generators/cpp_builder/hints/includes.yaml
new file mode 100644
index 00000000..37dc4f60
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/hints/includes.yaml
@@ -0,0 +1,7 @@
+# custom setters may require additional includes
+
+MosaicDefinitionTransaction:
+ - plugins/txes/mosaic/src/model/MosaicIdGenerator.h
+
+NamespaceRegistrationTransaction:
+ - plugins/txes/namespace/src/model/NamespaceIdGenerator.h
diff --git a/catbuffer-generators/generators/cpp_builder/hints/namespaces.yaml b/catbuffer-generators/generators/cpp_builder/hints/namespaces.yaml
new file mode 100644
index 00000000..92d071bf
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/hints/namespaces.yaml
@@ -0,0 +1,56 @@
+# some transactions use types that need to be namespace-qualified in c++ builders
+
+VotingKeyLinkTransaction:
+ LinkAction: model
+
+VrfKeyLinkTransaction:
+ LinkAction: model
+
+AddressAliasTransaction:
+ AliasAction: model
+
+MosaicAliasTransaction:
+ AliasAction: model
+
+AccountKeyLinkTransaction:
+ LinkAction: model
+
+NodeKeyLinkTransaction:
+ LinkAction: model
+
+HashLockTransaction:
+ UnresolvedMosaic: model
+
+MosaicDefinitionTransaction:
+ MosaicFlags: model
+ MosaicProperty: model
+
+MosaicSupplyChangeTransaction:
+ MosaicSupplyChangeAction: model
+
+SecretLockTransaction:
+ LockHashAlgorithm: model
+ UnresolvedMosaic: model
+
+SecretProofTransaction:
+ LockHashAlgorithm: model
+
+TransferTransaction:
+ Mosaic: model
+ UnresolvedMosaic: model
+
+NamespaceRegistrationTransaction:
+ NamespaceRegistrationType: model
+
+AccountAddressRestrictionTransaction:
+ AccountRestrictionFlags: model
+
+AccountMosaicRestrictionTransaction:
+ AccountRestrictionFlags: model
+
+AccountOperationRestrictionTransaction:
+ AccountRestrictionFlags: model
+ EntityType: model
+
+MosaicGlobalRestrictionTransaction:
+ MosaicRestrictionType: model
diff --git a/catbuffer-generators/generators/cpp_builder/hints/plugin.yaml b/catbuffer-generators/generators/cpp_builder/hints/plugin.yaml
new file mode 100644
index 00000000..84a41a1d
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/hints/plugin.yaml
@@ -0,0 +1,37 @@
+# transaction plugin name needs to be known in order to produce correct include paths in c++ builders
+
+VotingKeyLinkTransaction: ../coresystem
+VrfKeyLinkTransaction: ../coresystem
+
+AccountKeyLinkTransaction: account_link
+NodeKeyLinkTransaction: account_link
+
+AggregateCompleteTransaction: aggregate
+AggregateBondedTransaction: aggregate
+
+HashLockTransaction: lock_hash
+
+SecretLockTransaction: lock_secret
+SecretProofTransaction: lock_secret
+
+AccountMetadataTransaction: metadata
+MosaicMetadataTransaction: metadata
+NamespaceMetadataTransaction: metadata
+
+MosaicDefinitionTransaction: mosaic
+MosaicSupplyChangeTransaction: mosaic
+
+MultisigAccountModificationTransaction: multisig
+
+AddressAliasTransaction: namespace
+MosaicAliasTransaction: namespace
+NamespaceRegistrationTransaction: namespace
+
+AccountAddressRestrictionTransaction: restriction_account
+AccountMosaicRestrictionTransaction: restriction_account
+AccountOperationRestrictionTransaction: restriction_account
+
+MosaicAddressRestrictionTransaction: restriction_mosaic
+MosaicGlobalRestrictionTransaction: restriction_mosaic
+
+TransferTransaction: transfer
diff --git a/catbuffer-generators/generators/cpp_builder/hints/rewrites.yaml b/catbuffer-generators/generators/cpp_builder/hints/rewrites.yaml
new file mode 100644
index 00000000..ec44290e
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/hints/rewrites.yaml
@@ -0,0 +1,3 @@
+# rewrite transaction field access
+
+placeholder:
diff --git a/catbuffer-generators/generators/cpp_builder/hints/setters.yaml b/catbuffer-generators/generators/cpp_builder/hints/setters.yaml
new file mode 100644
index 00000000..047b54bc
--- /dev/null
+++ b/catbuffer-generators/generators/cpp_builder/hints/setters.yaml
@@ -0,0 +1,13 @@
+# 1. suppress generation of setters for field listed in cats file
+# 2. when setting the field in builder replace with specified formula
+# note, currently only fields with kind SIMPLE are supported
+
+MosaicDefinitionTransaction:
+ id: model::GenerateMosaicId(model::GetSignerAddress(*pTransaction), m_nonce)
+
+NamespaceRegistrationTransaction:
+ # disable setter for discriminator
+ registrationType: m_registrationType
+
+ # need to use quoted string with escape characters to break setter onto multiple lines to avoid line length warning
+ id: "model::GenerateNamespaceId(m_parentId, {{ reinterpret_cast(m_name.data()), m_name.size() }})"
diff --git a/catbuffer-generators/generators/java/JavaFileGenerator.py b/catbuffer-generators/generators/java/JavaFileGenerator.py
new file mode 100644
index 00000000..6f66cfea
--- /dev/null
+++ b/catbuffer-generators/generators/java/JavaFileGenerator.py
@@ -0,0 +1,25 @@
+from generators.common.FileGenerator import FileGenerator
+from .JavaHelper import JavaHelper
+
+
+class JavaFileGenerator(FileGenerator):
+ """Java file generator"""
+
+ def init_code(self):
+ code = super().init_code()
+ code += ['package io.nem.symbol.catapult.builders;'] + ['']
+ return code
+
+ def get_template_path(self):
+ return '../java/templates/'
+
+ def get_static_templates_file_names(self):
+ return ['BitMaskable', 'GeneratorUtils', 'AggregateTransactionBodyBuilder', 'TransactionBuilderHelper',
+ 'EmbeddedTransactionBuilderHelper',
+ 'Serializer']
+
+ def get_main_file_extension(self):
+ return '.java'
+
+ def create_helper(self):
+ return JavaHelper()
diff --git a/catbuffer-generators/generators/java/JavaHelper.py b/catbuffer-generators/generators/java/JavaHelper.py
new file mode 100644
index 00000000..be364ad6
--- /dev/null
+++ b/catbuffer-generators/generators/java/JavaHelper.py
@@ -0,0 +1,74 @@
+from generators.common.Helper import Helper, AttributeKind
+
+
+class JavaHelper(Helper):
+
+ def get_body_class_name(self, name):
+ body_name = name if not name.startswith('Embedded') else name[8:]
+ if name.startswith('Aggregate') and any(name.endswith(postfix) for postfix in ('Transaction', 'TransactionV1')):
+ body_name = 'AggregateTransaction'
+
+ return '{0}Body'.format(body_name)
+
+ def get_builtin_type(self, size):
+ builtin_types = {1: 'byte', 2: 'short', 4: 'int', 8: 'long'}
+ builtin_type = builtin_types[size]
+ return builtin_type
+
+ def get_read_method_name(self, size):
+ if isinstance(size, str) or size > 8:
+ method_name = 'readFully'
+ else:
+ type_size_method_name = {1: 'readByte', 2: 'readShort', 4: 'readInt', 8: 'readLong'}
+ method_name = type_size_method_name[size]
+ return method_name
+
+ def get_load_from_binary_factory(self, attribute_class_name):
+ if attribute_class_name == 'EmbeddedTransactionBuilder':
+ return 'TransactionBuilderFactory'
+ return attribute_class_name
+
+ def get_condition_operation_text(self, op):
+ if op == 'has':
+ return '{0}.contains({1})'
+ return '{0} == {1}'
+
+ def get_reverse_method_name(self, size):
+ if isinstance(size, str) or size > 8 or size == 1:
+ method_name = '{0}'
+ else:
+ typesize_methodname = {2: 'Short.reverseBytes({0})',
+ 4: 'Integer.reverseBytes({0})',
+ 8: 'Long.reverseBytes({0})'}
+ method_name = typesize_methodname[size]
+ return method_name
+
+ def get_to_unsigned_method_name(self, size):
+ unsigned_methodname = {1: 'GeneratorUtils.toUnsignedInt({0})',
+ 2: 'GeneratorUtils.toUnsignedInt({0})'}
+ return unsigned_methodname[size]
+
+ def get_write_method_name(self, size):
+ if isinstance(size, str) or size > 8 or size == 0:
+ method_name = 'write'
+ else:
+ typesize_methodname = {1: 'writeByte',
+ 2: 'writeShort',
+ 4: 'writeInt',
+ 8: 'writeLong'}
+ method_name = typesize_methodname[size]
+ return method_name
+
+ def get_generated_type(self, schema, attribute, attribute_kind):
+ typename = attribute['type']
+ if attribute_kind in (AttributeKind.SIMPLE, AttributeKind.SIZE_FIELD):
+ return self.get_builtin_type(self.get_attribute_size(schema, attribute))
+ if attribute_kind == AttributeKind.BUFFER:
+ return 'ByteBuffer'
+ if not self.is_byte_type(typename):
+ typename = self.get_generated_class_name(typename, attribute, schema)
+ if self.is_any_array_kind(attribute_kind):
+ return 'List<{0}>'.format(typename)
+ if attribute_kind == AttributeKind.FLAGS:
+ return 'EnumSet<{0}>'.format(typename)
+ return typename
diff --git a/catbuffer-generators/generators/java/VectorTest.java b/catbuffer-generators/generators/java/VectorTest.java
new file mode 100644
index 00000000..41498117
--- /dev/null
+++ b/catbuffer-generators/generators/java/VectorTest.java
@@ -0,0 +1,110 @@
+package io.nem.symbol.catapult.builders;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.platform.commons.util.ExceptionUtils;
+import org.yaml.snakeyaml.Yaml;
+
+
+public class VectorTest {
+
+ public static final String TEST_RESOURCES_VECTOR = "src/test/resources/vector";
+
+ public static class BuilderTestItem {
+
+ public final String filename;
+
+ public final String builder;
+
+ public final String payload;
+
+ public final String comment;
+
+ public BuilderTestItem(String filename, String builder, String payload, String comment) {
+ this.filename = filename;
+ this.builder = builder;
+ this.payload = payload;
+ this.comment = comment;
+ }
+
+ @Override
+ public String toString() {
+ String commentSuffix = comment == null ? hash(payload) : comment;
+ return filename + " - " + builder + " - " + commentSuffix;
+ }
+
+ public static String hash(String stringToHash) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(stringToHash.getBytes());
+ return GeneratorUtils.toHex(messageDigest.digest());
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ }
+
+ private static List vectors() throws Exception {
+ List walk = Files.walk(Paths.get(TEST_RESOURCES_VECTOR)).collect(Collectors.toList());
+ try (Stream paths = walk.stream()) {
+ return paths
+ .filter(Files::isRegularFile).map(Path::toFile)
+ .flatMap(VectorTest::getVectorFromFile).collect(Collectors.toList());
+ }
+ }
+
+ private static Stream getVectorFromFile(File file) {
+ try {
+ InputStream input = new FileInputStream(file);
+ Yaml yaml = new Yaml();
+ List