diff --git a/RateLimiter.Tests/CacheHelperTests.cs b/RateLimiter.Tests/CacheHelperTests.cs
new file mode 100644
index 00000000..ac9ce6de
--- /dev/null
+++ b/RateLimiter.Tests/CacheHelperTests.cs
@@ -0,0 +1,84 @@
+using NUnit.Framework;
+using RateLimiter;
+using System;
+using System.Collections.Generic;
+
+namespace RateLimiter.Tests
+{
+ [TestFixture]
+ public class CacheHelperTests
+ {
+ private CacheHelper _cacheHelper;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _cacheHelper = new CacheHelper();
+ }
+
+ [Test]
+ public void LastRequestTime_ShouldReturnMaxValue_WhenNoRequests()
+ {
+ // Act
+ var result = _cacheHelper.LastRequestTime("test-key");
+
+ // Assert
+ Assert.AreEqual(TimeSpan.MaxValue, result);
+ }
+
+ [Test]
+ public void LastRequestTime_ShouldReturnTimeSinceLastRequest_WhenRequestsExist()
+ {
+ // Arrange
+ var key = "test-key";
+ _cacheHelper.AddRequest(key);
+ System.Threading.Thread.Sleep(1000); // Wait for 1 second
+
+ // Act
+ var result = _cacheHelper.LastRequestTime(key);
+
+ // Assert
+ Assert.IsTrue(result.TotalMilliseconds >= 1000);
+ }
+
+ [Test]
+ public void RequestsCount_ShouldReturnZero_WhenNoRequests()
+ {
+ // Act
+ var result = _cacheHelper.RequestsCount("test-key", TimeSpan.FromSeconds(1));
+
+ // Assert
+ Assert.AreEqual(0, result);
+ }
+
+ [Test]
+ public void RequestsCount_ShouldReturnCorrectCount_WhenRequestsExist()
+ {
+ // Arrange
+ var key = "test-key";
+ _cacheHelper.AddRequest(key);
+ _cacheHelper.AddRequest(key);
+ System.Threading.Thread.Sleep(1000); // Wait for 1 second
+
+ // Act
+ var result = _cacheHelper.RequestsCount(key, TimeSpan.FromSeconds(2));
+
+ // Assert
+ Assert.AreEqual(2, result);
+ }
+
+ [Test]
+ public void AddRequest_ShouldAddRequestToCache()
+ {
+ // Arrange
+ var key = "test-key";
+
+ // Act
+ _cacheHelper.AddRequest(key);
+ var result = _cacheHelper.RequestsCount(key, TimeSpan.FromSeconds(1));
+
+ // Assert
+ Assert.AreEqual(1, result);
+ }
+ }
+}
diff --git a/RateLimiter.Tests/RateLimiter.Tests.csproj b/RateLimiter.Tests/RateLimiter.Tests.csproj
index 5cbfc4e8..215c77da 100644
--- a/RateLimiter.Tests/RateLimiter.Tests.csproj
+++ b/RateLimiter.Tests/RateLimiter.Tests.csproj
@@ -8,7 +8,9 @@
+
+
diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs
index 172d44a7..bfb81b4b 100644
--- a/RateLimiter.Tests/RateLimiterTest.cs
+++ b/RateLimiter.Tests/RateLimiterTest.cs
@@ -1,13 +1,68 @@
-using NUnit.Framework;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using NUnit.Framework;
+using System.Threading.Tasks;
-namespace RateLimiter.Tests;
-
-[TestFixture]
-public class RateLimiterTest
+namespace RateLimiter.Tests
{
- [Test]
- public void Example()
- {
- Assert.That(true, Is.True);
- }
-}
\ No newline at end of file
+ [TestFixture]
+ public class RequestLimiterTests
+ {
+ private Mock _mockNext;
+ private Mock _mockRulesFactory;
+ private Mock _mockContextHelper;
+ private Mock _mockCacheHelper;
+ private RequestLimiter _requestLimiter;
+ private DefaultHttpContext _httpContext;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _mockNext = new Mock();
+ _mockRulesFactory = new Mock();
+ _mockContextHelper = new Mock();
+ _mockContextHelper.Setup(ch => ch.GetDto(It.IsAny())).Returns(new ContextDto { Id = "test-id", Path = "", Region = "US" });
+ _mockCacheHelper = new Mock();
+ _requestLimiter = new RequestLimiter(_mockNext.Object, _mockRulesFactory.Object, _mockContextHelper.Object, _mockCacheHelper.Object);
+ _httpContext = new DefaultHttpContext();
+ }
+
+ [Test]
+ public async Task InvokeAsync_ShouldCallNextDelegate_WhenRequestIsAllowed()
+ {
+ // Arrange
+ var contextDto = new ContextDto { Id = "test-id" };
+ var mockRule = new Mock();
+ mockRule.Setup(r => r.CheckLimit()).Returns(true);
+ _mockContextHelper.Setup(ch => ch.GetDto(_httpContext)).Returns(contextDto);
+ _mockRulesFactory.Setup(rf => rf.GetRule(contextDto)).Returns(mockRule.Object);
+
+ // Act
+ await _requestLimiter.InvokeAsync(_httpContext);
+
+ // Assert
+ _mockNext.Verify(next => next(_httpContext), Times.Once);
+ _mockCacheHelper.Verify(ch => ch.AddRequest(contextDto.Id), Times.Once);
+ }
+
+ [Test]
+ public async Task InvokeAsync_ShouldReturnTooManyRequests_WhenRequestIsNotAllowed()
+ {
+ // Arrange
+ var contextDto = new ContextDto { Id = "test-id" };
+ var mockRule = new Mock();
+ mockRule.Setup(r => r.CheckLimit()).Returns(false);
+ _mockContextHelper.Setup(ch => ch.GetDto(_httpContext)).Returns(contextDto);
+ _mockRulesFactory.Setup(rf => rf.GetRule(contextDto)).Returns(mockRule.Object);
+
+ // Act
+ await _requestLimiter.InvokeAsync(_httpContext);
+
+ // Assert
+ Assert.AreEqual(StatusCodes.Status429TooManyRequests, _httpContext.Response.StatusCode);
+ _mockNext.Verify(next => next(_httpContext), Times.Never);
+ _mockCacheHelper.Verify(ch => ch.AddRequest(It.IsAny()), Times.Never);
+ }
+ }
+}
+
diff --git a/RateLimiter.Tests/RulesFactoryTests.cs b/RateLimiter.Tests/RulesFactoryTests.cs
new file mode 100644
index 00000000..9d26d225
--- /dev/null
+++ b/RateLimiter.Tests/RulesFactoryTests.cs
@@ -0,0 +1,75 @@
+using NUnit.Framework;
+using Moq;
+using RateLimiter;
+using RateLimiter.Rules;
+using System.Collections.Generic;
+
+namespace RateLimiter.Tests
+{
+ [TestFixture]
+ public class RulesFactoryTests
+ {
+ private Mock _mockConfigLoader;
+ private Mock _mockCacheHelper;
+ private RulesFactory _rulesFactory;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _mockConfigLoader = new Mock();
+ _mockCacheHelper = new Mock();
+ _rulesFactory = new RulesFactory(_mockConfigLoader.Object, _mockCacheHelper.Object);
+ }
+
+ [Test]
+ public void GetRule_ShouldReturnEmptyRule_WhenNoRulesAreLoaded()
+ {
+ // Arrange
+ var context = new ContextDto {Id = "test-id", Path = "", Region = "US" };
+ _mockConfigLoader.Setup(cl => cl.LoadRules(context)).Returns(new List());
+
+ // Act
+ var result = _rulesFactory.GetRule(context);
+
+ // Assert
+ Assert.IsInstanceOf(result);
+ }
+
+ [Test]
+ public void GetRule_ShouldReturnRequestsLimitRule_WhenRequestsLimitRuleIsLoaded()
+ {
+ // Arrange
+ var context = new ContextDto {Id = "test-id", Path = "", Region = "US" };
+ var rules = new List
+ {
+ new RuleDefinition { Name = "RequestsLimit", Variables = new Dictionary() { { "time", "2000" }, { "requests", "40" } } }
+ };
+ _mockConfigLoader.Setup(cl => cl.LoadRules(context)).Returns(rules);
+
+ // Act
+ var result = _rulesFactory.GetRule(context);
+
+ // Assert
+ Assert.IsInstanceOf(result);
+ }
+
+ [Test]
+ public void GetRule_ShouldReturnTimeLimitRule_WhenTimeLimitRuleIsLoaded()
+ {
+ // Arrange
+ var context = new ContextDto { Id = "test-id", Path = "", Region = "US" };
+ var rules = new List
+ {
+ new RuleDefinition { Name = "TimeLimit", Variables = new Dictionary() { { "time", "2000" } } }
+ };
+ _mockConfigLoader.Setup(cl => cl.LoadRules(context)).Returns(rules);
+
+ // Act
+ var result = _rulesFactory.GetRule(context);
+
+ // Assert
+ Assert.IsInstanceOf(result);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/RateLimiter/CacheHelper.cs b/RateLimiter/CacheHelper.cs
new file mode 100644
index 00000000..0d41cc63
--- /dev/null
+++ b/RateLimiter/CacheHelper.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+
+ public class CacheHelper : ICacheHelper
+ {
+ private readonly Dictionary> _cache = new();
+ public int MaxTimes { get; set; } = 60000; // Default: 60 seconds
+
+ public TimeSpan LastRequestTime(string key)
+ {
+ if (_cache.TryGetValue(key, out var timestamps) && timestamps.Count > 0)
+ {
+ return DateTime.UtcNow - timestamps[^1];
+ }
+ return TimeSpan.MaxValue;
+ }
+
+ public int RequestsCount(string key, TimeSpan time)
+ {
+ if (_cache.TryGetValue(key, out var timestamps))
+ {
+ var threshold = DateTime.UtcNow - time;
+ return timestamps.RemoveAll(t => t > threshold);
+ }
+ return 0;
+ }
+
+ public void AddRequest(string key)
+ {
+ if (!_cache.ContainsKey(key))
+ {
+ _cache[key] = new List();
+ }
+ _cache[key].Add(DateTime.UtcNow);
+ }
+ }
+}
diff --git a/RateLimiter/ContextDto.cs b/RateLimiter/ContextDto.cs
new file mode 100644
index 00000000..e037a38f
--- /dev/null
+++ b/RateLimiter/ContextDto.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public class ContextDto
+ {
+ public string Path { get; set; }
+ public string Region { get; set; }
+ public string Id { get; set; }
+ }
+}
diff --git a/RateLimiter/ICacheHelper.cs b/RateLimiter/ICacheHelper.cs
new file mode 100644
index 00000000..0a8e1e5b
--- /dev/null
+++ b/RateLimiter/ICacheHelper.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public interface ICacheHelper
+ {
+ int MaxTimes { get; set; }
+ TimeSpan LastRequestTime(string key);
+ int RequestsCount(string key, TimeSpan time);
+ void AddRequest(string key);
+ }
+}
diff --git a/RateLimiter/IConfigLoader.cs b/RateLimiter/IConfigLoader.cs
new file mode 100644
index 00000000..7c1d6247
--- /dev/null
+++ b/RateLimiter/IConfigLoader.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Configuration;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public interface IConfigLoader
+ {
+ IEnumerable LoadRules(ContextDto contextDto);
+ }
+
+ internal class ConfigLoader : IConfigLoader
+ {
+ public IEnumerable LoadRules(ContextDto contextDto)
+ {
+ var json = File.ReadAllText("config.json");//hardcoded path and another types of hardcode are bad in real project, but it can be another way to configure rules
+ var section = JsonConvert.DeserializeObject>>>(json) ?? new Dictionary>>();
+ var result = section.Where(x => x.Key == contextDto.Path)
+ .SelectMany(x => x.Value)
+ .Where(x => x.Key == contextDto.Region)
+ .SelectMany(x => x.Value)
+ .Where(x => x.Variables.All(v => contextDto.Id.Contains(v.Value)))
+ .ToList();
+
+ return result;
+
+ }
+ }
+
+
+}
diff --git a/RateLimiter/IContextHelper.cs b/RateLimiter/IContextHelper.cs
new file mode 100644
index 00000000..f59b4b17
--- /dev/null
+++ b/RateLimiter/IContextHelper.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public interface IContextHelper
+ {
+ ContextDto GetDto(HttpContext context);
+ }
+}
diff --git a/RateLimiter/IRequestLimiter.cs b/RateLimiter/IRequestLimiter.cs
new file mode 100644
index 00000000..fac5c314
--- /dev/null
+++ b/RateLimiter/IRequestLimiter.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ internal interface IRequestLimiter
+ {
+ //IHttpContextAccessor
+ //bool CheckLimit(AccsessToken accsessToken);
+ }
+}
diff --git a/RateLimiter/IRule.cs b/RateLimiter/IRule.cs
new file mode 100644
index 00000000..b0de83d7
--- /dev/null
+++ b/RateLimiter/IRule.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public interface IRule
+ {
+ bool CheckLimit();
+ }
+}
diff --git a/RateLimiter/IRulesFactory.cs b/RateLimiter/IRulesFactory.cs
new file mode 100644
index 00000000..5a2c31bc
--- /dev/null
+++ b/RateLimiter/IRulesFactory.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public interface IRulesFactory
+ {
+ IRule GetRule(ContextDto context);
+ }
+}
diff --git a/RateLimiter/RateLimiter.csproj b/RateLimiter/RateLimiter.csproj
index 19962f52..eac798e7 100644
--- a/RateLimiter/RateLimiter.csproj
+++ b/RateLimiter/RateLimiter.csproj
@@ -4,4 +4,10 @@
latest
enable
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RateLimiter/RequestLimiter.cs b/RateLimiter/RequestLimiter.cs
new file mode 100644
index 00000000..b20c758b
--- /dev/null
+++ b/RateLimiter/RequestLimiter.cs
@@ -0,0 +1,47 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public class RequestLimiter : IRequestLimiter
+ {
+ private readonly RequestDelegate _next;
+ private readonly IRulesFactory _rules;
+ private IContextHelper _contextHelper;
+ private ICacheHelper _cacheHelper;
+
+ public RequestLimiter(RequestDelegate next, IRulesFactory rules, IContextHelper contextHelper, ICacheHelper cacheHelper)
+ {
+ _next = next;
+ _rules = rules;
+ _contextHelper = contextHelper;
+ _cacheHelper = cacheHelper;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var contextDto = _contextHelper.GetDto(context);
+ var rule = _rules.GetRule(contextDto);
+ //var path = context.Request.Path.ToString();
+
+ var isAllowed = rule?.CheckLimit() ?? true;
+
+ if (!isAllowed)
+ {
+ context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
+ await context.Response.WriteAsync("Rate limit exceeded. Try again later.");
+ return;
+ }
+
+ _cacheHelper.AddRequest(contextDto.Id);
+
+ await _next(context);
+ }
+ }
+}
diff --git a/RateLimiter/RuleDefinition.cs b/RateLimiter/RuleDefinition.cs
new file mode 100644
index 00000000..453a7993
--- /dev/null
+++ b/RateLimiter/RuleDefinition.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public class RuleDefinition
+ {
+ public string Name { get; set; }
+ public Dictionary Variables { get; set; }
+ }
+}
diff --git a/RateLimiter/Rules/AbstractRule.cs b/RateLimiter/Rules/AbstractRule.cs
new file mode 100644
index 00000000..3e1cf960
--- /dev/null
+++ b/RateLimiter/Rules/AbstractRule.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter.Rules
+{
+ public abstract class AbstractRule : IRule
+ {
+ private IRule _rule;
+ protected ICacheHelper _cacheHelper;
+
+ public AbstractRule(IRule rule, ICacheHelper cacheHelper)
+ {
+ _rule = rule;
+ _cacheHelper = cacheHelper;
+ }
+
+ protected abstract bool CheckRuleLimit();
+
+ public bool CheckLimit()
+ {
+ return CheckRuleLimit() & (_rule?.CheckLimit() ?? true);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/RateLimiter/Rules/EmptyRule.cs b/RateLimiter/Rules/EmptyRule.cs
new file mode 100644
index 00000000..01baeb57
--- /dev/null
+++ b/RateLimiter/Rules/EmptyRule.cs
@@ -0,0 +1,18 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter.Rules
+{
+ public class EmptyRule(IRule rule = null, ICacheHelper cacheHelper = null) : AbstractRule(rule, cacheHelper)
+ {
+ protected override bool CheckRuleLimit()
+ {
+ return true;
+ }
+ }
+}
diff --git a/RateLimiter/Rules/RequestsLimitRule.cs b/RateLimiter/Rules/RequestsLimitRule.cs
new file mode 100644
index 00000000..3aee0b0e
--- /dev/null
+++ b/RateLimiter/Rules/RequestsLimitRule.cs
@@ -0,0 +1,27 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter.Rules
+{
+ public class RequestsLimitRule : AbstractRule
+ {
+ private TimeSpan _timeLimit;
+ private int _requestsCount;
+ private string _id;
+
+ public RequestsLimitRule(IRule rule, Dictionary Variables, ICacheHelper cacheHelper, string id) :base (rule, cacheHelper)
+ {
+ _timeLimit = TimeSpan.FromMilliseconds(int.Parse(Variables["time"]));
+ _requestsCount = int.Parse(Variables["requests"]);
+ }
+
+ protected override bool CheckRuleLimit()
+ {
+ return _cacheHelper.RequestsCount(_id, _timeLimit)>= _requestsCount;
+ }
+ }
+}
diff --git a/RateLimiter/Rules/TimeLimitRule.cs b/RateLimiter/Rules/TimeLimitRule.cs
new file mode 100644
index 00000000..f6b17852
--- /dev/null
+++ b/RateLimiter/Rules/TimeLimitRule.cs
@@ -0,0 +1,25 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter.Rules
+{
+ public class TimeLimitRule : AbstractRule
+ {
+ private TimeSpan _timeLimit;
+ private string _id;
+
+ public TimeLimitRule(IRule rule, Dictionary Variables, ICacheHelper cacheHelper, string id) : base(rule, cacheHelper)
+ {
+ _timeLimit = TimeSpan.FromMilliseconds(int.Parse(Variables["time"]));
+ }
+
+ override protected bool CheckRuleLimit()
+ {
+ return _cacheHelper.LastRequestTime(_id) < _timeLimit;
+ }
+ }
+}
diff --git a/RateLimiter/RulesFactory.cs b/RateLimiter/RulesFactory.cs
new file mode 100644
index 00000000..eca8b1a8
--- /dev/null
+++ b/RateLimiter/RulesFactory.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Http;
+using RateLimiter.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RateLimiter
+{
+ public class RulesFactory : IRulesFactory
+ {
+ private IConfigLoader _configLoader;
+ private ICacheHelper _cacheHelper;
+
+ public RulesFactory(IConfigLoader configLoader, ICacheHelper cacheHelper)
+ {
+ _configLoader = configLoader;
+ _cacheHelper = cacheHelper;
+ }
+
+ public IRule GetRule(ContextDto context)
+ {
+ var rules = _configLoader.LoadRules(context);
+ IRule tempRule = new EmptyRule();
+
+ foreach (var rule in rules)
+ {
+ switch (rule.Name ?? string.Empty)
+ {
+ case "RequestsLimit":
+ tempRule = new RequestsLimitRule(tempRule, rule.Variables, _cacheHelper, context.Id);
+ break;
+ case "TimeLimit":
+ tempRule = new TimeLimitRule(tempRule, rule.Variables, _cacheHelper, context.Id);
+ break;
+ default:
+ tempRule = new EmptyRule(tempRule);
+ break;
+ }
+ }
+ return tempRule;
+ }
+ }
+}
diff --git a/RateLimiter/config.json b/RateLimiter/config.json
new file mode 100644
index 00000000..3eb7b376
--- /dev/null
+++ b/RateLimiter/config.json
@@ -0,0 +1,54 @@
+{
+ "RequestEndpoint1": {
+ "EU": [
+ {
+ "Name": "RequestsLimit",
+ "Variables": {
+ "time": "1000",
+ "requests": "20"
+ }
+ }
+ ],
+ "US": [
+ {
+ "Name": "RequestsLimit",
+ "Variables": {
+ "time": "5000",
+ "requests": "20"
+ }
+ },
+ {
+ "Name": "TimeSinceLastCall",
+ "Variables": {
+ "time": "200"
+ }
+ }
+ ]
+ },
+ "RequestEndpoint2": {
+ "EU": [
+ {
+ "Name": "RequestsLimit",
+ "Variables": {
+ "time": "2000",
+ "requests": "40"
+ }
+ }
+ ],
+ "US": [
+ {
+ "Name": "RequestsLimit",
+ "Variables": {
+ "time": "5000",
+ "requests": "20"
+ }
+ },
+ {
+ "Name": "TimeSinceLastCall",
+ "Variables": {
+ "time": "100"
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file