From 2c6239cff38b3bc69e5d8b3ca4924489edf0d555 Mon Sep 17 00:00:00 2001
From: Andrey Nosov <107930557+swankygawk@users.noreply.github.com>
Date: Wed, 1 Oct 2025 00:30:17 +0200
Subject: [PATCH 1/7] chore: clean up and rename `Main` to `Application`,
update `docker-compose.yaml` to use specific version for `redis`, update
`.gitignore`, update dependencies
---
.gitignore | 6 ++----
.idea/.gitignore | 8 --------
.idea/jpa-buddy.xml | 6 ------
docker-compose.yaml | 4 +---
pom.xml | 18 ++++++++++++++++--
src/main/java/org/redis/Application.java | 5 +++++
src/main/java/org/redis/Main.java | 7 -------
7 files changed, 24 insertions(+), 30 deletions(-)
delete mode 100644 .idea/.gitignore
delete mode 100644 .idea/jpa-buddy.xml
create mode 100644 src/main/java/org/redis/Application.java
delete mode 100644 src/main/java/org/redis/Main.java
diff --git a/.gitignore b/.gitignore
index 5ff6309..0d5994f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,10 +4,8 @@ target/
!**/src/test/**/target/
### IntelliJ IDEA ###
-.idea/modules.xml
-.idea/jarRepositories.xml
-.idea/compiler.xml
-.idea/libraries/
+.idea
+.run
*.iws
*.iml
*.ipr
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/jpa-buddy.xml b/.idea/jpa-buddy.xml
deleted file mode 100644
index 966d5f5..0000000
--- a/.idea/jpa-buddy.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 23de504..701f295 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,9 +1,7 @@
-version: '3.3'
-
services:
redis:
container_name: redis
- image: redis:latest
+ image: redis:8.2.1
restart: always
ports:
- "6379:6379"
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index b4c0aac..e5befda 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,15 +15,29 @@
+
+ org.projectlombok
+ lombok
+ 1.18.42
+ provided
+
+
redis.clientsjedis
- 5.1.5
+ 6.2.0
+
org.junit.jupiterjunit-jupiter
- 5.8.1
+ 5.13.4
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.27.6test
diff --git a/src/main/java/org/redis/Application.java b/src/main/java/org/redis/Application.java
new file mode 100644
index 0000000..15a2a90
--- /dev/null
+++ b/src/main/java/org/redis/Application.java
@@ -0,0 +1,5 @@
+package org.redis;
+
+public final class Application {
+ public static void main(String[] args) {}
+}
\ No newline at end of file
diff --git a/src/main/java/org/redis/Main.java b/src/main/java/org/redis/Main.java
deleted file mode 100644
index 68b3db6..0000000
--- a/src/main/java/org/redis/Main.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.redis;
-
-public class Main {
- public static void main(String[] args) {
- System.out.println("Hello world!");
- }
-}
\ No newline at end of file
From 49294c3ad8d80e564659111d7fc0284b24216edf Mon Sep 17 00:00:00 2001
From: Andrey Nosov <107930557+swankygawk@users.noreply.github.com>
Date: Wed, 1 Oct 2025 00:41:19 +0200
Subject: [PATCH 2/7] feat: create `RedisMap` stub and corresponding test class
---
src/main/java/org/redis/RedisMap.java | 120 ++++++++++++++++++++++
src/test/java/org/redis/RedisMapTest.java | 4 +
2 files changed, 124 insertions(+)
create mode 100644 src/main/java/org/redis/RedisMap.java
create mode 100644 src/test/java/org/redis/RedisMapTest.java
diff --git a/src/main/java/org/redis/RedisMap.java b/src/main/java/org/redis/RedisMap.java
new file mode 100644
index 0000000..cebd931
--- /dev/null
+++ b/src/main/java/org/redis/RedisMap.java
@@ -0,0 +1,120 @@
+package org.redis;
+
+import redis.clients.jedis.Jedis;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class RedisMap implements Map {
+ private final Jedis jedis;
+ private final String mapKey;
+
+ public RedisMap(Jedis jedis, String mapKey) {
+ this.jedis = jedis;
+ this.mapKey = mapKey;
+ }
+
+ /**
+ * @return
+ */
+ @Override
+ public int size() {
+ return 0;
+ }
+
+ /**
+ * @return
+ */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /**
+ * @param key key whose presence in this map is to be tested
+ * @return
+ */
+ @Override
+ public boolean containsKey(Object key) {
+ return false;
+ }
+
+ /**
+ * @param value value whose presence in this map is to be tested
+ * @return
+ */
+ @Override
+ public boolean containsValue(Object value) {
+ return false;
+ }
+
+ /**
+ * @param key the key whose associated value is to be returned
+ * @return
+ */
+ @Override
+ public String get(Object key) {
+ return "";
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ */
+ @Override
+ public String put(String key, String value) {
+ return "";
+ }
+
+ /**
+ * @param key key whose mapping is to be removed from the map
+ * @return
+ */
+ @Override
+ public String remove(Object key) {
+ return "";
+ }
+
+ /**
+ * @param m
+ */
+ @Override
+ public void putAll(Map extends String, ? extends String> m) {
+
+ }
+
+ /**
+ *
+ */
+ @Override
+ public void clear() {
+
+ }
+
+ /**
+ * @return
+ */
+ @Override
+ public Set keySet() {
+ return Set.of();
+ }
+
+ /**
+ * @return
+ */
+ @Override
+ public Collection values() {
+ return List.of();
+ }
+
+ /**
+ * @return
+ */
+ @Override
+ public Set> entrySet() {
+ return Set.of();
+ }
+}
diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java
new file mode 100644
index 0000000..fe8d73b
--- /dev/null
+++ b/src/test/java/org/redis/RedisMapTest.java
@@ -0,0 +1,4 @@
+package org.redis;
+
+public class RedisMapTest {
+}
From 9c76098f2dfadab2f12d1266bc5d128d919e7bc0 Mon Sep 17 00:00:00 2001
From: Andrey Nosov <107930557+swankygawk@users.noreply.github.com>
Date: Wed, 1 Oct 2025 17:31:44 +0200
Subject: [PATCH 3/7] chore: update dependencies
---
pom.xml | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/pom.xml b/pom.xml
index e5befda..0364e6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,12 +22,23 @@
provided
+
+ com.google.guava
+ guava
+ 33.5.0-jre
+ redis.clientsjedis6.2.0
+
+ org.slf4j
+ slf4j-simple
+ 1.7.36
+ test
+ org.junit.jupiterjunit-jupiter
@@ -40,6 +51,34 @@
3.27.6test
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.20.0
+ test
+
+
+
+
+ org.testcontainers
+ testcontainers
+ 1.21.3
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ 1.21.3
+ test
+
+
+
+
+ com.redis.testcontainers
+ testcontainers-redis
+ 1.6.4
+ test
+
\ No newline at end of file
From 59897dad77521e7d65e0abc3f6ae96acbc7668b4 Mon Sep 17 00:00:00 2001
From: Andrey Nosov <107930557+swankygawk@users.noreply.github.com>
Date: Wed, 1 Oct 2025 17:43:23 +0200
Subject: [PATCH 4/7] feat: implement Redis-backed `Map` interface
---
src/main/java/org/redis/RedisMap.java | 223 ++++++++++++++++++++++----
1 file changed, 191 insertions(+), 32 deletions(-)
diff --git a/src/main/java/org/redis/RedisMap.java b/src/main/java/org/redis/RedisMap.java
index cebd931..dd26b4d 100644
--- a/src/main/java/org/redis/RedisMap.java
+++ b/src/main/java/org/redis/RedisMap.java
@@ -1,120 +1,279 @@
package org.redis;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.NonNull;
import redis.clients.jedis.Jedis;
import java.util.Collection;
-import java.util.List;
+import java.util.HashMap;
import java.util.Map;
import java.util.Set;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Реализация интерфейса {@link Map} для строк, использующая Redis в качестве хранилища
+ *
+ * Все пары ключ-значение хранятся в одной структуре данных Redis Hash,
+ * имя которой задаётся через конструктор
+ *
+ * Особенности реализации:
+ *
+ *
Не поддерживает {@code null} в качестве ключей и значений.
+ * При попытке использовать {@code null} будет выброшено исключение {@code NullPointerException}
+ *
Методы, возвращающие коллекции ({@link #keySet()}, {@link #values()}, {@link #entrySet()}),
+ * возвращают копию данных на момент вызова, а не "живое" представление
+ *
Потокобезопасность этого класса зависит от потокобезопасности переданного клиента {@link Jedis}.
+ * Стандартные экземпляры {@code Jedis} не являются потокобезопасными
+ *
+ */
+@SuppressWarnings("ClassCanBeRecord")
public class RedisMap implements Map {
private final Jedis jedis;
private final String mapKey;
+ /**
+ * Создаёт новый экземпляр {@code RedisMap}, связанный с определённым ключом в Redis
+ *
+ * @param jedis клиент Jedis для взаимодействия с Redis
+ * @param mapKey ключ, под которым в Redis будет храниться хэш
+ *
+ * @throws NullPointerException если {@code jedis} или {@code mapKey} являются {@code null}
+ */
public RedisMap(Jedis jedis, String mapKey) {
- this.jedis = jedis;
- this.mapKey = mapKey;
+ this.jedis = checkNotNull(jedis, "jedis client cannot be null");
+ this.mapKey = checkNotNull(mapKey, "mapKey cannot be null");
}
/**
- * @return
+ * Возвращает количество пар ключ-значение в этом {@code RedisMap}
+ *
+ * @return количество пар ключ-значение
+ *
+ * @throws IllegalStateException если количество элементов в Redis превышает {@link Integer#MAX_VALUE}
*/
@Override
public int size() {
- return 0;
+ long realSize = jedis.hlen(mapKey);
+ checkState(
+ realSize <= Integer.MAX_VALUE,
+ "RedisMap size exceeds Integer.MAX_VALUE: " + realSize
+ );
+
+ return (int) realSize;
}
/**
- * @return
+ * Проверяет, содержит ли этот {@code RedisMap} пары ключ-значение
+ *
+ * @return {@code true} если RedisMap пуст
*/
@Override
public boolean isEmpty() {
- return false;
+ return jedis.hlen(mapKey) == 0L;
}
/**
- * @param key key whose presence in this map is to be tested
- * @return
+ * Проверяет, содержит ли этот {@code RedisMap} сопоставление для указанного ключа
+ *
+ * @param key ключ, наличие которого необходимо проверить
+ *
+ * @return • {@code true} если сопоставление существует
+ *
+ * • {@code false} если сопоставления не существует или если {@code key} не является {@code String}
*/
@Override
public boolean containsKey(Object key) {
- return false;
+ if (!(key instanceof String)) {
+ return false; // работает и с key == null
+ }
+
+ return jedis.hexists(mapKey, (String) key);
}
/**
- * @param value value whose presence in this map is to be tested
- * @return
+ * Проверяет, сопоставляет ли этот {@code RedisMap} один или несколько ключей с указанным значением
+ *
+ * @param value значение, наличие которого необходимо проверить
+ *
+ * @return • {@code true} если сопоставление существует
+ *
+ * • {@code false} если сопоставления не существует или если {@code value} не является {@code String}
*/
@Override
public boolean containsValue(Object value) {
- return false;
+ if (!(value instanceof String)) {
+ return false; // работает и с value == null
+ }
+
+ // может быть медленным для больших мап
+ // в таком случае можно использовать итеративный HSCAN
+ return jedis.hvals(mapKey).contains((String) value);
}
/**
- * @param key the key whose associated value is to be returned
- * @return
+ * Получает значение, с которым сопоставлен указанный ключ
+ *
+ * @param key ключ, сопоставленное значение которого нужно получить
+ *
+ * @return • полученное значение
+ *
+ * • {@code null} если сопоставления нет или если {@code key} не является {@code String}
*/
@Override
public String get(Object key) {
- return "";
+ if (!(key instanceof String)) {
+ return null; // работает и с key == null
+ }
+
+ return jedis.hget(mapKey, (String) key);
}
/**
- * @param key
- * @param value
- * @return
+ * Связывает указанное значение с указанным ключом в этом {@code RedisMap}
+ *
+ * @param key ключ, с которым должно быть связано указанное значение
+ * @param value значение, которое должно быть связано с указанным ключом
+ *
+ * @return • предыдущее значение, связанное с {@code key}
+ *
+ * • {@code null} если его не было
+ *
+ * @throws NullPointerException если {@code key} или {@code value} являются {@code null}
*/
@Override
public String put(String key, String value) {
- return "";
+ checkNotNull(key, "key cannot be null");
+ checkNotNull(value, "value cannot be null");
+
+ String oldValue = jedis.hget(mapKey, key);
+ jedis.hset(mapKey, key, value);
+ return oldValue;
}
/**
- * @param key key whose mapping is to be removed from the map
- * @return
+ * Удаляет сопоставление для ключа из этого {@code RedisMap}, если оно существует
+ *
+ * @param key ключ, чьё значение должно быть удалено
+ *
+ * @return • значение, которое было сопоставлено с указанным ключом
+ *
+ * • {@code null} если сопоставления не было или если {@code key} не является {@code String}
*/
@Override
public String remove(Object key) {
- return "";
+ if (!(key instanceof String keyString)) {
+ return null; // работает и с key == null
+ }
+
+ String oldValue = jedis.hget(mapKey, keyString);
+ if (oldValue != null) {
+ jedis.hdel(mapKey, keyString);
+ }
+
+ return oldValue;
}
/**
- * @param m
+ * Копирует все сопоставления из указанного {@code Map} в этот {@code RedisMap}.
+ *
+ * Атомарная операция: либо все элементы добавляются успешно, либо, в случае ошибки,
+ * не будет добавлен ни один элемент
+ *
+ * @param m {@code Map}, элементы которого будут добавлены
+ *
+ * @throws NullPointerException
+ *
+ * • если {@code m} является {@code null}
+ *
+ * • если любой из ключей в {@code m} является {@code null}
+ *
+ * • если любое из значений в {@code m} является {@code null}
*/
@Override
+ @SuppressWarnings("NullableProblems") // проверяем m на null вручную
public void putAll(Map extends String, ? extends String> m) {
+ checkNotNull(m, "m cannot be null");
+
+ if (m.isEmpty()) {
+ return;
+ }
+ for (Map.Entry extends String, ? extends String> entry : m.entrySet()) {
+ checkNotNull(entry.getKey(), "key in m cannot be null");
+ checkNotNull(entry.getValue(), "value in m cannot be null");
+ }
+
+ jedis.hset(mapKey, new HashMap<>(m));
}
/**
- *
+ * Удаляет все сопоставления из этого {@code RedisMap}
*/
@Override
public void clear() {
-
+ jedis.del(mapKey);
}
+ // -------------------------------------------------------------------------------------
+ // Следующие методы возвращают снимок данных (копию), а не "живое" представление (view)
+ // Изменения в возвращенной коллекции не повлияют на данные в Redis, и наоборот
+
/**
- * @return
+ * Возвращает {@link Set} ключей, содержащихся в этом {@code RedisMap}
+ *
+ * Возвращаемый {@code Set} является копией данных из Redis на момент вызова.
+ * Изменения в этом {@code Set} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap}
+ * не отразятся в ранее полученном {@code Set}
+ *
+ * @return множество ключей из этого {@code RedisMap}
*/
@Override
+ @NonNull
public Set keySet() {
- return Set.of();
+ return jedis.hkeys(mapKey);
}
/**
- * @return
+ * Возвращает {@link Collection} значений, содержащихся в этом {@code RedisMap}
+ *
+ * Возвращаемый {@code Collection} является копией данных из Redis на момент вызова.
+ * Изменения в этом {@code Collection} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap}
+ * не отразятся в ранее полученном {@code Collection}
+ *
+ * @return коллекция значений из этого {@code RedisMap}
*/
@Override
+ @NonNull
public Collection values() {
- return List.of();
+ return jedis.hvals(mapKey);
}
/**
- * @return
+ * Возвращает {@link Set} сопоставлений ключ-значение, содержащихся в этом {@code RedisMap}
+ *
+ * Возвращаемый {@code Set} является копией данных из Redis на момент вызова.
+ * Изменения в этом {@code Set} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap}
+ * не отразятся в ранее полученном {@code Set}
+ *
+ * @return множество сопоставлений ключ-значение из этого {@code RedisMap}
*/
@Override
+ @NonNull
public Set> entrySet() {
- return Set.of();
+ return jedis.hgetAll(mapKey).entrySet();
+ }
+ // -------------------------------------------------------------------------------------
+
+
+ /**
+ * Удаляет весь хэш из Redis.
+ *
+ * Предназначен только для использования в тестах для очистки состояния
+ */
+ @VisibleForTesting
+ void destroyForTesting() {
+ jedis.del(mapKey);
}
}
From cb10d88c467b618f55bab2038d1ecd11c23b142f Mon Sep 17 00:00:00 2001
From: Andrey Nosov <107930557+swankygawk@users.noreply.github.com>
Date: Wed, 1 Oct 2025 17:45:13 +0200
Subject: [PATCH 5/7] test: implement unit and integration tests for `RedisMap`
---
src/test/java/org/redis/RedisMapTest.java | 296 ++++++++++++++++++
src/test/java/org/redis/RedisMapUnitTest.java | 35 +++
2 files changed, 331 insertions(+)
create mode 100644 src/test/java/org/redis/RedisMapUnitTest.java
diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java
index fe8d73b..5ae6765 100644
--- a/src/test/java/org/redis/RedisMapTest.java
+++ b/src/test/java/org/redis/RedisMapTest.java
@@ -1,4 +1,300 @@
package org.redis;
+import com.redis.testcontainers.RedisContainer;
+import org.junit.jupiter.api.*;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import redis.clients.jedis.Jedis;
+
+import java.util.*;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@Testcontainers
+@SuppressWarnings("ConstantValue")
public class RedisMapTest {
+ @Container
+ private static final RedisContainer redisContainer = new RedisContainer(
+ RedisContainer.DEFAULT_IMAGE_NAME.withTag("8.2.1")
+ );
+
+ private static Jedis jedis;
+ private static final String TEST_MAP_KEY = "test:redis:map";
+
+ private RedisMap redisMap;
+
+ @BeforeAll
+ static void setUpAll() {
+ String redisUri = redisContainer.getRedisURI();
+ jedis = new Jedis(redisUri);
+ }
+
+ @BeforeEach
+ void setUp() {
+ redisMap = new RedisMap(jedis, TEST_MAP_KEY);
+ redisMap.destroyForTesting();
+ }
+
+ @AfterAll
+ static void tearDownAll() {
+ jedis.close();
+ }
+
+ @Test
+ @DisplayName("get должен возвращать null, если key не String")
+ void getShouldReturnNull_whenKeyIsNotString() {
+ // Act & Assert
+ assertThat(redisMap.get(null)).isNull();
+ }
+
+ @Test
+ @DisplayName("put должен выбрасывать NPE, если key null")
+ void putShouldThrowNPE_whenKeyIsNull() {
+ // Act & Assert
+ assertThatThrownBy(() -> redisMap.put(null, "value"))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("key cannot be null");
+ }
+
+ @Test
+ @DisplayName("put должен выбрасывать NPE, если value null")
+ void putShouldThrowNPE_whenValueIsNull() {
+ // Act & Assert
+ assertThatThrownBy(() -> redisMap.put("key", null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("value cannot be null");
+ }
+
+ @Test
+ @DisplayName("Должен класть и получать значение")
+ void shouldPutAndGetValue() {
+ // Act
+ redisMap.put("key1", "value1");
+ String retrievedValue = redisMap.get("key1");
+
+ // Assert
+ assertThat(retrievedValue).isEqualTo("value1");
+ }
+
+
+ @Test
+ @DisplayName("put должен возвращать предыдущее значение")
+ void putShouldReturnPreviousValue() {
+ // Arrange
+ redisMap.put("key1", "value1");
+
+ // Act
+ String previousValue = redisMap.put("key1", "newValue");
+
+ // Assert
+ assertThat(previousValue).isEqualTo("value1");
+ assertThat(redisMap.get("key1")).isEqualTo("newValue");
+ }
+
+ @Test
+ @DisplayName("Должен корректно считать размер и проверять на пустоту")
+ void shouldCorrectlyCalculateSizeAndIsEmpty() {
+ // Arrange & Assert
+ assertThat(redisMap.isEmpty()).isTrue();
+ assertThat(redisMap.size()).isZero();
+
+ // Act
+ redisMap.put("key1", "value1");
+
+ // Assert
+ assertThat(redisMap.isEmpty()).isFalse();
+ assertThat(redisMap.size()).isEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("containsKey должен возвращать false, если key не String")
+ void containsKeyShouldReturnFalse_whenKeyIsNotString() {
+ // Act & Assert
+ assertThat(redisMap.containsKey(null)).isFalse();
+ }
+
+ @Test
+ @DisplayName("containsValue должен возвращать false, если value не String")
+ void containsValueShouldReturnFalse_whenValueIsNotString() {
+ // Act & Assert
+ assertThat(redisMap.containsValue(null)).isFalse();
+ }
+
+ @Test
+ @DisplayName("Должен проверять наличие ключа и значения")
+ void shouldCheckContainsKeyAndValue() {
+ // Arrange
+ redisMap.put("key1", "value1");
+
+ // Assert
+ assertThat(redisMap.containsKey("key1")).isTrue();
+ assertThat(redisMap.containsKey("non-existent-key")).isFalse();
+
+ assertThat(redisMap.containsValue("value1")).isTrue();
+ assertThat(redisMap.containsValue("non-existent-value")).isFalse();
+ }
+
+ @Test
+ @DisplayName("remove должен возвращать null, если key не String")
+ void removeShouldReturnNull_whenKeyIsNotString() {
+ // Act & Assert
+ assertThat(redisMap.remove(null)).isNull();
+ }
+
+ @Test
+ @DisplayName("remove должен возвращать null, если для key нет сопоставления")
+ void removeShouldReturnNull_whenKeyDoesNotExist() {
+ // Act & Assert
+ assertThat(redisMap.remove("key")).isNull();
+ }
+
+ @Test
+ @DisplayName("Должен удалять элемент и возвращать его значение")
+ void shouldRemoveElementAndReturnValue() {
+ // Arrange
+ redisMap.put("key1", "value1");
+
+ // Act
+ String removedValue = redisMap.remove("key1");
+
+ // Assert
+ assertThat(removedValue).isEqualTo("value1");
+ assertThat(redisMap.size()).isZero();
+ assertThat(redisMap.isEmpty()).isTrue();
+ assertThat(redisMap.containsKey("key1")).isFalse();
+ assertThat(redisMap.containsValue("value1")).isFalse();
+ }
+
+ @Test
+ @DisplayName("putAll должен добавлять все элементы из другой Map")
+ void shouldPutAllElementsFromOtherMap() {
+ // Arrange
+ Map map = Map.of(
+ "key1", "value1",
+ "key2", "value2",
+ "key3", "value3"
+ );
+
+ // Act
+ redisMap.putAll(map);
+
+ // Assert
+ assertThat(redisMap.size()).isEqualTo(3);
+ assertThat(redisMap.entrySet()).containsAll(map.entrySet());
+ }
+
+ @Test
+ @DisplayName("putAll должен выбрасывать NPE, если m null")
+ void putAllShouldThrowNPE_whenMIsNull() {
+ // Act & Assert
+ assertThatThrownBy(() -> redisMap.putAll(null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("m cannot be null");
+ }
+
+ @Test
+ @DisplayName("putAll должен выбрасывать NPE, если в m есть null ключ")
+ void putAllShouldThrowNPE_whenMContainsNullKey() {
+ // Arrange
+ Map map = new HashMap<>();
+ map.put("key1", "value1");
+ map.put(null, "value2");
+
+ // Act & Assert
+ assertThatThrownBy(() -> redisMap.putAll(map))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("key in m cannot be null");
+ assertThat(redisMap.isEmpty()).isTrue(); // check for atomicity
+ }
+
+ @Test
+ @DisplayName("putAll должен выбрасывать NPE, если в m есть null значение")
+ void putAllShouldThrowNPE_whenMContainsNullValue() {
+ // Arrange
+ Map map = new HashMap<>();
+ map.put("key1", "value1");
+ map.put("key2", null);
+
+ // Act & Assert
+ assertThatThrownBy(() -> redisMap.putAll(map))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("value in m cannot be null");
+ assertThat(redisMap.isEmpty()).isTrue(); // check for atomicity
+ }
+
+ @Test
+ @DisplayName("putAll не должен ничего делать, если m.isEmpty()")
+ void putAllShouldDoNothing_whenMIsEmpty() {
+ // Arrange
+ Map map = Collections.emptyMap();
+ redisMap.put("key1", "value1");
+ redisMap.put("key2", "value2");
+
+ // Act
+ redisMap.putAll(map);
+
+ // Assert
+ assertThat(redisMap.size()).isEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("Должен очищать всю RedisMap")
+ void shouldClearMap() {
+ // Arrange
+ redisMap.put("key1", "value1");
+ redisMap.put("key2", "value2");
+
+ // Act
+ redisMap.clear();
+
+ // Assert
+ assertThat(redisMap.isEmpty()).isTrue();
+ }
+
+ @Test
+ @DisplayName("Должен возвращать корректный keySet")
+ void shouldReturnCorrectKeySet() {
+ // Arrange
+ redisMap.put("key1", "value1");
+ redisMap.put("key2", "value2");
+
+ // Act
+ Set keySet = redisMap.keySet();
+
+ // Assert
+ assertThat(keySet).containsExactlyInAnyOrder("key1", "key2");
+ }
+
+ @Test
+ @DisplayName("Должен возвращать корректный values")
+ void shouldReturnCorrectValues() {
+ // Arrange
+ redisMap.put("key1", "value1");
+ redisMap.put("key2", "value2");
+ redisMap.put("key3", "value1");
+
+ // Act
+ Collection values = redisMap.values();
+
+ // Assert
+ assertThat(values).containsExactlyInAnyOrder("value1", "value2", "value1");
+ }
+
+ @Test
+ @DisplayName("Должен возвращать корректный entrySet")
+ void shouldReturnCorrectEntrySet() {
+ // Arrange
+ redisMap.put("key1", "value1");
+ redisMap.put("key2", "value2");
+
+ // Act
+ Set> entrySet = redisMap.entrySet();
+
+ // Assert
+ assertThat(entrySet).containsExactlyInAnyOrder(
+ Map.entry("key1", "value1"),
+ Map.entry("key2", "value2")
+ );
+ }
}
diff --git a/src/test/java/org/redis/RedisMapUnitTest.java b/src/test/java/org/redis/RedisMapUnitTest.java
new file mode 100644
index 0000000..52d1787
--- /dev/null
+++ b/src/test/java/org/redis/RedisMapUnitTest.java
@@ -0,0 +1,35 @@
+package org.redis;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import redis.clients.jedis.Jedis;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class RedisMapUnitTest {
+ private static final String TEST_MAP_KEY = "test:redis:map";
+
+ @Mock
+ private Jedis jedisMock;
+
+ private RedisMap redisMap;
+
+ @Test
+ @DisplayName("Size должен выбрасывать IllegalStateException, если реальный размер превышает Integer.MAX_VALUE")
+ void sizeShouldThrowISE_whenRealSizeExceedsIntegerMaxValue() {
+ // Arrange
+ long exceedingSize = (long) Integer.MAX_VALUE + 1;
+ when(jedisMock.hlen(TEST_MAP_KEY)).thenReturn(exceedingSize);
+ redisMap = new RedisMap(jedisMock, TEST_MAP_KEY);
+
+ // Act & Assert
+ assertThatThrownBy(() -> redisMap.size())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("RedisMap size exceeds Integer.MAX_VALUE: " + exceedingSize);
+ }
+}
From 7e49b4cfc4f9ae19f08371e9b597494d297a675d Mon Sep 17 00:00:00 2001
From: Andrey Nosov <107930557+swankygawk@users.noreply.github.com>
Date: Wed, 1 Oct 2025 17:47:05 +0200
Subject: [PATCH 6/7] docs: add runnable example (`Application`) for `RedisMap`
usage
---
src/main/java/org/redis/Application.java | 77 +++++++++++++++++++++++-
1 file changed, 76 insertions(+), 1 deletion(-)
diff --git a/src/main/java/org/redis/Application.java b/src/main/java/org/redis/Application.java
index 15a2a90..844953b 100644
--- a/src/main/java/org/redis/Application.java
+++ b/src/main/java/org/redis/Application.java
@@ -1,5 +1,80 @@
package org.redis;
+import redis.clients.jedis.Jedis;
+
+import java.util.Map;
+
+@SuppressWarnings({"CallToPrintStackTrace", "ConstantValue"})
public final class Application {
- public static void main(String[] args) {}
+ public static void main(String[] args) {
+ try (Jedis jedis = new Jedis("localhost", 6379)) {
+ Map userCache = new RedisMap(jedis, "users-cache");
+ userCache.clear();
+
+ System.out.println("\n--- Демонстрация RedisMap ---");
+ System.out.println("\nНачальное состояние: isEmpty: " + userCache.isEmpty() + ", Размер: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n1. Добавление пользователей");
+
+ userCache.put("user:1", "Alice");
+ userCache.put("user:2", "Bob");
+ String previousValue = userCache.put("user:3", "Charlie");
+
+ System.out.println("put(\"user:3\", \"Charlie\") вернул: " + previousValue);
+ System.out.println("Текущий размер: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n2. Получение данных");
+
+ String user1 = userCache.get("user:1");
+
+ System.out.println("Значение для \"user:1\": " + user1);
+ System.out.println("Есть ли ключ \"user:2\"? " + userCache.containsKey("user:2"));
+ System.out.println("Есть ли значение \"Diana\"? " + userCache.containsValue("Diana"));
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n3. Обновление пользователя \"user:3\"");
+
+ previousValue = userCache.put("user:3", "Charles");
+
+ System.out.println("put(\"user:3\", \"Charles\") вернул старое значение: " + previousValue);
+ System.out.println("Новое значение для \"user:3\": " + userCache.get("user:3"));
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n4. Все пользователи:");
+
+ for (Map.Entry entry : userCache.entrySet()) {
+ System.out.printf(" - Ключ: %s, Значение: %s%n", entry.getKey(), entry.getValue());
+ }
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n5. Удаление пользователя \"user:2\"");
+
+ String removedUser = userCache.remove("user:2");
+
+ System.out.println("Удаленное значение: " + removedUser);
+ System.out.println("Размер после удаления: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n6. Очистка");
+
+ userCache.clear();
+
+ System.out.println("Финальное состояние: isEmpty: " + userCache.isEmpty() + ", Размер: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n--- Окончание демонстрации ---");
+ } catch (Exception e) {
+ System.err.println("Ошибка при работе с Redis: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
}
\ No newline at end of file
From d339583b2ad24170aaa915dc123119f7d42fd209 Mon Sep 17 00:00:00 2001
From: Andrey Nosov <107930557+swankygawk@users.noreply.github.com>
Date: Wed, 1 Oct 2025 17:48:32 +0200
Subject: [PATCH 7/7] docs: create README
---
README.md | 34 +++++++++++++++++-----------------
TASKREADME.md | 22 ++++++++++++++++++++++
2 files changed, 39 insertions(+), 17 deletions(-)
create mode 100644 TASKREADME.md
diff --git a/README.md b/README.md
index 5c936ee..b26576a 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,22 @@
-# Практическое задание 2 # Redis Map
+## Prerequisites
+- JDK 17+
+- Apache Maven 3.9+
+- Docker
-В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map
-Достаточно будет реализации Map
-
-**Описание:**
-✅ Написать реализацию методов Map, которая будет работать с Redis
-✅ Работать с redis можно через Jedis
-✅ Покрыть все методы тестами
-✅ Для поднятия redis, в корне проекта есть docker-compose файл
-
-Redis поднимается командой
+## Getting started
+```bash
+git clone https://github.com/swankygawk/RedisMap.git
+cd RedisMap
```
+
+## Launching demo
+```bash
docker-compose up -d
+mvn compile exec:java -Dexec.mainClass="org.redis.Application"
```
-**Как выполнить:**
-1. Сделать форк этого проекта
-2. Сделать ветку dev в своем проекте. Выполнять работу в ней
-3. После выполнения работы открыть Pull Request в master
-4. Назначить ментора ревьювером
-5. Отправить ссылку на PR в тг чат ментору
\ No newline at end of file
+## Launching tests
+```bash
+mvn test
+```
+Tests use testcontainers, so make sure to have a running Docker environment
diff --git a/TASKREADME.md b/TASKREADME.md
new file mode 100644
index 0000000..5c936ee
--- /dev/null
+++ b/TASKREADME.md
@@ -0,0 +1,22 @@
+# Практическое задание 2 # Redis Map
+
+В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map
+Достаточно будет реализации Map
+
+**Описание:**
+✅ Написать реализацию методов Map, которая будет работать с Redis
+✅ Работать с redis можно через Jedis
+✅ Покрыть все методы тестами
+✅ Для поднятия redis, в корне проекта есть docker-compose файл
+
+Redis поднимается командой
+```
+docker-compose up -d
+```
+
+**Как выполнить:**
+1. Сделать форк этого проекта
+2. Сделать ветку dev в своем проекте. Выполнять работу в ней
+3. После выполнения работы открыть Pull Request в master
+4. Назначить ментора ревьювером
+5. Отправить ссылку на PR в тг чат ментору
\ No newline at end of file