Skip to content

Commit

Permalink
[JsonGen] Support for case convention switching
Browse files Browse the repository at this point in the history
  • Loading branch information
sebaszm committed Oct 22, 2024
1 parent 86b117c commit 1cc4cee
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 53 deletions.
42 changes: 34 additions & 8 deletions JsonGenerator/source/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ class RpcFormat(Enum):
EXTENDED = "uncompliant-extended"
COLLAPSED = "uncompliant-collapsed"

class CaseConvention(Enum):
STANDARD = "standard"
LEGACY = "legacy"
KEEP = "keep"
CUSTOM = "custom"

DEFAULT_CASE_CONVENTION = CaseConvention.STANDARD

RPC_FORMAT = RpcFormat.COMPLIANT
RPC_FORMAT_FORCED = False

Expand Down Expand Up @@ -90,6 +98,7 @@ def Parse(cmdline):
global CLASSNAME_FROM_REF
global LEGACY_ALT
global AUTO_PREFIX
global DEFAULT_CASE_CONVENTION

argparser = argparse.ArgumentParser(
description='Generate JSON C++ classes, stub code and API documentation from JSON definition files and C++ header files',
Expand Down Expand Up @@ -195,9 +204,16 @@ def Parse(cmdline):
dest="format",
type=str,
action="store",
default="flexible",
default="default-compliant",
choices=["default-compliant", "force-compliant", "default-uncompliant-extended", "force-uncompliant-extended", "default-uncompliant-collapsed", "force-uncompliant-collapsed"],
help="select JSON-RPC data format (default: default-compliant)")
cpp_group.add_argument("--case-convention",
dest="case_convention",
type=str,
metavar="CONVENTION",
action="store",
default=DEFAULT_CASE_CONVENTION.value,
help="select JSON-RPC case convention (default: %s)" % DEFAULT_CASE_CONVENTION.value)

data_group = argparser.add_argument_group("C++ output arguments (optional)")
data_group.add_argument(
Expand Down Expand Up @@ -264,6 +280,13 @@ def Parse(cmdline):
action="store",
default=INDENT_SIZE,
help="code indentation in spaces (default: %i)" % INDENT_SIZE)
data_group.add_argument("--framework-namespace",
dest="framework_namespace",
metavar="NS",
type=str,
action="store",
default=FRAMEWORK_NAMESPACE,
help="set framework namespace")

doc_group = argparser.add_argument_group("Documentation output arguments (optional)")
doc_group.add_argument("--no-style-warnings",
Expand Down Expand Up @@ -292,13 +315,6 @@ def Parse(cmdline):
help="override interface source file revision to the commit id specified")

ts_group = argparser.add_argument_group("Troubleshooting arguments (optional)")
doc_group.add_argument("--framework-namespace",
dest="framework_namespace",
metavar="NS",
type=str,
action="store",
default=FRAMEWORK_NAMESPACE,
help="set framework namespace")
ts_group.add_argument("--verbose",
dest="verbose",
action="store_true",
Expand Down Expand Up @@ -334,6 +350,16 @@ def Parse(cmdline):
INTERFACE_SOURCE_REVISION = args.source_revision
AUTO_PREFIX = args.auto_prefix

if args.case_convention == "standard":
DEFAULT_CASE_CONVENTION = CaseConvention.STANDARD
elif args.case_convention == "legacy" or args.case_convention == "legacy_lowercase":
DEFAULT_CASE_CONVENTION = CaseConvention.LEGACY
elif args.case_convention == "keep":
DEFAULT_CASE_CONVENTION = CaseConvention.KEEP
else:
print("Invalid case convention")
exit(1)

if args.framework_namespace:
FRAMEWORK_NAMESPACE = args.framework_namespace
INTERFACE_NAMESPACES = ["::%s::Exchange::JSONRPC" % FRAMEWORK_NAMESPACE, "::%s::Exchange" % FRAMEWORK_NAMESPACE]
Expand Down
146 changes: 124 additions & 22 deletions JsonGenerator/source/header_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import json
import copy
import posixpath
import re
from collections import OrderedDict
from enum import Enum

import config

Expand All @@ -30,6 +32,110 @@
import ProxyStubGenerator.Interface as CppInterface


class CaseConverter:
METHODS = 0
EVENTS = 1
PARAMS = 2
MEMBERS = 3
ENUMS = 4

class Format(Enum):
LOWER = "lower" # lowercase
UPPER = "upper" # UPPERCASE
LOWERSNAKE = "lowersnake" # lower_snake_case
UPPERSNAKE = "uppersnake" # UPPER_SNAKE_CASE
CAMEL = "camel" # camelCase
PASCAL = "pascal" # PascalCase
KEEP = "keep" # ... keep as is

@staticmethod
def __to_pascal(string, uppercase=True):
_pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
_pascal = _pattern.sub('_', string.replace('__', '_').strip('_')).split('_')
_pascal = "".join([x.capitalize() for x in _pascal])
return (_pascal if uppercase else (_pascal[0].lower() + _pascal[1:]))

@staticmethod
def __to_snake(string, uppercase=True):
_pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
_snake = _pattern.sub('_', string.replace('__', '_').strip('_'))
return (_snake.upper() if uppercase else _snake.lower())

@staticmethod
def transform(string, format):
if format == CaseConverter.Format.LOWER:
return string.lower()
elif format == CaseConverter.Format.UPPER:
return string.upper()
elif format == CaseConverter.Format.CAMEL:
return CaseConverter.Format.__to_pascal(string, False)
elif format == CaseConverter.Format.PASCAL:
return CaseConverter.Format.__to_pascal(string, True)
elif format == CaseConverter.Format.LOWERSNAKE:
return CaseConverter.Format.__to_snake(string, False)
elif format == CaseConverter.Format.UPPERSNAKE:
return CaseConverter.Format.__to_snake(string, True)
elif format == CaseConverter.Format.KEEP:
return string
else:
assert False, "invalid case format"

def __init__(self, convention=None):
self._convention = None # error situation

self._map = {
# [ methods, events, parameters, struct members, enums ]
config.CaseConvention.STANDARD: [self.Format.CAMEL, self.Format.CAMEL, self.Format.CAMEL, self.Format.CAMEL, self.Format.UPPERSNAKE],
config.CaseConvention.LEGACY: [self.Format.LOWER, self.Format.LOWER, self.Format.LOWER, self.Format.LOWER, self.Format.PASCAL],
config.CaseConvention.KEEP: [self.Format.KEEP, self.Format.KEEP, self.Format.KEEP, self.Format.KEEP, self.Format.KEEP],
}

self._map[config.CaseConvention.CUSTOM] = self._map[config.DEFAULT_CASE_CONVENTION]

if convention:
if isinstance(convention, str):
if convention == "standard":
self._convention = config.CaseConvention.STANDARD
elif (convention == "legacy_lowercase") or (convention == "legacy"):
self._convention = config.CaseConvention.LEGACY
elif (convention == "keep"):
self._convention = config.CaseConvention.KEEP
elif convention.startswith("custom="):
_custom = convention[7:].split(',')
if len(_custom) == 5:
self._convention = config.CaseConvention.CUSTOM
if _custom[self.METHODS]:
self._map[config.CaseConvention.CUSTOM][self.METHODS] = self.Format[_custom[self.METHODS].upper()]
if _custom[self.EVENTS]:
self._map[config.CaseConvention.CUSTOM][self.EVENTS] = self.Format[_custom[self.EVENTS].upper()]
if _custom[self.PARAMS]:
self._map[config.CaseConvention.CUSTOM][self.PARAMS] = self.Format[_custom[self.PARAMS].upper()]
if _custom[self.MEMBERS]:
self._map[config.CaseConvention.CUSTOM][self.MEMBERS] = self.Format[_custom[self.MEMBERS].upper()]
if _custom[self.ENUMS]:
self._map[config.CaseConvention.CUSTOM][self.ENUMS] = self.Format[_custom[self.ENUMS].upper()]
else:
self._convention = convention
else:
self._convention = config.DEFAULT_CASE_CONVENTION

@property
def convention(self):
return self._convention

@property
def is_legacy(self):
return (self._convention == config.CaseConvention.LEGACY)

def transform(self, input, attr):
assert input
return self.Format.transform(input, self.__format(attr))

def __format(self, attr):
assert self.convention
return self._map[self.convention][attr]


class CppParseError(RuntimeError):
def __init__(self, obj, msg):
if obj:
Expand Down Expand Up @@ -87,20 +193,23 @@ def _EvaluateRpcFormat(obj):

verify = face.obj.is_json or face.obj.is_event

_case_format = face.obj.meta.text if face.obj.meta.text else "lower"
_case_converter = CaseConverter(face.obj.meta.text)

if not _case_converter.convention:
raise CppParseError(face.obj, "unknown interface-level @text parameter:%s" % face.obj.meta.text)
else:
log.Info("Case convention is %s" % _case_converter.convention.value)

def compute_name(obj, relay = None):
def compute_name(obj, arg, relay=None):
if not relay:
relay = obj

_default_name = relay.name if _case_format == "keep" else relay.name.lower()
_name = _case_converter.transform(relay.name, arg)

if obj.meta.text == _default_name:
log.WarnLine(method, "'%s': overriden name is same as default ('%s')" % (obj.meta.text, _default_name))
if obj.meta.text == _name:
log.WarnLine(obj, "'%s': overriden name is same as default ('%s')" % (obj.meta.text, _name))

_name = obj.meta.text if obj.meta.text else _default_name

return (_name)
return (obj.meta.text if obj.meta.text else _name)

schema["@interfaceonly"] = True
schema["configuration"] = { "nodefault" : True }
Expand Down Expand Up @@ -131,10 +240,6 @@ def compute_name(obj, relay = None):
info["title"] = info["class"] + " API"
info["description"] = info["class"] + " JSON-RPC interface"

if _case_format == "keep":
info["legacy"] = True # suppress case warnings
log.Info("@text:keep is used!")

schema["info"] = info

clash_msg = "JSON-RPC name clash detected"
Expand Down Expand Up @@ -276,10 +381,7 @@ def ConvertType(var, quiet=False, meta=None, no_array=False):
#if autos != 0 and (autos != len(cppType.items)):
# raise CppParseError(var, "enumerator values in an enum must all be explicit or all be implied")

if _case_format == "keep":
enum_spec = { "enum": [e.meta.text if e.meta.text else e.name for e in cppType.items] }
else:
enum_spec = { "enum": [e.meta.text if e.meta.text else e.name.replace("_"," ").title().replace(" ","") for e in cppType.items] }
enum_spec = { "enum": [compute_name(e, _case_converter.ENUMS) for e in cppType.items] }

if var_type.Type().scoped:
enum_spec["scoped"] = True
Expand Down Expand Up @@ -318,7 +420,7 @@ def GenerateObject(ctype, was_typdef):
required = []

for p in kind.vars:
name = compute_name(p)
name = compute_name(p, _case_converter.MEMBERS)

if isinstance(p.type, list):
raise CppParseError(p, "%s: undefined type" % " ".join(p.type))
Expand Down Expand Up @@ -491,7 +593,7 @@ def BuildParameters(obj, vars, rpc_format, is_property=False, test=False):
elif not var.meta.output:
log.WarnLine(var, "'%s': non-const parameter marked with @in tag (forgot 'const'?)" % var.name)

var_name = "value" if (is_property and _case_format == "lower") else compute_name(var)
var_name = "value" if (is_property and _case_converter.is_legacy) else compute_name(var, _case_converter.PARAMS)

if var_name.startswith("__unnamed") and not test:
raise CppParseError(var, "unnamed parameter, can't deduce parameter name (*1)")
Expand Down Expand Up @@ -562,7 +664,7 @@ def BuildResult(vars, is_property=False):
var_type = ResolveTypedef(var.type)

if var.meta.output:
var_name = "value" if (is_property and _case_format == "lower") else compute_name(var)
var_name = "value" if (is_property and _case_converter.is_legacy) else compute_name(var, _case_converter.PARAMS)

if var_name.startswith("__unnamed"):
raise CppParseError(var, "unnamed parameter, can't deduce parameter name (*2)")
Expand Down Expand Up @@ -647,7 +749,7 @@ def BuildResult(vars, is_property=False):
mm.retval.meta.alt = method.retval.meta.alt
break

method_name = compute_name(method.retval, method)
method_name = compute_name(method.retval, _case_converter.METHODS, relay=method)

if method.retval.meta.alt == method_name:
log.WarnLine(method, "'%s': alternative name is same as original name ('%s')" % (method.name, method.retval.meta.text))
Expand Down Expand Up @@ -711,7 +813,7 @@ def BuildResult(vars, is_property=False):
_index = obj["index"][_index_idx]

if _index:
_index["name"] = (method.vars[0].name if _case_format == "keep" else method.vars[0].name.capitalize())
_index["name"] = _case_converter.transform(method.vars[0].name, _case_converter.PARAMS)
_index["@originalname"] = method.vars[0].name

if "enum" in _index:
Expand Down Expand Up @@ -955,7 +1057,7 @@ def BuildResult(vars, is_property=False):
if params:
obj["params"] = params

method_name = compute_name(method.retval, method)
method_name = compute_name(method.retval, _case_converter.METHODS, relay=method)

if method.parent.is_event: # excludes .json inlcusion of C++ headers
for mm in events:
Expand Down
Loading

0 comments on commit 1cc4cee

Please sign in to comment.