Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate MiniLcm types #1344

Merged
merged 25 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
01a619e
Add validation for entries and most entry fields
rmunn Jan 6, 2025
8499ff5
Update entry validation tests to pass again
rmunn Jan 6, 2025
dcf2aef
Add entry validator tests for lexeme, citation form
rmunn Jan 6, 2025
4dd4032
Add entry validation tests for literal meaning, note
rmunn Jan 6, 2025
04b8b26
Add validation tests for senses, example sentences
rmunn Jan 6, 2025
87b2f8b
Address review comments so far
rmunn Jan 7, 2025
7577e83
Add list of canonical GUIDs for parts of speech
rmunn Jan 7, 2025
98fd92b
Add list of canonical GUIDs for semantic domains
rmunn Jan 7, 2025
68fe485
Mark FwLiteProjectSync tests as integration tests
rmunn Jan 7, 2025
b8e6b30
Add more entry validation tests
rmunn Jan 7, 2025
379d4d4
Also validate complex form types on updates
rmunn Jan 7, 2025
3bcd237
Actually use validators in MiniLCM API
rmunn Jan 7, 2025
7f38246
Don't check part of speech GUIDs yet
rmunn Jan 7, 2025
1cedbec
Adjust some test data to make it valid
rmunn Jan 7, 2025
e5b02b7
Fix test failures around semantic domain IDs
rmunn Jan 7, 2025
d046bdc
Push two missing files
rmunn Jan 7, 2025
8d5a2fe
Make EntryReadyForCreation create valid data
rmunn Jan 7, 2025
e894d40
Temporarily skip part of speech validation
rmunn Jan 7, 2025
9082936
Better comment
rmunn Jan 8, 2025
922f0a0
Example sentences may have empty Sentence fields
rmunn Jan 8, 2025
c3f0908
Address most review comments
rmunn Jan 8, 2025
0a39681
Rename PartOfSpeechIdValidator
rmunn Jan 8, 2025
7e0e604
Rename one overload that was missed earlier
rmunn Jan 9, 2025
913400a
Fix complex form test - this one should fail
rmunn Jan 9, 2025
9e4e4f1
Fix renames that the code rename somehow missed
rmunn Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 94 additions & 4 deletions backend/FwLite/MiniLcm.Tests/Validators/EntryValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,114 @@ public class EntryValidatorTests
public void Succeeds_WhenSenseEntryIdIsGuidEmpty()
{
var entryId = Guid.NewGuid();
var entry = new Entry() { Id = entryId, Senses = [new Sense() { EntryId = Guid.Empty, }] };
var entry = new Entry() { Id = entryId, LexemeForm = new MultiString(){{"en", "lexeme"}}, Senses = [new Sense() { EntryId = Guid.Empty, }] };
_validator.TestValidate(entry).ShouldNotHaveAnyValidationErrors();
}

[Fact]
public void Succeeds_WhenSenseEntryIdMatchesEntry()
{

var entryId = Guid.NewGuid();
var entry = new Entry() { Id = entryId, Senses = [new Sense() { EntryId = entryId, }] };
var entry = new Entry() { Id = entryId, LexemeForm = new MultiString(){{"en", "lexeme"}}, Senses = [new Sense() { EntryId = entryId, }] };
_validator.TestValidate(entry).ShouldNotHaveAnyValidationErrors();
}

[Fact]
public void Fails_WhenSenseEntryIdDoesNotMatchEntry()
{
var entryId = Guid.NewGuid();
var entry = new Entry() { Id = entryId, Senses = [new Sense() { EntryId = Guid.NewGuid(), }] };
var entry = new Entry() { Id = entryId, LexemeForm = new MultiString(){{"en", "lexeme"}}, Senses = [new Sense() { EntryId = Guid.NewGuid(), }] };
_validator.TestValidate(entry).ShouldHaveValidationErrorFor("Senses[0].EntryId");
}

[Fact]
public void Succeeds_WhenDeletedAtIsNull()
{
var entryId = Guid.NewGuid();
var entry = new Entry() { Id = entryId, DeletedAt = null, LexemeForm = new MultiString(){{"en", "lexeme"}}, Senses = [new Sense() { EntryId = entryId, }] };
_validator.TestValidate(entry).ShouldNotHaveAnyValidationErrors();
}

[Fact]
public void Fails_WhenDeletedAtIsNotNull()
{
var entryId = Guid.NewGuid();
var now = DateTime.UtcNow;
var entry = new Entry() { Id = entryId, DeletedAt = now, LexemeForm = new MultiString(){{"en", "lexeme"}}, Senses = [new Sense() { EntryId = Guid.Empty, }] };
_validator.TestValidate(entry).ShouldHaveValidationErrorFor("DeletedAt");
}

[Fact]
public void Succeeds_WhenLexemeFormIsPresent()
{
var entry = new Entry() { Id = Guid.NewGuid(), LexemeForm = new MultiString(){{"en", "lexeme"}} };
_validator.TestValidate(entry).ShouldNotHaveAnyValidationErrors();
}

[Fact]
public void Fails_WhenLexemeFormIsMissing()
{
var entry = new Entry() { Id = Guid.NewGuid() };
_validator.TestValidate(entry).ShouldHaveValidationErrorFor("LexemeForm");
}

[Fact]
public void Fails_WhenLexemeFormHasNoContent()
{
// Technically the same as Fails_WhenLexemeFormIsMissing -- should we combine them?
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
var entry = new Entry() { Id = Guid.NewGuid(), LexemeForm = new MultiString() };
_validator.TestValidate(entry).ShouldHaveValidationErrorFor("LexemeForm");
}

[Fact]
public void Fails_WhenLexemeFormHasWsWithEmptyContent()
{
var entry = new Entry() { Id = Guid.NewGuid(), LexemeForm = new MultiString(){{"en", ""}} };
_validator.TestValidate(entry).ShouldHaveValidationErrorFor("LexemeForm");
}

[Theory]
[InlineData("CitationForm")]
[InlineData("LiteralMeaning")]
[InlineData("Note")]
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
public void Succeeds_WhenNonEmptyFieldIsPresent(string fieldName)
{
var entry = new Entry() { Id = Guid.NewGuid(), LexemeForm = new MultiString(){{"en", "lexeme"}} };
SetProperty(entry, fieldName, "content");
_validator.TestValidate(entry).ShouldNotHaveAnyValidationErrors();
}

[Theory]
[InlineData("CitationForm")]
[InlineData("LiteralMeaning")]
[InlineData("Note")]
public void Succeeds_WhenNonEmptyFieldHasNoContent(string fieldName)
{
var entry = new Entry() { Id = Guid.NewGuid(), LexemeForm = new MultiString(){{"en", "lexeme"}} };
MakePropertyEmpty(entry, fieldName);
_validator.TestValidate(entry).ShouldNotHaveAnyValidationErrors();
}

[Theory]
[InlineData("CitationForm")]
[InlineData("LiteralMeaning")]
[InlineData("Note")]
public void Fails_WhenNonEmptyFieldHasWsWithEmptyContent(string fieldName)
{
var entry = new Entry() { Id = Guid.NewGuid(), LexemeForm = new MultiString(){{"en", "lexeme"}} };
SetProperty(entry, fieldName, "");
_validator.TestValidate(entry).ShouldHaveValidationErrorFor(fieldName);
}

private void SetProperty(Entry entry, string propName, string content)
{
var propInfo = typeof(Entry).GetProperty(propName);
propInfo?.SetValue(entry, new MultiString(){{"en", content}});
}

private void MakePropertyEmpty(Entry entry, string propName)
{
var propInfo = typeof(Entry).GetProperty(propName);
propInfo?.SetValue(entry, new MultiString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using FluentValidation.TestHelper;
using MiniLcm.Validators;

namespace MiniLcm.Tests.Validators;

public class ExampleSentenceValidatorTests
{
private readonly ExampleSentenceValidator _validator = new();

[Fact]
public void Succeeds_WhenDeletedAtIsNull()
{
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString(){{"en", "sentence"}}, DeletedAt = null };
_validator.TestValidate(example).ShouldNotHaveAnyValidationErrors();
}

[Fact]
public void Fails_WhenDeletedAtIsNotNull()
{
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString(){{"en", "sentence"}}, DeletedAt = DateTimeOffset.UtcNow };
_validator.TestValidate(example).ShouldHaveValidationErrorFor("DeletedAt");
}

[Fact]
public void Succeeds_WhenSentenceIsPresent()
{
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString(){{"en", "sentence"}} };
_validator.TestValidate(example).ShouldNotHaveAnyValidationErrors();
}

[Fact]
public void Fails_WhenSentenceIsMissing()
{
var example = new ExampleSentence() { Id = Guid.NewGuid() };
_validator.TestValidate(example).ShouldHaveValidationErrorFor("Sentence");
}

[Fact]
public void Fails_WhenSentenceHasNoContent()
{
// Technically the same as Fails_WhenSentenceIsMissing -- should we combine them?
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString() };
_validator.TestValidate(example).ShouldHaveValidationErrorFor("Sentence");
}

[Fact]
public void Fails_WhenSentenceHasWsWithEmptyContent()
{
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString(){{"en", ""}} };
_validator.TestValidate(example).ShouldHaveValidationErrorFor("Sentence");
}

[Theory]
[InlineData("Translation")]
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
public void Succeeds_WhenNonEmptyFieldIsPresent(string fieldName)
{
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString(){{"en", "sentence"}} };
SetProperty(example, fieldName, "content");
_validator.TestValidate(example).ShouldNotHaveAnyValidationErrors();
}

[Theory]
[InlineData("Translation")]
public void Succeeds_WhenNonEmptyFieldHasNoContent(string fieldName)
{
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString(){{"en", "sentence"}} };
MakePropertyEmpty(example, fieldName);
_validator.TestValidate(example).ShouldNotHaveAnyValidationErrors();
}

[Theory]
[InlineData("Translation")]
public void Fails_WhenNonEmptyFieldHasWsWithEmptyContent(string fieldName)
{
var example = new ExampleSentence() { Id = Guid.NewGuid(), Sentence = new MultiString(){{"en", "sentence"}} };
SetProperty(example, fieldName, "");
_validator.TestValidate(example).ShouldHaveValidationErrorFor(fieldName);
}

private void SetProperty(ExampleSentence example, string propName, string content)
{
var propInfo = typeof(ExampleSentence).GetProperty(propName);
propInfo?.SetValue(example, new MultiString(){{"en", content}});
}

private void MakePropertyEmpty(ExampleSentence example, string propName)
{
var propInfo = typeof(ExampleSentence).GetProperty(propName);
propInfo?.SetValue(example, new MultiString());
}
}
65 changes: 65 additions & 0 deletions backend/FwLite/MiniLcm.Tests/Validators/SenseValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using FluentValidation.TestHelper;
using MiniLcm.Validators;

namespace MiniLcm.Tests.Validators;

public class SenseValidatorTests
{
private readonly SenseValidator _validator = new();

[Fact]
public void Succeeds_WhenDeletedAtIsNull()
{
var sense = new Sense() { Id = Guid.NewGuid(), DeletedAt = null };
_validator.TestValidate(sense).ShouldNotHaveAnyValidationErrors();
}

[Fact]
public void Fails_WhenDeletedAtIsNotNull()
{
var sense = new Sense() { Id = Guid.NewGuid(), DeletedAt = DateTimeOffset.UtcNow };
_validator.TestValidate(sense).ShouldHaveValidationErrorFor("DeletedAt");
}

[Theory]
[InlineData("Definition")]
[InlineData("Gloss")]
public void Succeeds_WhenNonEmptyFieldIsPresent(string fieldName)
{
var sense = new Sense() { Id = Guid.NewGuid() };
SetProperty(sense, fieldName, "content");
_validator.TestValidate(sense).ShouldNotHaveAnyValidationErrors();
}

[Theory]
[InlineData("Definition")]
[InlineData("Gloss")]
public void Succeeds_WhenNonEmptyFieldHasNoContent(string fieldName)
{
var sense = new Sense() { Id = Guid.NewGuid() };
MakePropertyEmpty(sense, fieldName);
_validator.TestValidate(sense).ShouldNotHaveAnyValidationErrors();
}

[Theory]
[InlineData("Definition")]
[InlineData("Gloss")]
public void Fails_WhenNonEmptyFieldHasWsWithEmptyContent(string fieldName)
{
var sense = new Sense() { Id = Guid.NewGuid() };
SetProperty(sense, fieldName, "");
_validator.TestValidate(sense).ShouldHaveValidationErrorFor(fieldName);
}

private void SetProperty(Sense sense, string propName, string content)
{
var propInfo = typeof(Sense).GetProperty(propName);
propInfo?.SetValue(sense, new MultiString(){{"en", content}});
}

private void MakePropertyEmpty(Sense sense, string propName)
{
var propInfo = typeof(Sense).GetProperty(propName);
propInfo?.SetValue(sense, new MultiString());
}
}
34 changes: 33 additions & 1 deletion backend/FwLite/MiniLcm/Validators/EntryValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,39 @@ public class EntryValidator : AbstractValidator<Entry>
{
public EntryValidator()
{
RuleFor(e => e.DeletedAt).Null();
RuleFor(e => e.LexemeForm).Required();
RuleFor(e => e.CitationForm).NoEmptyValues();
RuleFor(e => e.LiteralMeaning).NoEmptyValues();
RuleFor(e => e.Note).NoEmptyValues();
RuleForEach(e => e.Senses).SetValidator(entry => new SenseValidator(entry));
//todo just a stub as an example for senses
RuleForEach(e => e.Components).Must(NotBeComponentSelfReference);
RuleForEach(e => e.Components).Must(HaveCorrectComponentEntryReference);
RuleForEach(e => e.ComplexForms).Must(NotBeComplexFormSelfReference);
RuleForEach(e => e.ComplexForms).Must(HaveCorrectComplexFormEntryReference);
// RuleForEach(e => e.Components).SetValidator(entry => new ComplexFormComponentValidator(entry)); // TODO: Not implemented yet
// RuleForEach(e => e.ComplexForms).SetValidator(entry => new ComplexFormComponentValidator(entry)); // TODO: Not implemented yet
// TODO: ComplexFormComponentValidator(entry) might need to know the "direction" of the entry it's validating, i.e. one class for "I'm a component" and another for "I'm the complex entry"
RuleForEach(e => e.ComplexFormTypes).SetValidator(new ComplexFormTypeValidator());
}

private bool NotBeComponentSelfReference(Entry entry, ComplexFormComponent component)
{
return component.ComponentEntryId != entry.Id;
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
}

private bool HaveCorrectComponentEntryReference(Entry entry, ComplexFormComponent component)
{
return component.ComplexFormEntryId == entry.Id;
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
}

private bool NotBeComplexFormSelfReference(Entry entry, ComplexFormComponent component)
{
return component.ComplexFormEntryId != entry.Id;
}

private bool HaveCorrectComplexFormEntryReference(Entry entry, ComplexFormComponent component)
{
return component.ComponentEntryId == entry.Id;
}
}
20 changes: 20 additions & 0 deletions backend/FwLite/MiniLcm/Validators/ExampleSentenceValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using FluentValidation;
using MiniLcm.Models;

namespace MiniLcm.Validators;

public class ExampleSentenceValidator : AbstractValidator<ExampleSentence>
{
public ExampleSentenceValidator()
{
RuleFor(es => es.DeletedAt).Null();
RuleFor(es => es.Sentence).Required();
RuleFor(es => es.Translation).NoEmptyValues();
}

public ExampleSentenceValidator(Sense sense) : this()
{
//it's ok if SenseId is an Empty guid
RuleFor(es => es.SenseId).Equal(sense.Id).When(es => es.SenseId != Guid.Empty).WithMessage(examplesentence => $"ExampleSentence (Id: {examplesentence.Id}) EntryId must match Sense {sense.Id}, but instead was {examplesentence.SenseId}");
}
}
18 changes: 18 additions & 0 deletions backend/FwLite/MiniLcm/Validators/PartOfSpeechIdValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using FluentValidation;
using MiniLcm.Models;

namespace MiniLcm.Validators;

public class PartOfSpeechIdValidator : AbstractValidator<Guid?>
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
{
public PartOfSpeechIdValidator()
{
RuleFor(id => id).Must(BeCanonicalGuid);
}

private bool BeCanonicalGuid(Guid? id)
{
// TODO: Load GOLDEtic.xml into app as a resource and add singleton providing access to it, then look up GUIDs there
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
return true;
}
}
21 changes: 21 additions & 0 deletions backend/FwLite/MiniLcm/Validators/SemanticDomainValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using FluentValidation;
using MiniLcm.Models;

namespace MiniLcm.Validators;

public class SemanticDomainValidator : AbstractValidator<SemanticDomain>
{
public SemanticDomainValidator()
{
RuleFor(s => s.Code).NotNull().NotEmpty();
RuleFor(s => s.DeletedAt).Null();
RuleFor(s => s.Id).Must(BeCanonicalGuid).When(s => s.Predefined);
RuleFor(s => s.Name).Required();
}

private bool BeCanonicalGuid(Guid id)
{
// TODO: Load SemDom.xml into app as a resource and add singleton providing access to it, then look up GUIDs there
return true;
}
}
8 changes: 7 additions & 1 deletion backend/FwLite/MiniLcm/Validators/SenseValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ public class SenseValidator : AbstractValidator<Sense>
{
public SenseValidator()
{
//todo add validation for the other properties
RuleFor(s => s.DeletedAt).Null();
RuleFor(s => s.Definition).NoEmptyValues();
RuleFor(s => s.Gloss).NoEmptyValues();
// RuleFor(s => s.PartOfSpeech).Empty(); // TODO: Comment out if we're not yet ready to move away from strings
RuleFor(s => s.PartOfSpeechId).SetValidator(new PartOfSpeechIdValidator());
RuleForEach(s => s.SemanticDomains).SetValidator(new SemanticDomainValidator());
RuleForEach(s => s.ExampleSentences).SetValidator(sense => new ExampleSentenceValidator(sense));
}

public SenseValidator(Entry entry): this()
Expand Down
Loading