diff --git a/src/BlueprintParser.h b/src/BlueprintParser.h index aefcff79..c6f5787c 100644 --- a/src/BlueprintParser.h +++ b/src/BlueprintParser.h @@ -38,13 +38,16 @@ namespace snowcrash { MarkdownNodeIterator cur = node; - if (cur->type == mdp::ParagraphMarkdownNodeType) { + while (cur->type == mdp::ParagraphMarkdownNodeType) { - parseMetadata(node, pd, report, out.metadata); + MetadataCollection metadata; + parseMetadata(cur, pd, report, metadata); // First block is paragraph and is not metadata (no API name) - if (out.metadata.empty()) { + if (metadata.empty()) { return processDescription(cur, pd, report, out); + } else { + out.metadata.insert(out.metadata.end(), metadata.begin(), metadata.end()); } cur++; @@ -148,23 +151,14 @@ namespace snowcrash { Report& report, Blueprint& out) { - if (out.name.empty()) { - - if (pd.options & RequireBlueprintNameOption) { - - // ERR: No API name specified - mdp::CharactersRangeSet sourceMap = mdp::BytesRangeSetToCharactersRangeSet(node->sourceMap, pd.sourceData); - report.error = Error(ExpectedAPINameMessage, - BusinessError, - sourceMap); - } else { + if (out.name.empty() && + (pd.options & RequireBlueprintNameOption)) { - // WARN: No API name - mdp::CharactersRangeSet sourceMap = mdp::BytesRangeSetToCharactersRangeSet(node->sourceMap, pd.sourceData); - report.warnings.push_back(Warning(ExpectedAPINameMessage, - APINameWarning, - sourceMap)); - } + // ERR: No API name specified + mdp::CharactersRangeSet sourceMap = mdp::BytesRangeSetToCharactersRangeSet(node->sourceMap, pd.sourceData); + report.error = Error(ExpectedAPINameMessage, + BusinessError, + sourceMap); } } diff --git a/src/CodeBlockUtility.h b/src/CodeBlockUtility.h index f08ef4d1..2c21cae7 100644 --- a/src/CodeBlockUtility.h +++ b/src/CodeBlockUtility.h @@ -94,10 +94,11 @@ namespace snowcrash { content += remainingContent; content += "\n"; - + // WARN: Not a preformatted code block but multiline signature size_t level = codeBlockIndentationLevel(pd.parentSectionContext()); std::stringstream ss; + ss << SectionName(pd.sectionContext()); ss << " is expected to be a pre-formatted code block, separate it by a newline and "; ss << "indent every of its line by "; @@ -212,7 +213,7 @@ namespace snowcrash { // WARN: Dangling asset std::stringstream ss; ss << "Dangling message-body asset, expected a pre-formatted code block, "; - ss << "indent every one of it's line by " << level*4 << " spaces or " << level << " tabs"; + ss << "indent every of it's line by " << level*4 << " spaces or " << level << " tabs"; mdp::CharactersRangeSet sourceMap = mdp::BytesRangeSetToCharactersRangeSet(node->sourceMap, pd.sourceData); report.warnings.push_back(Warning(ss.str(), diff --git a/src/HeadersParser.h b/src/HeadersParser.h index 145bbced..a8c49f78 100644 --- a/src/HeadersParser.h +++ b/src/HeadersParser.h @@ -83,7 +83,7 @@ namespace snowcrash { mdp::ByteBuffer subject = node->children().front().text; TrimString(subject); - + if (RegexMatch(subject, HeadersRegex)) return HeadersSectionType; } @@ -91,6 +91,21 @@ namespace snowcrash { return UndefinedSectionType; } + static void finalize(const MarkdownNodeIterator& node, + SectionParserData& pd, + Report& report, + Headers& out) { + + if (out.empty()) { + + // WARN: No headers defined + mdp::CharactersRangeSet sourceMap = mdp::BytesRangeSetToCharactersRangeSet(node->sourceMap, pd.sourceData); + report.warnings.push_back(Warning("No headers defined in headers section", + EmptyDefinitionWarning, + sourceMap)); + } + } + /** Retrieve headers from content */ static void headersFromContent(const MarkdownNodeIterator& node, const mdp::ByteBuffer& content, diff --git a/src/ParameterParser.h b/src/ParameterParser.h index 41810061..28c2cc3b 100644 --- a/src/ParameterParser.h +++ b/src/ParameterParser.h @@ -122,7 +122,8 @@ namespace snowcrash { if (node->type == mdp::ListItemMarkdownNodeType && !node->children().empty()) { - mdp::ByteBuffer subject = node->children().front().text; + mdp::ByteBuffer subject, remainingContent; + subject = GetFirstLine(node->children().front().text, remainingContent); TrimString(subject); if (RegexMatch(subject, ParameterAbbrevDefinitionRegex)) { diff --git a/src/PayloadParser.h b/src/PayloadParser.h index 1bb90256..b3428441 100644 --- a/src/PayloadParser.h +++ b/src/PayloadParser.h @@ -68,9 +68,8 @@ namespace snowcrash { if (!remainingContent.empty()) { if (!isAbbreviated(pd.sectionContext())) { out.description = remainingContent; - } else { + } else if (!parseSymbolReference(node, pd, remainingContent, report, out)) { CodeBlockUtility::signatureContentAsCodeBlock(node, pd, report, out.body); - parseSymbolReference(pd, report, out); } } @@ -83,7 +82,6 @@ namespace snowcrash { Payload& out) { mdp::ByteBuffer content; - CodeBlockUtility::contentAsCodeBlock(node, pd, report, content); if (!out.symbol.empty()) { //WARN: ignoring extraneous content after symbol reference @@ -97,9 +95,14 @@ namespace snowcrash { IgnoringWarning, sourceMap)); } else { - out.body += content; - parseSymbolReference(pd, report, out); + if (!out.body.empty() || + node->type != mdp::ParagraphMarkdownNodeType || + !parseSymbolReference(node, pd, node->text, report, out)) { + + CodeBlockUtility::contentAsCodeBlock(node, pd, report, content); + out.body += content; + } } return ++MarkdownNodeIterator(node); @@ -454,11 +457,12 @@ namespace snowcrash { return true; } - static void parseSymbolReference(SectionParserData& pd, + static bool parseSymbolReference(const MarkdownNodeIterator& node, + SectionParserData& pd, + mdp::ByteBuffer& source, Report& report, Payload& out) { - mdp::ByteBuffer source = out.body; SymbolName symbol; ResourceModel model; @@ -467,6 +471,19 @@ namespace snowcrash { if (GetSymbolReference(source, symbol)) { out.symbol = symbol; + // If symbol doesn't exist + if (pd.symbolTable.resourceModels.find(symbol) == pd.symbolTable.resourceModels.end()) { + + // ERR: Undefined symbol + std::stringstream ss; + ss << "Undefined symbol " << symbol; + + mdp::CharactersRangeSet sourceMap = mdp::BytesRangeSetToCharactersRangeSet(node->sourceMap, pd.sourceData); + report.error = Error(ss.str(), SymbolError, sourceMap); + + return true; + } + model = pd.symbolTable.resourceModels.at(symbol); out.description = model.description; @@ -474,7 +491,11 @@ namespace snowcrash { out.headers = model.headers; out.body = model.body; out.schema = model.schema; + + return true; } + + return false; } }; diff --git a/src/SectionParser.h b/src/SectionParser.h index b4b02e06..0b1f57d1 100644 --- a/src/SectionParser.h +++ b/src/SectionParser.h @@ -91,7 +91,10 @@ namespace snowcrash { cur = SectionProcessor::processUnexpectedNode(cur, collection, pd, lastSectionType, report, out); } - if (pd.sectionContext() != UndefinedSectionType) { + if (pd.sectionContext() != UndefinedSectionType || + (cur->type != mdp::ParagraphMarkdownNodeType && + cur->type != mdp::CodeMarkdownNodeType)) { + lastSectionType = pd.sectionContext(); } diff --git a/test/test-BlueprintParser.cc b/test/test-BlueprintParser.cc index 587819a4..54753b2c 100644 --- a/test/test-BlueprintParser.cc +++ b/test/test-BlueprintParser.cc @@ -70,6 +70,37 @@ TEST_CASE("Parse canonical blueprint", "[blueprint]") REQUIRE(blueprint.resourceGroups[1].resources.empty()); } +TEST_CASE("Parse blueprint with multiple metadata sections", "[blueprint]") +{ + mdp::ByteBuffer source = "FORMAT: 1A\n\n"; + source += BlueprintFixture; + + Blueprint blueprint; + Report report; + SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint); + + REQUIRE(report.error.code == Error::OK); + REQUIRE(report.warnings.empty()); + + REQUIRE(blueprint.metadata.size() == 2); + REQUIRE(blueprint.metadata[0].first == "FORMAT"); + REQUIRE(blueprint.metadata[0].second == "1A"); + REQUIRE(blueprint.metadata[1].first == "meta"); + REQUIRE(blueprint.metadata[1].second == "verse"); + + REQUIRE(blueprint.name == "Snowcrash API"); + REQUIRE(blueprint.description == "## Character\n\nUncle Enzo\n\n"); + REQUIRE(blueprint.resourceGroups.size() == 2); + + REQUIRE(blueprint.resourceGroups[0].name == "First"); + REQUIRE(blueprint.resourceGroups[0].description == "p1\n"); + REQUIRE(blueprint.resourceGroups[0].resources.size() == 1); + + REQUIRE(blueprint.resourceGroups[1].name == "Second"); + REQUIRE(blueprint.resourceGroups[1].description == "p2\n"); + REQUIRE(blueprint.resourceGroups[1].resources.empty()); +} + TEST_CASE("Parse API with Name and abbreviated resource", "[blueprint]") { mdp::ByteBuffer source = \ @@ -115,7 +146,7 @@ TEST_CASE("Parse nameless blueprint description", "[blueprint]") SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.name.empty()); REQUIRE(blueprint.description == "A\n\n# B\n"); @@ -131,7 +162,7 @@ TEST_CASE("Parse nameless blueprint with a list description", "[blueprint]") SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.name.empty()); REQUIRE(blueprint.description == "+ List\n"); @@ -175,7 +206,7 @@ TEST_CASE("Test parser options - required blueprint name", "[blueprint]") SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint, Symbols(), RequireBlueprintNameOption); REQUIRE(report.error.code != Error::OK); @@ -243,7 +274,7 @@ TEST_CASE("Blueprint starting with Resource Group should be parsed", "[blueprint SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.name.empty()); REQUIRE(blueprint.description.empty()); @@ -262,7 +293,7 @@ TEST_CASE("Blueprint starting with Resource should be parsed", "[blueprint]") SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.name.empty()); REQUIRE(blueprint.description.empty()); @@ -286,7 +317,7 @@ TEST_CASE("Checking a resource with global resources for duplicates", "[blueprin SectionParserHelper::parse(source, BlueprintSectionType, report, blueprint, Symbols(), 0, &blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 4); // expected API name & 2x no response & duplicate resource + REQUIRE(report.warnings.size() == 3); // 2x no response & duplicate resource REQUIRE(blueprint.name.empty()); REQUIRE(blueprint.description.empty()); diff --git a/test/test-HeadersParser.cc b/test/test-HeadersParser.cc index 18be7f5b..cc520359 100644 --- a/test/test-HeadersParser.cc +++ b/test/test-HeadersParser.cc @@ -84,7 +84,7 @@ TEST_CASE("parse malformed headers fixture", "[headers]") REQUIRE(headers[1].second == "Hello World!"); } -TEST_CASE("hparser/parse-multiple-blocks", "Parse header section composed of multiple blocks") +TEST_CASE("Parse header section composed of multiple blocks", "[headers]") { // Blueprint in question: //R"( @@ -116,3 +116,17 @@ TEST_CASE("hparser/parse-multiple-blocks", "Parse header section composed of mul REQUIRE(headers[2].first == "X-My-Header"); REQUIRE(headers[2].second == "42"); } + +TEST_CASE("Parse header section with missing headers", "[headers]") +{ + mdp::ByteBuffer source = "+ Headers\n\n"; + + Headers headers; + Report report; + SectionParserHelper::parse(source, HeadersSectionType, report, headers); + + REQUIRE(report.error.code == Error::OK); + REQUIRE(report.warnings.size() == 1); // no headers + + REQUIRE(headers.size() == 0); +} diff --git a/test/test-ParametersParser.cc b/test/test-ParametersParser.cc index b039e070..7612c097 100644 --- a/test/test-ParametersParser.cc +++ b/test/test-ParametersParser.cc @@ -150,3 +150,39 @@ TEST_CASE("Warn about multiple parameters with the same name", "[parameters]") REQUIRE(parameters[0].name == "id"); REQUIRE(parameters[0].exampleValue == "43"); } + +TEST_CASE("Recognize parameter when there is no description on its signature and remaining description is not a new node", "[parameters]") +{ + mdp::ByteBuffer source = \ + "+ Parameters\n\n"\ + " + id (number) ... The ID number of the car\n"\ + " + state (string)\n"\ + " The desired state of the panoramic roof. The approximate percent open values for each state are `open` = 100%, `close` = 0%, `comfort` = 80%, and `vent` = ~15%\n"\ + " + Values\n"\ + " + `open`\n"\ + " + `close`\n"\ + " + `comfort`\n"\ + " + `vent`"; + + Parameters parameters; + Report report; + SectionParserHelper::parse(source, ParametersSectionType, report, parameters); + + REQUIRE(report.error.code == Error::OK); + REQUIRE(report.warnings.empty()); + + REQUIRE(parameters.size() == 2); + REQUIRE(parameters[0].name == "id"); + REQUIRE(parameters[0].type == "number"); + REQUIRE(parameters[0].description == "The ID number of the car"); + + Parameter parameter = parameters[1]; + REQUIRE(parameter.name == "state"); + REQUIRE(parameter.type == "string"); + REQUIRE(parameter.description == "\nThe desired state of the panoramic roof. The approximate percent open values for each state are `open` = 100%, `close` = 0%, `comfort` = 80%, and `vent` = ~15%\n\n"); + REQUIRE(parameter.values.size() == 4); + REQUIRE(parameter.values[0] == "open"); + REQUIRE(parameter.values[1] == "close"); + REQUIRE(parameter.values[2] == "comfort"); + REQUIRE(parameter.values[3] == "vent"); +} diff --git a/test/test-PayloadParser.cc b/test/test-PayloadParser.cc index 07d16403..006cb407 100644 --- a/test/test-PayloadParser.cc +++ b/test/test-PayloadParser.cc @@ -251,7 +251,7 @@ TEST_CASE("Parse inline payload with symbol reference", "[payload]") SectionParserHelper::parse(SymbolFixture, RequestBodySectionType, report, payload, symbols); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); + REQUIRE(report.warnings.size() == 0); REQUIRE(payload.name.empty()); REQUIRE(payload.description == "Foo"); @@ -261,6 +261,34 @@ TEST_CASE("Parse inline payload with symbol reference", "[payload]") REQUIRE(payload.schema.empty()); } +TEST_CASE("Parse inline payload with symbol reference with extra indentation", "[payload]") +{ + ResourceModel model; + Symbols symbols; + + model.description = "Foo"; + model.body = "Bar"; + symbols.push_back(ResourceModelSymbol("Symbol", model)); + + mdp::ByteBuffer source = \ + "+ Request\n\n"\ + " [Symbol][]\n"; + + Payload payload; + Report report; + SectionParserHelper::parse(source, RequestBodySectionType, report, payload, symbols); + + REQUIRE(report.error.code == Error::OK); + REQUIRE(report.warnings.size() == 0); + + REQUIRE(payload.name.empty()); + REQUIRE(payload.description.empty()); + REQUIRE(payload.parameters.empty()); + REQUIRE(payload.headers.empty()); + REQUIRE(payload.body == "[Symbol][]\n"); + REQUIRE(payload.schema.empty()); +} + TEST_CASE("Parse inline payload with symbol reference with foreign content", "[payload]") { mdp::ByteBuffer source = SymbolFixture; @@ -278,7 +306,7 @@ TEST_CASE("Parse inline payload with symbol reference with foreign content", "[p SectionParserHelper::parse(source, RequestBodySectionType, report, payload, symbols); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 3); // ignoring foreign entry + REQUIRE(report.warnings.size() == 1); // ignoring foreign entry REQUIRE(payload.name.empty()); REQUIRE(payload.description == "Foo"); diff --git a/test/test-ResourceParser.cc b/test/test-ResourceParser.cc index eab811b5..4b8469d2 100644 --- a/test/test-ResourceParser.cc +++ b/test/test-ResourceParser.cc @@ -356,14 +356,12 @@ TEST_CASE("Parse named resource with nameless model", "[resource][model][source] { mdp::ByteBuffer source = \ "# Message [/message]\n"\ - "+ Model\n"\ - " \n"\ + "+ Model\n\n"\ " AAA\n"\ "\n"\ "## Retrieve a message [GET]\n"\ - "+ Response 200\n"\ - " \n"\ - " [Message][]\n\n"; + "+ Response 200\n\n"\ + " [Message][]\n\n"; Resource resource; Report report; @@ -383,6 +381,25 @@ TEST_CASE("Parse named resource with nameless model", "[resource][model][source] REQUIRE(resource.actions[0].examples[0].responses[0].body == "AAA\n"); } +TEST_CASE("Parse named resource with nameless model but reference a non-existing model", "[resource]") +{ + mdp::ByteBuffer source = \ + "# Posts [/posts]\n"\ + "+ Model\n\n"\ + " {}\n"\ + "\n"\ + "## List [GET]\n"\ + "+ Response 200\n\n"\ + " [Post][]\n"; + + Resource resource; + Report report; + SectionParserHelper::parse(source, ResourceSectionType, report, resource); + + REQUIRE(report.error.code == SymbolError); + REQUIRE(report.warnings.empty()); +} + TEST_CASE("Parse root resource", "[resource]") { mdp::ByteBuffer source = "# API Root [/]\n"; diff --git a/test/test-SymbolIdentifier.cc b/test/test-SymbolIdentifier.cc index d6a13e63..1f419689 100644 --- a/test/test-SymbolIdentifier.cc +++ b/test/test-SymbolIdentifier.cc @@ -21,7 +21,7 @@ TEST_CASE("Punctuation in identifiers", "[symbol_identifier]") parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.resourceGroups.size() == 1); REQUIRE(blueprint.resourceGroups[0].resources.size() == 1); @@ -45,7 +45,7 @@ TEST_CASE("Non ASCII characters in identifiers", "[symbol_identifier]") parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.resourceGroups.size() == 1); REQUIRE(blueprint.resourceGroups[0].resources.size() == 1); diff --git a/test/test-snowcrash.cc b/test/test-snowcrash.cc index 5761a02c..82c01120 100644 --- a/test/test-snowcrash.cc +++ b/test/test-snowcrash.cc @@ -113,7 +113,7 @@ TEST_CASE("Do not report duplicate response when media type differs", "[method][ parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); } TEST_CASE("Support description ending with an list item", "[parser][#8]") @@ -130,7 +130,7 @@ TEST_CASE("Support description ending with an list item", "[parser][#8]") parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.resourceGroups.size() == 1); REQUIRE(blueprint.resourceGroups[0].resources.size() == 1); @@ -154,7 +154,7 @@ TEST_CASE("Invalid ‘warning: empty body asset’ for certain status codes", "[ parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 1); // expected API name + REQUIRE(report.warnings.empty()); REQUIRE(blueprint.resourceGroups.size() == 1); REQUIRE(blueprint.resourceGroups[0].resources.size() == 1); @@ -214,7 +214,7 @@ TEST_CASE("Parse adjacent asset blocks", "[parser][#9]") parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 2); // expected API name + REQUIRE(report.warnings.size() == 1); REQUIRE(blueprint.resourceGroups.size() == 1); REQUIRE(blueprint.resourceGroups[0].resources.size() == 1); @@ -241,7 +241,7 @@ TEST_CASE("Parse adjacent asset list blocks", "[parser][#9]") parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 2); // expected API name + REQUIRE(report.warnings.size() == 1); REQUIRE(report.warnings[0].code == IgnoringWarning); REQUIRE(blueprint.resourceGroups.size() == 1); @@ -271,7 +271,7 @@ TEST_CASE("Parse adjacent nested asset blocks", "[parser][#9]") parse(source, 0, report, blueprint); REQUIRE(report.error.code == Error::OK); - REQUIRE(report.warnings.size() == 3); // expected API name + REQUIRE(report.warnings.size() == 2); REQUIRE(report.warnings[0].code == IndentationWarning); REQUIRE(report.warnings[1].code == IndentationWarning);