Skip to content

Commit

Permalink
Ensured metadata properties are strongly typed in the script host
Browse files Browse the repository at this point in the history
  • Loading branch information
daveaglick committed Mar 8, 2020
1 parent c0956e8 commit 9e1a9a2
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 94 deletions.
2 changes: 2 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 1.0.0-alpha.28

- Added `ctx` and `doc` shorthand properties to the scripted metadata script host.
- Ensured that scripted metadata uses a strongly-typed property in the script host for metadata properties like `Source` and `Destination`.
- Added ".yml" to file extensions mapped to the "text/yaml" media type.
- Added ability to include all inputs in generated feeds from `GenerateFeeds` by setting maximum items to 0
- Refactored Statiq.Hosting usage of Newtonsoft.Json to System.Text.Json.
Expand Down
19 changes: 1 addition & 18 deletions src/core/Statiq.Common/Documents/Document{TDocument}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,24 +252,7 @@ protected set
}

/// <inheritdoc />
public virtual async Task<int> GetCacheHashCodeAsync()
{
HashCode hash = default;
using (Stream stream = this.GetContentStream())
{
hash.Add(await Crc32.CalculateAsync(stream));
}

// We exclude ContentProvider from hash as we already added CRC for content above.
foreach (KeyValuePair<string, object> item in this
.Where(x => x.Key != nameof(ContentProvider)))
{
hash.Add(item.Key);
hash.Add(item.Value);
}

return hash.ToHashCode();
}
public virtual async Task<int> GetCacheHashCodeAsync() => await IDocument.GetCacheHashCodeAsync(this);

/// <inheritdoc />
public virtual string ToDisplayString() => Source.IsNull ? "unknown source" : Source.ToDisplayString();
Expand Down
32 changes: 32 additions & 0 deletions src/core/Statiq.Common/Documents/IDocument.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace Statiq.Common
Expand Down Expand Up @@ -59,5 +60,36 @@ IDocument Clone(

/// <inheritdoc />
string IDisplayable.ToDisplayString() => Source.IsNull ? "unknown source" : Source.ToDisplayString();

/// <summary>
/// Gets a hash of the provided document content and metadata appropriate for caching.
/// Custom <see cref="IDocument"/> implementations may also contribute additional state
/// data to the resulting hash code by overriding this method.
/// </summary>
/// <returns>A hash appropriate for caching.</returns>
Task<int> GetCacheHashCodeAsync();

/// <summary>
/// A default implementation of <see cref="GetCacheHashCodeAsync()"/>.
/// </summary>
/// <returns>A hash appropriate for caching.</returns>
public static async Task<int> GetCacheHashCodeAsync(IDocument document)
{
HashCode hash = default;
using (Stream stream = document.GetContentStream())
{
hash.Add(await Crc32.CalculateAsync(stream));
}

// We exclude ContentProvider from hash as we already added CRC for content above.
foreach (KeyValuePair<string, object> item in document.GetRawEnumerable()
.Where(x => x.Key != nameof(ContentProvider)))
{
hash.Add(item.Key);
hash.Add(item.Value);
}

return hash.ToHashCode();
}
}
}
26 changes: 0 additions & 26 deletions src/core/Statiq.Common/Documents/IDocumentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,32 +68,6 @@ public static async Task<byte[]> GetContentBytesAsync(this IDocument document)
public static bool MediaTypeEquals(this IDocument document, string mediaType) =>
string.Equals(document.ContentProvider.MediaType, mediaType, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Gets a hash of the provided document content and metadata appropriate for caching.
/// Custom <see cref="IDocument"/> implementations may also contribute additional state
/// data to the resulting hash code.
/// </summary>
/// <param name="document">The document.</param>
/// <returns>A hash appropriate for caching.</returns>
public static async Task<int> GetCacheHashCodeAsync(this IDocument document)
{
HashCode hash = default;
using (Stream stream = document.GetContentStream())
{
hash.Add(await Crc32.CalculateAsync(stream));
}

// We exclude ContentProvider from hash as we already added CRC for content above.
foreach (KeyValuePair<string, object> item in document
.Where(x => x.Key != nameof(IDocument.ContentProvider)))
{
hash.Add(item.Key);
hash.Add(item.Value);
}

return hash.ToHashCode();
}

/// <summary>
/// Gets a normalized title derived from the document source.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/core/Statiq.Common/Documents/ObjectDocument{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,5 +257,8 @@ public IEnumerator<KeyValuePair<string, object>> GetRawEnumerator()
}
}
}

/// <inheritdoc />
public async Task<int> GetCacheHashCodeAsync() => await IDocument.GetCacheHashCodeAsync(this);
}
}
15 changes: 13 additions & 2 deletions src/core/Statiq.Common/Scripting/IScriptHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,22 @@ public interface IScriptHelper
/// Compiles a script into an in-memory script assembly for later evaluation.
/// </summary>
/// <param name="code">The code to compile.</param>
/// <param name="metadataPropertyKeys">
/// <param name="metadataProperties">
/// Metadata property keys that will be exposed as properties in the script as
/// the name of the key and can be used directly in the script.
/// The <see cref="KeyValuePair{TKey, TValue}.Key"/> should be the name of the
/// property and <see cref="KeyValuePair{TKey, TValue}.Value"/> should be the name
/// of the property type (or <c>null</c> for "object").
/// </param>
/// <returns>Raw assembly bytes.</returns>
byte[] Compile(string code, IEnumerable<string> metadataPropertyKeys);
byte[] Compile(string code, IEnumerable<KeyValuePair<string, string>> metadataProperties);

/// <summary>
/// Gets property name and scripting types for a given <see cref="IMetadata"/> object
/// by mapping metadata properties to their defined types and other metadata to <see cref="object"/>.
/// </summary>
/// <param name="metadata">The metadata to get scriptable properties for.</param>
/// <returns>The property name and scriptable type for each property of the metadata.</returns>
IEnumerable<KeyValuePair<string, string>> GetMetadataProperties(IMetadata metadata);
}
}
10 changes: 5 additions & 5 deletions src/core/Statiq.Common/Scripting/ScriptMetadataValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class ScriptMetadataValue : IMetadataValue
private readonly string _originalPrefix;
private readonly string _script;
private readonly IExecutionState _executionState;
private HashSet<string> _cachedMetadataKeys;
private HashSet<KeyValuePair<string, string>> _cachedMetadataProperties;
private Type _cachedScriptType;

private ScriptMetadataValue(string key, string originalPrefix, string script, IExecutionState executionState)
Expand All @@ -41,12 +41,12 @@ public object Get(IMetadata metadata)
}

// Check if we've already cached a compilation for the current set of metadata keys
if (_cachedMetadataKeys?.SetEquals(metadata.Keys) != true)
KeyValuePair<string, string>[] metadataProperties = _executionState.ScriptHelper.GetMetadataProperties(metadata).ToArray();
if (_cachedMetadataProperties?.SetEquals(metadataProperties) != true)
{
// Compilation cache miss, not cached or the metadata keys are different
string[] keys = metadata.Keys.ToArray();
_cachedMetadataKeys = new HashSet<string>(keys);
byte[] rawAssembly = _executionState.ScriptHelper.Compile(_script, keys);
_cachedMetadataProperties = new HashSet<KeyValuePair<string, string>>(metadataProperties);
byte[] rawAssembly = _executionState.ScriptHelper.Compile(_script, metadataProperties);
_cachedScriptType = _executionState.ScriptHelper.Load(rawAssembly);
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/core/Statiq.Core/Scripting/ScriptBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ protected ScriptBase(IMetadata metadata, IExecutionState executionState, IExecut

public IExecutionContext Context { get; }

#pragma warning disable SA1300 // Element should begin with upper-case letter
public IExecutionContext ctx => Context;
#pragma warning restore SA1300 // Element should begin with upper-case letter

public IDocument Document => Metadata as IDocument ?? throw new InvalidOperationException("Script object is not a document");

#pragma warning disable SA1300 // Element should begin with upper-case letter
public IDocument doc => Document;
#pragma warning restore SA1300 // Element should begin with upper-case letter

public abstract Task<object> EvaluateAsync();

// Manually implement IExecutionContext pass-throughs since we don't
Expand Down
26 changes: 21 additions & 5 deletions src/core/Statiq.Core/Scripting/ScriptHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,25 @@ public Type Load(byte[] rawAssembly)
}

/// <inheritdoc/>
public byte[] Compile(string code, IMetadata metadata) => Compile(code, metadata?.Keys);
IEnumerable<KeyValuePair<string, string>> IScriptHelper.GetMetadataProperties(IMetadata metadata) => GetMetadataProperties(metadata);

public static IEnumerable<KeyValuePair<string, string>> GetMetadataProperties(IMetadata metadata)
{
if (metadata != null)
{
Type metadataType = metadata.GetType();
foreach (string key in metadata.Keys)
{
yield return new KeyValuePair<string, string>(key, metadataType.GetProperty(key)?.PropertyType.FullName);
}
}
}

/// <inheritdoc/>
public byte[] Compile(string code, IMetadata metadata) => Compile(code, GetMetadataProperties(metadata));

/// <inheritdoc/>
public byte[] Compile(string code, IEnumerable<string> metadataPropertyKeys)
public byte[] Compile(string code, IEnumerable<KeyValuePair<string, string>> metadataPropertyKeys)
{
_ = code ?? throw new ArgumentNullException(nameof(code));

Expand Down Expand Up @@ -171,7 +186,8 @@ private static string GetCompilationErrorMessage(Diagnostic diagnostic)
}

// Internal for testing
internal static string Parse(string code, IEnumerable<string> metadataPropertyKeys, IExecutionState executionState)
// metadataPropertyKeys.Key = property name, metadataPropertyKeys.Value = property type
internal static string Parse(string code, IEnumerable<KeyValuePair<string, string>> metadataPropertyKeys, IExecutionState executionState)
{
// Generate a syntax tree from the code
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(kind: SourceCodeKind.Script), cancellationToken: executionState.CancellationToken);
Expand All @@ -198,8 +214,8 @@ internal static string Parse(string code, IEnumerable<string> metadataPropertyKe
: string.Join(
Environment.NewLine,
metadataPropertyKeys
.Where(x => !string.IsNullOrWhiteSpace(x) && !ReservedPropertyNames.Contains(x))
.Select(x => $"public object {GetValidIdentifier(x)} => Metadata.Get(\"{x}\");"));
.Where(x => !string.IsNullOrWhiteSpace(x.Key) && !ReservedPropertyNames.Contains(x.Key))
.Select(x => $"public {x.Value ?? "object"} {GetValidIdentifier(x.Key)} => Metadata.Get<{x.Value ?? "object"}>(\"{x.Key}\");"));

// Determine if we need a return statement
string preScript = null;
Expand Down
Loading

0 comments on commit 9e1a9a2

Please sign in to comment.