diff --git a/src/.idea/.idea.L5Sharp/.idea/workspace.xml b/src/.idea/.idea.L5Sharp/.idea/workspace.xml index c5b5e11d..c8eed130 100644 --- a/src/.idea/.idea.L5Sharp/.idea/workspace.xml +++ b/src/.idea/.idea.L5Sharp/.idea/workspace.xml @@ -9,9 +9,37 @@ + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + - - - - + + + + + + + + + + @@ -434,7 +484,11 @@ - + + + + + 1677621948966 @@ -779,7 +833,7 @@ - @@ -815,7 +869,6 @@ + + + + file://$PROJECT_DIR$/../tests/L5Sharp.Tests/LogixParserTests.cs + 191 + + + + + + + + + diff --git a/src/L5Sharp.Core/Common/TagName.cs b/src/L5Sharp.Core/Common/TagName.cs index 50ee211c..ef6d303d 100644 --- a/src/L5Sharp.Core/Common/TagName.cs +++ b/src/L5Sharp.Core/Common/TagName.cs @@ -170,9 +170,9 @@ public static TagName Concat(string left, string right) if (string.IsNullOrEmpty(right)) return left; if (right[0] == ArrayOpenSeparator || right[0] == MemberSeparator) - return new TagName($"{left}{right}"); + return new TagName(left + right); - return new TagName($"{left}{MemberSeparator}{right}"); + return new TagName(left + MemberSeparator + right); } /// diff --git a/src/L5Sharp.Core/Components/AddOnInstruction.cs b/src/L5Sharp.Core/Components/AddOnInstruction.cs index a3bec05d..b967658d 100644 --- a/src/L5Sharp.Core/Components/AddOnInstruction.cs +++ b/src/L5Sharp.Core/Components/AddOnInstruction.cs @@ -16,7 +16,7 @@ namespace L5Sharp.Core; /// `Logix 5000 Controllers Import/Export` for more information. /// [L5XType(L5XName.AddOnInstructionDefinition)] -public class AddOnInstruction : LogixComponent +public class AddOnInstruction : LogixComponent { private const string DateFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; @@ -281,49 +281,25 @@ public LogixContainer Routines throw new InvalidOperationException("No Logic routine is defined for AOI."); /// - /// Returns the AoiBlock instruction logic with the parameters tag names replaced with the argument tag names of the - /// provided instruction instance. + /// Creates a new instance using the provided tagname and optional arguments. /// - /// The instruction instance for which to generate the underlying logic. - /// - /// A containing representing all the instruction's - /// logic, with each instruction parameter tag name replaced with the arguments from the provided text. - /// - /// - /// This is helpful when trying to perform deep analysis on logic. By "flattening" the logic we can - /// reason or evaluate it as if it was written in line. Currently only supports - /// content or code type. - /// - public IEnumerable LogicFor(Instruction instruction) + /// The tag name of the AOI instance. + /// The optional arguments to supply the instruction signatrue with. + /// A having this AOI's key and provided arguments. + public Instruction ToInstruction(TagName tagName, params Argument[] arguments) { - if (instruction is null) - throw new ArgumentNullException(nameof(instruction)); - - // All instructions primary logic is contained in the routine named 'Logic' - var logic = Routines.FirstOrDefault(r => r.Name == "Logic"); - - var rungs = logic?.Content(); - if (rungs is null) return Enumerable.Empty(); - - //Skip first operand as it is always the AoiBlock tag, which does not have corresponding parameter within the logic. - var arguments = instruction.Arguments.Select(a => a.ToString()).Skip(1).ToList(); - - //Only required parameters are part of the instruction signature - var parameters = Parameters.Where(p => p.Required is true).Select(p => p.Name).ToList(); - - //Generate a mapping of the provided instructions arguments to instruction parameters. - var mapping = arguments.Zip(parameters, (a, p) => new { Argument = a, Parameter = p }).ToList(); - - //Replace all parameter names with argument names in the instruction logic text, and return the results. - return rungs.Select(r => r.Text) - .Select(t => mapping.Aggregate(t, (current, pair) => - { - if (!TagName.IsTag(pair.Argument)) return current; - var replace = $@"(?<=[^.]){pair.Parameter}\b"; - return Regex.Replace(current, replace, pair.Argument.ToString()); - })) - .ToList(); + var args = new List { tagName }; + args.AddRange(arguments); + return Instruction.New(Name, args.ToArray()); } + + /// + /// Creates a new instance using the provided tagname and optional arguments. + /// + /// The tag name of the AOI instance. + /// The optional arguments to supply the instruction signatrue with. + /// A having this AOI's key and provided arguments. + public NeutralText ToText(TagName tagName, params Argument[] arguments) => ToInstruction(tagName, arguments).Text; /// /// Creates a new instance with data configured from this component. @@ -368,6 +344,51 @@ public LogixData ToData() return complexData; } + /// + /// Returns the AoiBlock instruction logic with the parameters tag names replaced with the argument tag names of the + /// provided instruction instance. + /// + /// The instruction instance for which to generate the underlying logic. + /// + /// A containing representing all the instruction's + /// logic, with each instruction parameter tag name replaced with the arguments from the provided text. + /// + /// + /// This is helpful when trying to perform deep analysis on logic. By "flattening" the logic we can + /// reason or evaluate it as if it was written in line. Currently only supports + /// content or code type. + /// + public IEnumerable LogicFor(Instruction instruction) + { + if (instruction is null) + throw new ArgumentNullException(nameof(instruction)); + + // All instructions primary logic is contained in the routine named 'Logic' + var logic = Routines.FirstOrDefault(r => r.Name == "Logic"); + + var rungs = logic?.Content(); + if (rungs is null) return Enumerable.Empty(); + + //Skip first operand as it is always the AoiBlock tag, which does not have corresponding parameter within the logic. + var arguments = instruction.Arguments.Select(a => a.ToString()).Skip(1).ToList(); + + //Only required parameters are part of the instruction signature + var parameters = Parameters.Where(p => p.Required is true).Select(p => p.Name).ToList(); + + //Generate a mapping of the provided instructions arguments to instruction parameters. + var mapping = arguments.Zip(parameters, (a, p) => new { Argument = a, Parameter = p }).ToList(); + + //Replace all parameter names with argument names in the instruction logic text, and return the results. + return rungs.Select(r => r.Text) + .Select(t => mapping.Aggregate(t, (current, pair) => + { + if (!TagName.IsTag(pair.Argument)) return current; + var replace = $@"(?<=[^.]){pair.Parameter}\b"; + return Regex.Replace(current, replace, pair.Argument.ToString()); + })) + .ToList(); + } + /// /// Returns the default built in EnableIn parameter. /// diff --git a/src/L5Sharp.Core/Components/Controller.cs b/src/L5Sharp.Core/Components/Controller.cs index afb02249..9d013917 100644 --- a/src/L5Sharp.Core/Components/Controller.cs +++ b/src/L5Sharp.Core/Components/Controller.cs @@ -28,7 +28,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public class Controller : LogixComponent +public class Controller : LogixComponent { private const string DateTimeFormat = "ddd MMM d HH:mm:ss yyyy"; diff --git a/src/L5Sharp.Core/Components/DataType.cs b/src/L5Sharp.Core/Components/DataType.cs index fa308ad0..42e4ff29 100644 --- a/src/L5Sharp.Core/Components/DataType.cs +++ b/src/L5Sharp.Core/Components/DataType.cs @@ -24,7 +24,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public class DataType : LogixComponent +public class DataType : LogixComponent { /// protected override List ElementOrder => diff --git a/src/L5Sharp.Core/Components/LocalTag.cs b/src/L5Sharp.Core/Components/LocalTag.cs index 3e783181..b33a1e26 100644 --- a/src/L5Sharp.Core/Components/LocalTag.cs +++ b/src/L5Sharp.Core/Components/LocalTag.cs @@ -13,7 +13,7 @@ namespace L5Sharp.Core; /// deriving a new specific class. /// [L5XType(L5XName.LocalTag)] -public class LocalTag : Tag +public class LocalTag : Tag, ILogixParsable { /// protected override List ElementOrder => @@ -50,4 +50,58 @@ public LocalTag(string name, LogixData value, string? description = default) : b Value = value; SetDescription(description); } + + /// + /// Returns a new deep cloned instance as the specified type. + /// + /// A new instance of the specified element type with the same property values. + public new LocalTag Clone() => new XElement(Serialize()).Deserialize(); + + /// + /// Parses the provided string and returned the strongly typed component object. + /// + /// The XML string value to parse. + /// A new instance that represents the parsed value. + /// + /// Internally this uses XElement.Parse along with our to instantiate the concrete instance. + /// This means the user can use the extensions to also parse XML into stongly tyed logix objects. + /// Also note that since this uses internal XElement and casts the type, this method can throw exceptions for invalid + /// XML or XML that is parsed to an different type thatn the one specified here. + /// + public new static LocalTag Parse(string value) + { + var element = XElement.Parse(value); + return element.Deserialize(); + } + + /// + /// Attempts to parse the provided string and returned the strongly typed component object. + /// If unsuccesful, then this method returns null. + /// + /// The XML string value to parse. + /// A new instance that represents the parsed value if successful, otherwise, null. + /// + /// Internally this uses XElement.Parse along with our to instantiate the concrete instance. + /// This means the user can use the extensions to also parse XML into stongly tyed logix objects. + /// Note that this method will just return null if any exception is caught. This could be for invalid XML formats + /// of invalid type casts. + /// + public new static LocalTag? TryParse(string? value) + { + if (string.IsNullOrEmpty(value)) + return default; + + var trimmed = value.Trim(); + if (trimmed.Length == 0 || trimmed[0] != '<') return default; + + try + { + var element = XElement.Parse(trimmed); + return element.Deserialize(); + } + catch (Exception) + { + return default; + } + } } \ No newline at end of file diff --git a/src/L5Sharp.Core/Components/Module.cs b/src/L5Sharp.Core/Components/Module.cs index e2e13baf..a5dc1117 100644 --- a/src/L5Sharp.Core/Components/Module.cs +++ b/src/L5Sharp.Core/Components/Module.cs @@ -14,7 +14,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public class Module : LogixComponent +public class Module : LogixComponent { /// protected override List ElementOrder => diff --git a/src/L5Sharp.Core/Components/Program.cs b/src/L5Sharp.Core/Components/Program.cs index 0e967a0e..6f91dce1 100644 --- a/src/L5Sharp.Core/Components/Program.cs +++ b/src/L5Sharp.Core/Components/Program.cs @@ -13,7 +13,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public class Program : LogixComponent +public class Program : LogixComponent { /// protected override List ElementOrder => diff --git a/src/L5Sharp.Core/Components/Routine.cs b/src/L5Sharp.Core/Components/Routine.cs index db6f9c13..9ac7c49c 100644 --- a/src/L5Sharp.Core/Components/Routine.cs +++ b/src/L5Sharp.Core/Components/Routine.cs @@ -13,7 +13,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public class Routine : LogixComponent +public class Routine : LogixComponent { /// protected override List ElementOrder => diff --git a/src/L5Sharp.Core/Components/Tag.cs b/src/L5Sharp.Core/Components/Tag.cs index 7662fc40..baad5938 100644 --- a/src/L5Sharp.Core/Components/Tag.cs +++ b/src/L5Sharp.Core/Components/Tag.cs @@ -17,7 +17,7 @@ namespace L5Sharp.Core; [L5XType(L5XName.ConfigTag)] [L5XType(L5XName.InputTag)] [L5XType(L5XName.OutputTag)] -public class Tag : LogixComponent +public class Tag : LogixComponent { /// /// The underlying member object containing the tag's value. All tags and nested tags wrap a simple member instance. diff --git a/src/L5Sharp.Core/Components/Task.cs b/src/L5Sharp.Core/Components/Task.cs index d7688e23..7ceb74c9 100644 --- a/src/L5Sharp.Core/Components/Task.cs +++ b/src/L5Sharp.Core/Components/Task.cs @@ -21,7 +21,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public sealed class Task : LogixComponent +public sealed class Task : LogixComponent { /// protected override List ElementOrder => diff --git a/src/L5Sharp.Core/Components/Trend.cs b/src/L5Sharp.Core/Components/Trend.cs index d7f119c4..1fb79c3f 100644 --- a/src/L5Sharp.Core/Components/Trend.cs +++ b/src/L5Sharp.Core/Components/Trend.cs @@ -14,7 +14,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public class Trend : LogixComponent +public class Trend : LogixComponent { /// protected override List ElementOrder => diff --git a/src/L5Sharp.Core/Components/WatchList.cs b/src/L5Sharp.Core/Components/WatchList.cs index c2cbdec0..d7914947 100644 --- a/src/L5Sharp.Core/Components/WatchList.cs +++ b/src/L5Sharp.Core/Components/WatchList.cs @@ -12,7 +12,7 @@ namespace L5Sharp.Core; /// `Logix 5000 Controllers Import/Export` for more information. /// [L5XType(L5XName.QuickWatchList)] -public class WatchList : LogixComponent +public class WatchList : LogixComponent { /// public WatchList() : base(new XElement(L5XName.QuickWatchList)) diff --git a/src/L5Sharp.Core/Elements/DataTypeMember.cs b/src/L5Sharp.Core/Elements/DataTypeMember.cs index ca16ccb0..7f265a67 100644 --- a/src/L5Sharp.Core/Elements/DataTypeMember.cs +++ b/src/L5Sharp.Core/Elements/DataTypeMember.cs @@ -11,7 +11,7 @@ namespace L5Sharp.Core; /// `Logix 5000 Controllers Import/Export` for more information. /// [L5XType(L5XName.Member)] -public class DataTypeMember : LogixObject +public class DataTypeMember : LogixObject { /// /// Creates a new with default values. diff --git a/src/L5Sharp.Core/Elements/Parameter.cs b/src/L5Sharp.Core/Elements/Parameter.cs index 8efb71c3..cc016138 100644 --- a/src/L5Sharp.Core/Elements/Parameter.cs +++ b/src/L5Sharp.Core/Elements/Parameter.cs @@ -11,7 +11,7 @@ namespace L5Sharp.Core; /// See /// `Logix 5000 Controllers Import/Export` for more information. /// -public class Parameter : LogixObject +public class Parameter : LogixObject { /// protected override List ElementOrder => diff --git a/src/L5Sharp.Core/ILogixParsable.cs b/src/L5Sharp.Core/ILogixParsable.cs index 8bc4ab36..ca029d16 100644 --- a/src/L5Sharp.Core/ILogixParsable.cs +++ b/src/L5Sharp.Core/ILogixParsable.cs @@ -6,7 +6,7 @@ /// The type of object to parse the string value into. public interface ILogixParsable where T : ILogixParsable { - //This is only available in newer versions of .NET and we are supporting .NET standard 2.0. Classes still have to + //This is only available in newer versions of .NET, and we are supporting .NET standard 2.0. Classes still have to //implement this interface where applied to satisfy the newer version, but will technically be an empty marker //interface for older versions. #if NET7_0_OR_GREATER diff --git a/src/L5Sharp.Core/L5Sharp.Core.csproj b/src/L5Sharp.Core/L5Sharp.Core.csproj index afeb3ebe..420c126c 100644 --- a/src/L5Sharp.Core/L5Sharp.Core.csproj +++ b/src/L5Sharp.Core/L5Sharp.Core.csproj @@ -7,9 +7,9 @@ true L5Sharp Timothy Nunnink - 2.3.2 - 2.3.2 - 2.3.2.0 + 3.0.0 + 3.0.0 + 3.0.0.0 A library for intuitively interacting with Rockwell's L5X import/export files. https://github.com/tnunnink/L5Sharp csharp allen-bradely l5x logix plc-programming rockwell-automation logix5000 @@ -18,7 +18,10 @@ git - Fixed bug with Routine Content for emprty routines. + LogixContainer now implements IList{T} and ICollection, slightly changes the API. + LogixObject{T}, LogixComponent{T} created and add ILogixParsable{T} making all components and some elements able to be parsed dynamically using LogixParser extensions. + Update TagName Concat() to reduce memory consumption. + Added methods to AddOnInstruction for generating NeutralText and Instruction instances. Copyright (c) Timothy Nunnink 2022 README.md diff --git a/src/L5Sharp.Core/LogixComponent.cs b/src/L5Sharp.Core/LogixComponent.cs index 7aa7ce4f..986b2508 100644 --- a/src/L5Sharp.Core/LogixComponent.cs +++ b/src/L5Sharp.Core/LogixComponent.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -115,14 +116,14 @@ public virtual string? Description public virtual void Delete() { if (Element.Parent is null || !IsAttached) return; - + var references = References(); foreach (var reference in references) { reference.Element.Remove(); } - + Element.Remove(); } @@ -176,4 +177,79 @@ public override bool Equals(object? obj) /// public override int GetHashCode() => Key.GetHashCode(); +} + +/// +/// A generic abstract that implements the interface. +/// This generic type class allow us to specify the strong return types for methods , +/// and . This means we don't have to implement these methods for every +/// derivative type, and allows these types to be used with the in a dynamic fashion. +/// +/// The type implementing +public abstract class LogixComponent : LogixComponent, ILogixParsable + where TComponent : LogixComponent, ILogixParsable +{ + /// + protected LogixComponent(string name) : base(name) + { + } + + /// + protected LogixComponent(XElement element) : base(element) + { + } + + /// + /// Returns a new deep cloned instance as the specified type. + /// + /// A new instance of the specified element type with the same property values. + public new TComponent Clone() => new XElement(Serialize()).Deserialize(); + + /// + /// Parses the provided string and returned the strongly typed component object. + /// + /// The XML string value to parse. + /// A new instance that represents the parsed value. + /// + /// Internally this uses XElement.Parse along with our to instantiate the concrete instance. + /// This means the user can use the extensions to also parse XML into stongly tyed logix objects. + /// Also note that since this uses internal XElement and casts the type, this method can throw exceptions for invalid + /// XML or XML that is parsed to an different type thatn the one specified here. + /// + public static TComponent Parse(string value) + { + var element = XElement.Parse(value); + return element.Deserialize(); + } + + /// + /// Attempts to parse the provided string and returned the strongly typed component object. + /// If unsuccesful, then this method returns null. + /// + /// The XML string value to parse. + /// A new instance that represents the parsed value if successful, otherwise, null. + /// + /// Internally this uses XElement.Parse along with our to instantiate the concrete instance. + /// This means the user can use the extensions to also parse XML into stongly tyed logix objects. + /// Note that this method will just return null if any exception is caught. This could be for invalid XML formats + /// of invalid type casts. + /// + public static TComponent? TryParse(string? value) + { + if (string.IsNullOrEmpty(value)) + return default; + + var trimmed = value.Trim(); + if (trimmed.Length == 0 || trimmed[0] != '<') return default; + + try + { + var element = XElement.Parse(trimmed); + return element.Deserialize(); + } + catch (Exception) + { + return default; + } + } } \ No newline at end of file diff --git a/src/L5Sharp.Core/LogixContainer.cs b/src/L5Sharp.Core/LogixContainer.cs index 92fa3eb0..fd8d483d 100644 --- a/src/L5Sharp.Core/LogixContainer.cs +++ b/src/L5Sharp.Core/LogixContainer.cs @@ -20,11 +20,11 @@ namespace L5Sharp.Core; /// /// /// The class is designed to only offer very basic operations, allowing it to be applicable to all container type elements, -/// However, the user can extended the API for any container type using extension methods. +/// However, the user can extend the API for any container type using extension methods. /// See for examples demonstrating extensions for containers of type . /// /// -public sealed class LogixContainer : LogixElement, IEnumerable where TObject : LogixObject +public sealed class LogixContainer : LogixElement, IList, ICollection where TObject : LogixObject { /// /// Creates a empty with the default type name. @@ -71,6 +71,12 @@ public LogixContainer(IEnumerable elements) : this() /// public override string L5XType { get; } + /// + /// Gets the number of elements contained in the collection. + /// + /// A representing the number of elements in the collection. + public int Count => Element.Elements(L5XType).Count(); + /// /// Accesses a single element at the specified index of the container collection. /// @@ -121,6 +127,35 @@ public void Add(TObject element) last.AddAfterSelf(xml); } + /// + /// Clears all elements in the container collection. + /// + public void Clear() + { + Element.Elements(L5XType).Remove(); + } + + /// + /// Determines whether a sequence contains a specified element by using the default equality comparer. + /// + /// The element to locate in the sequence. + /// true if the source sequence contains an element that has the specified value; otherwise,false + public bool Contains(TObject element) + { + return Element.Elements(L5XType).Select(e => e.Deserialize()).Contains(element); + } + + /// + /// Copies the entire to a compatible one-dimensional array, + /// starting at the specified index of the target array. + /// + /// + public void CopyTo(TObject[] array, int arrayIndex) + { + var list = Element.Elements(L5XType).Select(e => e.Deserialize()).ToList(); + list.CopyTo(array, arrayIndex); + } + /// /// Adds the provided elements to the logix container at the end of the collection. /// @@ -153,10 +188,25 @@ public void AddRange(IEnumerable elements) } /// - /// Gets the number of elements in the collection. + /// Determines the index of a specific item in the container collection. /// - /// A representing the number of elements in the collection. - public int Count() => Element.Elements(L5XType).Count(); + /// The to locate the index of. + /// + public int IndexOf(TObject element) + { + var index = 0; + foreach (var item in this) + { + if (item.Equals(element)) + { + return index; + } + + index++; + } + + return -1; + } /// /// Inserts the provided element at the specified index of the container collection. @@ -178,11 +228,6 @@ public void Insert(int index, TObject element) Element.Elements(L5XType).ElementAt(index).AddBeforeSelf(xml); } - /// - /// Removes all elements in the container collection. - /// - public void RemoveAll() => Element.Elements(L5XType).Remove(); - /// /// Removes all elements that satisfy the provided condition predicate. /// @@ -205,6 +250,26 @@ public void RemoveAt(int index) Element.Elements(L5XType).ElementAt(index).Remove(); } + /// + /// Removes the first occurrence of a specific object from the . + /// + /// The to remove from the collection. + /// + /// true if item was successfully removed from the collection; otherwise, false. + /// This method also returns false if item is not found in the original collection. + /// + public bool Remove(TObject element) + { + foreach (var item in this) + { + if (!item.Equals(element)) continue; + item.Remove(); + return true; + } + + return false; + } + /// /// Updates all elements in the container by applying the provided update action delegate. /// @@ -244,8 +309,17 @@ public void Update(Action update, Func condition) /// public IEnumerator GetEnumerator() => Element.Elements(L5XType).Select(e => e.Deserialize()).GetEnumerator(); + + void ICollection.CopyTo(Array array, int index) + { + var source = Element.Elements(L5XType).Select(e => e.Deserialize()).ToArray(); + Array.Copy(source, 0, array, index, source.Length); + } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + bool ICollection.IsReadOnly => false; + bool ICollection.IsSynchronized => true; + object ICollection.SyncRoot => Element; } /// diff --git a/src/L5Sharp.Core/LogixElement.cs b/src/L5Sharp.Core/LogixElement.cs index 76d77218..a63a13b7 100644 --- a/src/L5Sharp.Core/LogixElement.cs +++ b/src/L5Sharp.Core/LogixElement.cs @@ -88,18 +88,6 @@ public bool EquivalentTo(LogixElement other) /// This method will simply deserialize a new instance using the current underlying element data. public LogixElement Clone() => new XElement(Serialize()).Deserialize(); - /// - /// Returns a new deep cloned instance as the specified type. - /// - /// The type to cast to. - /// A new instance of the specified element type with the same property values. - /// The object being cloned does not have a constructor accepting a - /// single argument. - /// The deserialized type can not be cast to the specified generic type parameter. - /// This method will simply deserialize a new instance using the current underlying element data. - public TElement Clone() where TElement : LogixElement => - new XElement(Serialize()).Deserialize(); - /// /// Returns the underlying for the . /// diff --git a/src/L5Sharp.Core/LogixEnum.cs b/src/L5Sharp.Core/LogixEnum.cs index 7eb22c1a..19c55c14 100644 --- a/src/L5Sharp.Core/LogixEnum.cs +++ b/src/L5Sharp.Core/LogixEnum.cs @@ -75,6 +75,18 @@ public static IEnumerable Names() where TEnum : LogixEnum { return Enums.Value[typeof(TEnum)].Select(e => e.Name); } + + /// + /// Retrieves all options for each enum type in this library. + /// + /// + /// A collection of key value pairs where the key is the of the enum and the value + /// is the collection of for the type. + /// + public static IEnumerable>> Options() + { + return Enums.Value.Select(x => new KeyValuePair>(x.Key, x.Value)); + } /// /// Retrieves all options for the provided enumeration type. diff --git a/src/L5Sharp.Core/LogixObject.cs b/src/L5Sharp.Core/LogixObject.cs index fedfba9b..fe058e96 100644 --- a/src/L5Sharp.Core/LogixObject.cs +++ b/src/L5Sharp.Core/LogixObject.cs @@ -7,7 +7,7 @@ namespace L5Sharp.Core; /// /// A class implementing that adds common properties and methods shared by most elements or /// components. These features include reference to the containing document, the -/// and to identify where in the document they exists, and methods , +/// and to identify where in the document they exist, and methods , /// , , and which allow easy way to mutate the object /// or collection of objects. /// @@ -42,7 +42,7 @@ protected LogixObject(XElement element) : base(element) /// /// /// This allows attached logix elements to reach up to the L5X file in order to traverse or retrieve - /// other elements in the L5X. This is helpful/used for other extensions and cross referencing functions. + /// other elements in the L5X. This is helpful/used for other extensions and cross-referencing functions. /// public L5X? L5X => Element.Ancestors(L5XName.RSLogix5000Content).FirstOrDefault()?.Annotation(); @@ -77,7 +77,7 @@ protected LogixObject(XElement element) : base(element) /// /// /// - /// This value is retrieved from the ancestors of the underlying element. If no ancestors exists, meaning this + /// This value is retrieved from the ancestors of the underlying element. If no ancestors exist, meaning this /// element is not attached to a L5X tree, then this returns an empty string. /// /// @@ -96,7 +96,7 @@ protected LogixObject(XElement element) : base(element) /// the provided logix element is not the same type or convertable to the type of this logix element. /// /// - /// This method requires the component be attached to the , as it will + /// This method requires the component be attached to the as it will /// access the parent of the underlying to perform the function. /// It will also automatically perform the "type conversion" of the provided element if possible. /// This just means it will attempt to change the element name to match this element name so that the @@ -128,7 +128,7 @@ public void AddAfter(LogixObject item) /// the provided logix element is not the same type or convertable to the type of this logix element. /// /// - /// This method requires the component be attached to the , as it will + /// This method requires the component be attached to an , as it will /// access the parent of the underlying to perform the function. /// It will also automatically perform the "type conversion" of the provided element if possible. /// This just means it will attempt to change the element name to match this element name so that the @@ -259,4 +259,80 @@ public void Replace(LogixObject element) Element.ReplaceWith(element.Serialize()); } +} + +/// +/// A generic abstract that implements the interface. +/// This generic type class allow us to specify the strong return types for methods , +/// and . This means we don't have to implement these methods for every +/// derivative type, and allows these types to be used with the in a dynamic fashion. +/// +/// The type implementing +public abstract class LogixObject : LogixObject, ILogixParsable + where TObject : LogixObject, ILogixParsable +{ + /// + protected LogixObject(string name) : base(name) + { + } + + /// + protected LogixObject(XElement element) : base(element) + { + } + + /// + /// Returns a new deep cloned instance of this object. + /// + /// A new object instance of the same type with the same property values. + /// This method will simply deserialize a new instance using the current underlying element data. + public new TObject Clone() => new XElement(Serialize()).Deserialize(); + + /// + /// Parses the provided string as the specified generic . + /// + /// The XML string value to parse. + /// A new instance that represents the parsed value. + /// + /// Internally this uses XElement.Parse along with our to instantiate the concrete instance. + /// This means the user can use the extensions to also parse XML into stongly tyed logix objects. + /// Also note that since this uses internal XElement and casts the type, this method can throw exceptions for invalid + /// XML or XML that is parsed to an different type thatn the one specified here. + /// + public static TObject Parse(string value) + { + var element = XElement.Parse(value); + return element.Deserialize(); + } + + /// + /// Attempts to parse the provided string and returned the strongly typed object. If unsuccesful, then this method + /// returns null . + /// + /// The XML string value to parse. + /// A new instance that represents the parsed value if successful, otherwise, null. + /// + /// Internally this uses XElement.Parse along with our to instantiate the concrete instance. + /// This means the user can use the extensions to also parse XML into stongly tyed logix objects. + /// Note that this method will just return null if any exception is caught. This could be for invalid XML formats + /// of invalid type casts. + /// + public static TObject? TryParse(string? value) + { + if (string.IsNullOrEmpty(value)) + return default; + + var trimmed = value.Trim(); + if (trimmed.Length == 0 || trimmed[0] != '<') return default; + + try + { + var element = XElement.Parse(trimmed); + return element.Deserialize(); + } + catch (Exception) + { + return default; + } + } } \ No newline at end of file diff --git a/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs b/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs index 82a1442b..44561018 100644 --- a/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs +++ b/tests/L5Sharp.Tests/Components/AddOnInstructionTests.cs @@ -157,6 +157,14 @@ public void ToTag_InstructionWithValidParameters_ShouldBeExpected() tag.Members().ToList().Should().HaveCount(6); } + [Test] + public void ToText_ValidArguments_ShouldBeExpected() + { + var aoi = new AddOnInstruction("TestAOI"); + var text = aoi.ToText("TestAoiTag", "Param1", "Param2", 123, 0); + text.Should().Be("TestAOI(TestAoiTag,Param1,Param2,123,0)"); + } + /*[Test] public void Example() { diff --git a/tests/L5Sharp.Tests/Components/TaskTests.cs b/tests/L5Sharp.Tests/Components/TaskTests.cs index 4b900080..9aaa8312 100644 --- a/tests/L5Sharp.Tests/Components/TaskTests.cs +++ b/tests/L5Sharp.Tests/Components/TaskTests.cs @@ -77,7 +77,7 @@ public void Clone_WhenCalled_ShouldBeAllNewReferences() { var task = new LTask { Name = "Test" }; - var clone = task.Clone(); + var clone = task.Clone(); clone.Should().NotBeSameAs(task); clone.Name.Should().Be(task.Name); diff --git a/tests/L5Sharp.Tests/Data/ComplexDataTests.cs b/tests/L5Sharp.Tests/Data/ComplexDataTests.cs index ef1cecab..3b6005a4 100644 --- a/tests/L5Sharp.Tests/Data/ComplexDataTests.cs +++ b/tests/L5Sharp.Tests/Data/ComplexDataTests.cs @@ -1,7 +1,7 @@ using System.Xml.Linq; using FluentAssertions; -namespace L5Sharp.Tests.Types; +namespace L5Sharp.Tests.Data; [TestFixture] public class ComplexDataTests @@ -105,7 +105,7 @@ public void Clone_WhenCalled_ShouldNotBeSameAsButEqual() new("Member5", new TIMER()) }); - var clone = type.Clone(); + var clone = type.Clone().As(); clone.Should().BeOfType(); clone.Should().NotBeSameAs(type); diff --git a/tests/L5Sharp.Tests/Data/MemberTests.cs b/tests/L5Sharp.Tests/Data/MemberTests.cs index d6b62f5f..ca2a3128 100644 --- a/tests/L5Sharp.Tests/Data/MemberTests.cs +++ b/tests/L5Sharp.Tests/Data/MemberTests.cs @@ -3,7 +3,7 @@ // ReSharper disable UseObjectOrCollectionInitializer -namespace L5Sharp.Tests.Types; +namespace L5Sharp.Tests.Data; [TestFixture] public class MemberTests diff --git a/tests/L5Sharp.Tests/Data/StringDataTests.cs b/tests/L5Sharp.Tests/Data/StringDataTests.cs index 0f506bd2..b40d5204 100644 --- a/tests/L5Sharp.Tests/Data/StringDataTests.cs +++ b/tests/L5Sharp.Tests/Data/StringDataTests.cs @@ -1,7 +1,7 @@ using System.Xml.Linq; using FluentAssertions; -namespace L5Sharp.Tests.Types; +namespace L5Sharp.Tests.Data; [TestFixture] public class StringDataTests diff --git a/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs b/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs index 0703a18e..9c1e28d3 100644 --- a/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs +++ b/tests/L5Sharp.Tests/Elements/DataTypeMemberTests.cs @@ -63,7 +63,7 @@ public void Clone_WhenCalled_ShouldReturnExpectedType() BitNumber = 12 }; - var clone = member.Clone(); + var clone = member.Clone(); clone.Should().BeOfType(); clone.Should().NotBeSameAs(member); diff --git a/tests/L5Sharp.Tests/LogixContainerTests.cs b/tests/L5Sharp.Tests/LogixContainerTests.cs new file mode 100644 index 00000000..ed203b90 --- /dev/null +++ b/tests/L5Sharp.Tests/LogixContainerTests.cs @@ -0,0 +1,27 @@ +using FluentAssertions; + +// ReSharper disable CollectionNeverUpdated.Local + +namespace L5Sharp.Tests; + +[TestFixture] +public class LogixContainerTests +{ + [Test] + public void New_Default_ShouldBeEmpty() + { + var collection = new LogixContainer(); + + collection.Should().NotBeNull(); + collection.Should().BeEmpty(); + } + + [Test] + public void New_CollectionOverload_ShouldHaveExpectedCount() + { + var collection = new LogixContainer([new TestElement(), new TestElement(), new TestElement()]); + + collection.Should().HaveCount(3); + collection.Count.Should().Be(3); + } +} \ No newline at end of file diff --git a/tests/L5Sharp.Tests/LogixElementTests.cs b/tests/L5Sharp.Tests/LogixElementTests.cs index 1b2820f5..fe3bcfd6 100644 --- a/tests/L5Sharp.Tests/LogixElementTests.cs +++ b/tests/L5Sharp.Tests/LogixElementTests.cs @@ -104,17 +104,6 @@ public void Clone_WhenCalled_ShouldBeDifferentXElementInstance() xml.Should().NotBeSameAs(copy); } - [Test] - public void Clone_ValidGenericParameter_ShouldNotBeNullAndOfSpecifiedType() - { - var element = new TestElement(); - - var clone = element.Clone(); - - clone.Should().NotBeNull(); - clone.Should().BeOfType(); - } - [Test] public void GetValue_HasValue_ShouldHaveExpectedValue() { @@ -509,7 +498,7 @@ public Task SetContainer_NoValueToEmptyCollection_ShouldBeVerified() var xml = new XElement("Test"); var element = new TestElement(xml); - element.ChildElements = new LogixContainer(); + element.ChildElements = []; return Verify(element.Serialize().ToString()); } @@ -520,12 +509,12 @@ public Task SetContainer_NoValueCollectionWithElements_ShouldBeVerified() var xml = new XElement("Test"); var element = new TestElement(xml); - element.ChildElements = new LogixContainer - { - new() { SomeValue = "Child_1" }, - new() { SomeValue = "Child_2" }, - new() { SomeValue = "Child_3" } - }; + element.ChildElements = + [ + new ChildElement { SomeValue = "Child_1" }, + new ChildElement { SomeValue = "Child_2" }, + new ChildElement { SomeValue = "Child_3" } + ]; return Verify(element.Serialize().ToString()); } @@ -541,12 +530,12 @@ public Task SetContainer_HasValueDifferentValue_ShouldBeVerified() )); var element = new TestElement(xml); - element.ChildElements = new LogixContainer - { - new() { SomeValue = "Child_3" }, - new() { SomeValue = "Child_2" }, - new() { SomeValue = "Child_1" } - }; + element.ChildElements = + [ + new ChildElement { SomeValue = "Child_3" }, + new ChildElement { SomeValue = "Child_2" }, + new ChildElement { SomeValue = "Child_1" } + ]; return Verify(element.Serialize().ToString()); } @@ -1054,7 +1043,7 @@ public void EquivalentTo_DifferentType_ShouldBeFalse() [L5XType("Test")] [L5XType("Alternate")] -public class TestElement : LogixObject +public class TestElement : LogixObject { public TestElement() : base("Test") { diff --git a/tests/L5Sharp.Tests/Enums/LogixEnumTests.cs b/tests/L5Sharp.Tests/LogixEnumTests.cs similarity index 92% rename from tests/L5Sharp.Tests/Enums/LogixEnumTests.cs rename to tests/L5Sharp.Tests/LogixEnumTests.cs index e0bfb622..1956adec 100644 --- a/tests/L5Sharp.Tests/Enums/LogixEnumTests.cs +++ b/tests/L5Sharp.Tests/LogixEnumTests.cs @@ -1,6 +1,6 @@ using FluentAssertions; -namespace L5Sharp.Tests.Enums; +namespace L5Sharp.Tests; [TestFixture] public class LogixEnumTests @@ -76,4 +76,12 @@ public void Options_NonGenericValidType_ShouldReturnExpected() options.Should().Contain(ExternalAccess.ReadOnly); options.Should().Contain(ExternalAccess.ReadWrite); } + + [Test] + public void Options_WhenCalled_ShouldNotBeEmpty() + { + var options = LogixEnum.Options().ToList(); + + options.Should().NotBeEmpty(); + } } \ No newline at end of file diff --git a/tests/L5Sharp.Tests/LogixParserTests.cs b/tests/L5Sharp.Tests/LogixParserTests.cs index d3894d1a..e95a2b0e 100644 --- a/tests/L5Sharp.Tests/LogixParserTests.cs +++ b/tests/L5Sharp.Tests/LogixParserTests.cs @@ -19,7 +19,7 @@ public void IsParsable_NativeType_ShouldBeTrue(Type type) { type.IsParsable().Should().BeTrue(); } - + [Test] [TestCase(typeof(TagName))] [TestCase(typeof(Dimensions))] @@ -33,7 +33,7 @@ public void IsParsable_LogixType_ShouldBeTrue(Type type) { type.IsParsable().Should().BeTrue(); } - + [Test] [TestCase("true", true, typeof(bool))] [TestCase("123", 123, typeof(int))] @@ -82,7 +82,7 @@ public void Parse_RadixTypeFromDerivedEnumValue_ToEnsurePrivateClassesAreAlsoPar result.Should().NotBeNull(); result.Should().Be(Radix.Octal); } - + [Test] public void Parse_RadixTypeFromDerivedEnumValue_ToEnsureThatThisAlsoParsedToOtherRadixTypes() { @@ -179,7 +179,38 @@ public void Parse_GenericAtomicTypeBooleanValue_ShouldBeExpected() result.Should().NotBeNull(); } - + + [Test] + public void Parse_TagElement_ShouldBeExpected() + { + const string xml = + @""; + + var tag = xml.Parse(); + + tag.Should().NotBeNull(); + tag.Name.Should().Be("Test"); + tag.TagType.Should().Be(TagType.Base); + tag.Constant.Should().BeFalse(); + tag.ExternalAccess.Should().Be(ExternalAccess.ReadWrite); + } + + [Test] + public void Parse_ComplexTagElement_ShouldBeExpected() + { + var xml = Sample.TagElement.TestTimerTag(); + + var tag = xml.Parse(); + + tag.Should().NotBeNull(); + tag.Should().NotBeNull(); + tag.Name.Should().Be("TestTimer"); + tag.DataType.Should().Be("TIMER"); + tag.Value.Should().BeOfType(); + tag["PRE"].Value.Should().Be(1000); + tag["PRE"].Description.Should().Be("Test Timer PRE"); + } + [Test] public void IsItPossibleToFilterByWhatIsParsableToAGivenType() { @@ -190,7 +221,7 @@ public void IsItPossibleToFilterByWhatIsParsableToAGivenType() var stopwatch = Stopwatch.StartNew(); var typed = values.Where(v => v.TryParse(typeof(int)) is not null).ToList(); - + stopwatch.Stop(); Console.WriteLine(stopwatch.ElapsedMilliseconds);