blob: d354f9a1336b1e16881c545b0ac07bc950bc669e [file] [log] [blame]
# Copyright 2017-present Open Networking Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, print_function
import os
import jinja2
import plyxproto.parser as plyxproto
import yaml
from colorama import Fore
import sys
from . import jinja2_extensions
from .proto2xproto import Proto2XProto
from .xos2jinja import XOS2Jinja
from .validator import XProtoValidator
loader = jinja2.PackageLoader(__name__, "templates")
env = jinja2.Environment(loader=loader)
class XOSProcessorArgs:
""" Helper class for use cases that want to call XOSProcessor directly, rather than executing xosgenx from the
command line.
"""
default_rev = False
default_output = None
default_attic = None
default_kvpairs = None
default_write_to_file = None
default_dest_file = None
default_dest_extension = None
default_target = None
default_checkers = None
default_verbosity = (
0
) # Higher numbers = more verbosity, lower numbers = less verbosity
default_include_models = (
[]
) # If neither include_models nor include_apps is specified, then all models will
default_include_apps = [] # be included.
default_strict_validation = False
default_lint = False
def __init__(self, **kwargs):
# set defaults
self.rev = XOSProcessorArgs.default_rev
self.output = XOSProcessorArgs.default_output
self.attic = XOSProcessorArgs.default_attic
self.kvpairs = XOSProcessorArgs.default_kvpairs
self.verbosity = XOSProcessorArgs.default_verbosity
self.write_to_file = XOSProcessorArgs.default_write_to_file
self.default_dest_file = XOSProcessorArgs.default_dest_file
self.default_dest_extension = XOSProcessorArgs.default_dest_extension
self.default_target = XOSProcessorArgs.default_target
self.default_checkers = XOSProcessorArgs.default_target
self.include_models = XOSProcessorArgs.default_include_models
self.include_apps = XOSProcessorArgs.default_include_apps
self.strict_validation = XOSProcessorArgs.default_strict_validation
self.lint = XOSProcessorArgs.default_lint
# override defaults with kwargs
for (k, v) in kwargs.items():
setattr(self, k, v)
class XOSProcessor:
@staticmethod
def _read_input_from_files(files):
""" Read the files and return the combined text read.
Also returns a list of (line_number, filename) tuples that tell which
starting line corresponds to each file.
"""
line_map = []
input = ""
for fname in files:
with open(fname) as infile:
line_map.append((len(input.split("\n")), fname))
input += infile.read()
return (input, line_map)
@staticmethod
def _attach_parser(ast, args):
if hasattr(args, "rev") and args.rev:
v = Proto2XProto()
ast.accept(v)
v = XOS2Jinja(args)
ast.accept(v)
return v
@staticmethod
def _get_template(target):
if not os.path.isabs(target):
return os.path.abspath(
os.path.dirname(os.path.realpath(__file__)) + "/targets/" + target
)
return target
@staticmethod
def _file_exists(attic):
# NOTE this method can be used in the jinja template
def file_exists2(name):
if attic is not None:
path = attic + "/" + name
else:
path = name
return os.path.exists(path)
return file_exists2
@staticmethod
def _include_file(attic):
# NOTE this method can be used in the jinja template
def include_file2(name):
if attic is not None:
path = attic + "/" + name
else:
path = name
return open(path).read()
return include_file2
@staticmethod
def _load_jinja2_extensions(os_template_env, attic):
os_template_env.globals["include_file"] = XOSProcessor._include_file(
attic
) # Generates a function
os_template_env.globals["file_exists"] = XOSProcessor._file_exists(
attic
) # Generates a function
os_template_env.filters["yaml"] = yaml.dump
for f in dir(jinja2_extensions):
if f.startswith("xproto"):
os_template_env.globals[f] = getattr(jinja2_extensions, f)
return os_template_env
@staticmethod
def _add_context(args):
if not hasattr(args, "kv") or not args.kv:
return
try:
context = {}
for s in args.kv.split(","):
k, val = s.split(":")
context[k] = val
return context
except Exception as e:
print(e)
@staticmethod
def _write_single_file(rendered, dir, dest_file, quiet):
file_name = "%s/%s" % (dir, dest_file)
file = open(file_name, "w")
file.write(rendered)
file.close()
if not quiet:
print("Saved: %s" % file_name)
@staticmethod
def _write_split_target(rendered, dir, quiet):
lines = rendered.splitlines()
current_buffer = []
for line in lines:
if line.startswith("+++"):
if dir:
path = dir + "/" + line[4:].lower()
fil = open(path, "w")
buf = "\n".join(current_buffer)
obuf = buf
fil.write(obuf)
fil.close()
if not quiet:
print("Save file to: %s" % path)
current_buffer = []
else:
current_buffer.append(line)
@staticmethod
def _find_message_by_model_name(messages, model):
return next((x for x in messages if x["name"] == model), None)
@staticmethod
def _find_last_nonempty_line(text, pointer):
ne_pointer = pointer
found = False
while ne_pointer != 0 and not found:
ne_pointer = text[: (ne_pointer - 1)].rfind("\n")
if ne_pointer < 0:
ne_pointer = 0
if text[ne_pointer - 1] != "\n":
found = True
return ne_pointer
@staticmethod
def process(args, operator=None):
# Setting defaults
if not hasattr(args, "attic"):
args.attic = None
if not hasattr(args, "write_to_file"):
args.write_to_file = None
if not hasattr(args, "dest_file"):
args.dest_file = None
if not hasattr(args, "dest_extension"):
args.dest_extension = None
if not hasattr(args, "output"):
args.output = None
if not hasattr(args, "quiet"):
args.quiet = True
# Validating
if args.write_to_file == "single" and args.dest_file is None:
raise Exception(
"[XosGenX] write_to_file option is specified as 'single' but no dest_file is provided"
)
if args.write_to_file == "model" and (args.dest_extension is None):
raise Exception(
"[XosGenX] write_to_file option is specified as 'model' but no dest_extension is provided"
)
if args.output is not None and not os.path.isabs(args.output):
raise Exception("[XosGenX] The output dir (%s) must be an absolute path!" % args.output)
if args.output is not None and not os.path.isdir(args.output):
raise Exception("[XosGenX] The output dir (%s) must be a directory!" % args.output)
if hasattr(args, "files"):
(inputs, line_map) = XOSProcessor._read_input_from_files(args.files)
elif hasattr(args, "inputs"):
inputs = args.inputs
line_map = []
else:
raise Exception("[XosGenX] No inputs provided!")
context = XOSProcessor._add_context(args)
parser = plyxproto.ProtobufAnalyzer()
try:
ast = parser.parse_string(inputs, debug=0)
except plyxproto.ParsingError as e:
if e.message:
error = e.message
else:
error = "xproto parsing error"
if e.error_range is None:
# No line number information
print(error + "\n")
else:
line, start, end = e.error_range
ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
if start == 0:
beginning = ""
else:
beginning = inputs[ptr: start - 1]
line_end_char = inputs[start + end:].find("\n")
line_end = inputs[line_end_char]
print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE)
print(
beginning
+ Fore.YELLOW
+ inputs[start - 1: start + end]
+ Fore.WHITE
+ line_end
)
exit(1)
v = XOSProcessor._attach_parser(ast, args)
if args.include_models or args.include_apps:
for message in v.messages:
message["is_included"] = False
if message["name"] in args.include_models:
message["is_included"] = True
else:
app_label = (
message.get("options", {})
.get("app_label")
.strip('"')
)
if app_label in args.include_apps:
message["is_included"] = True
else:
for message in v.messages:
message["is_included"] = True
validator = XProtoValidator(v.models, line_map)
validator.validate()
if validator.errors:
if args.strict_validation or (args.verbosity >= 0):
validator.print_errors()
fatal_errors = [x for x in validator.errors if x["severity"] == "ERROR"]
if fatal_errors and args.strict_validation:
sys.exit(-1)
if args.lint:
return ""
if not operator:
operator = args.target
template_path = XOSProcessor._get_template(operator)
else:
template_path = operator
[template_folder, template_name] = os.path.split(template_path)
os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
os_template_env = jinja2.Environment(loader=os_template_loader)
os_template_env = XOSProcessor._load_jinja2_extensions(
os_template_env, args.attic
)
template = os_template_env.get_template(template_name)
if args.output is not None and args.write_to_file == "model":
# Handle the case where each model is written to a separate python file.
rendered = {}
for i, model in enumerate(v.models):
model_dict = v.models[model]
messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
rendered[model] = template.render(
{
"proto": {
"message_table": {model: model_dict},
"messages": messages,
"policies": v.policies,
"message_names": [m["name"] for m in v.messages],
},
"context": context,
"options": v.options,
}
)
if not rendered[model]:
print("Not saving model %s as it is empty" % model, file=sys.stderr)
else:
legacy = jinja2_extensions.base.xproto_list_evaluates_true(
[model_dict.get("options", {}).get("custom_python", None),
model_dict.get("options", {}).get("legacy", None),
v.options.get("custom_python", None),
v.options.get("legacy", None)])
if legacy:
file_name = "%s/%s_decl.%s" % (args.output, model.lower(), args.dest_extension)
else:
file_name = "%s/%s.%s" % (args.output, model.lower(), args.dest_extension)
file = open(file_name, "w")
file.write(rendered[model])
file.close()
if not args.quiet:
print("Saved: %s" % file_name, file=sys.stderr)
else:
# Handle the case where all models are written to the same python file.
rendered = template.render(
{
"proto": {
"message_table": v.models,
"messages": v.messages,
"policies": v.policies,
"message_names": [m["name"] for m in v.messages],
},
"context": context,
"options": v.options,
}
)
if args.output is not None and args.write_to_file == "target":
XOSProcessor._write_split_target(rendered, args.output, args.quiet)
elif args.output is not None and args.write_to_file == "single":
XOSProcessor._write_single_file(
rendered, args.output, args.dest_file, args.quiet
)
return rendered