From 2c2d1b511ed2b18ee6e89c6c092e5426f73c1fbe Mon Sep 17 00:00:00 2001 From: Pavan Kumar Sunkara Date: Thu, 5 Feb 2015 20:53:06 +0530 Subject: [PATCH] Finished inheritance rule --- src/drafter.coffee | 12 +- src/rules/mson-inheritance.coffee | 85 ++++++ src/rules/mson-member-type-name.coffee | 3 + src/rules/mson-mixin.coffee | 3 + test/fixtures/dataStructures.apib | 34 +++ test/fixtures/dataStructures.ast.json | 374 +++++++++++++++++++++++++ test/unit/drafter-test.coffee | 12 + 7 files changed, 517 insertions(+), 6 deletions(-) create mode 100644 src/rules/mson-inheritance.coffee create mode 100644 src/rules/mson-member-type-name.coffee create mode 100644 src/rules/mson-mixin.coffee create mode 100644 test/fixtures/dataStructures.apib create mode 100644 test/fixtures/dataStructures.ast.json diff --git a/src/drafter.coffee b/src/drafter.coffee index 224951c..8a7de2e 100644 --- a/src/drafter.coffee +++ b/src/drafter.coffee @@ -8,7 +8,7 @@ fs = require 'fs' class Drafter # List of data structures - @dataStructures: [] + @dataStructures: {} # Default configuration @defaultConfig: @@ -41,10 +41,10 @@ class Drafter protagonist.parse source, @config, (error, result) => callback error if error - ruleList = [] + ruleList = ['mson-inheritance'] rules = (require './rules/' + rule for rule in ruleList) - @dataStructures = [] + @dataStructures = {} @expandNode result.ast, rules, 'blueprint' callback error, result @@ -63,15 +63,15 @@ class Drafter if element.element is 'category' for subElement in element.content - @dataStructures.push subElement if subElement.element is 'dataStructure' + @dataStructures[subElement.name.literal] = subElement if subElement.element is 'dataStructure' # Expand the gathered data structures for rule in rules - rule.dataStructures.call @dataStructures if 'dataStructures' in rule + rule.init.call rule, @dataStructures if 'dataStructures' in Object.keys(rule) # Apply rules to the current node for rule in rules - rule[elementType].call node, @dataStructures if elementType in rule + rule[elementType].call rule, node if elementType in Object.keys(rule) # Recursively do the same for children nodes switch elementType diff --git a/src/rules/mson-inheritance.coffee b/src/rules/mson-inheritance.coffee new file mode 100644 index 0000000..f4f7575 --- /dev/null +++ b/src/rules/mson-inheritance.coffee @@ -0,0 +1,85 @@ +module.exports = + + # Variables + expanded: {} + dataStructures: {} + + # Copy all member types from one data structure to another + # + # @param supertTypeName [String] The name of the super type data structure + # @param memberTypeSection [Object] Member Type Section to be copied into + copyMembers: (superTypeName, memberTypeSection) -> + return if not @dataStructures[superTypeName] + + for section in @dataStructures[superTypeName].sections + if section['class'] is 'memberType' + + for member in section.content + memberTypeSection.content.push member if member['class'] is 'property' + + # Given a data structure, expand it's inheritance recursively + # + # @param name [String] Name of the data structure + # @param dataStructure [Object] Data structure + expandInheritance: (name, dataStructure) -> + return if @expanded[name] + + # Check for inheritance + superType = dataStructure.typeDefinition.typeSpecification.name + + if typeof superType isnt 'object' or not superType?.literal + return @expanded[superType] = true + + # Expand the super type first + @expandInheritance superType, @dataStructures[superType.literal] + + # If super type is not an object + superTypeBaseName = @dataStructures[superType.literal].typeDefinition.typeSpecification.name + + if superTypeBaseName isnt 'object' + dataStructure.typeDefinition.typeSpecification.name = superTypeBaseName + memberTypeSection = + content: [] + + memberTypeSection['class'] = 'memberType' + @copyMembers superType.literal, memberTypeSection + + dataStructure.sections.push memberTypeSection if memberTypeSection.content.length + return @expanded[name] = true + + # Find member type section of the current data structure + memberTypeSection = null + push = false + + for section in dataStructure.sections + memberTypeSection = section if section['class'] is 'memberType' + + # If no member type sections, create one + if not memberTypeSection + memberTypeSection = + content: [] + + memberTypeSection['class'] = 'memberType' + push = true + + # Copy super-type and all the member types to sub type + dataStructure.typeDefinition.typeSpecification.name = superTypeBaseName + @copyMembers superType.literal, memberTypeSection + + # Push the created type section + dataStructure.sections.push memberTypeSection if push and memberTypeSection.content.length + + # Denote this type as expanded + @expanded[name] = true + + init: (dataStructures) -> + @expanded = {} + @dataStructures = dataStructures + + # Initiate flags + for name, dataStructure of @dataStructures + @expanded[name] = false + + # Actual expansion + for name, dataStructure of @dataStructures + @expandInheritance name, dataStructure diff --git a/src/rules/mson-member-type-name.coffee b/src/rules/mson-member-type-name.coffee new file mode 100644 index 0000000..8829f3f --- /dev/null +++ b/src/rules/mson-member-type-name.coffee @@ -0,0 +1,3 @@ +module.exports = + + dataStructures: (dataStructures) -> diff --git a/src/rules/mson-mixin.coffee b/src/rules/mson-mixin.coffee new file mode 100644 index 0000000..8829f3f --- /dev/null +++ b/src/rules/mson-mixin.coffee @@ -0,0 +1,3 @@ +module.exports = + + dataStructures: (dataStructures) -> diff --git a/test/fixtures/dataStructures.apib b/test/fixtures/dataStructures.apib new file mode 100644 index 0000000..2b3b8fa --- /dev/null +++ b/test/fixtures/dataStructures.apib @@ -0,0 +1,34 @@ +FORMAT: 1A + +# Stripe +Inspired by stripe API + +# Data Structures + +## Timestamp (number) +Unix timestamp as an integer + +## Timestamps (object) +This object contains the following unix timestamps + ++ created - Denoting the time when the object is created ++ modified - Denoting the time when the object has been recently updated + +### Properties ++ created (Timestamp) + +### Properties ++ modified (Timestamp) + +## Coupon Base (Timestamps) ++ percent_off: 25 (number) + + A positive integer between 1 and 100 that represents the discount the coupon will apply. + ++ redeem_by (number) - Date after which the coupon can no longer be redeemed + +## Coupon Base Clone (Coupon Base) +A clone of Coupon Base to be used for testing + +## Timestamp Clone (Timestamp) +A clone of timestamp to be used for testing diff --git a/test/fixtures/dataStructures.ast.json b/test/fixtures/dataStructures.ast.json new file mode 100644 index 0000000..4633373 --- /dev/null +++ b/test/fixtures/dataStructures.ast.json @@ -0,0 +1,374 @@ +{ + "_version": "2.1", + "metadata": [ + { + "name": "FORMAT", + "value": "1A" + } + ], + "name": "Stripe", + "description": "Inspired by stripe API\n\n", + "element": "category", + "resourceGroups": [], + "content": [ + { + "element": "category", + "content": [ + { + "element": "dataStructure", + "name": { + "literal": "Timestamp", + "variable": false + }, + "typeDefinition": { + "typeSpecification": { + "name": "number", + "nestedTypes": [] + }, + "attributes": [] + }, + "sections": [ + { + "class": "blockDescription", + "content": "Unix timestamp as an integer\n\n" + } + ] + }, + { + "element": "dataStructure", + "name": { + "literal": "Timestamps", + "variable": false + }, + "typeDefinition": { + "typeSpecification": { + "name": "object", + "nestedTypes": [] + }, + "attributes": [] + }, + "sections": [ + { + "class": "blockDescription", + "content": "This object contains the following unix timestamps\n\n+ created - Denoting the time when the object is created\n\n+ modified - Denoting the time when the object has been recently updated\n\n" + }, + { + "class": "memberType", + "content": [ + { + "content": { + "name": { + "literal": "created" + }, + "description": "", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": { + "literal": "Timestamp", + "variable": false + }, + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + } + ] + }, + { + "class": "memberType", + "content": [ + { + "content": { + "name": { + "literal": "modified" + }, + "description": "", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": { + "literal": "Timestamp", + "variable": false + }, + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + } + ] + } + ] + }, + { + "element": "dataStructure", + "name": { + "literal": "Coupon Base", + "variable": false + }, + "typeDefinition": { + "typeSpecification": { + "name": "object", + "nestedTypes": [] + }, + "attributes": [] + }, + "sections": [ + { + "class": "memberType", + "content": [ + { + "content": { + "name": { + "literal": "percent_off" + }, + "description": "", + "valueDefinition": { + "values": [ + { + "literal": "25", + "variable": false + } + ], + "typeDefinition": { + "typeSpecification": { + "name": "number", + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [ + { + "class": "blockDescription", + "content": "A positive integer between 1 and 100 that represents the discount the coupon will apply.\n" + } + ] + }, + "class": "property" + }, + { + "content": { + "name": { + "literal": "redeem_by" + }, + "description": "Date after which the coupon can no longer be redeemed", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": "number", + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + }, + { + "content": { + "name": { + "literal": "created" + }, + "description": "", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": { + "literal": "Timestamp", + "variable": false + }, + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + }, + { + "content": { + "name": { + "literal": "modified" + }, + "description": "", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": { + "literal": "Timestamp", + "variable": false + }, + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + } + ] + } + ] + }, + { + "element": "dataStructure", + "name": { + "literal": "Coupon Base Clone", + "variable": false + }, + "typeDefinition": { + "typeSpecification": { + "name": "object", + "nestedTypes": [] + }, + "attributes": [] + }, + "sections": [ + { + "class": "blockDescription", + "content": "A clone of Coupon Base to be used for testing\n\n" + }, + { + "class": "memberType", + "content": [ + { + "content": { + "name": { + "literal": "percent_off" + }, + "description": "", + "valueDefinition": { + "values": [ + { + "literal": "25", + "variable": false + } + ], + "typeDefinition": { + "typeSpecification": { + "name": "number", + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [ + { + "class": "blockDescription", + "content": "A positive integer between 1 and 100 that represents the discount the coupon will apply.\n" + } + ] + }, + "class": "property" + }, + { + "content": { + "name": { + "literal": "redeem_by" + }, + "description": "Date after which the coupon can no longer be redeemed", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": "number", + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + }, + { + "content": { + "name": { + "literal": "created" + }, + "description": "", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": { + "literal": "Timestamp", + "variable": false + }, + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + }, + { + "content": { + "name": { + "literal": "modified" + }, + "description": "", + "valueDefinition": { + "values": [], + "typeDefinition": { + "typeSpecification": { + "name": { + "literal": "Timestamp", + "variable": false + }, + "nestedTypes": [] + }, + "attributes": [] + } + }, + "sections": [] + }, + "class": "property" + } + ] + } + ] + }, + { + "element": "dataStructure", + "name": { + "literal": "Timestamp Clone", + "variable": false + }, + "typeDefinition": { + "typeSpecification": { + "name": "number", + "nestedTypes": [] + }, + "attributes": [] + }, + "sections": [ + { + "class": "blockDescription", + "content": "A clone of timestamp to be used for testing\n" + } + ] + } + ] + } + ] +} diff --git a/test/unit/drafter-test.coffee b/test/unit/drafter-test.coffee index 06b64f7..83fe9a9 100644 --- a/test/unit/drafter-test.coffee +++ b/test/unit/drafter-test.coffee @@ -1,4 +1,5 @@ {assert} = require 'chai' +fs = require 'fs' Drafter = require '../../src/drafter' @@ -11,3 +12,14 @@ describe 'Drafter Class', -> assert.isNull error assert.ok result.ast done() + + it 'parses and expands a blueprint', (done) -> + drafter = new Drafter + + drafter.make fs.readFileSync('./test/fixtures/dataStructures.apib', 'utf8'), (error, result) -> + assert.isNull error + assert.ok result.ast + + assert.deepEqual result.ast, require '../fixtures/dataStructures.ast.json' + + done()