Skip to content

Commit

Permalink
feat: adding CRUD and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wjohnsto committed Jan 24, 2025
1 parent c5d5b4b commit 49e86e2
Show file tree
Hide file tree
Showing 15 changed files with 445 additions and 90 deletions.
2 changes: 0 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
<description>A starter project for working with Redis and Java</description>
<properties>
<java.version>17</java.version>
<jedis.version>5.2.0</jedis.version>
</properties>
<dependencies>
<dependency>
Expand All @@ -33,7 +32,6 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>

<dependency>
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/io/redis/todoapp/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

@SpringBootApplication
public class App {

public static void main(String[] args) {
public static void loadDotEnv() {
Dotenv dotenv = Dotenv.configure().load();
dotenv.entries().forEach(e -> System.setProperty(e.getKey(), e.getValue()));
}

public static void main(String[] args) {
loadDotEnv();

SpringApplication.run(App.class, args);
}
Expand Down
17 changes: 8 additions & 9 deletions src/main/java/io/redis/todoapp/GsonConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@

import java.time.Instant;

import org.springframework.boot.autoconfigure.gson.GsonBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import io.redis.todoapp.util.Gson_InstantTypeAdapter;

@Configuration
public class GsonConfiguration {
@Primary
@Bean
public GsonBuilderCustomizer typeAdapterRegistration() {
return builder -> {
builder
public Gson getGson() {
return new GsonBuilder()
.registerTypeAdapter(Instant.class, new Gson_InstantTypeAdapter())
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'");
};
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")
.create();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package io.redis.todoapp.components.todos;

import java.lang.invoke.MethodHandles;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import io.redis.todoapp.components.todos.models.CreateTodoDto;
import io.redis.todoapp.components.todos.models.UpdateTodoDto;

@RestController
@RequestMapping("/api/todos")
public class TodoController {
private static final Logger logger = LoggerFactory.getLogger(TodoController.class);
private final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

@Autowired
private TodoRepository repository;
Expand All @@ -23,11 +33,12 @@ public TodoController() {

@GetMapping("")
public ResponseEntity<?> all() {
logger.info("GET request access '/api/todos' path.");
logger.info("GET /api/todos.");
try {
var todos = repository.all();
return new ResponseEntity<>(todos, HttpStatus.OK);
} catch (Exception e) {
logger.error(e.getMessage());
return new ResponseEntity<>(String.format("Error getting all todos %s", e), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Expand All @@ -37,8 +48,63 @@ public ResponseEntity<?> allTrailing() {
return all();
}

@GetMapping("/search")
public ResponseEntity<?> search(@RequestParam(required = false) String name, @RequestParam(required = false) String status) {
logger.info("GET /api/todos/search");
try {
var todos = repository.search(name, status);
return new ResponseEntity<>(todos, HttpStatus.OK);
} catch (Exception e) {
logger.error(e.getMessage());
return new ResponseEntity<>(String.format("Error getting all todos %s", e), HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@GetMapping("/search/")
public ResponseEntity<?> searchTrailing(@RequestParam(required = false) String name, @RequestParam(required = false) String status) {
return search(name, status);
}

@GetMapping("/{id}")
public ResponseEntity<?> one(@PathVariable String id) {
logger.info(String.format("GET /api/todos/%s", id));
return new ResponseEntity<>(repository.one(id), HttpStatus.OK);
}

@GetMapping("/{id}/")
public ResponseEntity<?> oneTrailing(@PathVariable String id) {
return one(id);
}

@PostMapping("")
public ResponseEntity<?> create(@RequestBody CreateTodoDto todo) {
logger.info(String.format("POST /api/todos/ (%s)", todo));
return new ResponseEntity<>(repository.create(todo), HttpStatus.OK);
}

@PostMapping("/")
public ResponseEntity<?> createTrailing(@RequestBody CreateTodoDto todo) {
return create(todo);
}

@PatchMapping("/{id}")
public ResponseEntity<?> update(@PathVariable String id, @RequestBody UpdateTodoDto todo) {
logger.info(String.format("PATCH /api/todos/%s (%s)", id, todo));
return new ResponseEntity<>(repository.update(id, todo), HttpStatus.OK);
}

@PatchMapping("/{id}/")
public ResponseEntity<?> updateTrailing(@PathVariable String id, @RequestBody UpdateTodoDto todo) {
return update(id, todo);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable String id) {
repository.delete(id);
}

@DeleteMapping("/{id}/")
public void deleteTrailing(@PathVariable String id) {
delete(id);
}
}
132 changes: 115 additions & 17 deletions src/main/java/io/redis/todoapp/components/todos/TodoRepository.java
Original file line number Diff line number Diff line change
@@ -1,48 +1,56 @@
package io.redis.todoapp.components.todos;

import java.lang.invoke.MethodHandles;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.gson.GsonBuilderCustomizer;
import org.springframework.stereotype.Repository;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import io.redis.todoapp.components.todos.models.CreateTodoDto;
import io.redis.todoapp.components.todos.models.Todo;
import io.redis.todoapp.components.todos.models.TodoDocument;
import io.redis.todoapp.components.todos.models.TodoDocuments;
import io.redis.todoapp.components.todos.models.UpdateTodoDto;
import jakarta.annotation.PostConstruct;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.search.FTCreateParams;
import redis.clients.jedis.search.IndexDataType;
import redis.clients.jedis.search.SearchResult;
import redis.clients.jedis.search.schemafields.SchemaField;
import redis.clients.jedis.search.schemafields.TextField;

@Repository
public class TodoRepository {
private final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

public static final String TODOS_INDEX = "todos-idx";
public static final String TODOS_PREFIX = "todos:";

@Autowired
private GsonBuilderCustomizer gsonCustomizer;

private Gson gson;

@Autowired
private UnifiedJedis redis;

@PostConstruct
public void init() {
var gsonBuilder = new GsonBuilder();
gsonCustomizer.customize(gsonBuilder);
gson = gsonBuilder.create();

this.createIndexIfNotExists();
}

private String formatId(String id) {
if (id.matches("^" + TODOS_PREFIX + ".*")) {
return id;
}

return TODOS_PREFIX + id;
}

private boolean haveIndex() {
var indexes = redis.ftList();

Expand All @@ -66,31 +74,121 @@ public void createIndexIfNotExists() {
);
}

public TodoDocuments all() {
var result = redis.ftSearch(TODOS_INDEX, "*");
public void dropIndex() {
if (!haveIndex()) {
return;
}

redis.ftDropIndex(TODOS_INDEX);
}

private TodoDocuments toTodoDocuments(SearchResult result) {
var total = result.getTotalResults();
var documents = result.getDocuments();
List<TodoDocument> todos = new ArrayList<>();

logger.debug("found " + documents.size() + " documents");
for (var doc : documents) {
var name = doc.getString("name");
var status = doc.getString("status");
var createdDate = doc.getString("created_date");
var updatedDate = doc.getString("updated_date");
var todo = new Todo(name, status, createdDate, updatedDate);
var todo = jsonToTodo(doc.get("$"));
todos.add(new TodoDocument(doc.getId(), todo));
}

return new TodoDocuments(total, todos);
var todoDocuments = new TodoDocuments(total, todos);

logger.debug(String.format("\n%s\n", todoDocuments.toString()));

return todoDocuments;
}

public TodoDocuments all() {
var result = redis.ftSearch(TODOS_INDEX, "*");
return toTodoDocuments(result);
}

public TodoDocuments search(String name, String status) {
List<String> searches = new ArrayList<>();

if (name != null) {
searches.add(String.format("@name:(%s)", name));
}

if (status != null) {
searches.add(String.format("@status:%s", status));
}

var result = redis.ftSearch(TODOS_INDEX, String.join(" ", searches));

return toTodoDocuments(result);
}

public TodoDocument create(CreateTodoDto todoDto) {
todoDto.setId(formatId(todoDto.getId()));
var todoDocument = todoDto.toTodoDocument();
var ok = redis.jsonSet(todoDocument.getId(), gson.toJson(todoDocument.getValue()));

logger.debug(String.format("jsonSet(%s, %s) == %s", todoDocument.getId(), todoDocument.getValue(), ok));

if (ok.equals("ok")) {
logger.debug(String.format("Todo created: %s", todoDocument));
return todoDocument;
}

return todoDocument;
}

public Todo update(String id, UpdateTodoDto todoDto) {
var status = todoDto.getStatus();
switch (status) {
case "todo", "in progress", "complete" -> {
}
default -> throw new AssertionError(String.format("invalid status %s", status));
}

id = formatId(id);
var todo = one(id);
todo.setStatus(status);
todo.setUpdatedDate(Instant.now());
var ok = redis.jsonSet(id, gson.toJson(todo));

if (ok.equals("ok")) {
logger.debug(String.format("Todo updated: %s", todo));
return todo;
}

return todo;
}

public Todo one(String id) {
var result = redis.jsonGet(id);
var result = redis.jsonGet(formatId(id));

return jsonToTodo(result);
}

public void delete(String id) {
redis.jsonDel(formatId(id));
}

public void deleteAll() {
var todos = all();

if (todos.getTotal() <= 0) {
return;
}

List<String> ids = new ArrayList<>();

for (var doc: todos.getDocuments()) {
ids.add(doc.getId());
}

redis.del(ids.toArray(String[]::new));
}

private Todo jsonToTodo(Object obj) {
if (obj instanceof String string) {
return gson.fromJson(string, Todo.class);
}

var json = gson.toJson(obj);
return gson.fromJson(json, Todo.class);
}
Expand Down
Loading

0 comments on commit 49e86e2

Please sign in to comment.