blob: 9c02b716cbddde4585ff6f58c40e00af901e8942 [file] [log] [blame] [edit]
#!/usr/bin/env python3.8
# Copyright 2022 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import os
import re
import sys
import textwrap
from pathlib import Path
from string import Template
# Output target paths that may be modified.
FUCHSIA_DIR = Path(os.environ["FUCHSIA_DIR"])
FIDL_DOCS_ROOT_DIR = FUCHSIA_DIR / "docs/development/languages/fidl"
EXAMPLE_CODE_BASE_DIR = FUCHSIA_DIR / "examples/fidl/new"
EXAMPLE_DOCS_BASE_DIR = FIDL_DOCS_ROOT_DIR / "examples"
CONCEPT_DOCS_BASE_DIR = FIDL_DOCS_ROOT_DIR / "concepts"
DOCS_TOC_YAML_FILE = FIDL_DOCS_ROOT_DIR / "_toc.yaml"
DOCS_ROOT_README_FILE = EXAMPLE_DOCS_BASE_DIR / "README.md"
CODE_ROOT_BUILD_GN_FILE = EXAMPLE_CODE_BASE_DIR / "BUILD.gn"
# Template source paths.
TEMPLATES_DIR = FUCHSIA_DIR / "tools/fidl/scripts/canonical_example/templates"
CREATE_CODE_NEW_TEMPLATES_DIR = TEMPLATES_DIR / "create_code_new"
CREATE_CODE_VARIANT_TEMPLATES_DIR = TEMPLATES_DIR / "create_code_variant"
CREATE_DOCS_NEW_TEMPLATES_DIR = TEMPLATES_DIR / "create_docs_new"
CREATE_DOCS_VARIANT_TEMPLATES_DIR = TEMPLATES_DIR / "create_docs_variant"
DOCUMENT_CONCEPT_TEMPLATES_DIR = TEMPLATES_DIR / "document_concept"
DOCUMENT_WIDGET_TEMPLATES_DIR = TEMPLATES_DIR / "document_widget"
# Startup-compiled regexes.
REGEX_IS_PASCAL_CASE = re.compile(r"^(?:[A-Z][a-z0-9]+)+$")
REGEX_IS_SNAKE_CASE = re.compile(r"^[a-z][a-z0-9_]+$")
REGEX_TO_SNAKE_CASE = re.compile(r"([A-Z]+)")
REGEX_TO_TEXT_CASE = re.compile(r"_+")
# Other important defaults.
BASELINE = "baseline"
DNS = "DO" + "NOT" + "SUBMIT"
DNRC = "DO_NOT_REMOVE_COMMENT"
BINDINGS = {
"cpp_natural": "main.cc",
"cpp_wire": "main.cc",
"hlcpp": "main.cc",
"rust": "src/main.rs",
}
# Comment guard patterns for various file types.
COMMENT_GUARDS = {
".cc": ("// ", ""),
".gn": ("# ", ""),
".md": ("<!-- ", " -->"),
".rs": ("// ", ""),
}
# Custom exceptions.
class AlreadyExistsError(Exception):
pass
class InputError(Exception):
pass
class StepError(Exception):
pass
def is_pascal_case(input):
"""Check if a string is PascalCase, aka (upper) CamelCase
"""
return REGEX_IS_PASCAL_CASE.match(input)
def is_snake_case(input):
"""Check if a string is snake_case.
"""
return REGEX_IS_SNAKE_CASE.match(input)
def to_flat_case(input):
"""Converts a snake_case string to (lower) camelCase, ex "iamflatcase".
"""
return input.replace("_", "")
def to_snake_case(input):
"""Converts a PascalCase string to snake_case, ex "i_am_snake_case".
"""
return re.sub(REGEX_TO_SNAKE_CASE, r"_\1", input).lower()
def to_sentence_case(input):
"""Converts a snake_case string to a sentence case text case string, ex "I am sentence case".
"""
return to_text_case(input).capitalize()
def to_text_case(input):
"""Converts a snake_case string to an all lowercase text case string, ex "i am text case".
"""
return re.sub(REGEX_TO_TEXT_CASE, " ", input)
def build_subs(series, variant, protocol, bug):
"""Builds the substitution map.
"""
if not is_snake_case(series):
raise InputError("Expected series '%s' to be snake_case" % series)
if not is_snake_case(variant):
raise InputError("Expected variant '%s' to be snake_case" % variant)
if protocol and not is_pascal_case(protocol):
raise InputError("Expected protocol '%s' to be pascal_case" % protocol)
if bug and not (isinstance(bug, int) and bug > 0):
raise InputError("Expected bug '%s' to be an unsigned integer" % bug)
if not protocol:
protocol = ""
# LINT.IfChange
return {
'bug': "%s" % bug if bug else "",
'dns': DNS,
'protocol_pascal_case': protocol,
'protocol_snake_case': to_snake_case(protocol),
'series_flat_case': to_flat_case(series),
'series_sentence_case': to_sentence_case(series),
'series_snake_case': series,
'series_text_case': to_text_case(series),
'variant_flat_case': to_flat_case(variant),
'variant_snake_case': variant,
}
# LINT.ThenChange(/tools/fidl/scripts/canonical_example/README.md)
def build_doc_subs(series, variant):
"""Builds the substitution map, but ignores `protocol` and `bug`.
"""
return build_subs(series, variant, None, None)
def get_dns_count(input):
"""Count `TODO(DNS)` occurrences.
"""
return input.count("TODO(%s)" % DNS)
def apply_subs(subdir, subs, source, target):
"""Apply substitutions to templates located in the source dir, and write them to the target dir.
Returns a dict of paths to numbers, wih the number representing the count of outstanding DNS
TODOs in the file.
"""
dns_counts = {}
for path, subdirs, files in os.walk(source):
for file_name in files:
# Process the filename, including doing any substitutions necessary.
temp_path = os.path.join(path, file_name)
rel_path = os.path.relpath(
temp_path[:temp_path.rindex('.')], source)
out_path = target / subdir / Template(
str(rel_path)).substitute(subs)
# Do substitutions on the file text as well.
out_contents = ""
with open(temp_path, 'r') as f:
out_contents = Template(f.read()).substitute(subs)
# Count DNS occurrences.
dns_counts[str(out_path)] = get_dns_count(out_contents)
# Do the actual file writes.
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "wt") as f:
f.write(out_contents)
return dns_counts
def lines_between_dnrc_tags(lines, src_path, identifier=""):
"""Take a list of lines, and return the sub-list between the DNRC tags.
Returns the selected lines, plus the index at which they are located in the source file.
"""
if identifier:
identifier = ":" + identifier
REGEX_OPEN_DNRC = re.compile("\\sDO_NOT_REMOVE_COMMENT%s" % identifier)
REGEX_CLOSE_DNRC = re.compile("\\s\/DO_NOT_REMOVE_COMMENT%s" % identifier)
opened = -1
for line_num, line in enumerate(lines):
if opened >= 0:
if REGEX_CLOSE_DNRC.search(line):
return opened + 1, line_num
elif REGEX_OPEN_DNRC.search(line):
opened = line_num
if opened < 0:
raise InputError(
"Closing /DO_NOT_REMOVE_COMMENT comment missing from '%s'" %
src_path)
raise InputError(
"DO_NOT_REMOVE_COMMENT comment missing from '%s'" % src_path)
def validate_entry_does_not_already_exist(needle, files):
"""Check to see if a canonical example series already exists anywhere we want to insert it.
For each validated file, return a three-tuple containing all the lines of the file, the start,
and end of the range covered by the `DO_NOT_REMOVE_COMMENT` tags.
"""
ranges = {}
for path, identifier in files.items():
# Find `DO_NOT_REMOVE_COMMENT` and `/DO_NOT_REMOVE_COMMENT`
lines = []
with open(path, 'r') as f:
lines = f.readlines()
start, end = lines_between_dnrc_tags(lines, path, identifier)
# Does the needle appear anywhere in between those bounds?
for line in lines:
if needle in line:
raise AlreadyExistsError(
"An entry for '%s' already exists in '%s'" % (needle, path))
ranges[path] = (lines, start, end)
return ranges
def validate_todo_md_still_exists(series, variant, subdir):
"""Ensure that TODO.md for the implementation we are about to begin is still there.
This file being missing indicate corrupt state.
"""
# Validate that the TODO.md file is still where we expect it - if not, this step may have
# already been completed.
todo_md_file_path = EXAMPLE_CODE_BASE_DIR / series / variant / subdir / "TODO.md"
if not os.path.exists(todo_md_file_path):
raise StepError(
"Cannot scaffold the %s implementation for '%s/%s' because the TODO.md is missing"
% (subdir, series, variant))
def resolve_create_templates(subs, source_to_target_map):
"""Do all of the raw copying necessary to create a canonical example variant.
Returns a dict of paths to numbers, wih the number representing the count of outstanding DNS
TODOs in the created files.
"""
# Copy over each doc template file, processing substitutions on both file names and contents as
# we go.
dns_counts = {}
for source, target in source_to_target_map.items():
for path, count in apply_subs(subs['series_snake_case'], subs, source,
target).items():
dns_counts[path] = count
return dns_counts
def resolve_document_templates(subs):
"""Do all of the raw copying necessary to document a single concept.
Returns a dict of paths to numbers, wih the number representing the count of outstanding DNS
TODOs in the file.
"""
dns_counts = {}
source_to_subdir_map = {
DOCUMENT_CONCEPT_TEMPLATES_DIR: "concepts",
DOCUMENT_WIDGET_TEMPLATES_DIR: "widgets",
}
for source, subdir in source_to_subdir_map.items():
for path, count in apply_subs(subdir, subs, source,
FIDL_DOCS_ROOT_DIR).items():
dns_counts[path] = count
return dns_counts
def create_variant(series, variant, protocol, bug):
"""Create a canonical example series variant.
Returns a dict of paths to numbers, wih the number representing the count of outstanding DNS
TODOs in the file.
"""
# Build the substitution dictionary.
subs = build_subs(series, variant, protocol, bug)
# Make sure the baseline case exists. Also specifically validate that the baseline FIDL
# definition and top-level docs README both exist, as we will need to modify them directly.
series_root_gn_file_path = EXAMPLE_CODE_BASE_DIR / series / "BUILD.gn"
baseline_code_dir_path = EXAMPLE_CODE_BASE_DIR / series / "baseline"
series_docs_dir_path = EXAMPLE_DOCS_BASE_DIR / series
baseline_fidl_file_path = baseline_code_dir_path / "fidl" / (
"%s.test.fidl" % series)
series_docs_readme_path = series_docs_dir_path / "README.md"
if not os.path.exists(baseline_code_dir_path):
raise StepError(
"Cannot create variant '%s' because its baseline source directory %s does not exist"
% (variant, baseline_code_dir_path))
if not os.path.exists(series_docs_dir_path):
raise StepError(
"Cannot create variant '%s' because its series example docs directory %s does not exist"
% (variant, series_docs_dir_path))
if not os.path.exists(baseline_fidl_file_path):
raise StepError(
"Cannot create variant '%s' because its baseline FIDL file %s does not exist"
% (variant, baseline_fidl_file_path))
if not os.path.exists(series_root_gn_file_path):
raise StepError(
"Cannot create variant '%s' because its series BUILD.gn file %s does not exist"
% (variant, series_root_gn_file_path))
series_gn_dnrc_lines = validate_entry_does_not_already_exist(
"%s:tests" % variant,
{series_root_gn_file_path: ''})[series_root_gn_file_path]
if not os.path.exists(series_docs_readme_path):
raise StepError(
"Cannot create variant '%s' because its series README.md file %s does not exist"
% (variant, series_docs_readme_path))
readme_dnrc_lines = validate_entry_does_not_already_exist(
"{#%s}" % variant,
{series_docs_readme_path: ''})[series_docs_readme_path]
# Create the raw variant. We'll follow this up by replacing the "raw" FIDL file with a copy
# of the one from the baseline case.
dns_counts = resolve_create_templates(
subs, {
CREATE_DOCS_VARIANT_TEMPLATES_DIR: EXAMPLE_DOCS_BASE_DIR,
CREATE_CODE_VARIANT_TEMPLATES_DIR: EXAMPLE_CODE_BASE_DIR,
})
# Copy the FIDL definition from the baseline case, and update the dns count so the user is aware
# that they need to make further edits.
variant_fidl_file_path = EXAMPLE_CODE_BASE_DIR / series / variant / "fidl" / (
"%s.test.fidl" % series)
with open(baseline_fidl_file_path, "r") as f:
# Replace the ".baseline" library name suffix with this variant's name.
lines = f.readlines()
for i in range(len(lines)):
if lines[i].startswith("library"):
lines[i] = lines[i].replace(
BASELINE, subs['variant_flat_case'], 1)
break
# Add a DNS to the end of the file.
lines.append(
textwrap.dedent(
"""
// TODO(%s): Modify this FIDL to reflect the changes explored by this variant.
""" % DNS))
# Overwrite the raw FIDL file with this transformed baseline version, and update the dns
# count so the user is aware that they need to make further edits in this file.
out_contents = "".join(lines)
dns_counts[str(variant_fidl_file_path)] = get_dns_count(out_contents)
with open(variant_fidl_file_path, "wt") as f:
f.write(out_contents)
# Add an entry to the root BUILD.gn file for this series.
with open(series_root_gn_file_path, "wt") as f:
lines, start, end = series_gn_dnrc_lines
lines.insert(end, """ "%s:tests",\n""" % variant)
f.write("".join(lines))
# Add an entry to the root docs README.md file's example section for this series, and update the
# dns count so the user is aware that they need to make further edits in this file.
with open(series_docs_readme_path, "wt") as f:
lines, start, end = readme_dnrc_lines
lines.insert(end, """<<_%s_tutorial.md>>\n\n""" % variant)
lines.insert(
end,
"""### <!-- TODO(%s): Add title -->{#%s}\n\n""" % (DNS, variant))
out_contents = "".join(lines)
dns_counts[str(series_docs_readme_path)] = get_dns_count(out_contents)
f.write(out_contents)
return dns_counts
def create_new(series, protocol, bug):
"""Create a new canonical example series.
Returns a dict of paths to numbers, wih the number representing the count of outstanding DNS
TODOs in the file.
"""
# Build the substitution dictionary.
subs = build_subs(series, BASELINE, protocol, bug)
# Do a first pass through the files we'll be editing, to ensure that they are in a good state.
edit_ranges = validate_entry_does_not_already_exist(
series, {
DOCS_TOC_YAML_FILE: '',
DOCS_ROOT_README_FILE: 'examples',
CODE_ROOT_BUILD_GN_FILE: '',
})
# Update the _toc.yaml file to include an entry pointing to this canonical example series.
with open(DOCS_TOC_YAML_FILE, "wt") as f:
lines, start, end = edit_ranges[DOCS_TOC_YAML_FILE]
lines.insert(
end,
""" path: /docs/development/languages/fidl/examples/%s/README.md\n"""
% series)
lines.insert(
end, """ - title: "%s"\n""" % subs['series_sentence_case'])
f.write("".join(lines))
# Update the root README to include this series.
with open(DOCS_ROOT_README_FILE, "wt") as f:
lines, start, end = edit_ranges[DOCS_ROOT_README_FILE]
lines.insert(
end,
"""## %s\n\n<!-- TODO(fxbug.dev/%s): DOCUMENT[%s/%s] (brief description) -->\n\n"""
% (
subs['series_sentence_case'], subs['bug'],
subs['series_snake_case'], subs['variant_snake_case']))
f.write("".join(lines))
# Update /examples/fidl/new/BUILD.gn to support this series.
with open(CODE_ROOT_BUILD_GN_FILE, "wt") as f:
lines, start, end = edit_ranges[CODE_ROOT_BUILD_GN_FILE]
lines.insert(end, """ "%s:tests",\n""" % subs['series_snake_case'])
f.write("".join(lines))
# Copy over each doc template file, processing substitutions on both file names and contents as
# we go.
dns_counts = resolve_create_templates(
subs, {
CREATE_DOCS_NEW_TEMPLATES_DIR: EXAMPLE_DOCS_BASE_DIR,
CREATE_DOCS_VARIANT_TEMPLATES_DIR: EXAMPLE_DOCS_BASE_DIR,
CREATE_CODE_NEW_TEMPLATES_DIR: EXAMPLE_CODE_BASE_DIR,
CREATE_CODE_VARIANT_TEMPLATES_DIR: EXAMPLE_CODE_BASE_DIR,
})
return dns_counts
def create(name, protocol, bug, series):
"""Create a new variant in a canonical example series.
Can create an entirely new series or simply extend an existing one.
"""
dns_counts = {}
if series:
dns_counts = create_variant(series, name, protocol, bug)
else:
dns_counts = create_new(name, protocol, bug)
report_success(dns_counts)
def document(name, concepts):
"""Generate documentation instructions for an already-created canonical example variant.
"""
# Validate that the variant already exists. The name argument can refer to either a series'
# baseline case or a named variant, so try both options.
series = ""
variant = ""
for outer in os.listdir(EXAMPLE_DOCS_BASE_DIR):
maybe_dir = os.path.join(str(EXAMPLE_DOCS_BASE_DIR), outer)
if os.path.isdir(maybe_dir):
if outer == name:
series = name
variant = BASELINE
break
else:
for inner in os.listdir(maybe_dir):
if inner.startswith("_%s" % name):
series = outer
variant = name
break
if not (series and variant):
raise StepError(
"Cannot find already created variant '%s' to document" % name)
# Validate that the root docs README.md file exists, and that its concepts section is well
# marked, since we will need to modify it.
readme_concepts_lines = validate_entry_does_not_already_exist(
"### %s" % variant,
{DOCS_ROOT_README_FILE: 'concepts'})[DOCS_ROOT_README_FILE]
# Build the substitution dictionary. We don't need to do protocol or bug number substitution, so
# ignore those arguments.
subs = build_doc_subs(series, variant)
# Create the files for each concept.
dns_counts = {}
for concept in sorted(concepts, reverse=True):
subs['concept_snake_case'] = concept
subs['concept_sentence_case'] = to_sentence_case(concept)
# Add an entry to the root docs README.md file's example section for this concept, and update
# the dns count so the user is aware that they need to make further edits in this file.
with open(DOCS_ROOT_README_FILE, "wt") as f:
prefix = "<<../widgets/_"
lines, start, end = readme_concepts_lines
line_num = start
while line_num < end:
line = lines[line_num]
if line.startswith(prefix) and concept < line[len(prefix):]:
break
line_num = line_num + 1
lines.insert(line_num, """%s%s.md>>\n\n""" % (prefix, concept))
lines.insert(
line_num, """### %s\n\n""" % subs['concept_sentence_case'])
out_contents = "".join(lines)
f.write(out_contents)
# Now generate the actual file from a template.
for path, count in resolve_document_templates(subs).items():
dns_counts[path] = count
# Replace documentation TODOs generated in the create step with DNS ones.
REGEX_DOCUMENT_TODO = re.compile(
"TODO\\(fxbug.dev\\/\\d+\\): DOCUMENT\\[%s\\/%s]" % (series, variant))
possible_add_doc_files = [
DOCS_ROOT_README_FILE,
EXAMPLE_DOCS_BASE_DIR / series / "README.md",
EXAMPLE_DOCS_BASE_DIR / series / ("_%s_tutorial.md" % variant),
]
for path in possible_add_doc_files:
with open(path, "r") as f:
path_str = str(path)
replaced, count = re.subn(
REGEX_DOCUMENT_TODO, "TODO(%s):" % DNS, f.read())
if dns_counts.get(path_str):
dns_counts[path_str] = dns_counts[path_str] + count
else:
dns_counts[path_str] = count
with open(path, "wt") as f:
f.write(replaced)
report_success(dns_counts)
def copy_baseline_implementation(series, variant, subdir, subs):
"""
"""
dns_counts = {}
series_code_root_path = EXAMPLE_CODE_BASE_DIR / series
baseline_impl_root_path = series_code_root_path / "baseline" / subdir
variant_impl_root_path = series_code_root_path / variant / subdir
# Loop through the baseline files. For each file, perform a number of replacements to swap the
# word "baseline" with the variant name we are looking at.
for path, subdirs, files in os.walk(baseline_impl_root_path):
for file_name in files:
# Process the filename to be in the variant's, rather than baseline's, directory.
src_path = Path(os.path.join(path, file_name))
out_path = Path(
str(src_path).replace(
str(baseline_impl_root_path), str(variant_impl_root_path),
1))
ext = src_path.suffix
(comment_start, comment_end) = COMMENT_GUARDS[ext]
with open(src_path, "r") as f:
contents = f.read()
# We need to do very different replacement operations depending on whether or not
# this is a gn file.
if ext == ".gn":
# No DNS at the end of the file, and do one replacement each for strings where
# series and variant are split by "/" and "_".
dns = ""
contents = contents.replace(
"%s/%s" % (subs['series_snake_case'], "baseline"),
"%s/%s" %
(subs['series_snake_case'], subs['variant_snake_case']))
contents = contents.replace(
"%s_%s" % (subs['series_flat_case'], "baseline"),
"%s_%s" %
(subs['series_flat_case'], subs['variant_flat_case']))
contents = contents.replace(
".baseline", "." + subs['variant_flat_case'])
else:
dns = textwrap.dedent(
"""
%sTODO(%s): Edit this file to reflect the changes needed for this variant.%s
""" % (comment_start, DNS, comment_end))
# Special case: HLCPP uses flat_case in the path to fidl.h.
contents = contents.replace(
"%s/%s/cpp/fidl.h" %
(subs['series_snake_case'], "baseline"),
"%s/%s/cpp/fidl.h" %
(subs['series_snake_case'], subs['variant_flat_case']))
contents = contents.replace(
"%s/%s" % (subs['series_snake_case'], "baseline"),
"%s/%s" %
(subs['series_snake_case'], subs['variant_snake_case']))
contents = contents.replace(
"baseline", subs['variant_flat_case'])
# Do the actual file writes, and update the dns count so the user is aware that they
# need to make further edits in this file.
os.makedirs(os.path.dirname(out_path), exist_ok=True)
dns_counts[str(
out_path)] = get_dns_count(contents) + (1 if dns else 0)
with open(out_path, "wt") as f:
f.write(contents + dns)
return dns_counts
def maybe_implement_test(series, variant, subs, fresh):
"""Implement the test implementation, and add a TODO to client.cml, if they do not yet exist.
"""
dns_counts = {}
# Ensure that the to-be-replaced TODO.md still exists.
try:
validate_todo_md_still_exists(series, variant, "test")
except StepError:
return dns_counts
variant_code_root_path = EXAMPLE_CODE_BASE_DIR / series / variant
todo_md_file_path = variant_code_root_path / "test" / "TODO.md"
client_cml_path = variant_code_root_path / "meta/client.cml"
# Ensure that the BUILD.gn for this variant still exists.
variant_root_gn_file_path = variant_code_root_path / "BUILD.gn"
if not os.path.exists(variant_root_gn_file_path):
raise StepError(
"Cannot scaffold an implementation for '%s/%s' because variant's BUILD.gn is missing"
% (series, variant))
# Validate that the client.cml file is where we expect it to be.
if not os.path.exists(client_cml_path):
raise StepError(
"Cannot scaffold an implementation for '%s/%s' because the client.cml is missing"
% (series, variant))
# Update the client.cml file with a TODO telling the user to add some structured config.
with open(client_cml_path, "r") as f:
content = f.read()
dns_count = get_dns_count(content)
dns_counts[client_cml_path] = dns_count + 1
with open(client_cml_path, "wt") as f:
dns = textwrap.dedent(
"""
// TODO(%s): Expand the structured config as necessary to set up this variant.
""" % DNS)
f.write(content + dns)
# Update the variant BUILD.gn file with a TODO telling the user to add structured config
# defaults to `client_config_values`.
with open(variant_root_gn_file_path, "r") as f:
content = f.read()
dns_count = get_dns_count(content)
dns_counts[variant_root_gn_file_path] = dns_count + 1
with open(variant_root_gn_file_path, "wt") as f:
dns = textwrap.dedent(
"""
# TODO(%s): Add a `values` field to `client_config_values` after editing client.cml.
""" % DNS)
f.write(content + dns)
# Remove the TODO.md file, then generate the templates in its place.
os.remove(todo_md_file_path)
binding_templates_dir = TEMPLATES_DIR / "implement_test"
for path, count in apply_subs("test", subs, binding_templates_dir,
variant_code_root_path).items():
dns_counts[path] = count
return dns_counts
def implement(name, binding, protocol, series, fresh):
"""Implement the canonical example in a single binding language.
"""
variant = BASELINE
if series:
variant = name
else:
series = name
fresh = True
tutorial_md_file_path = EXAMPLE_DOCS_BASE_DIR / series / (
"_%s_tutorial.md" % variant)
variant_code_root_path = EXAMPLE_CODE_BASE_DIR / series / variant
todo_md_file_path = variant_code_root_path / binding / "TODO.md"
variant_root_gn_file_path = variant_code_root_path / "BUILD.gn"
# Validate the presence of the tutorial markdown file.
if not (os.path.exists(tutorial_md_file_path) and
os.path.exists(variant_code_root_path)):
raise StepError(
"Cannot scaffold %s binding for '%s/%s' because this variant has not yet been created"
% (binding, series, variant))
# Ensure that the variant's BUILD.gn file exists.
if not os.path.exists(variant_root_gn_file_path):
raise StepError(
"Cannot scaffold %s binding for '%s/%s' because root BUILD.gn file %s does not exist"
% (binding, variant, variant_root_gn_file_path))
# Build the subs, and ensure that the to-be-replaced TODO.md still exists.
subs = build_subs(series, variant, protocol, None)
validate_todo_md_still_exists(series, variant, binding)
# If it doesn't exist yet, generate the test implementation as well.
dns_counts = maybe_implement_test(series, variant, subs, fresh)
# Load this file now, since it was just updated.
variant_gn_dnrc_lines = validate_entry_does_not_already_exist(
"%s:tests" % binding,
{variant_root_gn_file_path: ''})[variant_root_gn_file_path]
# Add an entry to the root BUILD.gn file for this variant.
with open(variant_root_gn_file_path, "wt") as f:
lines, start, end = variant_gn_dnrc_lines
lines.insert(end, """ "%s:tests",\n""" % binding)
f.write("".join(lines))
# Update the tutorial markdown file to no longer point to the TODO.md file, and instead point it
# at the actual client and server implementations.
with open(tutorial_md_file_path, "r") as f:
replaced_client_todo = f.read().replace(
"""%s/TODO.md" region_tag="todo" """ % binding,
"""%s/client/%s" highlight="TODO(%s)" """ %
(binding, BINDINGS[binding], DNS), 1)
replaced_both_todos = replaced_client_todo.replace(
"""%s/TODO.md" region_tag="todo" """ % binding,
"""%s/server/%s" highlight="TODO(%s)" """ %
(binding, BINDINGS[binding], DNS), 1)
dns_counts[tutorial_md_file_path] = 2
with open(tutorial_md_file_path, "wt") as f:
f.write(replaced_both_todos)
# Remove the TODO.md file.
os.remove(todo_md_file_path)
# If the user request a "fresh" instance, generate the templates. Otherwise, copy them over from
# the baseline case.
generated = {}
if fresh:
binding_templates_dir = TEMPLATES_DIR / ("implement_%s" % binding)
generated = apply_subs(
binding, subs, binding_templates_dir, variant_code_root_path)
else:
generated = copy_baseline_implementation(series, variant, binding, subs)
# Update the DNS counts to reflect the newly added impl files and report to the user.
for path, count in generated.items():
dns_counts[path] = count
report_success(dns_counts)
def report_success(dns_counts):
"""Print some useful output to the user describing what they need to do next.
"""
if len(dns_counts):
print(
textwrap.dedent(
"""
Success!
Several generated files contain comments of the form "TODO(%s)".
Please replace them with the appropriate content, as specified by in their descriptions:
""" % DNS))
for path, count in dns_counts.items():
if count > 0:
print(" * %d occurrences in %s" % (count, path))
print()
def pascal_case(arg):
"""Check that command-line argument strings are PascalCase.
"""
if not is_pascal_case(arg):
raise argparse.ArgumentTypeError("'%s' must be PascalCase" % arg)
return arg
def snake_case(arg):
"""Check that command-line argument strings are snake_case.
"""
if not is_snake_case(arg):
raise argparse.ArgumentTypeError("'%s' must be snake_case" % arg)
return arg
def snake_case_list(arg):
"""Check that command-line argument lists are all snake_case strings.
"""
as_list = []
for item in arg.split(","):
item = item.strip()
if not is_snake_case(item):
raise argparse.ArgumentTypeError(
"'%s' must be a list of snake_case strings" % arg)
as_list.append(item)
return as_list
def unsigned_int(arg):
"""Argument parsing helper to validate unsigned integers.
"""
as_int = int(arg)
if as_int <= 0:
raise argparse.ArgumentTypeError(
"'%s' must be an unsigned integer" % as_int)
return as_int
def main(args):
"""Scaffolding scripts for the FIDL canonical examples effort.
"""
if args.command_used == "create":
return create(args.name, args.protocol, args.bug, args.series)
if args.command_used == "document":
return document(args.name, args.concepts[0])
if args.command_used == "implement":
return implement(
args.name, args.binding, args.protocol, args.series, args.fresh)
raise argparse.ArgumentTypeError("Unknown command '%s'" % args.command_used)
if __name__ == '__main__':
details = {
'create':
"""Create a new canonical example variant, with `TODO.md` placeholders files in place of
future implementations.""",
'document':
"""Document a canonical example variant, which enables writing all of the tutorial text
that accompanies the variant.""",
'implement':
"""Implement a canonical example variant in a single binding language. If this is the
first implementation of the canonical example variant, test scripts will need to be
filled in as well.""",
}
helps = {
'binding':
"""The language binding being implemented.""",
'bug':
"""The bug associated with this canonical example entry.""",
'concepts':
"""The concepts associated with this variant - each will receive its own widget.""",
'fresh':
"""When creating a new variant implementation, enable this flag to create a blank
scaffold, rather than copying the baseline implementation.""",
'from':
"""The name of the canonical example series that this example is an extension of. If
this argument is omitted, the --baseline flag must be specified instead to indicate that
we are creating the baseline case of a brand new canonical example series of the
specified name.""",
'baseline':
"""If this is a new canonical example series, this flag needs to be included to indicate
this fact.""",
'name':
"""The name of the canonical example variant being affected by this command.""",
'protocol':
"""The @discoverable protocol that serves as the first contact point for the client and
server in this example.""",
}
args = argparse.ArgumentParser(
description="Create or modify FIDL canonical example.")
commands = args.add_subparsers(
dest="command_used",
help="Commands (specify command followed by --help for details)",
)
# Specify the create command, used to create a new canonical example series, or a new variant in
# an already existing one.
create_cmd = commands.add_parser("create", help=details["create"])
create_cmd.add_argument(
"name", metavar="name", type=snake_case, help=helps['name'])
create_cmd.add_argument(
"--protocol", type=pascal_case, required=True, help=helps['protocol'])
create_cmd.add_argument(
"--bug", type=unsigned_int, required=True, help=helps['bug'])
create_cmd_baseline_or_extend = create_cmd.add_mutually_exclusive_group(
required=True)
create_cmd_baseline_or_extend.add_argument(
"--from", dest="series", type=snake_case, help=helps['from'])
create_cmd_baseline_or_extend.add_argument(
"--baseline", action='store_true', help=helps['baseline'])
create_cmd_baseline_or_extend.set_defaults(baseline=True)
# Specify the document command.
document_cmd = commands.add_parser("document", help=details["document"])
document_cmd.add_argument(
"name", metavar="name", type=snake_case, help=helps['name'])
document_cmd.add_argument(
"--concepts", nargs='+', type=snake_case_list, help=helps['concepts'])
# Specify the implement command.
implement_cmd = commands.add_parser("implement", help=details["implement"])
implement_cmd.add_argument(
"name", metavar="name", type=snake_case, help=helps['name'])
implement_cmd.add_argument(
"--binding",
type=snake_case,
choices=BINDINGS.keys(),
required=True,
help=helps['binding'])
implement_cmd.add_argument(
"--protocol", type=pascal_case, required=True, help=helps['protocol'])
implement_cmd.add_argument(
"--fresh", action='store_true', help=helps['fresh'])
implement_cmd.set_defaults(fresh=False)
implement_cmd_baseline_or_extend = implement_cmd.add_mutually_exclusive_group(
required=True)
implement_cmd_baseline_or_extend.add_argument(
"--from", dest="series", type=snake_case, help=helps['from'])
implement_cmd_baseline_or_extend.add_argument(
"--baseline", action='store_true', help=helps['baseline'])
implement_cmd_baseline_or_extend.set_defaults(baseline=True)
# Parse arguments.
main(args.parse_args())