Add Razor Evolution Engine and primitives

This commit is contained in:
Ryan Nowak 2016-10-20 15:18:16 -07:00 committed by N. Taylor Mullen
parent 43caef488c
commit 6b9b75841f
24 changed files with 848 additions and 0 deletions

View File

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DefaultItemCollection : ItemCollection
{
private readonly Dictionary<object, object> _items;
public DefaultItemCollection()
{
_items = new Dictionary<object, object>();
}
public override object this[object key]
{
get
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
object value;
_items.TryGetValue(key, out value);
return value;
}
set
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
_items[key] = value;
}
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DefaultRazorCodeDocument : RazorCodeDocument
{
public DefaultRazorCodeDocument(RazorSourceDocument source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Source = source;
Items = new DefaultItemCollection();
}
public override ItemCollection Items { get; }
public override RazorSourceDocument Source { get; }
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DefaultRazorEngine : RazorEngine
{
public DefaultRazorEngine(IRazorEngineFeature[] features, IRazorEnginePhase[] phases)
{
if (features == null)
{
throw new ArgumentNullException(nameof(features));
}
if (phases == null)
{
throw new ArgumentNullException(nameof(phases));
}
Features = features;
Phases = phases;
for (var i = 0; i < features.Length; i++)
{
features[i].Engine = this;
}
for (var i = 0; i < phases.Length; i++)
{
phases[i].Engine = this;
}
}
public override IReadOnlyList<IRazorEngineFeature> Features { get; }
public override IReadOnlyList<IRazorEnginePhase> Phases { get; }
public override void Process(RazorCodeDocument document)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
for (var i = 0; i < Phases.Count; i++)
{
var phase = Phases[i];
phase.Execute(document);
}
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DefaultRazorEngineBuilder : IRazorEngineBuilder
{
public DefaultRazorEngineBuilder()
{
Features = new List<IRazorEngineFeature>();
Phases = new List<IRazorEnginePhase>();
}
public ICollection<IRazorEngineFeature> Features { get; }
public IList<IRazorEnginePhase> Phases { get; }
public RazorEngine Build()
{
var features = new IRazorEngineFeature[Features.Count];
Features.CopyTo(features, arrayIndex: 0);
var phases = new IRazorEnginePhase[Phases.Count];
Phases.CopyTo(phases, arrayIndex: 0);
return new DefaultRazorEngine(features, phases);
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DefaultRazorSourceDocument : RazorSourceDocument
{
private MemoryStream _stream;
public DefaultRazorSourceDocument(MemoryStream stream, Encoding encoding, string filename)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
_stream = stream;
Encoding = encoding;
Filename = filename;
}
public Encoding Encoding { get; }
public override string Filename { get; }
public override TextReader CreateReader()
{
var copy = new MemoryStream(_stream.ToArray());
return Encoding == null
? new StreamReader(copy, detectEncodingFromByteOrderMarks: true)
: new StreamReader(copy, Encoding);
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public interface IRazorEngineBuilder
{
ICollection<IRazorEngineFeature> Features { get; }
IList<IRazorEnginePhase> Phases { get; }
RazorEngine Build();
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Razor.Evolution
{
public interface IRazorEngineFeature
{
RazorEngine Engine { get; set; }
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Razor.Evolution
{
public interface IRazorEnginePhase
{
RazorEngine Engine { get; set; }
void Execute(RazorCodeDocument document);
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Razor.Evolution
{
public abstract class ItemCollection
{
public abstract object this[object key] { get; set; }
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public abstract class RazorCodeDocument
{
public static RazorCodeDocument Create(RazorSourceDocument source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
return new DefaultRazorCodeDocument(source);
}
public abstract ItemCollection Items { get; }
public abstract RazorSourceDocument Source { get; }
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public abstract class RazorEngine
{
public static RazorEngine Create()
{
return Create(configure: null);
}
public static RazorEngine Create(Action<IRazorEngineBuilder> configure)
{
var builder = new DefaultRazorEngineBuilder();
configure?.Invoke(builder);
return builder.Build();
}
public abstract IReadOnlyList<IRazorEngineFeature> Features { get; }
public abstract IReadOnlyList<IRazorEnginePhase> Phases { get; }
public abstract void Process(RazorCodeDocument document);
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public abstract class RazorSourceDocument
{
public static RazorSourceDocument ReadFrom(Stream stream, string filename)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
return ReadFromInternal(stream, filename, encoding: null);
}
public static RazorSourceDocument ReadFrom(Stream stream, string filename, Encoding encoding)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
}
return ReadFromInternal(stream, filename, encoding);
}
private static RazorSourceDocument ReadFromInternal(Stream stream, string filename, Encoding encoding)
{
var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
return new DefaultRazorSourceDocument(memoryStream, encoding, filename);
}
public abstract string Filename { get; }
public abstract TextReader CreateReader();
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class DefaultItemCollectionTest
{
[Fact]
public void Get_MissingValueReturnsNull()
{
// Arrange
var items = new DefaultItemCollection();
// Act
var value = items["foo"];
// Assert
Assert.Null(value);
}
[Fact]
public void GetAndSet_ReturnsValue()
{
// Arrange
var items = new DefaultItemCollection();
var expected = "bar";
items["foo"] = expected;
// Act
var value = items["foo"];
// Assert
Assert.Same(expected, value);
}
[Fact]
public void Set_CanSetValueToNull()
{
// Arrange
var items = new DefaultItemCollection();
items["foo"] = "bar";
// Act
items["foo"] = null;
// Assert
Assert.Null(items["foo"]);
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class DefaultRazorCodeDocumentTest
{
[Fact]
public void Ctor()
{
// Arrange
var source = TestRazorSourceDocument.Create();
// Act
var code = new DefaultRazorCodeDocument(source);
// Assert
Assert.Same(source, code.Source);
Assert.NotNull(code.Items);
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class DefaultRazorEngineBuilderTest
{
[Fact]
public void Build_AddsFeaturesAndPhases()
{
// Arrange
var builder = new DefaultRazorEngineBuilder();
builder.Features.Add(Mock.Of<IRazorEngineFeature>());
builder.Features.Add(Mock.Of<IRazorEngineFeature>());
builder.Phases.Add(Mock.Of<IRazorEnginePhase>());
builder.Phases.Add(Mock.Of<IRazorEnginePhase>());
var features = builder.Features.ToArray();
var phases = builder.Phases.ToArray();
// Act
var engine = builder.Build();
// Assert
Assert.Collection(
engine.Features,
f => Assert.Same(features[0], f),
f => Assert.Same(features[1], f));
Assert.Collection(
engine.Phases,
p => Assert.Same(phases[0], p),
p => Assert.Same(phases[1], p));
}
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class DefaultRazorEngineTest
{
[Fact]
public void Ctor_InitializesPhasesAndFeatures()
{
// Arrange
var features = new IRazorEngineFeature[]
{
Mock.Of<IRazorEngineFeature>(),
Mock.Of<IRazorEngineFeature>(),
};
var phases = new IRazorEnginePhase[]
{
Mock.Of<IRazorEnginePhase>(),
Mock.Of<IRazorEnginePhase>(),
};
// Act
var engine = new DefaultRazorEngine(features, phases);
// Assert
for (var i = 0; i < features.Length; i++)
{
Assert.Same(engine, features[i].Engine);
}
for (var i = 0; i < phases.Length; i++)
{
Assert.Same(engine, phases[i].Engine);
}
}
[Fact]
public void Process_CallsAllPhases()
{
// Arrange
var features = new IRazorEngineFeature[]
{
Mock.Of<IRazorEngineFeature>(),
Mock.Of<IRazorEngineFeature>(),
};
var phases = new IRazorEnginePhase[]
{
Mock.Of<IRazorEnginePhase>(),
Mock.Of<IRazorEnginePhase>(),
};
var engine = new DefaultRazorEngine(features, phases);
var document = TestRazorCodeDocument.CreateEmpty();
// Act
engine.Process(document);
// Assert
for (var i = 0; i < phases.Length; i++)
{
var mock = Mock.Get(phases[i]);
mock.Verify(p => p.Execute(document), Times.Once());
}
}
}
}

View File

@ -0,0 +1,99 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class DefaultRazorSourceDocumentTest
{
[Fact]
public void Filename()
{
// Arrange
var content = CreateContent();
// Act
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: "file.cshtml");
// Assert
Assert.Equal("file.cshtml", document.Filename);
}
[Fact]
public void Filename_Null()
{
// Arrange
var content = CreateContent();
// Act
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Assert
Assert.Null(document.Filename);
}
[Fact]
public void CreateReader_WithEncoding()
{
// Arrange
var content = CreateContent("Hi", encoding: Encoding.UTF8);
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
using (var reader = document.CreateReader())
{
// Assert
Assert.Equal("Hi", reader.ReadToEnd());
}
}
[Fact]
public void CreateReader_Null_DetectsEncoding()
{
// Arrange
var content = CreateContent("Hi", encoding: Encoding.UTF32);
var document = new DefaultRazorSourceDocument(content, encoding: null, filename: null);
// Act
using (var reader = document.CreateReader())
{
// Assert
Assert.Equal("Hi", reader.ReadToEnd());
}
}
[Fact]
public void CreateReader_DisposeReader_DoesNotDirtyDocument()
{
// Arrange
var content = CreateContent("Hi", encoding: Encoding.UTF32);
var document = new DefaultRazorSourceDocument(content, encoding: null, filename: null);
// Act & Assert
//
// (we should be able to do this twice to prove that the underlying data isn't disposed)
for (var i = 0; i < 2; i++)
{
using (var reader = document.CreateReader())
{
// Assert
Assert.Equal("Hi", reader.ReadToEnd());
}
}
}
private static MemoryStream CreateContent(string content = "Hello, World!", Encoding encoding = null)
{
var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, encoding ?? Encoding.UTF8, bufferSize: 1024, leaveOpen: true))
{
writer.Write(content);
}
return stream;
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class IntegrationTest
{
[Fact]
public void BuildEngine_CallProcess()
{
// Arrange
var engine = RazorEngine.Create();
var document = RazorCodeDocument.Create(TestRazorSourceDocument.Create());
// Act
engine.Process(document);
// Assert
// (nothing to verify yet)
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class RazorCodeDocumentTest
{
[Fact]
public void Create()
{
// Arrange
var source = TestRazorSourceDocument.Create();
// Act
var code = RazorCodeDocument.Create(source);
// Assert
Assert.Same(source, code.Source);
Assert.NotNull(code.Items);
}
}
}

View File

@ -0,0 +1,70 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class RazorEngineTest
{
[Fact]
public void Create_NoArg_CreatesDefaultEngine()
{
// Arrange
// Act
var engine = RazorEngine.Create();
// Assert
Assert.IsType<DefaultRazorEngine>(engine);
Assert.Empty(engine.Features);
Assert.Empty(engine.Phases);
}
[Fact]
public void Create_Null_CreatesDefaultEngine()
{
// Arrange
// Act
var engine = RazorEngine.Create(configure: null);
// Assert
Assert.IsType<DefaultRazorEngine>(engine);
Assert.Empty(engine.Features);
Assert.Empty(engine.Phases);
}
[Fact]
public void Create_Lambda_AddsFeaturesAndPhases()
{
// Arrange
IRazorEngineFeature[] features = null;
IRazorEnginePhase[] phases = null;
// Act
var engine = RazorEngine.Create(builder =>
{
builder.Features.Add(Mock.Of<IRazorEngineFeature>());
builder.Features.Add(Mock.Of<IRazorEngineFeature>());
builder.Phases.Add(Mock.Of<IRazorEnginePhase>());
builder.Phases.Add(Mock.Of<IRazorEnginePhase>());
features = builder.Features.ToArray();
phases = builder.Phases.ToArray();
});
// Assert
Assert.Collection(
engine.Features,
f => Assert.Same(features[0], f),
f => Assert.Same(features[1], f));
Assert.Collection(
engine.Phases,
p => Assert.Same(phases[0], p),
p => Assert.Same(phases[1], p));
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class RazorSourceDocumentTest
{
[Fact]
public void Create()
{
// Arrange
var content = TestRazorSourceDocument.CreateContent();
// Act
var document = RazorSourceDocument.ReadFrom(content, "file.cshtml");
// Assert
Assert.Equal("file.cshtml", document.Filename);
Assert.Null(Assert.IsType<DefaultRazorSourceDocument>(document).Encoding);
}
[Fact]
public void Create_WithEncoding()
{
// Arrange
var content = TestRazorSourceDocument.CreateContent(encoding: Encoding.UTF32);
// Act
var document = RazorSourceDocument.ReadFrom(content, "file.cshtml", Encoding.UTF32);
// Assert
Assert.Equal("file.cshtml", document.Filename);
Assert.Same(Encoding.UTF32, Assert.IsType<DefaultRazorSourceDocument>(document).Encoding);
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class TestRazorCodeDocument : DefaultRazorCodeDocument
{
public static TestRazorCodeDocument CreateEmpty()
{
var source = TestRazorSourceDocument.Create(content: string.Empty);
return new TestRazorCodeDocument(source);
}
private TestRazorCodeDocument(RazorSourceDocument source)
: base(source)
{
}
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class TestRazorSourceDocument : DefaultRazorSourceDocument
{
public static MemoryStream CreateContent(string content = "Hello, World!", Encoding encoding = null)
{
var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, encoding ?? Encoding.UTF8, bufferSize: 1024, leaveOpen: true))
{
writer.Write(content);
}
stream.Seek(0L, SeekOrigin.Begin);
return stream;
}
public static RazorSourceDocument Create(string content = "Hello, world!", Encoding encoding = null)
{
var stream = CreateContent(content, encoding);
return new TestRazorSourceDocument(stream, encoding ?? Encoding.UTF8, "test.cshtml");
}
public TestRazorSourceDocument(MemoryStream stream, Encoding encoding, string filename)
: base(stream, encoding, filename)
{
}
}
}

View File

@ -3,6 +3,7 @@
"dependencies": {
"dotnet-test-xunit": "2.2.0-*",
"Microsoft.AspNetCore.Razor.Evolution": "1.1.0-*",
"Moq": "4.6.36-*",
"xunit": "2.2.0-*"
},
"testRunner": "xunit",