diff --git a/odata-parser.pegjs b/odata-parser.pegjs index 6e9b8d5..6eb8ed5 100644 --- a/odata-parser.pegjs +++ b/odata-parser.pegjs @@ -107,7 +107,7 @@ OData = { return model } / '/$metadata' { return { resource: '$metadata' } } - / '/' + / Slash { return { resource: '$serviceroot' } } QueryOptions = @@ -134,7 +134,7 @@ Dollar '$ query options' = / '%24' ParameterAliasOption = - '@' name:Text '=' + AtSign name:Text Equals value:( n:Number { return [ 'Real', n ] } @@ -154,12 +154,12 @@ ParameterAliasOption = } OperationParam = - name:Text '=' value:Text + name:Text Equals value:Text { return { name, value } } SortOption = 'orderby=' - properties:SortProperty|1..,','| + properties:SortProperty|1..,Comma| { return { name: '$orderby', value: { properties } } } SortProperty = @@ -209,7 +209,7 @@ ExpandOption = SelectOption = 'select=' value:( - '*' + Asterisk / properties:PropertyPathList { return { properties } } ) @@ -235,9 +235,9 @@ FormatOption = 'format=' type:ContentType @( - ';' + Semicolon 'odata.'? - 'metadata=' + 'metadata' Equals metadata:( 'none' / 'minimal' @@ -299,7 +299,7 @@ FilterByValue = / Primitive Primitive = - '(' spaces @Primitive spaces ')' + ParenOpen spaces @Primitive spaces ParenClose / QuotedTextBind / NumberBind / BooleanBind @@ -310,7 +310,7 @@ Primitive = / PropertyPath GroupedPrecedenceExpression = - '(' spaces @FilterByExpression spaces ')' + ParenOpen spaces @FilterByExpression spaces ParenClose FilterByOperand = spaces @@ -337,13 +337,13 @@ FilterNegateExpression = boundary @( FilterByValue - / '(' spaces @FilterByExpression spaces ')' + / ParenOpen spaces @FilterByExpression spaces ParenClose ) GroupedPrimitive = - '(' spaces - @Primitive|1..,',' spaces| - ')' + ParenOpen spaces + @Primitive|1..,Comma spaces| + ParenClose FilterMethodCallExpression = methodName:( @@ -379,15 +379,15 @@ FilterMethodCallExpression = / 'trim' / 'year' ) - '(' + ParenOpen spaces args:( - @FilterByExpression|1..,spaces ',' spaces| + @FilterByExpression|1..,spaces Comma spaces| spaces / '' { return [] } ) &{ return args.length === methods[methodName] || (Array.isArray(methods[methodName]) && methods[methodName].includes(args.length)) } - ')' + ParenClose { return [ 'call', { args, method: methodName } ] } LambdaMethodCall = @@ -395,50 +395,50 @@ LambdaMethodCall = 'any' / 'all' ) - '(' + ParenOpen spaces identifier:ResourceName - ':' + Colon expression:FilterByExpression spaces - ')' + ParenClose { return { expression, identifier, method: name } } ResourceMethodCall = methodName:ResourceName - '(' + ParenOpen spaces args:( - @FilterByExpression|1..,spaces ',' spaces| + @FilterByExpression|1..,spaces Comma spaces| spaces / '' { return [] } ) - ')' + ParenClose { return [ 'call', { args, method: methodName } ] } PropertyPathList = - PropertyPath|1..,','| + PropertyPath|1..,Comma| PropertyPath = resource:ResourceName property:( - '/' + Slash @PropertyPath )? countOptions:( '/$count' optionsObj:( - '(' + ParenOpen @( Dollar option:FilterByOption { return CollapseObjectArray([option]) } ) - ')' + ParenClose )? { return { count: true, options: optionsObj } } )? { return { name: resource, property, ...countOptions } } ExpandPropertyPathList = - ExpandPropertyPath|1..,','| + ExpandPropertyPath|1..,Comma| ExpandPropertyPath = resource:ResourceName count:( @@ -446,22 +446,22 @@ ExpandPropertyPath = { return true } )? optionsObj:( - '(' + ParenOpen @( options:QueryOption|1..,[&;]| { return CollapseObjectArray(options) } / '' { return {} } ) - ')' + ParenClose )? next:( - '/' + Slash @PropertyPath )? { return { name: resource, property: next, count, options: optionsObj} } LambdaPropertyPath = resource:ResourceName - '/' + Slash @( next:LambdaPropertyPath { return { name: resource, property: next } } / lambda:LambdaMethodCall @@ -470,14 +470,14 @@ LambdaPropertyPath = { return { name: resource, method } } ) Key = - '(' + ParenOpen @( KeyBind - / keyBinds:NamedKeyBind|1..,','| + / keyBinds:NamedKeyBind|1..,Comma| { return CollapseObjectArray(keyBinds) } ) - ')' + ParenClose NamedKeyBind = - name:ResourceName '=' value:KeyBind + name:ResourceName Equals value:KeyBind { return { name, value }} KeyBind = NumberBind @@ -490,7 +490,7 @@ Links = PathSegment = result:( - '/' + Slash result:( resource:ResourceName { return { resource } } @@ -523,7 +523,7 @@ PathSegment = )? { return result } SubPathSegment = - '/' + Slash result:( resource: ResourceName { return { resource } } @@ -645,11 +645,70 @@ Text = Sign = '+' + / '%2b' + { return '+' } / '%2B' { return '+' } / '-' / '' +Slash = + '/' + / '%2f' + { return '/' } + / '%2F' + { return '/' } + +Asterisk = + '*' + / '%2a' + { return '*' } + / '%2A' + { return '*' } + +Equals = + '=' + / '%3d' + { return '=' } + / '%3D' + { return '=' } + +Comma = + ',' + / '%2c' + { return ',' } + / '%2C' + { return ',' } + +Semicolon = + ';' + / '%3b' + { return ';' } + / '%3B' + { return ';' } + +AtSign = + '@' + / '%40' + { return '@' } + +Colon = + ':' + / '%3a' + { return ':' } + / '%3A' + { return ':' } + +ParenOpen = + '(' + / '%28' + { return '(' } + +ParenClose = + ')' + / '%29' + { return ')' } + // TODO: This should really be done treating everything the same, but for now this hack should allow FF to work passably. Apostrophe = '\'' @@ -667,7 +726,7 @@ QuotedText = { return decodeURIComponent(text.join('')) } ParameterAlias = - '@' param:ResourceName + AtSign param:ResourceName { return { bind: '@' + param } } NumberBind = @@ -709,7 +768,7 @@ QuotedTextBind = { return Bind('Text', t) } boundary = - ( &'(' + ( &ParenOpen / space+ ) diff --git a/test/encoding.js b/test/encoding.js index a4a92f1..3269ce0 100644 --- a/test/encoding.js +++ b/test/encoding.js @@ -1,6 +1,7 @@ import test from './test'; import * as assert from 'assert'; import { SyntaxError } from '../odata-parser'; +import { expect } from 'chai'; describe('Encoding', function () { test('foo=hello%20world', (result) => @@ -13,4 +14,107 @@ describe('Encoding', function () { assert(result instanceof SyntaxError); }), ); + + test( + `%24filter=child%2fany%28long_name%3Along_name%2Fname eq %27cake%27%29`, + ['cake'], + function (result, err) { + if (err) { + throw err; + } + + it('A filter should be present from encoded `(`, `)`, `:`, and `/`', () => + assert.notEqual(result.options.$filter, null)); + + it('Filter should be on the child resource from encoded `(`, `)`, `:`, and `/`', () => + assert.equal(result.options.$filter.name, 'child')); + }, + ); + + test('$expand=Products%2cSuppliers%2CCars', function (result) { + it('has an $expand value from encoded comma', () => { + assert.notEqual(result.options.$expand, null); + }); + it('has a resource of Products from encoded comma', () => { + assert.equal(result.options.$expand.properties[0].name, 'Products'); + }); + it('has a resource of Suppliers from encoded comma', () => { + assert.equal(result.options.$expand.properties[1].name, 'Suppliers'); + }); + it('has a resource of Cars from encoded comma', () => { + assert.equal(result.options.$expand.properties[2].name, 'Cars'); + }); + }); + + test('$format=json%3bodata.metadata%3dnone', (result) => { + it('has a valid $format value from lowercase encoded semicolon and equal', () => { + assert.deepEqual(result.options.$format, { + type: 'json', + metadata: 'none', + }); + }); + }); + + test('$format=json%3Bodata.metadata%3Dnone', (result) => { + it('has a valid $format value from uppercase encoded semicolon and equal', () => { + assert.deepEqual(result.options.$format, { + type: 'json', + metadata: 'none', + }); + }); + }); + + test.raw( + '/model(%40id)?%40id=1', + undefined, + undefined, + { '@id': 1 }, + function (result) { + it('should have the resource specified from encoded @ sign', () => { + expect(result).to.have.property('resource').that.equals('model'); + }); + it('should have the key specified for the source from encoded @ sign', () => { + expect(result) + .to.have.property('key') + .that.has.property('bind') + .that.equals('@id'); + }); + }, + ); + + test.raw('/model(a%3d1,b%3D2)', [1, 2], function (result) { + it('should have the resource specified from encoded equals', () => { + expect(result).to.have.property('resource').that.equals('model'); + }); + it('should have the both keys specified from encoded equals', function () { + expect(result) + .to.have.property('key') + .that.has.property('a') + .that.has.property('bind') + .that.equals(0); + expect(result) + .to.have.property('key') + .that.has.property('b') + .that.has.property('bind') + .that.equals(1); + }); + }); + + test('$select=%2a', function (result) { + it('has a $select value from lowercase encoded asterisk', () => { + assert.notEqual(result.options.$select, null); + }); + it('property name is * from lowercase encoded asterisk', () => { + assert.equal(result.options.$select, '*'); + }); + }); + + test('$select=%2A', function (result) { + it('has a $select value from uppercase encoded asterisk', () => { + assert.notEqual(result.options.$select, null); + }); + it('property name is * from uppercase encoded asterisk', () => { + assert.equal(result.options.$select, '*'); + }); + }); });