This commit is contained in:
Andrew Miner
2016-04-13 22:31:11 -07:00
parent 14278c81df
commit 31f6e12844
15 changed files with 386 additions and 297 deletions
+65 -19
View File
@@ -6,49 +6,73 @@
# #
LexicalAnalyzer = require './lexical_analyzer' LexicalAnalyzer = require './lexical_analyzer'
ParserState = require './parser_state' ModPack = require '../game/mod_pack'
ParserData = require './parser_data'
######################################################################################################################## ########################################################################################################################
module.exports = class Parser module.exports = class Parser
constructor: (state, fileName, rawText)-> constructor: (modPack, fileName, rawText)->
if not state? then throw new Error 'state is required'
@_extensions = [] @_extensions = []
@_fileName = fileName @_fileName = fileName
@_lexer = new LexicalAnalyzer fileName, rawText @_lexer = new LexicalAnalyzer fileName, rawText
@_state = state @_modPack = new ModPack
@_data = new ParserData
modPackData = @_data.create {}, 'modPack'
modPackData.instance = @_modPack
@_loadSchema() @_loadSchema()
# Public Methods ############################################################################### # Public Methods ###############################################################################
processNextCommand: w.lift -> next: ->
command = @_lexer.next() if not @_lexer.isFinished
return unless command? @_processNextCommand()
else if not @_data.isValidated
for extension in @_extensions @_validateNextEntry()
continue unless extension.accepts command else if not @_data.isBuilt
extension.execute command @_buildNextEntry()
return else if not @_data.isAssembled
@_assembleNextEntry()
throw new Error "unknown command: #{command.name}"
# Property Methods ############################################################################# # Property Methods #############################################################################
getIsFinished: -> getIsFinished: ->
return @_lexer.isFinished return @_lexer.isFinished and @_data.isValidated and @_data.isBuilt and @_data.isAssembled
getState: -> getData: ->
return @_state return @_data
getModPack: ->
return @_modPack
Object.defineProperties @prototype, Object.defineProperties @prototype,
data: { get:@::getData }
isFinished: { get:@::getIsFinished } isFinished: { get:@::getIsFinished }
state: { get:@::getState } modPack: { get:@::getModPack }
# Private Methods ############################################################################## # Private Methods ##############################################################################
_assembleNextEntry: ->
entry = @_data.popNextToAssemble()
return unless entry?
for extensions in @_extensions
continue unless extension.canAssemble entry
extension.assemble entry
return
_buildNextEntry: ->
entry = @_data.popNextToBuild()
return unless entry?
for extensions in @_extensions
continue unless extension.canBuild entry
extension.build entry
return
_loadSchema: -> _loadSchema: ->
command = @_lexer.next() command = @_lexer.next()
if command.name isnt 'schema' then throw new Error "#{@_fileName} must start with the \"schema\" command" if command.name isnt 'schema' then throw new Error "#{@_fileName} must start with the \"schema\" command"
@@ -56,11 +80,33 @@ module.exports = class Parser
schema = command.argText schema = command.argText
if schema is '1' if schema is '1'
@_use new require './parser_extensions/current_pe_v1' @_use new require './parser_extensions/current_pe_v1'
@_use new require './parser_extensions/item_pe_v1'
@_use new require './parser_extensions/mod_pe_v1' @_use new require './parser_extensions/mod_pe_v1'
@_use new require './parser_extensions/mod_version_pe_v1' @_use new require './parser_extensions/mod_version_pe_v1'
@_use new require './parser_extensions/recipe_pe_v1'
else else
throw new Error "unknown schema version: #{schema}" throw new Error "unknown schema version: #{schema}"
_processNextCommand: ->
command = @_lexer.next()
return unless command?
for extension in @_extensions
continue unless extension.canExecute command
extension.execute command
return
@_data.addError command, "unknown command: #{command.name}"
_use: (parserExtension)-> _use: (parserExtension)->
parserExtension.state = @_state parserExtension.state = @_state
@_extensions.push parserExtension @_extensions.push parserExtension
_validateNextEntry: ->
entry = @_data.popNextToValidate()
return unless entry?
for extensions in @_extensions
continue unless extension.canValidate entry
extension.validate entry
return
@@ -1,5 +1,5 @@
# #
# Crafting Guide - parser_state.coffee # Crafting Guide - parser_data.coffee
# #
# Copyright © 2014-2016 by Redwood Labs # Copyright © 2014-2016 by Redwood Labs
# All rights reserved. # All rights reserved.
@@ -7,11 +7,15 @@
######################################################################################################################## ########################################################################################################################
module.exports = class ParserState module.exports = class ParserData
constructor: -> constructor: ->
@clear() @clear()
@_toAssemble = []
@_toBuild = []
@_toValidate = []
# Public Methods ############################################################################### # Public Methods ###############################################################################
addError: (command, message)-> addError: (command, message)->
@@ -24,6 +28,11 @@ module.exports = class ParserState
typeData = @_findOrCreateType type typeData = @_findOrCreateType type
itemData = @_current = typeData['current'] = typeData[id] = id:id, command:command, type:type itemData = @_current = typeData['current'] = typeData[id] = id:id, command:command, type:type
@_toAssemble.push itemData
@_toBuild.push itemData
@_toValidate.push itemData
return itemData return itemData
clear: -> clear: ->
@@ -31,20 +40,6 @@ module.exports = class ParserState
@_data = {} @_data = {}
@_errors = [] @_errors = []
each: (callback)->
for type, typeData of @_data
for id, itemData of typeData
continue if id is 'current'
continue if id is 'type'
callback itemData
eachOfType: (type, callback)->
typeData = @_findOrCreateType type
for id, itemData of typeData
continue if id is 'current'
continue if id is 'type'
callback itemData
get: (type, id)-> get: (type, id)->
typeData = @_findOrCreateType type typeData = @_findOrCreateType type
itemData = typeData[id] or null itemData = typeData[id] or null
@@ -57,13 +52,35 @@ module.exports = class ParserState
current = typeData['current'] or null current = typeData['current'] or null
return current return current
popNextToAssemble: ->
return @_toAssemble.pop() or null
popNextToBuild: ->
return @_toBuild.pop() or null
popNextToValidate: ->
return @_toValidate.pop() or null
# Property Methods ############################################################################# # Property Methods #############################################################################
getErrors: -> getErrors: ->
return @_errors[..] return @_errors[..]
getIsAssembled: ->
return @_toAssemble.length > 0
getIsBuilt: ->
return @_toBuild.length > 0
getIsValidated: ->
return @_toValidate.length > 0
Object.defineProperties @prototype, Object.defineProperties @prototype,
errors: { get:@::getErrors } errors: { get:@::getErrors }
isAssembled: { get:@::getIsAssembled }
isBuilt: { get:@::getIsBuilt }
isValidated: { get:@::getIsValidated }
# Private Methods ############################################################################## # Private Methods ##############################################################################
@@ -0,0 +1,55 @@
#
# Crafting Guide - parser_data.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserCommand = require './parser_command'
ParserData = require './parser_data'
########################################################################################################################
command = state = null
########################################################################################################################
describe 'parser_data.coffee', ->
beforeEach ->
state = new ParserData
command = new ParserCommand name:'alpha', fileName:'file.cg', lineNumber:1
describe 'create', ->
it 'returns an object with the given id', ->
alpha = state.create command, 'alpha', 0
alpha.id.should.equal 0
alpha.command.location.should.equal 'file.cg:1'
it 'returns an object with a newly created id when none is given', ->
alpha = state.create command, 'alpha'
alpha.id.should.not.be.null
alpha.id.should.match /^alpha-[0-9]+$/
alpha.command.location.should.equal 'file.cg:1'
describe 'get', ->
it 'returns a previously created item', ->
alpha1 = state.create command, 'alpha', 0
alpha2 = state.get 'alpha', 0
alpha1.should.equal alpha2
it 'returns null when no such item exists', ->
alpha = state.create command, 'alpha', 0
expect(state.get 'alpha', 1).to.be.null
expect(state.get 'bravo', 0).to.be.null
describe 'getCurrent', ->
it 'returns the new item each time one is created', ->
alpha1 = state.create command, 'alpha', 0
state.getCurrent('alpha').id.should.equal 0
alpha2 = state.create command, 'alpha', 1
state.getCurrent('alpha').id.should.equal 1
@@ -9,55 +9,77 @@
module.exports = class ParserExtension module.exports = class ParserExtension
constructor: (state)-> constructor: (data, type, commandFunctions)->
@state = state @data = data
@_commandFunctions = commandFunctions or {}
@_type = type or null
# Public Methods ############################################################################### # Public Methods ###############################################################################
accepts: (command)-> canAssemble: (entry)->
return @_findCommandMethod(command)? return entry.type is @_type
execute: w.lift (command)-> canBuild: (entry)->
if not @_state? then throw new Error '@state must be defined before executing commands' return entry.type is @_type
method = @_findCommandMethod command canExecute: (command)->
method.call this, command _.isFunction @_commandFunctions[command.name]
canValidate: (entry)->
return entry.type is @_type
execute: (command)->
if not @_data? then throw new Error '@data must be defined before executing commands'
func = @_commandFunctions[command.name]
if not func then throw new Error "#{@constructor.name} cannot handle command: #{command.name}"
func.call this, command
return this
# Error Checking Helpers ####################################################################### # Error Checking Helpers #######################################################################
alreadyExists: (command, type, id)-> alreadyExists: (command, type, id)->
obj = @state.get(type, id) obj = @data.get(type, id)
if obj? if obj?
@state.addError command, "a #{type} called #{id} has already been declared" @data.addError command, "a #{type} called #{id} has already been declared"
return true return true
return false return false
duplicateField: (command, obj, field)-> duplicateField: (command, obj, field)->
if obj[field]? if obj[field]?
@state.addError command, "duplicate #{field}" @data.addError command, "duplicate #{field}"
return true return true
return false return false
missingCurrent: (command, type)-> missingCurrent: (command, type)->
obj = @state.getCurrent type obj = @data.getCurrent type
if not obj? if not obj?
@state.addError command, "you must declare a #{type} before using #{command.name}" @data.addError command, "you must declare a #{type} before using #{command.name}"
return true return true
return false return false
missingArgs: (command)-> missingArgs: (command)->
if not command.args or command.args.length is 0 if not command.args or command.args.length is 0
@state.addError command, "#{command.name} requires at least one item" @data.addError command, "#{command.name} requires at least one item"
return true return true
return false return false
missingArgText: (command)-> missingArgText: (command)->
if command.argText.length is 0 if command.argText.length is 0
@state.addError command, "#{command.name} cannot be empty" @data.addError command, "#{command.name} cannot be empty"
return true
return false
tooFewArguments: (command, minimum)->
if command.args.length < minimum
@data.addError command, "#{command.name} requires at least #{minimum} arguments"
return true return true
return false return false
@@ -71,7 +93,7 @@ module.exports = class ParserExtension
when "yes" then return true when "yes" then return true
when "no" then return false when "no" then return false
@state.addError command, "#{command.name} requires \"yes\" or \"no\"" @data.addError command, "#{command.name} requires \"yes\" or \"no\""
return null return null
parseInt: (command, text)-> parseInt: (command, text)->
@@ -86,20 +108,43 @@ module.exports = class ParserExtension
# Property Methods ############################################################################# # Property Methods #############################################################################
getState: -> getData: ->
return @_state return @_data
setState: (state)-> setData: (data)->
if not state? then throw new Error 'state cannot be undefined' if not data? then throw new Error 'data cannot be undefined'
@_state = state @_data = data
Object.defineProperties @prototype, Object.defineProperties @prototype,
state: { get:@::getState, set:@::setState } data: { get:@::getData, set:@::setData }
# Overridable Methods ##########################################################################
assmeble: (entry)->
throw new Error "#{@constructor.name} must override the `assemble` method"
build: (entry)->
throw new Error "#{@constructor.name} must override the `build` method"
validate: (entry)->
throw new Error "#{@constructor.name} must override the `validate` method"
# Private Methods ############################################################################## # Private Methods ##############################################################################
_findBuildMethod: (entry)->
methodName = "_build_#{entry.type}"
method = this[methodName]
return null unless _.isFunction method
return method
_findCommandMethod: (command)-> _findCommandMethod: (command)->
methodName = "_command_#{command.name}" methodName = "_command_#{command.name}"
method = this[methodName] method = this[methodName]
return null unless _.isFunction(method) return null unless _.isFunction method
return method
_findValidateMethod: (command)->
methodName = "_validate_#{command.name}"
method = this[methodName]
return null unless _.isFunction method
return method return method
@@ -11,24 +11,30 @@ ParserExtension = require '../parser_extension'
module.exports = class CurrentParserExtensionV1 extends ParserExtension module.exports = class CurrentParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, null,
documentationUrl: @_command_documentationUrl
description: @_command_description
name: @_command_name
# Command Methods ############################################################################## # Command Methods ##############################################################################
_command_documentationUrl: (command)-> _command_documentationUrl: (command)->
current = @state.getCurrent() current = @data.getCurrent()
return if @duplicateField command, current, 'documentationUrl' return if @duplicateField command, current, 'documentationUrl'
return if @missingArgText command return if @missingArgText command
current.documentationUrl = command.argText current.documentationUrl = command.argText
_command_description: (command)-> _command_description: (command)->
current = @state.getCurrent() current = @data.getCurrent()
return if @duplicateField command, current, 'description' return if @duplicateField command, current, 'description'
return if @missingArgText command return if @missingArgText command
current.description = command.argText current.description = command.argText
_command_name: (command)-> _command_name: (command)->
current = @state.getCurrent() current = @data.getCurrent()
return if @duplicateField command, current, 'name' return if @duplicateField command, current, 'name'
return if @missingArgText command return if @missingArgText command
@@ -6,7 +6,7 @@
# #
CurrentParserExtensionV1 = require './current_pe_v1' CurrentParserExtensionV1 = require './current_pe_v1'
ParserState = require '../parser_state' ParserData = require '../parser_data'
######################################################################################################################## ########################################################################################################################
@@ -17,7 +17,7 @@ parser = state = null
describe 'current_pe_v1.coffee', -> describe 'current_pe_v1.coffee', ->
beforeEach -> beforeEach ->
state = new ParserState state = new ParserData
parser = new CurrentParserExtensionV1 state parser = new CurrentParserExtensionV1 state
state.create {}, 'alpha' state.create {}, 'alpha'
@@ -26,22 +26,19 @@ describe 'current_pe_v1.coffee', ->
it 'assigns to the current object', -> it 'assigns to the current object', ->
parser.execute name:'documentationUrl', argText:'bravo' parser.execute name:'documentationUrl', argText:'bravo'
.then -> state.getCurrent().documentationUrl.should.equal 'bravo'
state.getCurrent().documentationUrl.should.equal 'bravo' state.errors.should.eql []
state.errors.should.eql []
describe 'description', -> describe 'description', ->
it 'assigns to the current object', -> it 'assigns to the current object', ->
parser.execute name:'description', argText:'bravo' parser.execute name:'description', argText:'bravo'
.then -> state.getCurrent().description.should.equal 'bravo'
state.getCurrent().description.should.equal 'bravo' state.errors.should.eql []
state.errors.should.eql []
describe 'name', -> describe 'name', ->
it 'assigns to the current object', -> it 'assigns to the current object', ->
parser.execute name:'name', argText:'bravo' parser.execute name:'name', argText:'bravo'
.then -> state.getCurrent().name.should.equal 'bravo'
state.getCurrent().name.should.equal 'bravo' state.errors.should.eql []
state.errors.should.eql []
@@ -11,6 +11,22 @@ ParserExtension = require '../parser_extension'
module.exports = class ItemParserExtensionV1 extends ParserExtension module.exports = class ItemParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'item',
gatherable: @_command_gatherable
item: @_command_item
# ParserExtensions Overrides ###################################################################
assmeble: (entry)->
throw new Error "#{@constructor.name} must override the `assemble` method"
build: (entry)->
entry.instance = new
throw new Error "#{@constructor.name} must override the `build` method"
validate: (entry)-> # do nothing
# Command Methods ############################################################################## # Command Methods ##############################################################################
_command_gatherable: (command)-> _command_gatherable: (command)->
@@ -18,7 +34,7 @@ module.exports = class ItemParserExtensionV1 extends ParserExtension
return if @missingCurrent command, 'item' return if @missingCurrent command, 'item'
item = @state.getCurrent 'item' item = @data.getCurrent 'item'
return if @duplicateField command, 'gatherable', item return if @duplicateField command, 'gatherable', item
gatherable = @parseBoolean command, command.argText gatherable = @parseBoolean command, command.argText
@@ -31,10 +47,19 @@ module.exports = class ItemParserExtensionV1 extends ParserExtension
return if @missingCurrent command, 'modVersion' return if @missingCurrent command, 'modVersion'
return if @alreadyExists command, 'item', command.argText return if @alreadyExists command, 'item', command.argText
item = @state.create command, 'item', command.argText item = @data.create command, 'item', command.argText
item.name = command.argText item.name = command.argText
group = @state.getCurrent 'itemGroup' group = @data.getCurrent 'itemGroup'
if group? then item.group = group if group? then item.group = group
item.modVersion = @state.getCurrent 'modVersion' item.modVersion = @data.getCurrent 'modVersion'
_command_video: (command)->
return if @missingArgs command
return if @missingCurrent command, 'item'
return if @tooFewArguments command, 2
item = @data.getCurrent 'item'
item.videos ?= []
item.videos.push youTubeId:command.args[0], caption:command.args[1..].join(', ')
@@ -6,41 +6,39 @@
# #
ItemParserExtensionV1 = require './item_pe_v1' ItemParserExtensionV1 = require './item_pe_v1'
ParserState = require '../parser_state' ParserData = require '../parser_data'
######################################################################################################################## ########################################################################################################################
parser = state = null parser = data = null
######################################################################################################################## ########################################################################################################################
describe 'item_pe_v1.coffee', -> describe 'item_pe_v1.coffee', ->
beforeEach -> beforeEach ->
state = new ParserState data = new ParserData
parser = new ItemParserExtensionV1 state parser = new ItemParserExtensionV1 data
state.create {}, 'modVersion', 0 data.create {}, 'modVersion', 0
describe 'gatherable', -> describe 'gatherable', ->
it 'assigns to the current item', -> it 'assigns to the current item', ->
state.create {}, 'item', 1 data.create {}, 'item', 1
parser.execute name:'gatherable', argText:'yes' parser.execute name:'gatherable', argText:'yes'
.then -> data.getCurrent('item').gatherable.should.be.true
state.getCurrent('item').gatherable.should.be.true data.errors.should.eql []
state.errors.should.eql []
describe 'item', -> describe 'item', ->
it 'creates a new item', -> it 'creates a new item', ->
state.create {}, 'itemGroup', 2 data.create {}, 'itemGroup', 2
parser.execute name:'item', argText:'alpha' parser.execute name:'item', argText:'alpha'
.then -> item = data.getCurrent 'item'
item = state.getCurrent 'item' item.id.should.equal 'alpha'
item.id.should.equal 'alpha' item.name.should.equal 'alpha'
item.name.should.equal 'alpha' item.modVersion.id.should.equal 0
item.modVersion.id.should.equal 0 item.group.id.should.equal 2
item.group.id.should.equal 2
@@ -11,12 +11,19 @@ ParserExtension = require '../parser_extension'
module.exports = class ModParserExtensionV1 extends ParserExtension module.exports = class ModParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'mod',
author: @_command_author
downloadUrl: @_command_downloadUrl
homePageUrl: @_command_homePageUrl
mod: @_command_mod
# Command Methods ############################################################################## # Command Methods ##############################################################################
_command_author: (command)-> _command_author: (command)->
return if @missingCurrent command, 'mod' return if @missingCurrent command, 'mod'
mod = @state.getCurrent 'mod' mod = @data.getCurrent 'mod'
return if @duplicateField command, mod, 'author' return if @duplicateField command, mod, 'author'
return if @missingArgText command return if @missingArgText command
@@ -25,7 +32,7 @@ module.exports = class ModParserExtensionV1 extends ParserExtension
_command_downloadUrl: (command)-> _command_downloadUrl: (command)->
return if @missingCurrent command, 'mod' return if @missingCurrent command, 'mod'
mod = @state.getCurrent 'mod' mod = @data.getCurrent 'mod'
return if @duplicateField command, mod, 'downloadUrl' return if @duplicateField command, mod, 'downloadUrl'
return if @missingArgText command return if @missingArgText command
@@ -34,7 +41,7 @@ module.exports = class ModParserExtensionV1 extends ParserExtension
_command_homePageUrl: (command)-> _command_homePageUrl: (command)->
return if @missingCurrent command, 'mod' return if @missingCurrent command, 'mod'
mod = @state.getCurrent 'mod' mod = @data.getCurrent 'mod'
return if @duplicateField command, mod, 'homePageUrl' return if @duplicateField command, mod, 'homePageUrl'
return if @missingArgText command return if @missingArgText command
@@ -44,4 +51,4 @@ module.exports = class ModParserExtensionV1 extends ParserExtension
return if @missingArgText command return if @missingArgText command
return if @alreadyExists command, 'mod', command.argText return if @alreadyExists command, 'mod', command.argText
@state.create command, 'mod', command.argText @data.create command, 'mod', command.argText
@@ -6,50 +6,46 @@
# #
ModParserExtensionV1 = require './mod_pe_v1' ModParserExtensionV1 = require './mod_pe_v1'
ParserState = require '../parser_state' ParserData = require '../parser_data'
######################################################################################################################## ########################################################################################################################
parser = state = null parser = data = null
######################################################################################################################## ########################################################################################################################
describe 'mod_pe_v1.coffee', -> describe 'mod_pe_v1.coffee', ->
beforeEach -> beforeEach ->
state = new ParserState data = new ParserData
parser = new ModParserExtensionV1 state parser = new ModParserExtensionV1 data
state.create {}, 'mod', 0 data.create {}, 'mod', 0
describe 'author', -> describe 'author', ->
it 'assigns to the current mod', -> it 'assigns to the current mod', ->
parser.execute name:'author', argText:'alpha' parser.execute name:'author', argText:'alpha'
.then -> data.getCurrent('mod').author.should.equal 'alpha'
state.getCurrent('mod').author.should.equal 'alpha' data.errors.should.eql []
state.errors.should.eql []
describe 'downloadUrl', -> describe 'downloadUrl', ->
it 'assigns to the current mod', -> it 'assigns to the current mod', ->
parser.execute name:'downloadUrl', argText:'alpha' parser.execute name:'downloadUrl', argText:'alpha'
.then -> data.getCurrent('mod').downloadUrl.should.equal 'alpha'
state.getCurrent('mod').downloadUrl.should.equal 'alpha' data.errors.should.eql []
state.errors.should.eql []
describe 'homePageUrl', -> describe 'homePageUrl', ->
it 'assigns to the current mod', -> it 'assigns to the current mod', ->
parser.execute name:'homePageUrl', argText:'alpha' parser.execute name:'homePageUrl', argText:'alpha'
.then -> data.getCurrent('mod').homePageUrl.should.equal 'alpha'
state.getCurrent('mod').homePageUrl.should.equal 'alpha' data.errors.should.eql []
state.errors.should.eql []
describe 'mod', -> describe 'mod', ->
it 'creates a new mod', -> it 'creates a new mod', ->
parser.execute name:'mod', argText:'alpha' parser.execute name:'mod', argText:'alpha'
.then -> mod = data.getCurrent 'mod'
mod = state.getCurrent 'mod' mod.id.should.equal 'alpha'
mod.id.should.equal 'alpha'
@@ -11,18 +11,23 @@ ParserExtension = require '../parser_extension'
module.exports = class ModVersionParserExtensionV1 extends ParserExtension module.exports = class ModVersionParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'modVersion',
group: @_command_group
version: @_command_version
# Command Methods ############################################################################## # Command Methods ##############################################################################
_command_group: (command)-> _command_group: (command)->
return if @missingArgText command return if @missingArgText command
return if @missingCurrent command, 'modVersion' return if @missingCurrent command, 'modVersion'
group = @state.create command, 'itemGroup', command.argText group = @data.create command, 'itemGroup', command.argText
group.modVersion = @state.getCurrent 'modVersion' group.modVersion = @data.getCurrent 'modVersion'
_command_version: (command)-> _command_version: (command)->
return if @missingArgText command return if @missingArgText command
return if @missingCurrent command, 'mod' return if @missingCurrent command, 'mod'
modVersion = @state.create command, 'modVersion', command.argText modVersion = @data.create command, 'modVersion', command.argText
modVersion.mod = @state.getCurrent 'mod' modVersion.mod = @data.getCurrent 'mod'
@@ -6,38 +6,36 @@
# #
ModVersionParserExtensionV1 = require './mod_version_pe_v1' ModVersionParserExtensionV1 = require './mod_version_pe_v1'
ParserState = require '../parser_state' ParserData = require '../parser_data'
######################################################################################################################## ########################################################################################################################
parser = state = null parser = data = null
######################################################################################################################## ########################################################################################################################
describe 'mod_version_pe_v1.coffee', -> describe 'mod_version_pe_v1.coffee', ->
beforeEach -> beforeEach ->
state = new ParserState data = new ParserData
parser = new ModVersionParserExtensionV1 state parser = new ModVersionParserExtensionV1 data
state.create {}, 'mod', 0 data.create {}, 'mod', 0
describe 'group', -> describe 'group', ->
it 'creates a new item group', -> it 'creates a new item group', ->
state.create {}, 'modVersion', 1 data.create {}, 'modVersion', 1
parser.execute name:'group', argText:'alpha' parser.execute name:'group', argText:'alpha'
.then -> itemGroup = data.getCurrent('itemGroup')
itemGroup = state.getCurrent('itemGroup') itemGroup.id.should.equal 'alpha'
itemGroup.id.should.equal 'alpha' itemGroup.modVersion.id.should.equal 1
itemGroup.modVersion.id.should.equal 1
describe 'version', -> describe 'version', ->
it 'creates a new mod version', -> it 'creates a new mod version', ->
parser.execute name:'version', argText:'alpha' parser.execute name:'version', argText:'alpha'
.then -> modVersion = data.getCurrent 'modVersion'
modVersion = state.getCurrent 'modVersion' modVersion.id.should.equal 'alpha'
modVersion.id.should.equal 'alpha' modVersion.mod.id.should.equal 0
modVersion.mod.id.should.equal 0
@@ -11,6 +11,17 @@ ParserExtension = require '../parser_extension'
module.exports = class RecipeParserExtensionV1 extends ParserExtension module.exports = class RecipeParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'recipe',
extras: @_command_extras
input: @_command_input
pattern: @_command_pattern
recipe: @_command_recipe
quantity: @_command_quantity
tools: @_command_tools
# Class Methods ################################################################################
@::PATTERN = /[0-9. ]+/ @::PATTERN = /[0-9. ]+/
@::STACK = /^([0-9]+) +(.+)$/ @::STACK = /^([0-9]+) +(.+)$/
@@ -20,7 +31,7 @@ module.exports = class RecipeParserExtensionV1 extends ParserExtension
invalidPattern: (command)-> invalidPattern: (command)->
match = command.argText.match @PATTERN match = command.argText.match @PATTERN
if not match? if not match?
@state.addError command, "invalid pattern: \"#{command.argText}\"" @data.addError command, "invalid pattern: \"#{command.argText}\""
return true return true
return false return false
@@ -31,7 +42,7 @@ module.exports = class RecipeParserExtensionV1 extends ParserExtension
return if @missingCurrent command, 'recipe' return if @missingCurrent command, 'recipe'
return if @missingArgs command return if @missingArgs command
recipe = @state.getCurrent 'recipe' recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'extras', recipe return if @duplicateField command, 'extras', recipe
recipe.extras = command.args recipe.extras = command.args
@@ -40,7 +51,7 @@ module.exports = class RecipeParserExtensionV1 extends ParserExtension
return if @missingCurrent command, 'recipe' return if @missingCurrent command, 'recipe'
return if @missingArgs command return if @missingArgs command
recipe = @state.getCurrent 'recipe' recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'input', recipe return if @duplicateField command, 'input', recipe
recipe.input = (@_parseStack(arg) for arg in command.args) recipe.input = (@_parseStack(arg) for arg in command.args)
@@ -50,7 +61,7 @@ module.exports = class RecipeParserExtensionV1 extends ParserExtension
return if @missingArgText command return if @missingArgText command
return if @invalidPattern command return if @invalidPattern command
recipe = @state.getCurrent 'recipe' recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'pattern', recipe return if @duplicateField command, 'pattern', recipe
recipe.pattern = command.argText recipe.pattern = command.argText
@@ -58,8 +69,8 @@ module.exports = class RecipeParserExtensionV1 extends ParserExtension
_command_recipe: (command)-> _command_recipe: (command)->
return if @missingCurrent command, 'item' return if @missingCurrent command, 'item'
recipe = @state.create command, 'recipe' recipe = @data.create command, 'recipe'
recipe.item = @state.getCurrent 'item' recipe.item = @data.getCurrent 'item'
_command_quantity: (command)-> _command_quantity: (command)->
return if @missingCurrent command, 'recipe' return if @missingCurrent command, 'recipe'
@@ -68,7 +79,7 @@ module.exports = class RecipeParserExtensionV1 extends ParserExtension
value = @parseInt command, command.argText value = @parseInt command, command.argText
return unless value? return unless value?
recipe = @state.getCurrent 'recipe' recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'quantity', recipe return if @duplicateField command, 'quantity', recipe
recipe.quantity = value recipe.quantity = value
@@ -77,7 +88,7 @@ module.exports = class RecipeParserExtensionV1 extends ParserExtension
return if @missingCurrent command, 'recipe' return if @missingCurrent command, 'recipe'
return if @missingArgs command return if @missingArgs command
recipe = @state.getCurrent 'recipe' recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'tools', recipe return if @duplicateField command, 'tools', recipe
recipe.tools = (@_parseStack(arg) for arg in command.args) recipe.tools = (@_parseStack(arg) for arg in command.args)
@@ -6,109 +6,100 @@
# #
RecipeParserExtensionV1 = require './recipe_pe_v1' RecipeParserExtensionV1 = require './recipe_pe_v1'
ParserState = require '../parser_state' ParserData = require '../parser_data'
######################################################################################################################## ########################################################################################################################
parser = state = null parser = data = null
######################################################################################################################## ########################################################################################################################
describe 'recipe_pe_v1.coffee', -> describe 'recipe_pe_v1.coffee', ->
beforeEach -> beforeEach ->
state = new ParserState data = new ParserData
parser = new RecipeParserExtensionV1 state parser = new RecipeParserExtensionV1 data
state.create {}, 'item', 0 data.create {}, 'item', 0
describe 'extras', -> describe 'extras', ->
it 'assigns to the current recipe', -> it 'assigns to the current recipe', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'extras', args:['bravo'] parser.execute name:'extras', args:['bravo']
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.extras.should.eql ['bravo']
recipe.extras.should.eql ['bravo'] data.errors.should.eql []
state.errors.should.eql []
describe 'input', -> describe 'input', ->
it 'correctly reads a single item', -> it 'correctly reads a single item', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'input', args:['bravo'] parser.execute name:'input', args:['bravo']
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.input.should.eql [name:'bravo', quantity:1]
recipe.input.should.eql [name:'bravo', quantity:1] data.errors.should.eql []
state.errors.should.eql []
it 'correctly reads multiple items', -> it 'correctly reads multiple items', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'input', args:['bravo', 'charlie'] parser.execute name:'input', args:['bravo', 'charlie']
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.input.should.eql [{name:'bravo', quantity:1}, {name:'charlie', quantity:1}]
recipe.input.should.eql [{name:'bravo', quantity:1}, {name:'charlie', quantity:1}] data.errors.should.eql []
state.errors.should.eql []
it 'correctly reads a stack', -> it 'correctly reads a stack', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'input', args:['10 bravo'] parser.execute name:'input', args:['10 bravo']
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.input.should.eql [name:'bravo', quantity:10]
recipe.input.should.eql [name:'bravo', quantity:10] data.errors.should.eql []
state.errors.should.eql []
describe 'pattern', -> describe 'pattern', ->
it 'assigns to the current recipe', -> it 'assigns to the current recipe', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'pattern', argText:'00. 00. ...' parser.execute name:'pattern', argText:'00. 00. ...'
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.pattern.should.equal '00. 00. ...'
recipe.pattern.should.equal '00. 00. ...' data.errors.should.eql []
state.errors.should.eql []
it 'rejects an invalid pattern', -> it 'rejects an invalid pattern', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'pattern', argText:'alpha' parser.execute name:'pattern', argText:'alpha'
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' expect(recipe.pattern).to.be.undefined
expect(recipe.pattern).to.be.undefined (e.message for e in data.errors).should.eql ['invalid pattern: "alpha"']
(e.message for e in state.errors).should.eql ['invalid pattern: "alpha"']
describe 'recipe', -> describe 'recipe', ->
it 'creates a new recipe', -> it 'creates a new recipe', ->
parser.execute name:'recipe' parser.execute name:'recipe'
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.item.id.should.equal 0
recipe.item.id.should.equal 0 data.errors.should.eql []
state.errors.should.eql []
describe 'quantity', -> describe 'quantity', ->
it 'assigns to the current recipe', -> it 'assigns to the current recipe', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'quantity', argText:'42' parser.execute name:'quantity', argText:'42'
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.quantity.should.equal 42
recipe.quantity.should.equal 42 data.errors.should.eql []
state.errors.should.eql []
describe 'tools', -> describe 'tools', ->
it 'assigns to the current recipe', -> it 'assigns to the current recipe', ->
state.create {}, 'recipe' data.create {}, 'recipe'
parser.execute name:'tools', args:['bravo'] parser.execute name:'tools', args:['bravo']
.then -> recipe = data.getCurrent 'recipe'
recipe = state.getCurrent 'recipe' recipe.tools.should.eql [name:'bravo', quantity:1]
recipe.tools.should.eql [name:'bravo', quantity:1] data.errors.should.eql []
state.errors.should.eql []
@@ -1,108 +0,0 @@
#
# Crafting Guide - parser_state.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserCommand = require './parser_command'
ParserState = require './parser_state'
########################################################################################################################
command = state = null
########################################################################################################################
describe 'parser_state.coffee', ->
beforeEach ->
state = new ParserState
command = new ParserCommand name:'alpha', fileName:'file.cg', lineNumber:1
describe 'create', ->
it 'returns an object with the given id', ->
alpha = state.create command, 'alpha', 0
alpha.id.should.equal 0
alpha.command.location.should.equal 'file.cg:1'
it 'returns an object with a newly created id when none is given', ->
alpha = state.create command, 'alpha'
alpha.id.should.not.be.null
alpha.id.should.match /^alpha-[0-9]+$/
alpha.command.location.should.equal 'file.cg:1'
describe 'each', ->
it 'ignores the callback when there are no items', ->
items = []
state.each (item)-> items.push item
items.length.should.equal 0
it 'calls the callback once with a single item', ->
state.create command, 'alpha', 0
items = []
state.each (item)-> items.push item
items.length.should.equal 1
items[0].id.should.equal 0
it 'calls the callback for each item of a single type', ->
state.create command, 'alpha', 0
state.create command, 'alpha', 1
state.create command, 'alpha', 2
ids = []
state.each (item)-> ids.push item.id
ids.should.eql [0, 1, 2]
it 'calls the callback for each item across multiple types', ->
state.create command, 'alpha', 0
state.create command, 'alpha', 1
state.create command, 'bravo', 0
state.create command, 'bravo', 1
ids = []
state.each (item)-> ids.push "#{item.type}-#{item.id}"
ids.should.eql ['alpha-0', 'alpha-1', 'bravo-0', 'bravo-1']
describe 'eachOfType', ->
it 'ignores the callback if there are no items of that type', ->
state.create command, 'alpha', 0
state.create command, 'alpha', 1
ids = []
state.eachOfType 'bravo', (item)-> ids.push "#{item.type}-#{item.id}"
ids.length.should.equal 0
it 'calls the callback once for each item of the given type', ->
state.create command, 'alpha', 0
state.create command, 'alpha', 1
state.create command, 'bravo', 0
state.create command, 'bravo', 1
ids = []
state.eachOfType 'bravo', (item)-> ids.push "#{item.type}-#{item.id}"
ids.should.eql ['bravo-0', 'bravo-1']
describe 'get', ->
it 'returns a previously created item', ->
alpha1 = state.create command, 'alpha', 0
alpha2 = state.get 'alpha', 0
alpha1.should.equal alpha2
it 'returns null when no such item exists', ->
alpha = state.create command, 'alpha', 0
expect(state.get 'alpha', 1).to.be.null
expect(state.get 'bravo', 0).to.be.null
describe 'getCurrent', ->
it 'returns the new item each time one is created', ->
alpha1 = state.create command, 'alpha', 0
state.getCurrent('alpha').id.should.equal 0
alpha2 = state.create command, 'alpha', 1
state.getCurrent('alpha').id.should.equal 1