diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..69917c32 Binary files /dev/null and b/.DS_Store differ diff --git a/.classpath b/.classpath new file mode 100644 index 00000000..f0e98045 --- /dev/null +++ b/.classpath @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..30cf57ed --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..584cb668 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 00000000..fb6eb39d --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + Requête + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..712ab9d9 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..1b4a03b8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 00000000..f2be8237 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + JaxRSOpenAPI + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..2af1e7b9 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..f897a7f1 --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/README.md b/README.md index 09032612..e46bb73a 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,79 @@ -## JaxRS + openAPI +# Nom du Projet : E-Ticket -1. Import this project in your IDE, -2. Start the database -3. Start the database viewer -4. Start the backend. There is a main class to start the backend +## Lancer le projet +###Prérequis +- Java & Maven installés +- Un serveur MySQL en cours d'exécution +- Eclipse (ou tout IDE supportant Maven) +###Une fois le projet cloné +1. Se placer sur la branche backend : **git checkout back** +2. Builder le projet avec Maven Faire un clic droit sur le projet → Run As → Maven Build -# Task Open API Integration +3. Configurer la base de données: + - Créer une base de données vide nommée **mydatabase** dans MySQL. + - Si votre configuration MySQL est différente (port, host, identifiants), ouvrez le fichier **src/main/resources/META-INF/persistence** et modifiez votre config. -Now, we would like to ensure that our API can be discovered. The OpenAPI Initiative (OAI) was created by a consortium of forward-looking industry experts who recognize the immense value of standardizing on how REST APIs are described. As an open governance structure under the Linux Foundation, the OAI is focused on creating, evolving and promoting a vendor neutral description format. +4. Exécuter les migrations + Lancer la classe suivante pour créer les tables en base de données : **src/main/java/fr.istic.taa.jaxrs.jpa/JpaTest.java** + puis faire Clic droit sur cette classe → Run As → Java Application -APIs form the connecting glue between modern applications. Nearly every application uses APIs to connect with corporate data sources, third party data services or other applications. Creating an open description format for API services that is vendor neutral, portable and open is critical to accelerating the vision of a truly connected world. +5. Démarrer le serveur + Lancer la classe suivante pour démarrer le serveur REST : **src/main/java/fr.istic.taa.jaxrs.rest/RestServer.java** + puis faire Clic droit sur cette classe → Run As → Java Application -To do this integration first, I already add a dependencies to openAPI libraries. +Le serveur est maintenant opérationnel. -```xml - - io.swagger.core.v3 - swagger-jaxrs2-jakarta - 2.2.15 - +## Quelques Endpoints +| Méthode | URL | Description | +|---------|-----|-------------| +| POST | http://localhost:8080/client/inscription | s'inscrire comme client | +| POST | http://localhost:8080/manager/ajouter | enregistrer un manager | +| POST | http://localhost:8080/event/create | Créer un événement | +| GET | http://localhost:8080/event/all/{managerId} | Événements par manager | +| POST | http://localhost:8080/ticket/acheter | acheter un ticket | - - io.swagger.core.v3 - swagger-jaxrs2-servlet-initializer-v2 - 2.2.15 - -``` +## Swagger +La documentation complète des api est disponible à : + **http://localhost:8080/swagger-api/** -Next you have to add OpenAPI Resource to your application +## Description du projet +Il s'agit d'une application web de billetterie en ligne dédiée à la gestion et à la vente de billets pour des événements (concerts, spectacles, etc.). +L'application s'adresse à trois types d'utilisateurs : +- Les clients peuvent parcourir le catalogue d'événements disponibles et acheter leurs billets directement en ligne. +- Les managers peuvent gérer leurs événements : dates, lieux, capacités, tarifs, etc. +- Les administrateurs gèreent l'ensemble des utilisateurs de la plateforme (validation, suspension, suppression de comptes, etc.). -Your application could be something like that. -```java -@ApplicationPath("/") -public class RestApplication extends Application { +## Modèle métier - @Override - public Set> getClasses() { - final Set> resources = new HashSet<>(); +### Diagramme de classe +![Diagramme de classe](diagramme.png) +### Entités +- **User** : représente un utilisateur de l'application (Client, Manager, Admin) +- **Manager** : hérite de User, gère les événements +- **Admin** : hérite de User, valide les événements +- **Client** : hérite de User, s'incrit sur la plateforme et achete les tickets pour des evements +- **Event** : un événement créé par un Manager +- **Ticket** : acheté par un Client pour un événement (Event) - // SWAGGER endpoints - resources.add(OpenApiResource.class); +### Regles de gestion +1. Client +- Il s'inscrit librement sur la plateforme sans intervention d'un administrateur +- Il ne peut acheter qu'un seul billet par événement. +- Il peut consulter l'historique de ses achats, et les evenements disponibles - //Your own resources. - resources.add(PersonResource.class); -.... - return resources; - } -} -``` +2. Manager +- Il est créé uniquement par un administrateur et un mot de passe generique est affecté au manager que lui meme pourra modifier. +- Il gère exclusivement ses propres événements (creer, modifier, ou supprimer des events) +- Il gère le stock de ticket pour ses événements et suit les ventes des tickets de son événement -Next start your server, you must have your api description available at [http://localhost:8080/openapi.json](http://localhost:8080/openapi.json) +3. Administrateur +- Il est créé et géré uniquement par le Super Administrateur(SUPER_ADMIN). +- Il a une vue globale sur l'ensemble des événements et des transactions de la plateforme. +- Il peut effectuer les actions suivantes sur les utilisateurs (clients et managers) : Valider, bloquer / débloquer un compte. -### Integrate Swagger UI. -Next we have to integrate Swagger UI. We will first download it. -https://github.com/swagger-api/swagger-ui - -Copy dist folder content in src/main/webapp/swagger in your project. - -Edit index.html file to automatically load your openapi.json file. - -At the end of the index.html, your must have something like that. - -```js - // Build a system - const ui = SwaggerUIBundle({ - url: "http://localhost:8080/openapi.json", - dom_id: '#swagger-ui', - - ... -``` - -Next add a new resources to create a simple http server when your try to access to http://localhost:8080/api/. - -This new resources can be developped as follows - -```java -package app.web.rest; - -import java.io.IOException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.util.logging.Logger; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; - -@Path("/api") -public class SwaggerResource { - - private static final Logger logger = Logger.getLogger(SwaggerResource.class.getName()); - - @GET - public byte[] Get1() { - try { - return Files.readAllBytes(FileSystems.getDefault().getPath("src/main/webapp/swagger/index.html")); - } catch (IOException e) { - return null; - } - } - - @GET - @Path("{path:.*}") - public byte[] Get(@PathParam("path") String path) { - try { - return Files.readAllBytes(FileSystems.getDefault().getPath("src/main/webapp/swagger/"+path)); - } catch (IOException e) { - return null; - } - } - -} -``` - -Add this new resources in your application - -```java -@ApplicationPath("/") -public class RestApplication extends Application { - - - @Override - public Set> getClasses() { - final Set> resources = new HashSet<>(); - - - // SWAGGER endpoints - resources.add(OpenApiResource.class); - resources.add(PersonResource.class); - //NEW LINE TO ADD - resources.add(SwaggerResource.class); - - return resources; - } -} -``` - -Restart your server and access to http://localhost:8080/api/, you should access to a swagger ui instance that provides documentation on your api. - -You can follow this guide to show how you can specialise the documentation through annotations. - -https://github.com/swagger-api/swagger-samples/blob/2.0/java/java-resteasy-appclasses/src/main/java/io/swagger/sample/resource/PetResource.java \ No newline at end of file diff --git a/data/test.log b/data/test.log new file mode 100644 index 00000000..e69de29b diff --git a/data/test.properties b/data/test.properties new file mode 100644 index 00000000..ee984c5f --- /dev/null +++ b/data/test.properties @@ -0,0 +1,5 @@ +#HSQL Database Engine 2.7.2 +#Wed Apr 08 12:36:51 CEST 2026 +modified=no +tx_timestamp=270 +version=2.7.2 diff --git a/data/test.script b/data/test.script new file mode 100644 index 00000000..357ee08e --- /dev/null +++ b/data/test.script @@ -0,0 +1,70 @@ +SET DATABASE UNIQUE NAME HSQLDB9C7A129D76 +SET DATABASE DEFAULT RESULT MEMORY ROWS 0 +SET DATABASE EVENT LOG LEVEL 0 +SET DATABASE TRANSACTION CONTROL LOCKS +SET DATABASE DEFAULT ISOLATION LEVEL READ COMMITTED +SET DATABASE TRANSACTION ROLLBACK ON CONFLICT TRUE +SET DATABASE TEXT TABLE DEFAULTS '' +SET DATABASE SQL NAMES FALSE +SET DATABASE SQL RESTRICT EXEC FALSE +SET DATABASE SQL REFERENCES FALSE +SET DATABASE SQL SIZE TRUE +SET DATABASE SQL TYPES FALSE +SET DATABASE SQL TDC DELETE TRUE +SET DATABASE SQL TDC UPDATE TRUE +SET DATABASE SQL SYS INDEX NAMES TRUE +SET DATABASE SQL CONCAT NULLS TRUE +SET DATABASE SQL UNIQUE NULLS TRUE +SET DATABASE SQL CONVERT TRUNCATE TRUE +SET DATABASE SQL AVG SCALE 0 +SET DATABASE SQL DOUBLE NAN TRUE +SET FILES WRITE DELAY 500 MILLIS +SET FILES BACKUP INCREMENT TRUE +SET FILES CACHE SIZE 10000 +SET FILES CACHE ROWS 50000 +SET FILES SCALE 32 +SET FILES LOB SCALE 32 +SET FILES DEFRAG 0 +SET FILES NIO TRUE +SET FILES NIO SIZE 256 +SET FILES LOG TRUE +SET FILES LOG SIZE 50 +SET FILES CHECK 270 +SET DATABASE COLLATION "SQL_TEXT" PAD SPACE +CREATE USER SA PASSWORD DIGEST 'd41d8cd98f00b204e9800998ecf8427e' +ALTER USER SA SET LOCAL TRUE +CREATE SCHEMA PUBLIC AUTHORIZATION DBA +CREATE SEQUENCE PUBLIC.EVENT_SEQ AS INTEGER START WITH 1 INCREMENT BY 50 +CREATE SEQUENCE PUBLIC.USER_SEQ AS INTEGER START WITH 1 INCREMENT BY 50 +CREATE MEMORY TABLE PUBLIC.ADMIN(USERID BIGINT NOT NULL PRIMARY KEY) +CREATE MEMORY TABLE PUBLIC.CLIENT(USERID BIGINT NOT NULL PRIMARY KEY,STATUTCLIENT VARCHAR(255)) +CREATE MEMORY TABLE PUBLIC.EVENT(EVENTID BIGINT NOT NULL PRIMARY KEY,MANAGER_USERID BIGINT) +CREATE MEMORY TABLE PUBLIC.MANAGER(USERID BIGINT NOT NULL PRIMARY KEY,STATUTMANAGER VARCHAR(255)) +CREATE MEMORY TABLE PUBLIC.TICKET(DATEACHAT DATE,DATEANNULATION DATE,DATEREMBOURSEMENT DATE,NUMEROPLACE INTEGER NOT NULL,PRIX NUMERIC(38,2),EVENTID BIGINT NOT NULL,USERID BIGINT NOT NULL,STATUT VARCHAR(255),PRIMARY KEY(EVENTID,USERID),CONSTRAINT FKG99AEFCEA91OV184GCQ27RW38 FOREIGN KEY(USERID) REFERENCES PUBLIC.CLIENT(USERID),CONSTRAINT FKMCDJ38KCLK095O7XCSRQ1L6PL FOREIGN KEY(EVENTID) REFERENCES PUBLIC.EVENT(EVENTID)) +CREATE MEMORY TABLE PUBLIC.USER(USERID BIGINT NOT NULL PRIMARY KEY,NOM VARCHAR(255),PRENOM VARCHAR(255)) +ALTER TABLE PUBLIC.ADMIN ADD CONSTRAINT FKJOAV33P64SUIKUB3369FPAJY4 FOREIGN KEY(USERID) REFERENCES PUBLIC.USER(USERID) +ALTER TABLE PUBLIC.CLIENT ADD CONSTRAINT FKMEGT8W509PVKVJOTT5J6PVULQ FOREIGN KEY(USERID) REFERENCES PUBLIC.USER(USERID) +ALTER TABLE PUBLIC.EVENT ADD CONSTRAINT FKPROJTK8GIX7GKR8YL2L2UWF4E FOREIGN KEY(MANAGER_USERID) REFERENCES PUBLIC.MANAGER(USERID) +ALTER TABLE PUBLIC.MANAGER ADD CONSTRAINT FK4K4DKJ2CBEM84XEDBYQ0HCVKL FOREIGN KEY(USERID) REFERENCES PUBLIC.USER(USERID) +ALTER SEQUENCE SYSTEM_LOBS.LOB_ID RESTART WITH 1 +ALTER SEQUENCE PUBLIC.EVENT_SEQ RESTART WITH 1 +ALTER SEQUENCE PUBLIC.USER_SEQ RESTART WITH 451 +SET DATABASE DEFAULT INITIAL SCHEMA PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.CARDINAL_NUMBER TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.YES_OR_NO TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.CHARACTER_DATA TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.SQL_IDENTIFIER TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.TIME_STAMP TO PUBLIC +GRANT DBA TO SA +SET SCHEMA SYSTEM_LOBS +INSERT INTO BLOCKS VALUES(0,2147483647,0) +SET SCHEMA PUBLIC +INSERT INTO USER VALUES(1,'Ange','Stephanie') +INSERT INTO USER VALUES(2,'Ange','Stephanie') +INSERT INTO USER VALUES(52,'Ange','Stephanie') +INSERT INTO USER VALUES(102,'Ange','Stephanie') +INSERT INTO USER VALUES(152,'Ange','Stephanie') +INSERT INTO USER VALUES(202,'Ange','Stephanie') +INSERT INTO USER VALUES(252,'Ange','Stephanie') +INSERT INTO USER VALUES(302,'lo','mori') +INSERT INTO USER VALUES(352,'lo','mori') diff --git a/diagramme.png b/diagramme.png new file mode 100644 index 00000000..6bcb6674 Binary files /dev/null and b/diagramme.png differ diff --git a/pom.xml b/pom.xml index 1b1c1ebe..8ef2d304 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,35 @@ swagger-jaxrs2-servlet-initializer-v2 2.2.15 + + + + + org.webjars + swagger-ui + 5.0.0 + + + + + + mysql + mysql-connector-java + 8.0.22 + + + + + org.mindrot + jbcrypt + 0.4 + + + + + + + diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..bfd612ca Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 00000000..22a2f5e2 Binary files /dev/null and b/src/main/.DS_Store differ diff --git a/src/main/java/fr/istic/taa/jaxrs/RestServer.java b/src/main/java/fr/istic/taa/jaxrs/RestServer.java index d2eeaeb6..68b52b72 100644 --- a/src/main/java/fr/istic/taa/jaxrs/RestServer.java +++ b/src/main/java/fr/istic/taa/jaxrs/RestServer.java @@ -1,7 +1,11 @@ package fr.istic.taa.jaxrs; +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.domain.Admin; +import fr.istic.taa.jaxrs.domain.enumeration.Role; import io.undertow.Undertow; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; +import org.mindrot.jbcrypt.BCrypt; import java.util.logging.Logger; @@ -17,16 +21,39 @@ public static void main( String[] args ) { UndertowJaxrsServer ut = new UndertowJaxrsServer(); - TestApplication ta = new TestApplication(); - - ut.deploy(ta); + ut.deploy(TestApplication.class); ut.start( Undertow.builder() .addHttpListener(8080, "localhost") ); + initialiserAdmin(); + + logger.info("JAX-RS based micro-service running! \n" + + "API running on: http://localhost:8080 \n" + + "Swagger UI: http://localhost:8080/swagger-api/"); + } + - logger.info("JAX-RS based micro-service running!"); + private static void initialiserAdmin() { + UserDao userDao = new UserDao(); + + // Vérifie si un admin existe déjà + if (userDao.findByEmailNamedQuery("admin@concert.com") == null) { + Admin admin = new Admin(); + admin.setNom("Admin"); + admin.setPrenom("Super"); + admin.setEmail("admin@concert.com"); + admin.setPassword(BCrypt.hashpw("admin123", BCrypt.gensalt())); + admin.setTelephone("0600000000"); + admin.setRole(Role.ADMIN); + admin.setStatut_user(true); + + userDao.save(admin); + logger.info("Admin par défaut créé : admin@concert.com / admin123"); + } else { + logger.info("Admin déjà existant, pas de création."); + } } } diff --git a/src/main/java/fr/istic/taa/jaxrs/TestApplication.java b/src/main/java/fr/istic/taa/jaxrs/TestApplication.java index c479dad0..86354c46 100644 --- a/src/main/java/fr/istic/taa/jaxrs/TestApplication.java +++ b/src/main/java/fr/istic/taa/jaxrs/TestApplication.java @@ -18,15 +18,13 @@ import java.util.HashSet; import java.util.Set; - -import fr.istic.taa.jaxrs.rest.PetResource; +import fr.istic.taa.jaxrs.rest.*; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("/") public class TestApplication extends Application { - @Override public Set> getClasses() { @@ -34,10 +32,15 @@ public Set> getClasses() { final Set> clazzes = new HashSet>(); clazzes.add(OpenApiResource.class); - clazzes.add(PetResource.class); -// clazzes.add(AcceptHeaderOpenApiResource.class); + clazzes.add(UserResource.class); + clazzes.add(ClientResource.class); + clazzes.add(AdminResource.class); + clazzes.add(ManagerResource.class); + clazzes.add(EventResource.class); + clazzes.add(TicketResource.class); + clazzes.add(SwaggerResource.class); + clazzes.add(CorsFilter.class); - return clazzes; } diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/AdminDao.java b/src/main/java/fr/istic/taa/jaxrs/dao/AdminDao.java new file mode 100644 index 00000000..3cfc2f93 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dao/AdminDao.java @@ -0,0 +1,11 @@ +package fr.istic.taa.jaxrs.dao; + +import fr.istic.taa.jaxrs.dao.generic.AbstractJpaDao; +import fr.istic.taa.jaxrs.domain.Admin; + +public class AdminDao extends AbstractJpaDao{ + + public AdminDao() { + this.setClazz(Admin.class); + } +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/ClientDao.java b/src/main/java/fr/istic/taa/jaxrs/dao/ClientDao.java new file mode 100644 index 00000000..f20d2025 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dao/ClientDao.java @@ -0,0 +1,13 @@ +package fr.istic.taa.jaxrs.dao; + +import fr.istic.taa.jaxrs.dao.generic.AbstractJpaDao; +import fr.istic.taa.jaxrs.domain.Client; + +public class ClientDao extends AbstractJpaDao{ + + public ClientDao() { + this.setClazz(Client.class); + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/EventDao.java b/src/main/java/fr/istic/taa/jaxrs/dao/EventDao.java new file mode 100644 index 00000000..43c5b7a5 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dao/EventDao.java @@ -0,0 +1,41 @@ +package fr.istic.taa.jaxrs.dao; + +import java.time.LocalDate; +import java.util.List; + +import fr.istic.taa.jaxrs.dao.generic.AbstractJpaDao; +import fr.istic.taa.jaxrs.domain.Event; + +public class EventDao extends AbstractJpaDao { + + public EventDao() { + this.setClazz(Event.class); + } + + public List findAvailableEventNamedQuery() { + + return entityManager + .createNamedQuery("Event.findAvailableEvent", Event.class) + .setParameter("now", LocalDate.now()) + .getResultList(); + } + + + //Requête JPQL + public List findByManagerId(Long managerId) { + String query_jpql = "SELECT e FROM Event e WHERE e.manager.userId = :manager_id AND e.is_deleted = false"; + + return entityManager + .createQuery(query_jpql, Event.class) + .setParameter("manager_id", managerId) + .getResultList(); + + } + + public List findAll() { + return entityManager + .createQuery("SELECT e FROM Event e WHERE e.is_deleted = false", Event.class) + .getResultList(); + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/ManagerDao.java b/src/main/java/fr/istic/taa/jaxrs/dao/ManagerDao.java new file mode 100644 index 00000000..f2d0cbed --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dao/ManagerDao.java @@ -0,0 +1,16 @@ +package fr.istic.taa.jaxrs.dao; + +import fr.istic.taa.jaxrs.dao.generic.AbstractJpaDao; +import fr.istic.taa.jaxrs.domain.Manager; +import jakarta.persistence.NoResultException; + +public class ManagerDao extends AbstractJpaDao{ + + public ManagerDao() { + this.setClazz(Manager.class); + } + + +} + + diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/TicketDao.java b/src/main/java/fr/istic/taa/jaxrs/dao/TicketDao.java new file mode 100644 index 00000000..a11b636b --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dao/TicketDao.java @@ -0,0 +1,64 @@ +package fr.istic.taa.jaxrs.dao; + +import java.util.List; +import fr.istic.taa.jaxrs.dao.generic.AbstractJpaDao; +import fr.istic.taa.jaxrs.domain.Ticket; +import fr.istic.taa.jaxrs.domain.TicketId; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +public class TicketDao extends AbstractJpaDao { + + public TicketDao() { + this.setClazz(Ticket.class); + } + + //Requete NamedQuery + public List findByClientIdNamedQuery(Long id) { + + return entityManager + .createNamedQuery("Ticket.findByClientId", Ticket.class) + .setParameter("client_id", id) + .getResultList(); + } + + // Criteria Query + public List findByEventId(Long eid) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Ticket.class); + + Root ticket = cq.from(Ticket.class); + + Predicate eventCondition = cb.equal(ticket.get("event").get("eventId"), eid); + + cq.where(eventCondition); + + return entityManager.createQuery(cq).getResultList(); + } + + //Requete jpql + public long countTicketsByEvent(Long eventId) { + TypedQuery query = entityManager + .createQuery("SELECT COUNT(t) FROM Ticket t WHERE t.event.eventId = :event_id", Long.class) + .setParameter("event_id", eventId); + + return query.getSingleResult(); + } + + public List findByClientId(Long clientId) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Ticket.class); + + Root ticket = cq.from(Ticket.class); + + Predicate clientCondition = cb.equal(ticket.get("client").get("userId"), clientId); + + cq.where(clientCondition); + + return entityManager.createQuery(cq).getResultList(); + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/UserDao.java b/src/main/java/fr/istic/taa/jaxrs/dao/UserDao.java new file mode 100644 index 00000000..ee0da68b --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dao/UserDao.java @@ -0,0 +1,44 @@ +package fr.istic.taa.jaxrs.dao; + +import java.util.List; +import fr.istic.taa.jaxrs.dao.generic.AbstractJpaDao; +import fr.istic.taa.jaxrs.domain.*; +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + + +public class UserDao extends AbstractJpaDao { + + public UserDao() { + this.setClazz(User.class); + } + + //Requete NamedQuery + public User findByEmailNamedQuery(String email) { + try { + return entityManager + .createNamedQuery("User.findByEmail", User.class) + .setParameter("emailValue", email) + .getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + // Criteria Query + public List findAllUser() { + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(User.class); + + Root userRoot = cq.from(User.class); + + cq.select(userRoot); + + return entityManager.createQuery(cq).getResultList(); + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/generic/AbstractJpaDao.java b/src/main/java/fr/istic/taa/jaxrs/dao/generic/AbstractJpaDao.java index 6adb209c..6b70d9c4 100644 --- a/src/main/java/fr/istic/taa/jaxrs/dao/generic/AbstractJpaDao.java +++ b/src/main/java/fr/istic/taa/jaxrs/dao/generic/AbstractJpaDao.java @@ -3,6 +3,7 @@ import java.io.Serializable; import java.util.List; +import fr.istic.taa.jaxrs.jpa.EntityManagerHelper; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityTransaction; diff --git a/src/main/java/fr/istic/taa/jaxrs/dao/generic/EntityManagerHelper.java b/src/main/java/fr/istic/taa/jaxrs/dao/generic/EntityManagerHelper.java deleted file mode 100644 index a9950dc2..00000000 --- a/src/main/java/fr/istic/taa/jaxrs/dao/generic/EntityManagerHelper.java +++ /dev/null @@ -1,50 +0,0 @@ -package fr.istic.taa.jaxrs.dao.generic; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.Persistence; - -public class EntityManagerHelper { - - private static final EntityManagerFactory emf; - private static final ThreadLocal threadLocal; - - static { - emf = Persistence.createEntityManagerFactory("dev"); - threadLocal = new ThreadLocal(); - } - - public static EntityManager getEntityManager() { - EntityManager em = threadLocal.get(); - - if (em == null) { - em = emf.createEntityManager(); - threadLocal.set(em); - } - return em; - } - - public static void closeEntityManager() { - EntityManager em = threadLocal.get(); - if (em != null) { - em.close(); - threadLocal.set(null); - } - } - - public static void closeEntityManagerFactory() { - emf.close(); - } - - public static void beginTransaction() { - getEntityManager().getTransaction().begin(); - } - - public static void rollback() { - getEntityManager().getTransaction().rollback(); - } - - public static void commit() { - getEntityManager().getTransaction().commit(); - } -} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/Admin.java b/src/main/java/fr/istic/taa/jaxrs/domain/Admin.java new file mode 100644 index 00000000..35afaee8 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/Admin.java @@ -0,0 +1,49 @@ +package fr.istic.taa.jaxrs.domain; + +import java.io.Serializable; +import java.util.List; +import fr.istic.taa.jaxrs.domain.enumeration.NiveauAdmin; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.OneToMany; + +@Entity +public class Admin extends User implements Serializable { + + @Enumerated(EnumType.STRING) + private NiveauAdmin admin_niveau; + + @OneToMany(mappedBy = "admin", cascade = CascadeType.PERSIST) + private List events; + + + //GETTERS - SETTERS + + + public NiveauAdmin getAdmin_niveau() { + return admin_niveau; + } + public void setAdmin_niveau(NiveauAdmin admin_niveau) { + this.admin_niveau = admin_niveau; + } + + + public List getEvents() { + return events; + } + public void setEvents(List events) { + this.events = events; + } + + //ToString Admin + @Override + public String toString() { + return "Admin [events=" + events + ", userId=" + userId + ", nom=" + nom + ", prenom=" + prenom + ", email=" + + email + ", telephone=" + telephone + ", date_naissance=" + date_naissance + ", password=" + password + + ", statut_User=" + statut_user + "]"; + } + +} + diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/Client.java b/src/main/java/fr/istic/taa/jaxrs/domain/Client.java new file mode 100644 index 00000000..4fdfacc6 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/Client.java @@ -0,0 +1,44 @@ +package fr.istic.taa.jaxrs.domain; + +import java.io.Serializable; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; + +@Entity +public class Client extends User implements Serializable{ + + private boolean client_newsletter; + + @OneToMany(mappedBy = "client", cascade = CascadeType.PERSIST) + @JsonManagedReference + private List tickets; + + + + //GETTERS - SETTERS + + public boolean isClient_newsletter() { + return client_newsletter; + } + public void setClient_newsletter(boolean client_newsletter) { + this.client_newsletter = client_newsletter; + } + + + public List getTickets() { + return tickets; + } + public void setTickets(List tickets) { + this.tickets = tickets; + } + + //ToString Client + @Override + public String toString() { + return "Client [tickets=" + tickets + ", userId=" + userId + ", nom=" + nom + ", prenom=" + prenom + ", email=" + + email + ", telephone=" + telephone + ", date_naissance=" + date_naissance + ", password=" + password + + ", statut_User=" + statut_user + "]"; + } + +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/Event.java b/src/main/java/fr/istic/taa/jaxrs/domain/Event.java new file mode 100644 index 00000000..4542bcd8 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/Event.java @@ -0,0 +1,201 @@ +package fr.istic.taa.jaxrs.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import fr.istic.taa.jaxrs.domain.enumeration.StatutEvent; +import jakarta.persistence.*; + +@Entity +@NamedQuery( + name = "Event.findAvailableEvent", + query = "SELECT e FROM Event e WHERE e.date_concert > :now AND e.nb_place_disponible > 0" + + "AND e.is_deleted = false AND e.statut_event != ANNULE" + ) +public class Event implements Serializable { + + @Id + @GeneratedValue + private Long eventId; + private String nom; + private String description; + private String artiste; + private String lieu; + private int duree_event; + private String genreMusical; + private int nb_place_disponible; + private boolean is_deleted; + + @Enumerated(EnumType.STRING) + private StatutEvent statut_event; + + @Column(precision = 8, scale = 2) + private BigDecimal prix_ticket; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate date_concert; + + @ManyToOne + @JoinColumn(name = "managerId", nullable = false) + private Manager manager; + + @ManyToOne + @JoinColumn(name = "adminId", nullable = true) + private Admin admin; + + + @OneToMany(mappedBy = "event", cascade = CascadeType.PERSIST) + private List tickets; + + + //GETTERS - SETTERS + + public Long getEventId() { + return eventId; + } + public void setEventId(Long eventId) { + this.eventId = eventId; + } + + + public String getNom() { + return nom; + } + public void setNom(String nom) { + this.nom = nom; + } + + + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + + + public String getArtiste() { + return artiste; + } + public void setArtiste(String artiste) { + this.artiste = artiste; + } + + + public String getLieu() { + return lieu; + } + public void setLieu(String lieu) { + this.lieu = lieu; + } + + + public int getDureeConcert() { + return duree_event; + } + public void setDureeConcert(int duree_event) { + this.duree_event = duree_event; + } + + + public String getGenreMusical() { + return genreMusical; + } + public void setGenreMusical(String genreMusical) { + this.genreMusical = genreMusical; + } + + + public int getNb_place_disponible() { + return nb_place_disponible; + } + public void setNb_place_disponible(int nb_place_disponible) { + this.nb_place_disponible = nb_place_disponible; + } + + + public StatutEvent getStatut_concert() { + return statut_event; + } + public void setStatut_concert(StatutEvent statut_concert) { + this.statut_event = statut_concert; + } + + + public BigDecimal getPrix_ticket() { + return prix_ticket; + } + public void setPrix_ticket(BigDecimal prix_ticket) { + this.prix_ticket = prix_ticket; + } + + + public LocalDate getDate_concert() { + return date_concert; + } + public void setDate_concert(LocalDate date_concert) { + this.date_concert = date_concert; + } + + + public boolean isDeleted() { + return is_deleted; + } + public void setDeleted(boolean is_deleted) { + this.is_deleted = is_deleted; + } + + + public Manager getManager() { + return manager; + } + public void setManager(Manager manager) { + this.manager = manager; + } + + + public Admin getAdmin() { + return admin; + } + public void setAdmin(Admin admin) { + this.admin = admin; + } + + + public List getTickets() { + return tickets; + } + public void setTickets(List tickets) { + this.tickets = tickets; + } + + + //ToString Event + @Override + public String toString() { + return "Event [eventId=" + eventId + ", nom=" + nom + ", description=" + description + ", artiste=" + artiste + + ", lieu=" + lieu + ", dureeConcert=" + duree_event + ", genreMusical=" + genreMusical + + ", nb_place_disponible=" + nb_place_disponible + ", statut_concert=" + statut_event + + ", prix_ticket=" + prix_ticket + ", date_concert=" + date_concert + ", manager=" + manager + + ", admin=" + admin + ", tickets=" + tickets + "]"; + } + + + // diminuer le nombre de places disponibles + public void diminuerPlaces() { + if (this.nb_place_disponible <= 0) { + throw new IllegalArgumentException("Plus de places disponibles"); + } + this.nb_place_disponible--; + } + + +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/Manager.java b/src/main/java/fr/istic/taa/jaxrs/domain/Manager.java new file mode 100644 index 00000000..e4628192 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/Manager.java @@ -0,0 +1,46 @@ +package fr.istic.taa.jaxrs.domain; + +import java.io.Serializable; +import java.util.List; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.OneToMany; + +@Entity +@NamedQuery( + name = "Manager.findByEmail", + query = "SELECT m FROM Manager m WHERE m.email = :emailValue" +) +public class Manager extends User implements Serializable { + + private String manager_agence; + @OneToMany(mappedBy = "manager", cascade = CascadeType.PERSIST) + private List events; + + + //GETTERS - SETTERS + + + public List getEvents() { + return events; + } + public void setEvents(List events) { + this.events = events; + } + + //ToString Manager + @Override + public String toString() { + return "Manager [events=" + events + ", userId=" + userId + ", nom=" + nom + ", prenom=" + prenom + ", email=" + + email + ", telephone=" + telephone + ", date_naissance=" + date_naissance + ", password=" + password + + ", statut_User=" + statut_user + "]"; + } + public String getManager_agence() { + return manager_agence; + } + public void setManager_agence(String manager_agence) { + this.manager_agence = manager_agence; + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/Pet.java b/src/main/java/fr/istic/taa/jaxrs/domain/Pet.java deleted file mode 100644 index c8738b97..00000000 --- a/src/main/java/fr/istic/taa/jaxrs/domain/Pet.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.istic.taa.jaxrs.domain; - -import java.util.ArrayList; -import java.util.List; - -import io.swagger.v3.oas.models.tags.Tag; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlElementWrapper; -import jakarta.xml.bind.annotation.XmlRootElement; - -@XmlRootElement(name = "Pet") -public class Pet { - private long id; - private String name; - private List tags = new ArrayList(); - - @XmlElement(name = "id") - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - @XmlElement(name = "name") - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @XmlElementWrapper(name = "tags") - @XmlElement(name = "tag") - public List getTags() { - return tags; - } - - public void setTags(List tags) { - this.tags = tags; - } -} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/Ticket.java b/src/main/java/fr/istic/taa/jaxrs/domain/Ticket.java new file mode 100644 index 00000000..e59e3cc1 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/Ticket.java @@ -0,0 +1,105 @@ +package fr.istic.taa.jaxrs.domain; + +import java.io.Serializable; +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +import fr.istic.taa.jaxrs.domain.enumeration.StatutTicket; +import jakarta.persistence.*; + + +@Entity +@NamedQuery( + name = "Ticket.findByClientId", + query = "SELECT t FROM Ticket t WHERE t.client.userId = :client_id" + ) +public class Ticket implements Serializable { + + @EmbeddedId + private TicketId id; + + @Enumerated(EnumType.STRING) + private StatutTicket statut_ticket; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate dateAchat; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate dateAnnulation; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate dateRemboursement; + + @ManyToOne + @MapsId("userId") + @JoinColumn(name = "userId", nullable = false) + @JsonBackReference + private Client client; + + @ManyToOne + @MapsId("eventId") + @JoinColumn(name = "eventId", nullable = false) + @JsonBackReference + private Event event; + + // Getters et Setters + public TicketId getId() { return id; } + public void setId(TicketId id) { this.id = id; } + + public Client getClient() { return client; } + public void setClient(Client client) { this.client = client; } + + public Event getEvent() { return event; } + public void setEvent(Event event) { this.event = event; } + + + public StatutTicket getStatut() { + return statut_ticket; + } + public void setStatut(StatutTicket statut_ticket) { + this.statut_ticket = statut_ticket; + } + + public LocalDate getDateAchat() { + return dateAchat; + } + public void setDateAchat(LocalDate dateAchat) { + this.dateAchat = dateAchat; + } + + + public LocalDate getDateAnnulation() { + return dateAnnulation; + } + public void setDateAnnulation(LocalDate dateAnnulation) { + this.dateAnnulation = dateAnnulation; + } + + + public LocalDate getDateRemboursement() { + return dateRemboursement; + } + public void setDateRemboursement(LocalDate dateRemboursement) { + this.dateRemboursement = dateRemboursement; + } + + @Override + public String toString() { + return "Ticket [id=" + id + ", numeroPlace=" + "statut_ticket= " + statut_ticket + + ", dateAchat=" + dateAchat + ", dateAnnulation=" + dateAnnulation + ", dateRemboursement=" + + dateRemboursement + ", client=" + client + ", event=" + event + "]"; + } + +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/TicketId.java b/src/main/java/fr/istic/taa/jaxrs/domain/TicketId.java new file mode 100644 index 00000000..e7762613 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/TicketId.java @@ -0,0 +1,43 @@ +package fr.istic.taa.jaxrs.domain; + +import jakarta.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public class TicketId implements Serializable { + private Long userId; + private Long eventId; + + + //CONSTRUCTEURS + public TicketId() { + super(); + } + public TicketId(Long userId, Long eventId) { + this.userId = userId; + this.eventId = eventId; + } + + + //GETTERS - SETTERS + + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + + public Long getEventId() { return eventId; } + public void setEventId(Long eventId) { this.eventId = eventId; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TicketId ticketId = (TicketId) o; + return userId == ticketId.userId && eventId == ticketId.eventId; + } + + @Override + public int hashCode() { + return Objects.hash(userId, eventId); + } +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/User.java b/src/main/java/fr/istic/taa/jaxrs/domain/User.java new file mode 100644 index 00000000..7cf8a1a5 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/User.java @@ -0,0 +1,125 @@ +package fr.istic.taa.jaxrs.domain; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +import fr.istic.taa.jaxrs.domain.enumeration.Role; + + +@Entity +@NamedQuery( + name = "User.findByEmail", + query = "SELECT u FROM User u WHERE u.email = :emailValue" + ) +@Inheritance(strategy = InheritanceType.JOINED) +public class User implements Serializable { + + @Id + @GeneratedValue + protected Long userId; + + @Enumerated(EnumType.STRING) + protected Role role; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + protected LocalDate date_naissance; + + protected boolean statut_user = false; + protected String nom; + protected String prenom; + protected String email; + protected String password; + protected String telephone; + + + //Getters and Setters + + public Long getUserId() { + return userId; + } + public void setUserId(Long userId) { + this.userId = userId; + } + + + public Role getRole() { + return role; + } + public void setRole(Role role) { + this.role = role; + } + + + public LocalDate getDate_naissance() { + return date_naissance; + } + public void setDate_naissance(LocalDate date_naissance) { + this.date_naissance = date_naissance; + } + + + public boolean isStatut_user() { + return statut_user; + } + public void setStatut_user(boolean statut_user) { + this.statut_user = statut_user; + } + + + public String getNom() { + return nom; + } + public void setNom(String nom) { + this.nom = nom; + } + + + public String getPrenom() { + return prenom; + } + public void setPrenom(String prenom) { + this.prenom = prenom; + } + + + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + + + public String getPassword() { + return password; + } + public void setPassword(String password) { + this.password = password; + } + + + public String getTelephone() { + return telephone; + } + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + + //ToString User + @Override + public String toString() { + return "User [userId=" + userId + ", nom=" + nom + ", prenom=" + prenom + "]"; + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/NiveauAdmin.java b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/NiveauAdmin.java new file mode 100644 index 00000000..9d1d070c --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/NiveauAdmin.java @@ -0,0 +1,6 @@ +package fr.istic.taa.jaxrs.domain.enumeration; + +public enum NiveauAdmin { + + ADMIN, SUPER_ADMIN +} diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/Role.java b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/Role.java new file mode 100644 index 00000000..80c568bd --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/Role.java @@ -0,0 +1,6 @@ +package fr.istic.taa.jaxrs.domain.enumeration; + +public enum Role { + + CLIENT, MANAGER, ADMIN +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/StatutEvent.java b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/StatutEvent.java new file mode 100644 index 00000000..48db1672 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/StatutEvent.java @@ -0,0 +1,7 @@ +package fr.istic.taa.jaxrs.domain.enumeration; + +public enum StatutEvent { + + ATTENTE_VALIDATION, ANNULE, VALIDE + +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/StatutTicket.java b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/StatutTicket.java new file mode 100644 index 00000000..196a4201 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/domain/enumeration/StatutTicket.java @@ -0,0 +1,7 @@ +package fr.istic.taa.jaxrs.domain.enumeration; + +public enum StatutTicket { + + ACHETE, ANNULE, REMBOURSE + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/AchatTicketDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/AchatTicketDto.java new file mode 100644 index 00000000..17f3b05c --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/AchatTicketDto.java @@ -0,0 +1,43 @@ +package fr.istic.taa.jaxrs.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +public class AchatTicketDto { + + private Long clientId; + private Long eventId; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate dateAchat; + + public Long getClientId() { + return clientId; + } + public void setClientId(Long clientId) { + this.clientId = clientId; + } + + public Long getEventId() { + return eventId; + } + public void setEventId(Long eventId) { + this.eventId = eventId; + } + + + public LocalDate getDateAchat() { + return dateAchat; + } + public void setDateAchat(LocalDate dateAchat) { + this.dateAchat = dateAchat; + } +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/ConnexionDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/ConnexionDto.java new file mode 100644 index 00000000..a8be6d54 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/ConnexionDto.java @@ -0,0 +1,20 @@ +package fr.istic.taa.jaxrs.dto; + +public class ConnexionDto { + private String email; + private String password; + + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + public String getPassword() { + return password; + } + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/EnregistrerAdminDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/EnregistrerAdminDto.java new file mode 100644 index 00000000..3612e132 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/EnregistrerAdminDto.java @@ -0,0 +1,73 @@ +package fr.istic.taa.jaxrs.dto; + +import java.time.LocalDate; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +public class EnregistrerAdminDto { + + private String nom; + private String prenom; + private String email; + private String password; + private String telephone; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate date_naissance; + + + + public String getNom() { + return nom; + } + + public void setNom(String nom) { + this.nom = nom; + } + + public String getPrenom() { + return prenom; + } + + public void setPrenom(String prenom) { + this.prenom = prenom; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getTelephone() { + return telephone; + } + + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + public LocalDate getDate_naissance() { + return date_naissance; + } + public void setDate_naissance(LocalDate date_naissance) { + this.date_naissance = date_naissance; + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/EnregistrerManagerDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/EnregistrerManagerDto.java new file mode 100644 index 00000000..08db89f6 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/EnregistrerManagerDto.java @@ -0,0 +1,80 @@ +package fr.istic.taa.jaxrs.dto; + +import java.time.LocalDate; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +public class EnregistrerManagerDto { + + private String nom; + private String prenom; + private String email; + private String password; + private String telephone; + private String manager_agence; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate date_naissance; + + public String getNom() { + return nom; + } + + public void setNom(String nom) { + this.nom = nom; + } + + public String getPrenom() { + return prenom; + } + + public void setPrenom(String prenom) { + this.prenom = prenom; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getTelephone() { + return telephone; + } + + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + + public String getManager_agence() { + return manager_agence; + } + public void setManager_agence(String manager_agence) { + this.manager_agence = manager_agence; + } + + + public LocalDate getDate_naissance() { + return date_naissance; + } + public void setDate_naissance(LocalDate date_naissance) { + this.date_naissance = date_naissance; + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/EventDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/EventDto.java new file mode 100644 index 00000000..9a2b5ad0 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/EventDto.java @@ -0,0 +1,98 @@ +package fr.istic.taa.jaxrs.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +import jakarta.validation.constraints.Positive; + +public class EventDto { + private String nom; + private String description; + private String artiste; + private String lieu; + private LocalDate dateConcert; + private String genreMusical; + private int nbPlaceDispo; + private BigDecimal prix_ticket; + private int dureeConcert; + private Long managerId; + + public String getNom() { + return nom; + } + public void setNom(String nom) { + this.nom = nom; + } + + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + + public String getArtiste() { + return artiste; + } + public void setArtiste(String artiste) { + this.artiste = artiste; + } + + public String getLieu() { + return lieu; + } + public void setLieu(String lieu) { + this.lieu = lieu; + } + + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + public LocalDate getDateConcert() { + return dateConcert; + } + public void setDateConcert(LocalDate dateConcert) { + this.dateConcert = dateConcert; + } + + public String getGenreMusical() { + return genreMusical; + } + public void setGenreMusical(String genreMusical) { + this.genreMusical = genreMusical; + } + + public int getNbPlaceDispo() { + return nbPlaceDispo; + } + public void setNbPlaceDispo(int nbPlaceDispo) { + this.nbPlaceDispo = nbPlaceDispo; + } + + public int getDureeConcert() { + return dureeConcert; + } + public void setDureeConcert(int dureeConcert) { + this.dureeConcert = dureeConcert; + } + + public Long getManagerId() { + return managerId; + } + public void setManagerId(Long managerId) { + this.managerId = managerId; + } + + public BigDecimal getPrix_ticket() { + return prix_ticket; + } + public void setPrix_ticket(BigDecimal prix_ticket) { + this.prix_ticket = prix_ticket; + } +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/EventReponseDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/EventReponseDto.java new file mode 100644 index 00000000..41bcef73 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/EventReponseDto.java @@ -0,0 +1,137 @@ +package fr.istic.taa.jaxrs.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +import fr.istic.taa.jaxrs.domain.enumeration.StatutEvent; + +public class EventReponseDto { + + private long eventId; + private String nom; + private String description; + private String artiste; + private String lieu; + private String genreMusical; + private int nbPlaceDispo; + private int dureeConcert; + private Long managerId; + private Long adminId; + private BigDecimal prix_ticket; + private StatutEvent statut_concert; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate dateConcert; + + + // GETTERS-SETTERS + + public long getEventId() { + return eventId; + } + public void setEventId(long eventId) { + this.eventId = eventId; + } + + + public String getNom() { + return nom; + } + public void setNom(String nom) { + this.nom = nom; + } + + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + + public String getArtiste() { + return artiste; + } + public void setArtiste(String artiste) { + this.artiste = artiste; + } + + public String getLieu() { + return lieu; + } + public void setLieu(String lieu) { + this.lieu = lieu; + } + + + public LocalDate getDateConcert() { + return dateConcert; + } + public void setDateConcert(LocalDate dateConcert) { + this.dateConcert = dateConcert; + } + + + public String getGenreMusical() { + return genreMusical; + } + public void setGenreMusical(String genreMusical) { + this.genreMusical = genreMusical; + } + + public int getNbPlaceDispo() { + return nbPlaceDispo; + } + public void setNbPlaceDispo(int nbPlaceDispo) { + this.nbPlaceDispo = nbPlaceDispo; + } + + public int getDureeConcert() { + return dureeConcert; + } + public void setDureeConcert(int dureeConcert) { + this.dureeConcert = dureeConcert; + } + + + public StatutEvent getStatut_concert() { + return statut_concert; + } + public void setStatut_concert(StatutEvent statut_concert) { + this.statut_concert = statut_concert; + } + + + + public Long getManagerId() { + return managerId; + } + public void setManagerId(Long managerId) { + this.managerId = managerId; + } + + + public Long getAdminId() { + return adminId; + } + public void setAdminId(Long adminId) { + this.adminId = adminId; + } + + + + public BigDecimal getPrix_ticket() { + return prix_ticket; + } + public void setPrix_ticket(BigDecimal prix_ticket) { + this.prix_ticket = prix_ticket; + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/InscriptionClientDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/InscriptionClientDto.java new file mode 100644 index 00000000..4bf04939 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/InscriptionClientDto.java @@ -0,0 +1,76 @@ +package fr.istic.taa.jaxrs.dto; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class InscriptionClientDto { + + private String nom; + private String prenom; + private String email; + private String password; + private String telephone; + private boolean client_newsletter; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate date_naissance; + + public String getNom() { + return nom; + } + public void setNom(String nom) { + this.nom = nom; + } + + public String getPrenom() { + return prenom; + } + public void setPrenom(String prenom) { + this.prenom = prenom; + } + + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + public void setPassword(String password) { + this.password = password; + } + + public String getTelephone() { + return telephone; + } + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + + public LocalDate getDate_naissance() { + return date_naissance; + } + public void setDate_naissance(LocalDate date_naissance) { + this.date_naissance = date_naissance; + } + + public boolean isClient_newsletter() { + return client_newsletter; + } + public void setClient_newsletter(boolean client_newsletter) { + this.client_newsletter = client_newsletter; + } +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/PasswordDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/PasswordDto.java new file mode 100644 index 00000000..336f8e0b --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/PasswordDto.java @@ -0,0 +1,22 @@ +package fr.istic.taa.jaxrs.dto; + +public class PasswordDto { + + private String oldPassword; + private String newPassword; + + public String getOldPassword() { + return oldPassword; + } + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; + } + + public String getNewPassword() { + return newPassword; + } + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/StatutDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/StatutDto.java new file mode 100644 index 00000000..352836ca --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/StatutDto.java @@ -0,0 +1,18 @@ +package fr.istic.taa.jaxrs.dto; + +import fr.istic.taa.jaxrs.domain.enumeration.StatutEvent; + +public class StatutDto { + + private StatutEvent statut_concert; + + + public StatutEvent getStatut_concert() { + return statut_concert; + } + + public void setStatut_concert(StatutEvent statut_concert) { + this.statut_concert = statut_concert; + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/StatutUserDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/StatutUserDto.java new file mode 100644 index 00000000..74cd9e35 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/StatutUserDto.java @@ -0,0 +1,15 @@ +package fr.istic.taa.jaxrs.dto; + +public class StatutUserDto { + + private boolean statut_user; + + public boolean isStatut_user() { + return statut_user; + } + + public void setStatut_user(boolean statut_user) { + this.statut_user = statut_user; + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/dto/UserResponseDto.java b/src/main/java/fr/istic/taa/jaxrs/dto/UserResponseDto.java new file mode 100644 index 00000000..d165dce8 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/dto/UserResponseDto.java @@ -0,0 +1,126 @@ +package fr.istic.taa.jaxrs.dto; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +import fr.istic.taa.jaxrs.domain.enumeration.NiveauAdmin; +import fr.istic.taa.jaxrs.domain.enumeration.Role; +import io.swagger.v3.oas.annotations.media.Schema; + +public class UserResponseDto { + + private Long id; + private String nom; + private String prenom; + private String email; + private String telephone; + private Role role; + private boolean statut_user; + + // attributs spécifique pour Manager + @Schema(description = "Concerne seuleument CLIENT", nullable = true) + private boolean client_newsletter; + + @Schema(description = "Concerne seuleument MANAGER", nullable = true) + private String manager_agence; + + @Schema(description = "Niveau de l'admin (uniquement si rôle ADMIN)", example = "ADMIN", nullable = true) + private NiveauAdmin admin_niveau; + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate date_naissance; + + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + + + public String getNom() { + return nom; + } + public void setNom(String nom) { + this.nom = nom; + } + + + public String getPrenom() { + return prenom; + } + public void setPrenom(String prenom) { + this.prenom = prenom; + } + + + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + + + public String getTelephone() { + return telephone; + } + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + + public Role getRole() { + return role; + } + public void setRole(Role role) { + this.role = role; + } + + + public boolean isStatut_user() { + return statut_user; + } + public void setStatut_user(boolean statut_user) { + this.statut_user = statut_user; + } + + + public LocalDate getDate_naissance() { + return date_naissance; + } + public void setDate_naissance(LocalDate date_naissance) { + this.date_naissance = date_naissance; + } + + public boolean isClient_newsletter() { + return client_newsletter; + } + public void setClient_newsletter(boolean client_newsletter) { + this.client_newsletter = client_newsletter; + } + public String getManager_agence() { + return manager_agence; + } + public void setManager_agence(String manager_agence) { + this.manager_agence = manager_agence; + } + public NiveauAdmin getAdmin_niveau() { + return admin_niveau; + } + public void setAdmin_niveau(NiveauAdmin admin_niveau) { + this.admin_niveau = admin_niveau; + } + + + +} + diff --git a/src/main/java/fr/istic/taa/jaxrs/jpa/EntityManagerHelper.java b/src/main/java/fr/istic/taa/jaxrs/jpa/EntityManagerHelper.java new file mode 100644 index 00000000..b28f9cd2 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/jpa/EntityManagerHelper.java @@ -0,0 +1,60 @@ +package fr.istic.taa.jaxrs.jpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; + +public class EntityManagerHelper { + + private static final EntityManagerFactory emf; + private static final ThreadLocal threadLocal; + + + static { + EntityManagerFactory tempEmf = null; + try { + tempEmf = Persistence.createEntityManagerFactory("mysql"); + } catch (Exception e) { + e.printStackTrace(); // <- c'est tout ce qu'on ajoute + throw new RuntimeException("EMF init failed", e); + } + emf = tempEmf; + threadLocal = new ThreadLocal(); + } + + + + public static EntityManager getEntityManager() { + EntityManager em = threadLocal.get(); + + if (em == null) { + em = emf.createEntityManager(); + threadLocal.set(em); + } + return em; + } + + public static void closeEntityManager() { + EntityManager em = threadLocal.get(); + if (em != null) { + em.close(); + threadLocal.set(null); + } + } + + public static void closeEntityManagerFactory() { + emf.close(); + } + + public static void beginTransaction() { + getEntityManager().getTransaction().begin(); + } + + public static void rollback() { + getEntityManager().getTransaction().rollback(); + } + + public static void commit() { + getEntityManager().getTransaction().commit(); + } +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/jpa/JpaTest.java b/src/main/java/fr/istic/taa/jaxrs/jpa/JpaTest.java new file mode 100644 index 00000000..7d7d19a9 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/jpa/JpaTest.java @@ -0,0 +1,39 @@ +package fr.istic.taa.jaxrs.jpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; + +public class JpaTest { + + + private EntityManager manager; + + public JpaTest(EntityManager manager) { + this.manager = manager; + } + /** + * @param args + */ + public static void main(String[] args) { + EntityManager manager = EntityManagerHelper.getEntityManager(); + + JpaTest test = new JpaTest(manager); + + EntityTransaction tx = manager.getTransaction(); + tx.begin(); + + try { + + // TODO create and persist entity + } catch (Exception e) { + e.printStackTrace(); + } + tx.commit(); + + + manager.close(); + EntityManagerHelper.closeEntityManagerFactory(); + System.out.println(".. done"); + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/AdminResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/AdminResource.java new file mode 100644 index 00000000..82072bb8 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/AdminResource.java @@ -0,0 +1,72 @@ +package fr.istic.taa.jaxrs.rest; + +import fr.istic.taa.jaxrs.dao.AdminDao; +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.dto.EnregistrerAdminDto; +import fr.istic.taa.jaxrs.dto.UserResponseDto; +import fr.istic.taa.jaxrs.service.AdminService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + + +@Path("admin") +@Produces({"application/json", "application/xml"}) +@Tag(name = "Admin", description = "Gestion des Admin") +public class AdminResource { + + private AdminService adminService; + + public AdminResource() { + AdminDao adminDao = new AdminDao(); + UserDao userDao = new UserDao(); + this.adminService = new AdminService(adminDao, userDao); + } + + //ajouter un admin + @POST + @Path("/ajouter") + @Consumes("application/json") + @Operation(summary = "Ajouter Admin", description = "Enregistrer un Admin") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Ajout réussie", + content = @Content(schema = @Schema(implementation = UserResponseDto.class))), + @ApiResponse(responseCode = "400", description = "Email déjà utilisé") + }) + public Response ajouterManager( + @RequestBody( + description = "body de l'api", + required = true, + content = @Content(schema = @Schema(implementation = EnregistrerAdminDto.class)) + ) + EnregistrerAdminDto dto) { + try { + UserResponseDto response = adminService.ajouterAdmin(dto); + return Response.status(Response.Status.CREATED).entity(response).build(); + } catch (Exception e) { + // TODO: handle exception + System.out.println(e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } + } + + + //liste des admins + @GET + @Path("/all") + @Operation(summary = "Lister tous les Admins", description = "Retourne la liste de tous les Admins") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Admins trouvés avec succes", + content = @Content(schema = @Schema(implementation = UserResponseDto.class))) + }) + public Response listeUtilisateurs() { + return Response.ok(adminService.listeAdmins()).build(); + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/ClientResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/ClientResource.java new file mode 100644 index 00000000..6ff80cfc --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/ClientResource.java @@ -0,0 +1,62 @@ +package fr.istic.taa.jaxrs.rest; + +import fr.istic.taa.jaxrs.dao.ClientDao; +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.dto.InscriptionClientDto; +import fr.istic.taa.jaxrs.dto.UserResponseDto; +import fr.istic.taa.jaxrs.service.ClientService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + +@Path("client") +@Produces({"application/json", "application/xml"}) +@Tag(name = "Client", description = "Gestion des Clients") +public class ClientResource { + + private ClientService clientService; + + public ClientResource() { + ClientDao clientDao = new ClientDao(); + UserDao userDao = new UserDao(); + this.clientService = new ClientService(clientDao, userDao); + } + + + @POST + @Path("/inscription") + @Consumes("application/json") + @Operation(summary = "S'inscrire", description = "Permet à un utilisateur de S'inscrire") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Inscrition réussie", + content = @Content(schema = @Schema(implementation = UserResponseDto.class))), + @ApiResponse(responseCode = "400", description = "Email déjà utilisé") + }) + public Response inscription( + @RequestBody( + required = true, + content = @Content(schema = @Schema(implementation = InscriptionClientDto.class)) + ) + InscriptionClientDto dto) { + + try { + UserResponseDto response = clientService.inscription(dto); + return Response.status(Response.Status.CREATED).entity(response).build(); + } catch (Exception e) { + // TODO: handle exception + System.out.println(e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } + } + +} + + + + diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/CorsFilter.java b/src/main/java/fr/istic/taa/jaxrs/rest/CorsFilter.java new file mode 100644 index 00000000..a1ffe83a --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/CorsFilter.java @@ -0,0 +1,29 @@ +package fr.istic.taa.jaxrs.rest; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +@Provider +@PreMatching +public class CorsFilter implements ContainerRequestFilter, ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext req) { + if (req.getMethod().equalsIgnoreCase("OPTIONS")) { + req.abortWith(Response.ok().build()); + } + } + + @Override + public void filter(ContainerRequestContext req, ContainerResponseContext res) { + res.getHeaders().add("Access-Control-Allow-Origin", "http://localhost:4200"); + res.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH ,OPTIONS"); + res.getHeaders().add("Access-Control-Allow-Headers", "Content-Type, Authorization"); + res.getHeaders().add("Access-Control-Allow-Credentials", "true"); + } +} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/EventResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/EventResource.java new file mode 100644 index 00000000..1ff35c46 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/EventResource.java @@ -0,0 +1,215 @@ +package fr.istic.taa.jaxrs.rest; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import fr.istic.taa.jaxrs.dao.AdminDao; +import fr.istic.taa.jaxrs.dao.EventDao; +import fr.istic.taa.jaxrs.dao.ManagerDao; +import fr.istic.taa.jaxrs.domain.Event; +import fr.istic.taa.jaxrs.domain.enumeration.StatutEvent; +import fr.istic.taa.jaxrs.dto.*; +import fr.istic.taa.jaxrs.service.EventService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + + +@Path("event") +@Produces({"application/json", "application/xml"}) +@Tag(name = "Event", description = "Gestion des evenements (concerts)") +public class EventResource { + + private EventService eventService; + private StatutDto statutDTO; + public EventResource() { + EventDao eventDao = new EventDao(); + AdminDao adminDao = new AdminDao(); + ManagerDao managerDao = new ManagerDao(); + this.eventService = new EventService(managerDao, adminDao, eventDao); + } + + //Créer un événement + @POST + @Path("/create") + @Consumes("application/json") + @Operation(summary = "Créer un événement") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Événement créé", + content = @Content(schema = @Schema(implementation = EventReponseDto.class))), + @ApiResponse(responseCode = "400", description = "Manager introuvable ou Données invalides") + }) + public Response creerEvent( + @RequestBody( + description = "Email et mot de passe de l'utilisateur", + required = true, + content = @Content(schema = @Schema(implementation = EventDto.class)) + ) + EventDto dto) { + + try { + EventReponseDto newEvent = eventService.creerEvent(dto); + return Response.status(Response.Status.CREATED) + .entity(newEvent) + .build(); + }catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } + } + + + //afficher un event + @GET + @Path("/{eventId}") + @Operation( + summary = "Récupérer un evenement", + description = "Retourne les infos sur un evenement" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "evenement trouve avec succes", + content = @Content(schema = @Schema(implementation = EventReponseDto.class)) + ), + @ApiResponse( + responseCode = "404", + description = "evenement introuvable " + ) + }) + public Response findUserById( + @Parameter(description = "ID de l'evenement", required = true) + @PathParam("eventId") Long eventId) { + + + try { + return Response.ok(eventService.findEventById(eventId)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(e.getMessage()) + .build(); + } + } + + + //Événements par manager + @GET + @Path("/all/{managerId}") + @Operation(summary = "Événements par manager") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Liste des événements du manager", + content = @Content(schema = @Schema(implementation = EventReponseDto.class))), + @ApiResponse(responseCode = "404", description = "Manager introuvable") + }) + public Response findEventsByManagerId( + @Parameter(description = "ID du manager", required = true) + @PathParam("managerId") Long manager_Id) { + + try { + return Response.ok(eventService.findEventsByManagerId(manager_Id)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(e.getMessage()) + .build(); + } + + } + + //Événements par date + @GET + @Path("all/available") + @Operation(summary = "Événements disponibles") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Liste des événements disponibles", + content = @Content(schema = @Schema(implementation = EventReponseDto.class))), + @ApiResponse(responseCode = "400", description = "Données invalides") + }) + public Response findEventsByDate() { + try { + + return Response.ok(eventService.findAvailableEvents()).build(); + + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } + } + + //valider ou non un evenement + @PATCH + @Consumes("application/json") + @Path("/{eventId}/statut") + @Operation(summary = "modifier statut de l'event") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Statut bien modifié", + content = @Content(schema = @Schema(implementation = EventReponseDto.class))), + @ApiResponse(responseCode = "400", description = "Événement introuvable ou déjà été traité") + }) + public Response updateStatut( + @Parameter(description = "ID de l'event", required = true) + @PathParam("eventId") Long eventId, + @Parameter(description = "ID de l'admin", required = true) + @QueryParam("adminId") Long adminId, + @QueryParam("statut") StatutEvent statut){ + try { + + return Response.ok(eventService.updateStatut(eventId, adminId, statut)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(e.getMessage()) + .build(); + } + } + + //supprimer un evenement + @DELETE + @Path("/delete/{eventId}") + @Consumes("application/json") + @Operation(summary = "supprimer un event") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "evenement bien supprimé"), + @ApiResponse(responseCode = "400", description = "Événement introuvable ou déjà été supprimé") + }) + public Response deleteEvent( + + @Parameter(description = "ID de l'event", required = true) + @PathParam("eventId") Long eventId, + + @Parameter(description = "ID du manager", required = true) + @QueryParam("managerId") Long managerId) { + + try { + + eventService.deleteEvent(eventId, managerId); + return Response.ok("evenement bien supprimé").build(); + + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } + } + + @GET + @Path("/all") + @Operation(summary = "Lister tous les événements") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Liste des événements") + }) + public Response getAllEvents() { + return Response.ok(eventService.getAllEvents()).build(); + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/ManagerResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/ManagerResource.java new file mode 100644 index 00000000..b57e079e --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/ManagerResource.java @@ -0,0 +1,61 @@ +package fr.istic.taa.jaxrs.rest; + +import fr.istic.taa.jaxrs.dao.ManagerDao; +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.dto.EnregistrerManagerDto; +import fr.istic.taa.jaxrs.dto.UserResponseDto; +import fr.istic.taa.jaxrs.service.ManagerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + + +@Path("manager") +@Produces({"application/json", "application/xml"}) +@Tag(name = "Manager", description = "Gestion des Manager") +public class ManagerResource { + + private ManagerService managerService; + + public ManagerResource() { + ManagerDao managerDao = new ManagerDao(); + UserDao userDao = new UserDao(); + this.managerService = new ManagerService(managerDao, userDao); + } + + + + @POST + @Path("/ajouter") + @Consumes("application/json") + @Operation(summary = "Ajouter Manager", description = "Enregistrer un Manager") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Ajout réussie", + content = @Content(schema = @Schema(implementation = UserResponseDto.class))), + @ApiResponse(responseCode = "400", description = "Email déjà utilisé") + }) + public Response ajouterManager( + @RequestBody( + description = "body de l'api", + required = true, + content = @Content(schema = @Schema(implementation = EnregistrerManagerDto.class)) + ) + EnregistrerManagerDto dto) { + try { + UserResponseDto response = managerService.ajouterManager(dto); + return Response.status(Response.Status.CREATED).entity(response).build(); + } catch (Exception e) { + // TODO: handle exception + System.out.println(e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/PetResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/PetResource.java deleted file mode 100644 index 30873955..00000000 --- a/src/main/java/fr/istic/taa/jaxrs/rest/PetResource.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.istic.taa.jaxrs.rest; - -import fr.istic.taa.jaxrs.domain.Pet; -import io.swagger.v3.oas.annotations.Parameter; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Response; - -@Path("pet") -@Produces({"application/json", "application/xml"}) -public class PetResource { - - @GET - @Path("/{petId}") - public Pet getPetById(@PathParam("petId") Long petId) { - // return pet - return new Pet(); - } - - @GET - @Path("/") - public Pet getPet(Long petId) { - return new Pet(); - } - - - @POST - @Consumes("application/json") - public Response addPet( - @Parameter(description = "Pet object that needs to be added to the store", required = true) Pet pet) { - // add pet - return Response.ok().entity("SUCCESS").build(); - } -} \ No newline at end of file diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/SwaggerResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/SwaggerResource.java new file mode 100644 index 00000000..26448315 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/SwaggerResource.java @@ -0,0 +1,34 @@ +package fr.istic.taa.jaxrs.rest; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.util.logging.Logger; +import jakarta.ws.rs.*; +import jakarta.ws.rs.Path; + +@Path("/swagger-api") +public class SwaggerResource { + + private static final Logger logger = Logger.getLogger(SwaggerResource.class.getName()); + + @GET + public byte[] Get1() { + try { + return Files.readAllBytes(FileSystems.getDefault().getPath("src/main/webapp/swagger/dist/index.html")); + } catch (IOException e) { + return null; + } + } + + @GET + @Path("{path:.*}") + public byte[] Get(@PathParam("path") String path) { + try { + return Files.readAllBytes(FileSystems.getDefault().getPath("src/main/webapp/swagger/dist/"+path)); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/TicketResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/TicketResource.java new file mode 100644 index 00000000..6fc99be0 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/TicketResource.java @@ -0,0 +1,210 @@ +package fr.istic.taa.jaxrs.rest; + + +import fr.istic.taa.jaxrs.dao.ClientDao; +import fr.istic.taa.jaxrs.dao.EventDao; +import fr.istic.taa.jaxrs.dao.TicketDao; +import fr.istic.taa.jaxrs.domain.Ticket; +import fr.istic.taa.jaxrs.dto.AchatTicketDto; +import fr.istic.taa.jaxrs.service.TicketService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + +@Path("ticket") +@Produces({"application/json", "application/xml"}) +@Tag(name = "Ticket", description = "Gestion des tickets") +public class TicketResource { + + private TicketService ticketService; + + public TicketResource() { + ClientDao clientDao = new ClientDao(); + EventDao eventDao = new EventDao(); + TicketDao ticketDao = new TicketDao(); + this.ticketService = new TicketService(clientDao, eventDao, ticketDao); + } + + + //Acheter un ticket + @POST + @Path("/acheter") + @Consumes("application/json") + @Operation( + summary = "Acheter un ticket", + description = "Permet à un client d'acheter un ticket pour un événement" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "201", + description = "Ticket acheté avec succès", + content = @Content(schema = @Schema(implementation = Ticket.class)) + ), + @ApiResponse( + responseCode = "400", + description = "Erreur lors de l'achat (Plus de places, Vous avez déjà un ticket pour cet event ou client introuvable...)", + content = @Content(schema = @Schema(implementation = String.class)) + ) + }) + public Response acheterTicket( + @RequestBody( + description = "Email et mot de passe de l'utilisateur", + required = true, + content = @Content(schema = @Schema(implementation = AchatTicketDto.class)) + ) + AchatTicketDto dto) { + try { + Ticket ticket = ticketService.acheterTicket(dto); + return Response.status(Response.Status.CREATED).entity(ticket).build(); + } catch (Exception e) { + // TODO: handle exception + System.out.println(e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } + } + + //afficher un ticket + @GET + @Path("/{clientId}/{eventId}") + @Operation( + summary = "Récupérer un ticket", + description = "Retourne les infos sur un ticket" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "Ticket trouve avec succes", + content = @Content(schema = @Schema(implementation = Ticket.class)) + ), + @ApiResponse( + responseCode = "404", + description = "Ticket introuvable " + ) + }) + public Response findTicketById( + @Parameter(description = "ID du client", required = true) + @PathParam("clientId") Long clientId, + + @Parameter(description = "ID de l'event", required = true) + @PathParam("eventId") Long eventId) { + + + try { + return Response.ok(ticketService.findTicketById(clientId, eventId)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(e.getMessage()) + .build(); + } + } + + + //liste des tickets par client + @GET + @Path("/all/client/{clientId}") + @Operation( + summary = "Récupérer les tickets d'un client", + description = "Retourne la liste de tous les tickets achetés par un client" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "Liste des tickets du client", + content = @Content(schema = @Schema(implementation = Ticket.class)) + ), + @ApiResponse( + responseCode = "404", + description = "Client introuvable" + ) + }) + public Response findTicketsByClientId( + @Parameter(description = "ID du client", required = true) + @PathParam("clientId") Long clientId) { + + try { + return Response.ok(ticketService.findTicketsByClientId(clientId)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(e.getMessage()) + .build(); + } + } + + + //liste des tickets par event + @GET + @Path("/all/event/{eventId}") + @Operation( + summary = "Récupérer les tickets d'un evenement", + description = "Retourne la liste de tous les tickets achetés pour un evenement" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "Liste des tickets du client", + content = @Content(schema = @Schema(implementation = Ticket.class)) + ), + @ApiResponse( + responseCode = "404", + description = "Evenement introuvable" + ) + }) + public Response findTicketsByEventId ( + @Parameter(description = "ID de l'event", required = true) + @PathParam("eventId") Long eventId) { + + try { + return Response.ok(ticketService.findTicketsByClientId(eventId)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(e.getMessage()) + .build(); + } + } + + + //liste des tickets vendus + @GET + @Path("/total/event/{eventId}") + @Operation( + summary = "Récupérer le total des tickets d'un evenement", + description = "Retourne le nombre total de tickets donc de places vendues pour un evenement" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "le nombre total de tickets", + content = @Content(schema = @Schema(implementation = Ticket.class)) + ), + @ApiResponse( + responseCode = "400", + description = "Evenement introuvable ou Accès refusé au manager" + ) + }) + public Response getTicketsSold( + @Parameter(description = "ID de l'event", required = true) @PathParam("eventId") Long eventId, + + @Parameter(description = "ID du manager", required = true) @QueryParam("managerId") Long managerId) { + + try { + return Response.ok(ticketService.findTicketSoldByEvent(eventId, managerId)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/rest/UserResource.java b/src/main/java/fr/istic/taa/jaxrs/rest/UserResource.java new file mode 100644 index 00000000..26bbde36 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/rest/UserResource.java @@ -0,0 +1,169 @@ +package fr.istic.taa.jaxrs.rest; + +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.dto.ConnexionDto; +import fr.istic.taa.jaxrs.dto.PasswordDto; +import fr.istic.taa.jaxrs.dto.StatutUserDto; +import fr.istic.taa.jaxrs.dto.UserResponseDto; +import fr.istic.taa.jaxrs.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + +@Path("user") +@Produces({"application/json", "application/xml"}) +@Tag(name = "User", description = "Gestion des Utilisateurs") +public class UserResource { + + private UserService userService; + + public UserResource() { + UserDao userDao = new UserDao(); + this.userService = new UserService(userDao); + } + + // Connexion + @POST + @Path("/connexion") + @Consumes("application/json") + @Operation(summary = "Se connecter", description = "Permet à un utilisateur de se connecter") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Connexion réussie", + content = @Content(schema = @Schema(implementation = UserResponseDto.class))), + @ApiResponse(responseCode = "401", description = "Email ou mot de passe incorrect") + }) + public Response connexion( + @RequestBody( + description = "Email et mot de passe de l'utilisateur", + required = true, + content = @Content(schema = @Schema(implementation = ConnexionDto.class)) + ) + ConnexionDto dto) { + + // user contient juste email + password + try { + UserResponseDto response = userService.seConnecter(dto); + return Response.ok(response).build(); + } catch (Exception e) { + return Response.status(401).entity("Email ou mot de passe incorrect").build(); + } + + } + + + // Modification mot de passe + @PUT + @Path("/updatePassword/user_id/{user_id}") + @Consumes("application/json") + @Operation(summary = "Modifier le mot de passe", description = "Permet de changer le mot de passe d'un utilisateur") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Mot de passe modifié avec succès"), + @ApiResponse(responseCode = "400", description = "Ancien mot de passe incorrect ou données invalides"), + @ApiResponse(responseCode = "404", description = "Utilisateur non trouvé") + }) + public Response modifierPassword( + @Parameter(description = "ID de l'utilisateur", required = true, example = "1") + @PathParam("user_id") Long user_id, + + @RequestBody( + description = "Ancien et nouveau mot de passe", + required = true, + content = @Content(schema = @Schema(implementation = PasswordDto.class)) + )PasswordDto dto) { + + try { + userService.changerMdp(user_id, dto); + return Response.ok("Mot de passe modifié").build(); + } catch (Exception e) { + return Response.status(400).entity(e.getMessage()).build(); + } + } + + + //liste des utilisateurs + @GET + @Path("/all") + @Operation(summary = "Lister tous les utilisateurs", description = "Retourne la liste de tous les utilisateurs") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Utilisateurs trouvés avec succes", + content = @Content(schema = @Schema(implementation = UserResponseDto.class))) + }) + public Response listeUtilisateurs() { + return Response.ok(userService.listeUtilisateurs()).build(); + } + + + //aficher un user + @GET + @Path("/{userId}") + @Operation( + summary = "Récupérer un user", + description = "Retourne les infos sur un user" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "User trouve avec succes", + content = @Content(schema = @Schema(implementation = UserResponseDto.class)) + ), + @ApiResponse( + responseCode = "404", + description = "User introuvable " + ) + }) + public Response findUserById( + @Parameter(description = "ID du client", required = true) + @PathParam("userId") Long userId) { + + + try { + return Response.ok(userService.findUserById(userId)).build(); + + } catch (Exception e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(e.getMessage()) + .build(); + } + } + + + //suspendre utilisateur + @PATCH + @Path("/{userId}/statut") + @Consumes("application/json") + @Operation(summary = "suspendre ou reactiver un utilisateur") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Statut bien modifié", + content = @Content(schema = @Schema(implementation = UserResponseDto.class))), + @ApiResponse(responseCode = "400", description = "User introuvable ou Déjà bloqué") + }) + public Response suspendreUtilisateur( + @Parameter(description = "ID de l'utilisateur", required = true) + @PathParam("userId") Long userId, + @RequestBody( + description = "Example valeurs du champ : false pour reactiver - truee pour suspendre", + required = true, + content = @Content(schema = @Schema(implementation = StatutUserDto.class)) + )StatutUserDto dto) { + + try { + + return Response.ok(userService.suspendreUtilisateur(userId, dto)).build(); + + } catch (Exception e) { + + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } + } + + + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/service/AdminService.java b/src/main/java/fr/istic/taa/jaxrs/service/AdminService.java new file mode 100644 index 00000000..89210161 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/service/AdminService.java @@ -0,0 +1,87 @@ +package fr.istic.taa.jaxrs.service; + +import java.util.ArrayList; +import java.util.List; +import org.mindrot.jbcrypt.BCrypt; + +import fr.istic.taa.jaxrs.dao.AdminDao; +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.domain.Admin; +import fr.istic.taa.jaxrs.domain.enumeration.NiveauAdmin; +import fr.istic.taa.jaxrs.domain.enumeration.Role; +import fr.istic.taa.jaxrs.dto.EnregistrerAdminDto; +import fr.istic.taa.jaxrs.dto.UserResponseDto; + +public class AdminService { + + private AdminDao adminDao; + private UserDao userDao; + + // Injection par constructeur + public AdminService(AdminDao adminDao, UserDao userDao) { + this.adminDao = adminDao; + this.userDao = userDao; + } + + + // Ajouter Admin + public UserResponseDto ajouterAdmin(EnregistrerAdminDto dto) throws Exception { + + if (this.userDao.findByEmailNamedQuery(dto.getEmail()) != null) { + throw new Exception("Email déjà utilisé"); + } + + Admin admin = new Admin(); + + admin.setNom(dto.getNom()); + admin.setPrenom(dto.getPrenom()); + admin.setEmail(dto.getEmail()); + admin.setTelephone(dto.getEmail()); + admin.setDate_naissance(dto.getDate_naissance()); + admin.setRole(Role.ADMIN); + admin.setAdmin_niveau(NiveauAdmin.ADMIN); + + + admin.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt())); + + this.adminDao.save(admin); + + + // Mapper vers DTO de réponse + UserResponseDto response = new UserResponseDto(); + response.setId(admin.getUserId()); + response.setNom(admin.getNom()); + response.setPrenom(admin.getPrenom()); + response.setEmail(admin.getEmail()); + response.setTelephone(admin.getTelephone()); + response.setStatut_user(admin.isStatut_user()); + response.setDate_naissance(admin.getDate_naissance()); + response.setRole(admin.getRole()); + response.setAdmin_niveau(admin.getAdmin_niveau()); + + return response; + } + + + // Liste des Admins + public List listeAdmins() { + List admins = adminDao.findAll(); + List dtos = new ArrayList<>(); + + for (Admin a : admins) { + UserResponseDto dto = new UserResponseDto(); + dto.setId(a.getUserId()); + dto.setNom(a.getNom()); + dto.setPrenom(a.getPrenom()); + dto.setEmail(a.getEmail()); + dto.setTelephone(a.getTelephone()); + dto.setRole(a.getRole()); + dto.setStatut_user(a.isStatut_user()); + dto.setAdmin_niveau(a.getAdmin_niveau()); + dtos.add(dto); + } + return dtos; + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/service/ClientService.java b/src/main/java/fr/istic/taa/jaxrs/service/ClientService.java new file mode 100644 index 00000000..7ea06388 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/service/ClientService.java @@ -0,0 +1,61 @@ +package fr.istic.taa.jaxrs.service; + +import org.mindrot.jbcrypt.BCrypt; + +import fr.istic.taa.jaxrs.dao.ClientDao; +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.domain.Client; +import fr.istic.taa.jaxrs.domain.enumeration.Role; +import fr.istic.taa.jaxrs.dto.*; + +public class ClientService { + + private ClientDao clientDao; + private UserDao userDao; + + // Injection par constructeur + public ClientService(ClientDao clientDao, UserDao userDao) { + this.clientDao = clientDao; + this.userDao = userDao; + } + + + //Methode metier S'inscrire + public UserResponseDto inscription(InscriptionClientDto dto) throws Exception { + + if (this.userDao.findByEmailNamedQuery(dto.getEmail()) != null) { + throw new Exception("Email déjà utilisé"); + } + + Client client = new Client(); + + client.setNom(dto.getNom()); + client.setPrenom(dto.getPrenom()); + client.setEmail(dto.getEmail()); + client.setTelephone(dto.getTelephone()); + client.setDate_naissance(dto.getDate_naissance()); + client.setClient_newsletter(dto.isClient_newsletter()); + client.setRole(Role.CLIENT); + + String hashed = BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt()); + client.setPassword(hashed); + + this.clientDao.save(client); // Appel simple au DAO + + // Mapper vers DTO de réponse + UserResponseDto response = new UserResponseDto(); + response.setId(client.getUserId()); + response.setNom(client.getNom()); + response.setPrenom(client.getPrenom()); + response.setEmail(client.getEmail()); + response.setTelephone(client.getTelephone()); + response.setStatut_user(client.isStatut_user()); + response.setDate_naissance(client.getDate_naissance()); + response.setRole(client.getRole()); + response.setClient_newsletter(client.isClient_newsletter()); + + return response; + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/service/EventService.java b/src/main/java/fr/istic/taa/jaxrs/service/EventService.java new file mode 100644 index 00000000..16f935b5 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/service/EventService.java @@ -0,0 +1,270 @@ +package fr.istic.taa.jaxrs.service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import fr.istic.taa.jaxrs.dao.AdminDao; +import fr.istic.taa.jaxrs.dao.EventDao; +import fr.istic.taa.jaxrs.dao.ManagerDao; +import fr.istic.taa.jaxrs.domain.Admin; +import fr.istic.taa.jaxrs.domain.Event; +import fr.istic.taa.jaxrs.domain.Manager; +import fr.istic.taa.jaxrs.domain.enumeration.StatutEvent; +import fr.istic.taa.jaxrs.dto.EventDto; +import fr.istic.taa.jaxrs.dto.EventReponseDto; +import jakarta.persistence.EntityNotFoundException; + +public class EventService { + + private EventDao eventDao; + private ManagerDao managerDao; + private AdminDao adminDao; + + // Injection par constructeur + public EventService(ManagerDao managerDao, AdminDao adminDao, EventDao eventDao) { + this.managerDao = managerDao; + this.adminDao = adminDao; + this.eventDao = eventDao; + } + + //creer un evenement + public EventReponseDto creerEvent(EventDto dto) { + Manager manager = managerDao.findOne(dto.getManagerId()); + if (manager == null) { + throw new EntityNotFoundException("Manager introuvable"); + } + + Event event = new Event(); + event.setNom(dto.getNom()); + event.setDescription(dto.getDescription()); + event.setArtiste(dto.getArtiste()); + event.setLieu(dto.getLieu()); + event.setDate_concert(dto.getDateConcert()); + event.setGenreMusical(dto.getGenreMusical()); + event.setNb_place_disponible(dto.getNbPlaceDispo()); + event.setDureeConcert(dto.getDureeConcert()); + event.setPrix_ticket(dto.getPrix_ticket()); + event.setStatut_concert(StatutEvent.ATTENTE_VALIDATION); + event.setManager(manager); + + eventDao.save(event); + + // Mapper vers DTO de réponse + EventReponseDto response = new EventReponseDto(); + response.setEventId(event.getEventId()); + response.setNom(event.getNom()); + response.setArtiste(event.getArtiste()); + response.setDescription(event.getDescription()); + response.setDateConcert(event.getDate_concert()); + response.setDureeConcert(event.getDureeConcert()); + response.setGenreMusical(event.getGenreMusical()); + response.setLieu(event.getLieu()); + response.setNbPlaceDispo(event.getNb_place_disponible()); + response.setStatut_concert(event.getStatut_concert()); + response.setPrix_ticket(event.getPrix_ticket()); + response.setManagerId(event.getManager().getUserId()); + + if (event.getAdmin() != null) { + response.setAdminId(event.getAdmin().getUserId()); + } else { + response.setAdminId(null); + } + + return response; + } + + //liste des evenements disponibles + public List findAvailableEvents() { + + List events = eventDao.findAvailableEventNamedQuery(); + List dtos = new ArrayList<>(); + + + for (Event e : events) { + EventReponseDto dto = new EventReponseDto(); + dto.setEventId(e.getEventId()); + dto.setNom(e.getNom()); + dto.setArtiste(e.getArtiste()); + dto.setDescription(e.getDescription()); + dto.setDateConcert(e.getDate_concert()); + dto.setDureeConcert(e.getDureeConcert()); + dto.setGenreMusical(e.getGenreMusical()); + dto.setLieu(e.getLieu()); + dto.setNbPlaceDispo(e.getNb_place_disponible()); + dto.setStatut_concert(e.getStatut_concert()); + dto.setManagerId(e.getManager().getUserId()); + + if (e.getAdmin() != null) { + dto.setAdminId(e.getAdmin().getUserId()); + } else { + dto.setAdminId(null); + } + + dtos.add(dto); + } + return dtos; + } + + + //liste des evenements pour un manager + public List findEventsByManagerId(Long managerId) { + + if (this.managerDao.findOne(managerId) == null) { + throw new EntityNotFoundException("Manager Introuvable"); + } + + List events = eventDao.findByManagerId(managerId); + List dtos = new ArrayList<>(); + + for (Event e : events) { + EventReponseDto dto = new EventReponseDto(); + dto.setEventId(e.getEventId()); + dto.setNom(e.getNom()); + dto.setArtiste(e.getArtiste()); + dto.setDescription(e.getDescription()); + dto.setDateConcert(e.getDate_concert()); + dto.setDureeConcert(e.getDureeConcert()); + dto.setGenreMusical(e.getGenreMusical()); + dto.setLieu(e.getLieu()); + dto.setNbPlaceDispo(e.getNb_place_disponible()); + dto.setStatut_concert(e.getStatut_concert()); + dto.setManagerId(e.getManager().getUserId()); + + if (e.getAdmin() != null) { + dto.setAdminId(e.getAdmin().getUserId()); + } else { + dto.setAdminId(null); + } + + dtos.add(dto); + } + return dtos; + } + + + //afficher un Event + public EventReponseDto findEventById(Long eventId) { + + Event event = eventDao.findOne(eventId); + + if (event == null) { + throw new EntityNotFoundException("Event introuvable"); + } + EventReponseDto response = new EventReponseDto(); + response.setEventId(event.getEventId()); + response.setNom(event.getNom()); + response.setArtiste(event.getArtiste()); + response.setDescription(event.getDescription()); + response.setDateConcert(event.getDate_concert()); + response.setDureeConcert(event.getDureeConcert()); + response.setGenreMusical(event.getGenreMusical()); + response.setLieu(event.getLieu()); + response.setNbPlaceDispo(event.getNb_place_disponible()); + response.setStatut_concert(event.getStatut_concert()); + response.setPrix_ticket(event.getPrix_ticket()); + response.setManagerId(event.getManager().getUserId()); + + if (event.getAdmin() != null) { + response.setAdminId(event.getAdmin().getUserId()); + } else { + response.setAdminId(null); + } + + return response; + } + + //valider ou annuler concert + public EventReponseDto updateStatut(Long eventId, Long adminId, StatutEvent nouveauStatut) { + + Event event = eventDao.findOne(eventId); + Admin admin = adminDao.findOne(adminId); + + if (event == null) { + throw new EntityNotFoundException("Événement introuvable."); + } + + if (admin == null) { + throw new EntityNotFoundException("Admin introuvable."); + } + + if (!event.getStatut_concert().equals(StatutEvent.ATTENTE_VALIDATION)) { + throw new IllegalStateException("Cet événement a déjà été traité."); + } + + event.setStatut_concert(nouveauStatut); + event.setAdmin(admin); + eventDao.update(event); + + + // Mapper vers DTO de réponse + EventReponseDto response = new EventReponseDto(); + response.setEventId(event.getEventId()); + response.setNom(event.getNom()); + response.setArtiste(event.getArtiste()); + response.setDescription(event.getDescription()); + response.setDateConcert(event.getDate_concert()); + response.setDureeConcert(event.getDureeConcert()); + response.setGenreMusical(event.getGenreMusical()); + response.setLieu(event.getLieu()); + response.setNbPlaceDispo(event.getNb_place_disponible()); + response.setStatut_concert(event.getStatut_concert()); + response.setPrix_ticket(event.getPrix_ticket()); + response.setManagerId(event.getManager().getUserId()); + + if (event.getAdmin() != null) { + response.setAdminId(event.getAdmin().getUserId()); + } else { + response.setAdminId(null); + } + + return response; + } + + //supprimer un evenement soft delete + public void deleteEvent(Long eventId, Long managerId) { + Event event = eventDao.findOne(eventId); + + if (event == null) { + throw new RuntimeException("Événement introuvable."); + } + + if (!event.getManager().getUserId().equals(managerId)) { + throw new RuntimeException("Accès refusé."); + } + + if (event.isDeleted()) { + throw new RuntimeException("Événement déjà supprimé."); + } + + if (event.getStatut_concert() == StatutEvent.VALIDE || event.getStatut_concert() == StatutEvent.ANNULE) { + throw new RuntimeException("Impossible de supprimer cet événement."); + } + + event.setDeleted(true); + eventDao.update(event); + } + + // Recupère tous les events + public List getAllEvents() { + List events = eventDao.findAll(); + List dtos = new ArrayList<>(); + for (Event e : events) { + EventReponseDto dto = new EventReponseDto(); + dto.setEventId(e.getEventId()); + dto.setNom(e.getNom()); + dto.setDescription(e.getDescription()); + dto.setArtiste(e.getArtiste()); + dto.setLieu(e.getLieu()); + dto.setGenreMusical(e.getGenreMusical()); + dto.setNbPlaceDispo(e.getNb_place_disponible()); + dto.setDureeConcert(e.getDureeConcert()); + dto.setPrix_ticket(e.getPrix_ticket()); + dto.setStatut_concert(e.getStatut_concert()); + dto.setDateConcert(e.getDate_concert()); + dto.setManagerId(e.getManager() != null ? e.getManager().getUserId() : null); + dto.setAdminId(e.getAdmin() != null ? e.getAdmin().getUserId() : null); + dtos.add(dto); + } + return dtos; + } +} diff --git a/src/main/java/fr/istic/taa/jaxrs/service/ManagerService.java b/src/main/java/fr/istic/taa/jaxrs/service/ManagerService.java new file mode 100644 index 00000000..cdd5c692 --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/service/ManagerService.java @@ -0,0 +1,60 @@ +package fr.istic.taa.jaxrs.service; + +import org.mindrot.jbcrypt.BCrypt; +import fr.istic.taa.jaxrs.dao.*; +import fr.istic.taa.jaxrs.domain.*; +import fr.istic.taa.jaxrs.domain.enumeration.Role; +import fr.istic.taa.jaxrs.dto.EnregistrerManagerDto; +import fr.istic.taa.jaxrs.dto.UserResponseDto; + +public class ManagerService { + + private ManagerDao managerDao; + private UserDao userDao; + + public ManagerService(ManagerDao managerDao, UserDao userDao) { + this.managerDao = managerDao; + this.userDao = userDao; + } + + // Ajouter Manager + public UserResponseDto ajouterManager(EnregistrerManagerDto dto) throws Exception { + + if (this.userDao.findByEmailNamedQuery(dto.getEmail()) != null) { + throw new Exception("Email déjà utilisé"); + } + + Manager manager = new Manager(); + + manager.setNom(dto.getNom()); + manager.setPrenom(dto.getPrenom()); + manager.setEmail(dto.getEmail()); + manager.setTelephone(dto.getTelephone()); + manager.setDate_naissance(dto.getDate_naissance()); + manager.setRole(Role.MANAGER); + manager.setManager_agence(dto.getManager_agence()); + + + manager.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt())); + + this.managerDao.save(manager); + + + + // Mapper vers DTO de réponse + UserResponseDto response = new UserResponseDto(); + response.setId(manager.getUserId()); + response.setNom(manager.getNom()); + response.setPrenom(manager.getPrenom()); + response.setEmail(manager.getEmail()); + response.setTelephone(manager.getTelephone()); + response.setStatut_user(manager.isStatut_user()); + response.setDate_naissance(manager.getDate_naissance()); + response.setRole(manager.getRole()); + response.setManager_agence(manager.getManager_agence()); + + return response; + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/service/TicketService.java b/src/main/java/fr/istic/taa/jaxrs/service/TicketService.java new file mode 100644 index 00000000..4da9e69f --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/service/TicketService.java @@ -0,0 +1,123 @@ +package fr.istic.taa.jaxrs.service; + +import java.util.List; + +import fr.istic.taa.jaxrs.dao.ClientDao; +import fr.istic.taa.jaxrs.dao.EventDao; +import fr.istic.taa.jaxrs.dao.TicketDao; +import fr.istic.taa.jaxrs.domain.*; +import fr.istic.taa.jaxrs.domain.enumeration.StatutTicket; +import fr.istic.taa.jaxrs.dto.AchatTicketDto; +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; + +public class TicketService { + + + private ClientDao clientDao; + private EventDao eventDao; + private TicketDao ticketDao; + + // Injection par constructeur + public TicketService(ClientDao clientDao, EventDao eventDao, TicketDao ticketDao) { + this.clientDao = clientDao; + this.eventDao = eventDao; + this.ticketDao = ticketDao; + } + + // Acheter Ticket + @Transactional + public Ticket acheterTicket(AchatTicketDto dto) { + + Client client = clientDao.findOne(dto.getClientId()); + Event event = eventDao.findOne(dto.getEventId()); + + // 1. VERIFICATION EXISTENCE + if (client == null) throw new EntityNotFoundException("Client introuvable"); + + if (event == null) throw new EntityNotFoundException("Événement introuvable"); + + + // 2. ÉTAT DU MÉTIER (verification du nombre de places) + if (event.getNb_place_disponible() <= 0) { + throw new IllegalStateException("Plus de places disponibles"); + } + + + // 3. DOUBLON + + // Construire la clé composite + TicketId ticket_id = new TicketId(dto.getClientId(), dto.getEventId()); + + if (ticketDao.findOne(ticket_id) != null) { + throw new IllegalStateException("Vous avez déjà un ticket pour cet event"); + } + + // 4. CREATION DU TICLKET + Ticket ticket = new Ticket(); + ticket.setId(ticket_id); + ticket.setStatut(StatutTicket.ACHETE); + ticket.setDateAchat(dto.getDateAchat()); + ticket.setEvent(event); + ticket.setClient(client); + ticketDao.save(ticket); + + event.diminuerPlaces(); + eventDao.save(event); + + return ticket; + } + + + // afficher un Ticket + public Ticket findTicketById(Long userId, Long eventId) { + + TicketId ticket_id = new TicketId(userId, eventId); + Ticket ticket = ticketDao.findOne(ticket_id); + + if (ticket == null) { + throw new EntityNotFoundException("Ticket introuvable"); + } + + return ticket; + } + + + // Liste des Tickets pour un client + public List findTicketsByClientId(Long clientId) { + + if (this.clientDao.findOne(clientId) == null) { + throw new EntityNotFoundException("Client Introuvable"); + } + return ticketDao.findByClientId(clientId); + } + + + // Liste des Tickets par event + public List findTicketsByEventId(Long eventId) { + + if (this.eventDao.findOne(eventId) == null) { + throw new EntityNotFoundException("Event Introuvable"); + } + return ticketDao.findByEventId(eventId); + } + + + // nombre de tickets pour un événement + public long findTicketSoldByEvent(Long eventId, Long managerId) { + + Event event = eventDao.findOne(eventId); + + if (event == null) { + throw new EntityNotFoundException("Événement introuvable."); + } + + if (!event.getManager().getUserId().equals(managerId)) { + throw new IllegalStateException("Accès refusé : cet événement ne vous appartient pas."); + } + + return ticketDao.countTicketsByEvent(eventId); + } + + +} diff --git a/src/main/java/fr/istic/taa/jaxrs/service/UserService.java b/src/main/java/fr/istic/taa/jaxrs/service/UserService.java new file mode 100644 index 00000000..2463970c --- /dev/null +++ b/src/main/java/fr/istic/taa/jaxrs/service/UserService.java @@ -0,0 +1,153 @@ +package fr.istic.taa.jaxrs.service; + +import java.util.ArrayList; +import java.util.List; + +import org.mindrot.jbcrypt.BCrypt; +import fr.istic.taa.jaxrs.dao.UserDao; +import fr.istic.taa.jaxrs.domain.*; +import fr.istic.taa.jaxrs.dto.ConnexionDto; +import fr.istic.taa.jaxrs.dto.PasswordDto; +import fr.istic.taa.jaxrs.dto.StatutUserDto; +import fr.istic.taa.jaxrs.dto.UserResponseDto; +import jakarta.persistence.EntityNotFoundException; + +public class UserService { + + private UserDao userDao; + + // Injection par constructeur + public UserService(UserDao userDao) { + this.userDao = userDao; + } + + + // Methode seConnecter + public UserResponseDto seConnecter(ConnexionDto dto) { + // 1. On demande au DAO de nous donner l'utilisateur par son email + User user = userDao.findByEmailNamedQuery(dto.getEmail()); + + if (user == null) { + throw new RuntimeException("Utilisateur non trouvé"); + } + + // 2. on va utiliser la fonction de comparaison + boolean match = BCrypt.checkpw(dto.getPassword(), user.getPassword()); + + if (!match) { + throw new RuntimeException("Mot de passe incorrect"); + } + + + // 3. Mapper vers UserResponseDTO + UserResponseDto response = new UserResponseDto(); + response.setId(user.getUserId()); + response.setNom(user.getNom()); + response.setPrenom(user.getPrenom()); + response.setEmail(user.getEmail()); + response.setTelephone(user.getTelephone()); + response.setStatut_user(user.isStatut_user()); + response.setDate_naissance(user.getDate_naissance()); + response.setRole(user.getRole()); + + return response; // Connexion réussie + } + + + // Modifier MDP + public void changerMdp(Long user_id, PasswordDto dto) throws Exception { + + User user = userDao.findOne(user_id); + + if (user == null) { + throw new RuntimeException("Utilisateur non trouvé"); + } + + boolean match = BCrypt.checkpw(dto.getOldPassword(), user.getPassword()); + + if (!match) { + throw new RuntimeException("Ancien mot de passe incorrect ou données invalides"); + } + + String hashed = BCrypt.hashpw(dto.getNewPassword(), BCrypt.gensalt()); + user.setPassword(hashed); + + this.userDao.update(user); + + } + + + // Suspendre Utilisateur + public UserResponseDto suspendreUtilisateur(Long userId, StatutUserDto dto) { + User user = userDao.findOne(userId); + + if (user == null) { + throw new EntityNotFoundException("Utilisateur introuvable."); + } + + if (user.isStatut_user()) { + + user.setStatut_user(false); + }else { + user.setStatut_user(true); + } + + userDao.update(user); + // Mapper vers UserResponseDTO + UserResponseDto response = new UserResponseDto(); + response.setId(user.getUserId()); + response.setNom(user.getNom()); + response.setPrenom(user.getPrenom()); + response.setEmail(user.getEmail()); + response.setTelephone(user.getTelephone()); + response.setStatut_user(user.isStatut_user()); + response.setDate_naissance(user.getDate_naissance()); + response.setRole(user.getRole()); + + return response; + } + + + // Liste des utilisateurs + public List listeUtilisateurs() { + List users = userDao.findAllUser(); + List dtos = new ArrayList<>(); + + for (User u : users) { + UserResponseDto dto = new UserResponseDto(); + dto.setId(u.getUserId()); + dto.setNom(u.getNom()); + dto.setPrenom(u.getPrenom()); + dto.setEmail(u.getEmail()); + dto.setTelephone(u.getTelephone()); + dto.setRole(u.getRole()); + dto.setStatut_user(u.isStatut_user()); + dtos.add(dto); + } + return dtos; + } + + + // afficher un User + public UserResponseDto findUserById(Long userId) { + + User user = userDao.findOne(userId); + + if (user == null) { + throw new EntityNotFoundException("User introuvable"); + } + + UserResponseDto dto = new UserResponseDto(); + dto.setId(user.getUserId()); + dto.setNom(user.getNom()); + dto.setPrenom(user.getNom()); + dto.setEmail(user.getEmail()); + dto.setTelephone(user.getTelephone()); + dto.setRole(user.getRole()); + dto.setStatut_user(user.isStatut_user()); + + return dto; + } + + +} diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index 7277b7dd..3ce9e418 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -4,64 +4,27 @@ xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0"> - - org.hibernate.jpa.HibernatePersistenceProvider - - - - - - - - - - - - org.hibernate.jpa.HibernatePersistenceProvider - - - - - - - - - - - - - - org.hibernate.jpa.HibernatePersistenceProvider - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/.DS_Store b/src/main/webapp/.DS_Store new file mode 100644 index 00000000..5b3ba5f8 Binary files /dev/null and b/src/main/webapp/.DS_Store differ diff --git a/src/main/webapp/swagger/.agignore b/src/main/webapp/swagger/.agignore new file mode 100644 index 00000000..849ddff3 --- /dev/null +++ b/src/main/webapp/swagger/.agignore @@ -0,0 +1 @@ +dist/ diff --git a/src/main/webapp/swagger/.browserslistrc b/src/main/webapp/swagger/.browserslistrc new file mode 100644 index 00000000..f5d4abc4 --- /dev/null +++ b/src/main/webapp/swagger/.browserslistrc @@ -0,0 +1,29 @@ +[node-production] +maintained node versions + +[node-development] +node 24 + +[browser-production] +> 1% +last 2 versions +Firefox ESR +not dead + +[browser-development] +last 1 chrome version +last 1 firefox version +last 1 safari version + +[isomorphic-production] +> 1% +last 2 versions +Firefox ESR +not dead +maintained node versions + +[isomorphic-development] +last 1 chrome version +last 1 firefox version +last 1 safari version +node 24 diff --git a/src/main/webapp/swagger/.claude/SKILL_USAGE_EXAMPLE.md b/src/main/webapp/swagger/.claude/SKILL_USAGE_EXAMPLE.md new file mode 100644 index 00000000..af1b863b --- /dev/null +++ b/src/main/webapp/swagger/.claude/SKILL_USAGE_EXAMPLE.md @@ -0,0 +1,565 @@ +# Skill Usage Example: Adding OAS 4.0 Support + +This document demonstrates how to use the `/add-oas-support` skill to add OpenAPI 4.0 support to Swagger UI. + +## Scenario + +You want to add support for OpenAPI Specification 4.0 (a hypothetical major version) to Swagger UI. OAS 4.0 includes: +- New `workflows` top-level field +- New `asyncAPI` authentication type +- Updated JSON Schema to Draft 2024 +- Enhanced callback syntax +- New `info.license.attribution` field + +## Step-by-Step Usage + +### 1. Start the Skill + +```bash +/add-oas-support --version 4.0 --type major +``` + +### 2. Answer Questions + +Claude will ask you questions about the new version: + +**Q: What is the exact version number?** +A: 4.0 + +**Q: Is it a major or minor version?** +A: Major (breaking changes from OAS 3.x) + +**Q: Does it use a new JSON Schema version?** +A: Yes, JSON Schema Draft 2024 + +**Q: What are the new top-level fields?** +A: `workflows` - defines reusable workflow definitions + +**Q: What features are added/removed/changed?** +A: +- Added: `workflows` object for workflow automation +- Added: `asyncAPI` authentication type +- Added: `info.license.attribution` field +- Changed: Callback syntax enhanced with workflow references +- Removed: Deprecated `x-` extension fields from OAS 3.0 + +**Q: Are there new authentication types?** +A: Yes, `asyncAPI` type for asynchronous authentication flows + +### 3. Claude Creates Plugin Structure + +Claude will execute: + +```bash +mkdir -p src/core/plugins/oas40/{auth-extensions,oas31-extensions,spec-extensions,components/auth,wrap-components} +``` + +### 4. Claude Generates Files + +#### 4a. Version Detection (src/core/plugins/oas40/fn.js) + +```javascript +/** + * @prettier + */ + +export const isOAS40 = (jsSpec) => { + const oasVersion = jsSpec.get("openapi") + return ( + typeof oasVersion === "string" && + /^4\.0\.(?:[1-9]\d*|0)$/.test(oasVersion) + ) +} + +export const createOnlyOAS40Selector = + (selector) => + (state, ...args) => + (system) => { + if (system.getSystem().specSelectors.isOAS40()) { + const selectedValue = selector(state, ...args) + return typeof selectedValue === "function" + ? selectedValue(system) + : selectedValue + } else { + return null + } + } + +// ... other factory functions +``` + +#### 4b. Main Plugin Export (src/core/plugins/oas40/index.js) + +```javascript +/** + * @prettier + */ + +import afterLoad from "./after-load.js" +import { + isOAS40, + createSystemSelector, + createOnlyOAS40Selector, + createOnlyOAS40SelectorWrapper, + createOnlyOAS40ComponentWrapper, +} from "./fn.js" + +import Workflows from "./components/workflows.jsx" +import AsyncAPIAuth from "./components/auth/async-api-auth.jsx" +import OAS40License from "./components/license.jsx" +// ... other imports + +const OpenAPI40Plugin = ({ fn }) => { + return { + fn: { + oas40: { + isOAS40, + createSystemSelector, + createOnlyOAS40Selector, + createOnlyOAS40SelectorWrapper, + createOnlyOAS40ComponentWrapper, + }, + }, + components: { + Workflows, + AsyncAPIAuth, + OAS40License, + }, + wrapComponents: { + // Wrap existing components + }, + statePlugins: { + spec: { + selectors: { + isOAS40: createSystemSelector(selectIsOAS40), + workflows: createOnlyOAS40Selector(selectWorkflows), + }, + wrapSelectors: { + isOAS31: isOAS31SelectorWrapper, + isOAS3: isOAS3SelectorWrapper, + }, + }, + auth: { + wrapSelectors: { + definitionsToAuthorize: definitionsToAuthorizeWrapper, + }, + }, + oas40: { + selectors: {}, + }, + }, + afterLoad, + } +} + +export default OpenAPI40Plugin +``` + +#### 4c. Workflows Component (src/core/plugins/oas40/components/workflows.jsx) + +```javascript +/** + * @prettier + */ + +import React from "react" +import PropTypes from "prop-types" + +const Workflows = ({ specSelectors, getComponent }) => { + const workflows = specSelectors.workflows() + + if (!workflows || !workflows.size) { + return null + } + + const Operations = getComponent("operations", true) + + return ( +
+

Workflows

+
+

+ Reusable workflow definitions that can be referenced from operations. +

+
+ {workflows.entrySeq().map(([workflowName, workflow]) => ( +
+

{workflowName}

+

{workflow.get("description")}

+ {/* Render workflow steps */} +
+ ))} +
+ ) +} + +Workflows.propTypes = { + specSelectors: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, +} + +export default Workflows +``` + +#### 4d. AsyncAPI Auth Component (src/core/plugins/oas40/components/auth/async-api-auth.jsx) + +```javascript +/** + * @prettier + */ + +import React from "react" +import PropTypes from "prop-types" + +const AsyncAPIAuth = ({ schema, name, getComponent, onChange, authorized }) => { + const authValue = authorized && authorized.getIn([name]) + const Input = getComponent("Input") + const Button = getComponent("Button") + + return ( +
+

AsyncAPI Authentication

+

{schema.get("description")}

+ onChange({ key: e.target.value })} + /> + +
+ ) +} + +AsyncAPIAuth.propTypes = { + schema: PropTypes.object.isRequired, + name: PropTypes.string.isRequired, + getComponent: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + authorized: PropTypes.object, +} + +export default AsyncAPIAuth +``` + +### 5. Claude Registers Plugin + +Updates `src/core/presets/apis/index.js`: + +```javascript +import OpenAPI40Plugin from "../../plugins/oas40/index.js" + +export default function PresetApis() { + return [ + BasePreset, + OpenAPI30Plugin, + JSONSchema202012Plugin, + JSONSchema202012SamplesPlugin, + OpenAPI31Plugin, + JSONSchema2024Plugin, // New JSON Schema version + JSONSchema2024SamplesPlugin, + OpenAPI40Plugin, // LAST - overrides all previous + ] +} +``` + +### 6. Claude Adds Tests + +#### Unit Test (test/unit/core/plugins/oas40/fn.js) + +```javascript +/** + * @prettier + */ + +import { fromJS } from "immutable" +import { isOAS40 } from "src/core/plugins/oas40/fn.js" + +describe("oas40 plugin - fn - isOAS40", () => { + it("should match OpenAPI 4.0.0", () => { + const spec = fromJS({ openapi: "4.0.0" }) + expect(isOAS40(spec)).toBe(true) + }) + + it("should match OpenAPI 4.0.15", () => { + const spec = fromJS({ openapi: "4.0.15" }) + expect(isOAS40(spec)).toBe(true) + }) + + it("should NOT match OpenAPI 4.0", () => { + const spec = fromJS({ openapi: "4.0" }) + expect(isOAS40(spec)).toBe(false) + }) + + it("should NOT match OpenAPI 3.1.0", () => { + const spec = fromJS({ openapi: "3.1.0" }) + expect(isOAS40(spec)).toBe(false) + }) +}) +``` + +#### E2E Test (test/e2e-cypress/e2e/features/oas40/workflows.cy.js) + +```javascript +/** + * @prettier + */ + +describe("OpenAPI 4.0 - Workflows", () => { + it("should render workflows section", () => { + cy.visit("/e2e-cypress/static/documents/oas40/petstore-workflows.yaml") + cy.get(".opblock-tag").contains("Workflows").should("exist") + }) + + it("should display workflow definitions", () => { + cy.visit("/e2e-cypress/static/documents/oas40/petstore-workflows.yaml") + cy.get(".opblock-workflow").should("have.length.greaterThan", 0) + }) +}) +``` + +### 7. Claude Updates Documentation + +Updates to `README.md`: + +```markdown +## Compatibility + +Swagger UI supports the following OpenAPI Specification versions: + +- OpenAPI 2.0 (Swagger) +- OpenAPI 3.0.x +- OpenAPI 3.1.x +- OpenAPI 4.0.x ⭐ NEW +``` + +Updates to `CLAUDE.md`: + +```markdown +### 26 Core Plugins + +- `oas3` - OpenAPI 3.0.x support +- `oas31` - OpenAPI 3.1.x support +- `oas40` - OpenAPI 4.0.x support (NEW) +``` + +### 8. Claude Runs Tests + +```bash +# Lint +npm run lint-errors + +# Unit tests +npm run test:unit + +# Build +npm run build + +# Artifact tests +npm run test:artifact + +# E2E tests +npm run cy:ci +``` + +### 9. Claude Creates Commit + +```bash +git add src/core/plugins/oas40/ +git add src/core/presets/apis/index.js +git add test/unit/core/plugins/oas40/ +git add test/e2e-cypress/e2e/features/oas40/ +git add README.md CLAUDE.md + +git commit -m "$(cat <<'EOF' +feat(oas40): add support for OpenAPI 4.0 + +Implement OpenAPI 4.0.x specification support with: +- Version detection and plugin architecture +- Workflows rendering for reusable workflow definitions +- AsyncAPI authentication type support +- JSON Schema Draft 2024 integration +- Enhanced callback syntax with workflow references +- License attribution field support + +Breaking changes from OAS 3.1: +- Removed deprecated x- extension fields +- Updated JSON Schema to Draft 2024 + +Includes comprehensive unit and E2E tests. + +Co-Authored-By: Claude Sonnet 4.5 +EOF +)" +``` + +### 10. Result + +You now have complete OpenAPI 4.0 support in Swagger UI: + +``` +src/core/plugins/oas40/ +├── index.js (367 lines) +├── fn.js (124 lines) +├── selectors.js (45 lines) +├── after-load.js (78 lines) +├── auth-extensions/ +│ └── wrap-selectors.js (32 lines) +├── oas31-extensions/ +│ └── fn.js (56 lines) +├── spec-extensions/ +│ ├── selectors.js (89 lines) +│ └── wrap-selectors.js (23 lines) +├── components/ +│ ├── workflows.jsx (156 lines) +│ ├── license.jsx (67 lines) +│ ├── version-pragma-filter.jsx (78 lines) +│ └── auth/ +│ └── async-api-auth.jsx (54 lines) +└── wrap-components/ + ├── info.jsx (12 lines) + └── license.jsx (12 lines) + +test/unit/core/plugins/oas40/ +├── fn.js (67 lines) +└── components/ + └── version-pragma-filter.jsx (45 lines) + +test/e2e-cypress/e2e/features/oas40/ +├── workflows.cy.js (34 lines) +└── async-api-auth.cy.js (28 lines) +``` + +**Total:** ~1,367 lines of code, fully tested and documented. + +## Benefits of Using the Skill + +### Without the Skill (Manual Implementation) + +**Estimated Time:** 40-60 hours + +**Challenges:** +- Understanding plugin architecture (4-6 hours) +- Studying OAS 3.1 patterns (3-4 hours) +- Creating file structure (1 hour) +- Implementing version detection (2 hours) +- Creating selector factories (4-6 hours) +- Implementing components (12-16 hours) +- Writing tests (8-12 hours) +- Debugging integration issues (6-8 hours) +- Documentation (2-3 hours) + +**Error-prone areas:** +- Plugin loading order +- Selector wrapping logic +- Component lifecycle +- Redux state management +- afterLoad hook timing + +### With the Skill (Guided Implementation) + +**Estimated Time:** 8-12 hours + +**Benefits:** +- Automated boilerplate generation +- Follows established patterns +- Guided step-by-step process +- Built-in error checking +- Comprehensive test coverage +- Documentation templates +- Best practices enforced + +**What the skill handles:** +- ✅ File structure creation +- ✅ Boilerplate code generation +- ✅ Pattern adherence +- ✅ Test scaffolding +- ✅ Documentation updates +- ✅ Build verification +- ✅ Commit message formatting + +## Customization Options + +The skill is flexible and can be customized per version: + +### For Minor Versions (e.g., 3.2) + +```bash +/add-oas-support --version 3.2 --type minor +``` + +Claude will: +- Create lighter plugin structure +- Reuse more from previous version +- Focus on incremental additions +- Skip breaking change handling +- Minimize component wrapping + +### For Major Versions (e.g., 5.0) + +```bash +/add-oas-support --version 5.0 --type major +``` + +Claude will: +- Create comprehensive plugin structure +- Implement extensive component wrapping +- Handle breaking changes +- Create new base components if needed +- Add deprecation warnings +- Update preset configurations + +## Troubleshooting + +### Skill Not Found + +``` +Error: Skill 'add-oas-support' not found +``` + +**Solution:** Ensure the skill file exists in `.claude/skills/add-oas-support.md` + +### Version Already Exists + +``` +Error: Plugin for OAS 4.0 already exists +``` + +**Solution:** Remove existing plugin or choose a different version + +### Build Failures + +``` +Error: Module not found +``` + +**Solution:** Check plugin registration in presets and import paths + +### Test Failures + +``` +Error: isOAS40 is not a function +``` + +**Solution:** Verify selector is properly exported and registered + +## Next Steps + +After the skill completes: + +1. **Test with real specs** - Use actual OAS 4.0 examples +2. **Review generated code** - Ensure it matches your requirements +3. **Add custom features** - Extend beyond the boilerplate +4. **Optimize performance** - Profile and optimize hot paths +5. **Submit PR** - Follow the PR template and guidelines + +## Additional Resources + +- **OAS 4.0 Spec:** https://spec.openapis.org/oas/v4.0.0 (hypothetical) +- **Plugin API:** `docs/customization/plugin-api.md` +- **CLAUDE.md:** Complete codebase guide +- **Skill Source:** `.claude/skills/add-oas-support.md` + +--- + +**Note:** This example uses hypothetical OAS 4.0 features for demonstration purposes. Adapt the skill usage to match the actual specification features of the version you're implementing. diff --git a/src/main/webapp/swagger/.claude/skills/README.md b/src/main/webapp/swagger/.claude/skills/README.md new file mode 100644 index 00000000..0d554806 --- /dev/null +++ b/src/main/webapp/swagger/.claude/skills/README.md @@ -0,0 +1,151 @@ +# Swagger UI Claude Skills + +This directory contains custom skills for working with the Swagger UI codebase in Claude Code. + +## Available Skills + +### `/add-oas-support` - Add OpenAPI Specification Version Support + +Comprehensive skill for adding support for a new OpenAPI Specification version to Swagger UI. + +**Usage:** +``` +/add-oas-support --version 4.0 --type major +/add-oas-support --version 3.2 --type minor +``` + +**Parameters:** +- `--version` (required): The OpenAPI version to add support for (e.g., "3.2", "4.0") +- `--type` (optional): Version type - "major" or "minor" (default: "minor") + +**Structure:** +The skill includes both a **Quick Reference** section at the top for experienced developers, and a **comprehensive guide** below for detailed implementation instructions. + +**What it does:** +1. **Analyzes the target OAS version specification using WebFetch** + - Fetches official spec from https://spec.openapis.org/ + - Systematically identifies new/modified/removed fields + - Maps specification changes to Swagger UI components + - Creates specification change document +2. Creates the plugin directory structure +3. Implements version detection logic +4. Creates selector factories and component wrappers +5. Implements new feature components based on spec analysis +6. Handles authentication changes +7. Registers plugin in presets +8. Adds unit and E2E tests +9. Updates documentation +10. Runs full test suite + +**Key Features:** +- ✅ **Quick Reference** section for rapid development +- ✅ Comprehensive specification analysis workflow using WebFetch +- ✅ Detailed mapping table from spec changes → components (15+ change types) +- ✅ Real examples from OAS 3.1 implementation (6 detailed examples) +- ✅ Iterative spec-driven development approach +- ✅ Component-by-component verification checklist +- ✅ Best practices for continuous spec reference +- ✅ WebFetch query templates for spec analysis +- ✅ Common pitfalls and solutions +- ✅ Pre-submit checklist + +**Based on:** +This skill follows the patterns established by the OAS 3.1 implementation (commit history analyzed from `src/core/plugins/oas31/`). + +**Key patterns:** +- Plugin-based architecture with Redux state management +- Selector factories for version-specific logic +- Component wrapping for conditional rendering +- afterLoad lifecycle hooks for function overrides +- Plugin loading order (new version loaded LAST) + +**Prerequisites:** +- Understanding of Swagger UI plugin architecture +- Access to the new OAS specification document +- All existing tests passing + +**Example workflow:** + +Adding OAS 4.0 support: +``` +# Start the skill +/add-oas-support --version 4.0 --type major + +# Claude will: +# 1. Ask about new features in OAS 4.0 +# 2. Create plugin directory structure +# 3. Generate boilerplate code +# 4. Guide through implementation +# 5. Add tests +# 6. Update documentation +# 7. Verify build +``` + +## Creating New Skills + +To create a new skill for Swagger UI: + +1. Create a markdown file in `.claude/skills/` +2. Add frontmatter with skill metadata: + ```yaml + --- + name: skill-name + description: Brief description + args: + param1: + description: "Parameter description" + required: true + type: string + --- + ``` +3. Write comprehensive instructions following the patterns in existing skills +4. Document common pitfalls and best practices +5. Include code templates with placeholders +6. Add to this README + +## Skill Development Guidelines + +When creating skills for Swagger UI: + +1. **Follow project conventions:** + - No semicolons + - Double quotes + - @prettier pragma in all new files + - .jsx extension for React components + +2. **Use the plugin architecture:** + - Don't modify core unnecessarily + - Follow established patterns + - Load new plugins at the end of presets + +3. **Include comprehensive tests:** + - Unit tests for logic + - Component tests for UI + - E2E tests for integration + +4. **Document thoroughly:** + - Update CLAUDE.md + - Update relevant docs + - Add inline JSDoc comments + +5. **Security first:** + - Use DOMPurify for HTML + - Validate all input + - Follow OWASP guidelines + +## Contributing + +To contribute new skills: + +1. Fork the repository +2. Create skill in `.claude/skills/` +3. Test thoroughly +4. Update this README +5. Submit pull request + +## Resources + +- **CLAUDE.md** - Comprehensive codebase guide +- **Plugin API** - `docs/customization/plugin-api.md` +- **Development Setup** - `docs/development/setting-up.md` +- **Contributing Guide** - https://github.com/swagger-api/.github/blob/HEAD/CONTRIBUTING.md diff --git a/src/main/webapp/swagger/.claude/skills/add-oas-support.md b/src/main/webapp/swagger/.claude/skills/add-oas-support.md new file mode 100644 index 00000000..36062c30 --- /dev/null +++ b/src/main/webapp/swagger/.claude/skills/add-oas-support.md @@ -0,0 +1,2333 @@ +--- +name: add-oas-support +description: Add support for a new OpenAPI Specification version to Swagger UI +args: + version: + description: "OpenAPI version to add support for (e.g., 3.2, 4.0)" + required: true + type: string + type: + description: "Version type: 'major' for new major version, 'minor' for patch/minor" + required: false + type: string + default: "minor" +--- + +# Add OpenAPI Specification Version Support + +This skill guides you through adding support for a new OpenAPI Specification (OAS) version to Swagger UI, following the established architectural patterns from OAS 3.1 implementation. + +--- + +## 📌 Quick Reference + +> **New to this skill?** Skip to [Prerequisites](#prerequisites) for the full guide. +> **Already familiar?** Use this quick reference for rapid development. + +### 🚀 Quick Start + +```bash +# Major version (e.g., OAS 4.0) +/add-oas-support --version 4.0 --type major + +# Minor version (e.g., OAS 3.2) +/add-oas-support --version 3.2 --type minor +``` + +### 📋 Essential Checklist + +#### Phase 1: Specification Analysis ⭐ MOST IMPORTANT +- [ ] Access official spec at **https://spec.openapis.org/oas/v{VERSION}/** +- [ ] Use WebFetch to analyze spec systematically +- [ ] Create specification change document +- [ ] Map changes to components using [mapping table](#step-1d-map-specification-changes-to-swagger-ui-components) + +#### Phase 2: Implementation +- [ ] Create plugin directory structure +- [ ] Implement version detection (`isOAS{VERSION}`) +- [ ] Create selector factories +- [ ] Implement components for each spec change +- [ ] Wrap existing components if modified +- [ ] Register plugin in preset (LAST position) + +#### Phase 3: Testing & Documentation +- [ ] Add unit tests +- [ ] Add E2E tests with spec examples +- [ ] Update documentation +- [ ] Verify build & run full test suite + +### 🔍 WebFetch Queries for Spec Analysis + +```javascript +// 1. Fetch main spec differences +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "List all new top-level fields, modified fields, and removed fields compared to version {PREV_VERSION}" +) + +// 2. Analyze new feature +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "Describe the '{NEW_FIELD}' field: structure, type, purpose, required/optional, and provide examples" +) + +// 3. Check JSON Schema version +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "What JSON Schema version does this use? List changes from {PREV_JSON_SCHEMA_VERSION}" +) + +// 4. Security schemes +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "List all security scheme types, highlighting new or changed types" +) +``` + +### 🗺️ Spec Change → Component Mapping (Quick) + +| Spec Change | Component Action | File Location | +|-------------|------------------|---------------| +| **New top-level field (Object)** | Create new component | `components/{field}.jsx` | +| **New top-level field (String)** | Add to info or create display | `components/{field}.jsx` | +| **Modified Info object** | Wrap Info component | `wrap-components/info.jsx` | +| **New info subfield** | Create component + wrap Info | `components/info-{field}.jsx` | +| **New auth type** | Create auth component | `components/auth/{type}.jsx` | +| **Modified security** | Wrap auth selector | `auth-extensions/wrap-selectors.js` | +| **New operation field** | Extend Operation/selector | `spec-extensions/selectors.js` | +| **JSON Schema keyword** | Create keyword component | `json-schema-{V}-extensions/` | +| **JSON Schema version** | Create entire new plugin | `plugins/json-schema-{V}/` | + +See [full mapping table with examples](#step-1d-map-specification-changes-to-swagger-ui-components) for detailed guidance. + +### 📝 Component Implementation Pattern + +**For each new feature:** + +1. **Check spec first:** + ```javascript + WebFetch(url, "Describe {field} structure, type, examples") + ``` + +2. **Create selector:** + ```javascript + // spec-extensions/selectors.js + export const selectNewField = createSelector( + (state) => state, + (state) => state.getIn(["spec", "json", "newField"]) + ) + ``` + +3. **Create component:** + ```javascript + // components/new-field.jsx + const NewField = ({ specSelectors, getComponent }) => { + const data = specSelectors.selectNewField() + if (!data) return null + return
{/* render based on spec structure */}
+ } + ``` + +4. **Register in plugin:** + ```javascript + // index.js + components: { NewField }, + statePlugins: { + spec: { + selectors: { + selectNewField: createOnlyOAS{V}Selector(selectNewField) + } + } + } + ``` + +5. **Add tests** (unit + E2E with spec examples) + +### ⚠️ Common Pitfalls + +1. ❌ **Guessing field structure** → ✅ Always check spec with WebFetch +2. ❌ **Forgetting @prettier pragma** → ✅ Add to all new files +3. ❌ **Wrong plugin load order** → ✅ New plugin LAST in preset +4. ❌ **Using semicolons** → ✅ No semicolons (project convention) +5. ❌ **Single quotes** → ✅ Use double quotes +6. ❌ **Skipping spec examples** → ✅ Use as test fixtures +7. ❌ **Hardcoded assumptions** → ✅ Verify everything in spec +8. ❌ **`props.Ori` in ComponentWrapper** → ✅ Use `props.originalComponent` (see Pattern 3) +9. ❌ **Copying entire Info component for minor version** → ✅ Use OpenAPIVersion wrapper + `getComponent("OAS{PREV}Info")` +10. ❌ **OAS{VERSION} logic in OAS{PREV} plugin** → ✅ Each version's logic lives in its own plugin +11. ❌ **`isOAS{PREV}` wrapper on minor version** → ✅ Only add if regex overlaps; minor bumps don't need it +12. ❌ **New HTTP method in core `validOperationMethods`** → ✅ Add to `OPERATION_METHODS` + wrap with `createOnlyOAS{VERSION}SelectorWrapper` +13. ❌ **Assuming new OAS meta-schema = new JSON Schema dialect** → ✅ Verify — OAS 3.2 still uses JSON Schema 2020-12 +14. ❌ **Inline version guard in wrap-components** → ✅ Always use `createOnlyOAS{VERSION}ComponentWrapper`; never write `(Original, system) => (props) => { if (...isOAS{VERSION}...) }` by hand — even expanding an existing guard (`isOAS31 || isOAS32`) is wrong; add dedicated wrap-components in the new plugin instead + +### ✅ Pre-Submit Checklist + +- [ ] All spec changes mapped to components +- [ ] All components reference spec in comments +- [ ] Tests use examples from spec +- [ ] Version detection regex correct +- [ ] Plugin loaded last in preset +- [ ] All tests passing (lint + unit + E2E) +- [ ] Build completes successfully +- [ ] Documentation updated +- [ ] No semicolons, double quotes everywhere +- [ ] @prettier pragma in all new files + +### 📚 Quick Links + +- **OAS Specs:** https://spec.openapis.org/ +- **OAS 3.1:** https://spec.openapis.org/oas/v3.1.0/ +- **OAS 3.0:** https://spec.openapis.org/oas/v3.0.3/ +- **JSON Schema:** https://json-schema.org/ +- **CLAUDE.md:** Complete codebase guide + +### 🔖 Full Guide Navigation + +Need detailed guidance? Jump to these sections: + +- **[Prerequisites](#prerequisites)** - Setup and requirements +- **[Architecture Overview](#architecture-overview)** - Plugin system explained +- **[Key Design Patterns](#key-design-patterns-from-oas-31)** - Code patterns from OAS 3.1 +- **[Step 1: Analyze Specification](#step-1-analyze-new-oas-version-specification)** - ⭐ Complete spec analysis workflow + - [1D: Mapping Table](#step-1d-map-specification-changes-to-swagger-ui-components) - 15+ spec change types + - [1E: Real Examples](#step-1e-examples-oas-31-specification-changes--components) - 6 detailed examples + - [1H: WebFetch Workflow](#step-1h-using-webfetch-to-analyze-specifications) - Spec analysis with WebFetch +- **[Step 2-17: Implementation](#step-2-create-plugin-directory-structure)** - Complete implementation steps +- **[Practical Workflow](#practical-workflow-from-spec-to-components)** - End-to-end example +- **[Best Practices](#best-practices-iterative-spec-driven-development)** - Spec-driven development +- **[Common Pitfalls](#common-pitfalls)** - What to avoid +- **[Final Checklist](#final-checklist)** - Pre-submission verification + +--- + +**Remember: The specification at https://spec.openapis.org/ is your source of truth!** + +--- + +## Prerequisites + +Before starting, ensure: +- You understand the Swagger UI plugin architecture +- You've read the CLAUDE.md guide +- You have the OAS specification document for the new version +- All tests pass: `npm test` + +## Architecture Overview + +Adding OAS version support involves: + +1. **Plugin Creation** - New plugin following established patterns +2. **Version Detection** - Regex-based version identifier +3. **Selector Factories** - Conditional logic for version-specific features +4. **Component Implementation** - New/wrapped components for rendering +5. **State Management** - Redux selectors/actions for new features +6. **Lifecycle Hooks** - afterLoad modifications +7. **Preset Registration** - Plugin loading order +8. **Testing** - Unit and E2E tests +9. **Documentation** - Update guides and README + +## Key Design Patterns (from OAS 3.1) + +### Pattern 1: Version Detection +```javascript +export const isOAS{VERSION} = (jsSpec) => { + const oasVersion = jsSpec.get("openapi") + return ( + typeof oasVersion === "string" && /^{MAJOR}\.{MINOR}\.(?:[1-9]\d*|0)$/.test(oasVersion) + ) +} +``` + +### Pattern 2: Selector Factories + +**createOnlyOAS{VERSION}Selector** - Features exclusive to this version: +```javascript +export const createOnlyOAS{VERSION}Selector = + (selector) => + (state, ...args) => + (system) => { + if (system.getSystem().specSelectors.isOAS{VERSION}()) { + const selectedValue = selector(state, ...args) + return typeof selectedValue === "function" + ? selectedValue(system) + : selectedValue + } else { + return null + } + } +``` + +**createOnlyOAS{VERSION}SelectorWrapper** - Override previous versions: +```javascript +export const createOnlyOAS{VERSION}SelectorWrapper = + (selector) => + (oriSelector, system) => + (state, ...args) => { + if (system.getSystem().specSelectors.isOAS{VERSION}()) { + const selectedValue = selector(state, ...args) + return typeof selectedValue === "function" + ? selectedValue(oriSelector, system) + : selectedValue + } else { + return oriSelector(...args) + } + } +``` + +### Pattern 3: Component Wrappers + +**⚠️ IMPORTANT: `originalComponent` prop name, not `Ori`** + +`createOnlyOAS{VERSION}ComponentWrapper` passes the original component as `originalComponent` in props (NOT `Ori` — that's the OAS3/OAS30ComponentWrapFactory convention). Use `const { originalComponent: Ori } = props` to access it. + +**Pattern A — Reuse previous version's component via getComponent:** +```javascript +const ComponentWrapper = createOnlyOAS{VERSION}ComponentWrapper(({ getSystem }) => { + const system = getSystem() + const OAS{PREV_VERSION}Component = system.getComponent("OAS{PREV_VERSION}Component", true) + return +}) +``` + +**Pattern B — Render original component with extra props (e.g. version badge):** +```javascript +// Use `originalComponent` (NOT `Ori`) — that's the prop name createOnlyOAS{VERSION}ComponentWrapper passes +const OpenAPIVersionWrapper = createOnlyOAS{VERSION}ComponentWrapper((props) => { + const { originalComponent: Ori } = props + return +}) +``` + +### Pattern 4: Function Wrapping +```javascript +export const wrapOAS{VERSION}Fn = (fn, system) => { + const { fn: systemFn, specSelectors } = system + return Object.fromEntries( + Object.entries(fn).map(([name, newImpl]) => { + const oriImpl = systemFn[name] + const impl = (...args) => + specSelectors.isOAS{VERSION}() + ? newImpl(...args) + : typeof oriImpl === "function" + ? oriImpl(...args) + : undefined + return [name, impl] + }) + ) +} +``` + +## Implementation Steps + +### Step 1: Analyze New OAS Version Specification + +**CRITICAL: Always start by reviewing the official specification at https://spec.openapis.org/** + +#### 1A. Access the Official Specification + +**Primary Resource:** https://spec.openapis.org/ + +**Available Versions:** +- OAS 2.0: https://spec.openapis.org/oas/v2.0/ +- OAS 3.0.x: https://spec.openapis.org/oas/v3.0.3/ +- OAS 3.1.x: https://spec.openapis.org/oas/v3.1.0/ +- Future versions will follow the pattern: https://spec.openapis.org/oas/v{MAJOR}.{MINOR}.{PATCH}/ + +**What to download:** +1. The specification document (usually in HTML or Markdown format) +2. JSON Schema definitions (if available) +3. Example specification files +4. Change log or migration guide (if available) + +#### 1B. Systematic Specification Analysis + +Use the WebFetch tool to retrieve and analyze the specification: + +```bash +# Example for analyzing OAS 4.0 (hypothetical) +WebFetch("https://spec.openapis.org/oas/v4.0.0/", "List all new top-level fields, objects, and keywords introduced in this version compared to OAS 3.1") +``` + +**Read these sections carefully:** + +1. **Version Number Section** + - Confirm the exact version format (e.g., "4.0.0", "3.2.0") + - Note any version detection changes + +2. **OpenAPI Object (Root Level)** + - New top-level fields (e.g., `webhooks` in OAS 3.1) + - Modified existing fields + - Removed/deprecated fields + +3. **Info Object** + - New fields (e.g., `summary` in OAS 3.1) + - Changes to `license`, `contact`, etc. + +4. **Paths Object & Operations** + - New operation-level fields + - Changes to parameters, request bodies, responses + - New callback syntax or features + +5. **Components Object** + - New component types + - Schema changes (especially JSON Schema version) + - New security schemes + +6. **Security Object** + - New authentication types (e.g., `mutualTLS` in OAS 3.1) + - Changes to OAuth2 flows + - New security-related fields + +7. **Schema Object** + - JSON Schema version change (critical!) + - New keywords (e.g., `examples` vs `example`) + - Type system changes + +#### 1C. Create Specification Change Document + +Create a structured document listing all changes: + +**Template:** +```markdown +# OpenAPI {VERSION} Specification Analysis + +## Version Information +- Version: {MAJOR}.{MINOR}.{PATCH} +- Release Date: YYYY-MM-DD +- Previous Version: {PREVIOUS_VERSION} +- JSON Schema Version: Draft {VERSION} or {YEAR} + +## New Top-Level Fields +1. **`newField`** (Type: Object) + - Location: Root level + - Purpose: Description from spec + - Required: Yes/No + - Example from spec: {...} + - Swagger UI Impact: Needs new component in BaseLayout + +## Modified Existing Fields +1. **`info`** + - New subfields: `summary`, `newField` + - Changed behavior: Description + - Swagger UI Impact: Need to wrap Info component + +## New Operation-Level Features +1. **`newOperationField`** + - Purpose: Description + - Swagger UI Impact: Extend Operation component + +## New Authentication Types +1. **`newAuthType`** + - Type name: "newAuthType" + - Fields: {...} + - Swagger UI Impact: New auth component needed + +## JSON Schema Changes +- Version change: Draft 2020-12 → Draft 2024 +- New keywords: `keyword1`, `keyword2` +- Removed keywords: `oldKeyword` +- Swagger UI Impact: New json-schema plugin needed + +## Breaking Changes +1. Removed `x-deprecated-field` +2. Changed behavior of `existingField` +3. Required fields now enforced + +## Deprecated Features +1. `oldField` - Use `newField` instead +2. `oldAuthType` - Use `newAuthType` instead + +## Examples from Specification +- Link to example files +- Notable edge cases to test +``` + +#### 1D. Map Specification Changes to Swagger UI Components + +**CRITICAL MAPPING GUIDE:** Use this table to determine what components to create/modify: + +| Spec Change Type | Swagger UI Component Type | Location | Example from OAS 3.1 | +|-----------------|---------------------------|----------|---------------------| +| **New Top-Level Field (Object)** | New standalone component | `components/{feature-name}.jsx` | `webhooks` → `Webhooks.jsx` | +| **New Top-Level Field (String/Primitive)** | Add to existing layout component or create info component | `components/{field-name}.jsx` | `jsonSchemaDialect` → `JsonSchemaDialect.jsx` | +| **Modified Info Object** | Wrap Info component OR create OAS{VERSION}Info | `wrap-components/info.jsx` | `info.summary` → Wrapped InfoContainer | +| **New Info Subfield** | Create new component, render in Info | `components/info-{field}.jsx` | `info.license.identifier` → OAS31License | +| **Modified License/Contact** | Wrap component | `wrap-components/license.jsx` | `license.identifier` → Wrapped License | +| **New Authentication Type** | New auth component | `components/auth/{type}.jsx` | `mutualTLS` → `MutualTLSAuth.jsx` | +| **Modified Security Scheme** | Wrap auth selector | `auth-extensions/wrap-selectors.js` | mutualTLS → `definitionsToAuthorize` wrapper | +| **New Operation Field** | Extend Operation or create plugin selector | `spec-extensions/selectors.js` | N/A in OAS 3.1 | +| **New Callback Feature** | Extend Callbacks component or wrap | `wrap-components/callbacks.jsx` | Enhanced in OAS 3.1 | +| **New Parameter Type** | Extend Parameters component | `components/parameters/` | N/A in OAS 3.1 | +| **New Response Feature** | Extend Responses component | `components/responses/` | N/A in OAS 3.1 | +| **New Media Type Feature** | Modify media-type component or fn | `oas{PREV}-extensions/fn.js` | File upload detection changes | +| **JSON Schema Keyword** | New keyword component | `json-schema-{VERSION}-extensions/components/keywords/` | `example` → `JSONSchema202012KeywordExample.jsx` | +| **JSON Schema Version Change** | Entire new plugin + samples plugin | `plugins/json-schema-{VERSION}/` | JSON Schema 2020-12 plugin | +| **Modified Schema Rendering** | Wrap schema component | `wrap-components/model.jsx` | Models → OAS31Models wrapper | +| **Version Detection Field** | Update VersionPragmaFilter | `components/version-pragma-filter.jsx` | Updated for isOAS31 | +| **Deprecated Field** | Remove or add deprecation warning | Component showing warning | N/A in OAS 3.1 | + +#### 1E. Examples: OAS 3.1 Specification Changes → Components + +**Example 1: `webhooks` Top-Level Field** + +**From Spec (OAS 3.1):** +```yaml +openapi: 3.1.0 +webhooks: + newPet: + post: + requestBody: + description: Information about a new pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: Return a 200 status +``` + +**Mapping Decision:** +- Type: New top-level field (Object containing operations) +- Location in spec: Root level, same structure as `paths` +- Swagger UI component: New standalone component +- File: `src/core/plugins/oas31/components/webhooks.jsx` +- Rendering: Reuses Operations component to render webhook operations +- Selector: `selectWebhooks` in `spec-extensions/selectors.js` +- Layout integration: Added to BaseLayout or StandaloneLayout + +**Example 2: `info.summary` Field** + +**From Spec (OAS 3.1):** +```yaml +info: + title: My API + summary: A brief summary of the API # NEW in OAS 3.1 + description: Detailed description... + version: 1.0.0 +``` + +**Mapping Decision:** +- Type: New subfield in existing Info object +- Location: `info.summary` +- Swagger UI component: Wrap existing Info component +- File: `src/core/plugins/oas31/wrap-components/info.jsx` + `src/core/plugins/oas31/components/info.jsx` +- Selector: `selectInfoSummary` (if needed) +- Rendering: Display summary above or below title + +**Example 3: `mutualTLS` Authentication Type** + +**From Spec (OAS 3.1):** +```yaml +components: + securitySchemes: + myMutualTLS: + type: mutualTLS # NEW in OAS 3.1 + description: Client certificate authentication +``` + +**Mapping Decision:** +- Type: New authentication scheme type +- Location: `components.securitySchemes[name].type` +- Swagger UI component: New auth component +- File: `src/core/plugins/oas31/components/auth/mutual-tls-auth.jsx` +- Selector wrapper: `auth-extensions/wrap-selectors.js` → `definitionsToAuthorize` +- Rendering: Display in authorize modal with certificate upload UI + +**Example 4: `license.identifier` Field** + +**From Spec (OAS 3.1):** +```yaml +info: + license: + name: Apache 2.0 + identifier: Apache-2.0 # NEW in OAS 3.1 (SPDX ID) + url: https://www.apache.org/licenses/LICENSE-2.0.html +``` + +**Mapping Decision:** +- Type: New optional field in license object (alternative to url) +- Location: `info.license.identifier` +- Swagger UI component: Wrap License component + create selector +- File: `src/core/plugins/oas31/wrap-components/license.jsx` + selector in `spec-extensions/selectors.js` +- Logic: If `identifier` exists, construct URL from SPDX registry +- Rendering: Make license text a hyperlink to SPDX page + +**Example 5: JSON Schema 2020-12 Keywords** + +**From Spec (OAS 3.1):** +```yaml +# OAS 3.1 uses JSON Schema 2020-12 which includes: +components: + schemas: + Pet: + type: object + properties: + name: + type: string + example: Fluffy # Changed from 'examples' array in OAS 3.0 + xml: # Now a JSON Schema keyword, not OAS-specific + name: pet + externalDocs: # Now a JSON Schema keyword + url: https://example.com + discriminator: # Now a JSON Schema keyword + propertyName: petType +``` + +**Mapping Decision:** +- Type: JSON Schema version upgrade (Draft 7 → 2020-12) +- Location: Schema objects throughout spec +- Swagger UI components: Entire new plugin +- Files: + - `src/core/plugins/json-schema-2020-12/` (main plugin) + - `src/core/plugins/json-schema-2020-12-samples/` (sample generation) + - `src/core/plugins/json-schema-2020-12/components/keywords/example.jsx` + - `src/core/plugins/json-schema-2020-12/components/keywords/xml.jsx` + - `src/core/plugins/json-schema-2020-12/components/keywords/discriminator.jsx` + - `src/core/plugins/json-schema-2020-12/components/keywords/external-docs.jsx` +- Integration: OAS31 plugin extends JSON Schema 2020-12 keywords in `json-schema-2020-12-extensions/` + +**Example 6: `jsonSchemaDialect` Top-Level Field** + +**From Spec (OAS 3.1):** +```yaml +openapi: 3.1.0 +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base # NEW in OAS 3.1 +``` + +**Mapping Decision:** +- Type: New top-level field (String/URI) +- Location: Root level +- Swagger UI component: Simple display component +- File: `src/core/plugins/oas31/components/json-schema-dialect.jsx` +- Selector: `selectJsonSchemaDialectField` in `spec-extensions/selectors.js` +- Rendering: Show in info section with warning if non-default + +#### 1F. Specification Analysis Checklist + +Use WebFetch and Read tools to systematically check: + +- [ ] **Accessed official spec at https://spec.openapis.org/** +- [ ] **Downloaded specification document** +- [ ] **Read version number section** +- [ ] **Analyzed OpenAPI Object (root level) changes** +- [ ] **Reviewed Info Object modifications** +- [ ] **Checked Paths/Operations changes** +- [ ] **Examined Components Object updates** +- [ ] **Identified Security Scheme changes** +- [ ] **Analyzed Schema Object / JSON Schema version** +- [ ] **Listed all new top-level fields** +- [ ] **Listed all modified existing fields** +- [ ] **Documented breaking changes** +- [ ] **Identified deprecated features** +- [ ] **Found example specifications** +- [ ] **Created specification change document** +- [ ] **Mapped changes to Swagger UI components using table above** +- [ ] **Prioritized implementation order** + +#### 1G. Questions to Answer During Analysis + +**Version Type:** +- Is this a major version (4.0) or minor version (3.2)? +- Are there breaking changes? +- Is backward compatibility maintained? + +**JSON Schema:** +- Does it use a new JSON Schema version? +- What new keywords are available? +- Are there removed/deprecated keywords? + +**Top-Level Changes:** +- What new top-level fields are added? +- Which existing fields have new subfields? +- Any removed top-level fields? + +**Operation Changes:** +- Are there new operation-level features? +- Changes to parameters, request bodies, responses? +- New callback or link syntax? + +**Security Changes:** +- What authentication types are new/changed? +- OAuth2 flow modifications? +- New security requirements? + +**Media Type Changes:** +- Are there new media type features? +- File upload handling changes? +- Encoding changes? + +**Component Mapping:** +- Which components need to be created from scratch? +- Which existing components need to be wrapped? +- What selectors are required? +- Are new plugins needed (e.g., for JSON Schema)? + +**Implementation Priority:** +- What's the critical path for basic rendering? +- Which features can be added incrementally? +- What has the highest user impact? + +### Step 1H: Using WebFetch to Analyze Specifications + +**Always use WebFetch tool to retrieve and analyze the specification systematically.** + +**Step-by-step WebFetch workflow:** + +1. **Fetch the specification landing page:** +```javascript +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "Provide a summary of this OpenAPI Specification version, including links to the full specification document and any change logs or migration guides." +) +``` + +2. **Analyze the specification structure:** +```javascript +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "List all top-level fields in the OpenAPI Object for this version. For each field, indicate if it is new, modified, or unchanged from version {PREVIOUS_VERSION}." +) +``` + +3. **Deep dive into new features:** +```javascript +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "Describe the '{NEW_FIELD}' field in detail: its type, structure, purpose, whether it's required, and provide examples from the specification." +) +``` + +4. **Analyze security changes:** +```javascript +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "List all security scheme types supported in this version. Highlight any new or changed security scheme types compared to {PREVIOUS_VERSION}." +) +``` + +5. **Check JSON Schema compatibility:** +```javascript +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "What version of JSON Schema does this OpenAPI Specification version use? List any specific dialect or modifications to JSON Schema." +) +``` + +6. **Find example specifications:** +```javascript +WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "Provide links to example OpenAPI specification files for this version, particularly examples that demonstrate new features." +) +``` + +**Real example from analyzing OAS 3.1:** + +```javascript +// 1. Initial analysis +WebFetch( + "https://spec.openapis.org/oas/v3.1.0/", + "List all new fields and features introduced in OpenAPI 3.1.0 compared to OpenAPI 3.0.3" +) + +// Response would include: +// - webhooks field (new top-level) +// - info.summary (new) +// - info.license.identifier (new) +// - jsonSchemaDialect (new top-level) +// - mutualTLS security scheme type (new) +// - JSON Schema 2020-12 (changed from Draft 7) + +// 2. Deep dive into webhooks +WebFetch( + "https://spec.openapis.org/oas/v3.1.0/", + "Explain the 'webhooks' field in detail: structure, purpose, and provide a complete example" +) + +// 3. Understand JSON Schema changes +WebFetch( + "https://spec.openapis.org/oas/v3.1.0/", + "What are the differences between JSON Schema Draft 7 (used in OAS 3.0) and JSON Schema 2020-12 (used in OAS 3.1) that affect schema definitions?" +) +``` + +**Pro tip:** Use multiple targeted queries rather than one broad query. This gives more precise information for each component you need to implement. + +### Step 2: Create Plugin Directory Structure + +**Directory:** `src/core/plugins/oas{VERSION_NUMBER}/` + +**Files to Create:** +``` +src/core/plugins/oas{VERSION_NUMBER}/ +├── index.js # Main plugin export +├── fn.js # Selector/function factories +├── selectors.js # Version-specific selectors (if needed) +├── after-load.js # Lifecycle hook +├── auth-extensions/ # Authentication features +│ └── wrap-selectors.js +├── oas{PREVIOUS_VERSION}-extensions/ # Extensions to previous version +│ └── fn.js +├── spec-extensions/ # Spec-level features +│ ├── selectors.js +│ └── wrap-selectors.js +├── components/ # New components +│ ├── version-pragma-filter.jsx +│ ├── {new-feature-1}.jsx +│ └── auth/ +│ └── {new-auth-type}.jsx +├── wrap-components/ # Component wrappers +│ ├── info.jsx +│ ├── license.jsx +│ └── {existing-component}.jsx +└── json-schema-{VERSION}-extensions/ # If new JSON Schema version + ├── fn.js + ├── components/keywords/ + └── wrap-components/keywords/ +``` + +**Create the directories:** +```bash +mkdir -p src/core/plugins/oas{VERSION_NUMBER}/{auth-extensions,oas{PREVIOUS_VERSION}-extensions,spec-extensions,components/auth,wrap-components} +``` + +### Step 3: Implement Version Detection (fn.js) + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/fn.js` + +**Template:** +```javascript +/** + * @prettier + */ + +/** + * Detects if a spec is OpenAPI {MAJOR}.{MINOR}.x + */ +export const isOAS{VERSION} = (jsSpec) => { + const oasVersion = jsSpec.get("openapi") + return ( + typeof oasVersion === "string" && + /^{MAJOR}\.{MINOR}\.(?:[1-9]\d*|0)$/.test(oasVersion) + ) +} + +/** + * Creates a selector that only returns a value for OAS {VERSION} specs + */ +export const createOnlyOAS{VERSION}Selector = + (selector) => + (state, ...args) => + (system) => { + if (system.getSystem().specSelectors.isOAS{VERSION}()) { + const selectedValue = selector(state, ...args) + return typeof selectedValue === "function" + ? selectedValue(system) + : selectedValue + } else { + return null + } + } + +/** + * Creates a selector wrapper that uses new impl for OAS {VERSION}, falls back to original + */ +export const createOnlyOAS{VERSION}SelectorWrapper = + (selector) => + (oriSelector, system) => + (state, ...args) => { + if (system.getSystem().specSelectors.isOAS{VERSION}()) { + const selectedValue = selector(state, ...args) + return typeof selectedValue === "function" + ? selectedValue(oriSelector, system) + : selectedValue + } else { + return oriSelector(...args) + } + } + +/** + * Creates a system-aware selector + */ +export const createSystemSelector = + (selector) => + (state, ...args) => + (system) => { + const selectedValue = selector(state, system, ...args) + return typeof selectedValue === "function" + ? selectedValue(system) + : selectedValue + } + +/** + * Creates a component wrapper that only renders for OAS {VERSION}. + * When active, passes `originalComponent` (the unwrapped original) and + * `getSystem` as extra props. Access the original via: + * const { originalComponent: Ori } = props + * NOT via `props.Ori` — that's the OAS3/OAS30ComponentWrapFactory convention. + */ +export const createOnlyOAS{VERSION}ComponentWrapper = + (Component) => (Original, system) => (props) => { + if (system.specSelectors.isOAS{VERSION}()) { + return ( + + ) + } + + return + } + +/** + * Wraps functions to conditionally use OAS {VERSION} implementations + */ +export const wrapOAS{VERSION}Fn = (fn, system) => { + const { fn: systemFn, specSelectors } = system + return Object.fromEntries( + Object.entries(fn).map(([name, newImpl]) => { + const oriImpl = systemFn[name] + const impl = (...args) => + specSelectors.isOAS{VERSION}() + ? newImpl(...args) + : typeof oriImpl === "function" + ? oriImpl(...args) + : undefined + return [name, impl] + }) + ) +} +``` + +### Step 4: Create Main Plugin Export (index.js) + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/index.js` + +**Template:** +```javascript +/** + * @prettier + */ + +import afterLoad from "./after-load.js" +import { + isOAS{VERSION}, + createSystemSelector, + createOnlyOAS{VERSION}Selector, + createOnlyOAS{VERSION}SelectorWrapper, + createOnlyOAS{VERSION}ComponentWrapper, +} from "./fn.js" + +// Import spec extensions +import * as specExtensionsSelectors from "./spec-extensions/selectors.js" +import * as specExtensionsWrapSelectors from "./spec-extensions/wrap-selectors.js" + +// Import auth extensions +import * as authExtensionsWrapSelectors from "./auth-extensions/wrap-selectors.js" + +// Import OAS{PREVIOUS_VERSION} extensions +import * as oas{PREVIOUS_VERSION}ExtensionsFn from "./oas{PREVIOUS_VERSION}-extensions/fn.js" + +// Import components +import VersionPragmaFilter from "./components/version-pragma-filter.jsx" +// Import new feature components here + +// Import wrap components +import InfoWrapper from "./wrap-components/info.jsx" +// Import other wrappers here + +/** + * OpenAPI {MAJOR}.{MINOR} Plugin + * + * Adds support for OpenAPI Specification {MAJOR}.{MINOR}.x + * + * This plugin should be loaded AFTER: + * - oas{PREVIOUS_VERSION} plugin + * - json-schema-{JSON_SCHEMA_VERSION} plugin (if applicable) + * + * It wraps and overrides components/selectors from previous versions. + */ +const OpenAPI{VERSION}Plugin = ({ fn }) => { + return { + fn: { + oas{VERSION_NUMBER}: { + isOAS{VERSION}, + createSystemSelector, + createOnlyOAS{VERSION}Selector, + createOnlyOAS{VERSION}SelectorWrapper, + createOnlyOAS{VERSION}ComponentWrapper, + }, + }, + components: { + // New components + OAS{VERSION}VersionPragmaFilter: VersionPragmaFilter, + // Add new feature components here + }, + wrapComponents: { + // Wrapped components + VersionPragmaFilter: (Ori, system) => + system.specSelectors.isOAS{VERSION}() + ? system.getComponent("OAS{VERSION}VersionPragmaFilter", true) + : Ori, + InfoContainer: InfoWrapper, + // Add other wrappers here + }, + statePlugins: { + spec: { + selectors: { + // Add version detection selector + isOAS{VERSION}: createSystemSelector(specExtensionsSelectors.selectIsOAS{VERSION}), + // Add new feature selectors here + }, + wrapSelectors: { + // Wrap previous version selectors if needed + ...specExtensionsWrapSelectors, + }, + }, + auth: { + wrapSelectors: { + // Add auth extensions if needed + ...authExtensionsWrapSelectors, + }, + }, + oas{VERSION_NUMBER}: { + selectors: { + // Plugin-specific selectors + }, + }, + }, + afterLoad, + } +} + +export default OpenAPI{VERSION}Plugin +``` + +### Step 5: Implement Spec Extensions + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/spec-extensions/selectors.js` + +**Template:** +```javascript +/** + * @prettier + */ + +import { createSelector } from "reselect" +import { isOAS{VERSION} } from "../fn.js" + +/** + * Detects if the current spec is OAS {VERSION} + */ +export const selectIsOAS{VERSION} = (state, system) => () => { + const spec = system.specSelectors.specJson() + return isOAS{VERSION}(spec) +} + +// Add selectors for new OAS {VERSION} fields here +// Example: +// export const selectNewField = createSelector( +// (state) => state, +// (state) => { +// const spec = state.getIn(["spec", "json"]) +// return spec.get("newField") +// } +// ) +``` + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/spec-extensions/wrap-selectors.js` + +**⚠️ Minor version: DON'T wrap `isOAS{PREV}` unless the regex actually matches both versions.** For example, OAS 3.1's `isOAS31` regex (`/^3\.1\./`) will never match `3.2.x`, so wrapping it to return `false` is dead code. Only add the `isOAS{PREV}` override if the previous version's detection regex would incorrectly match the new version. + +**Template:** +```javascript +/** + * @prettier + */ + +import { createOnlyOAS{VERSION}SelectorWrapper } from "../fn.js" + +// Ensure OAS {VERSION} specs are recognized as OAS 3.x (needed when major version number didn't change) +export const isOAS3 = + (oriSelector, system) => + (state, ...args) => { + const isOAS{VERSION} = system.specSelectors.isOAS{VERSION}() + return isOAS{VERSION} || oriSelector(...args) + } + +// ONLY add isOAS{PREV} wrapper if the previous version's regex could match this new version. +// For a minor version bump (e.g. 3.1 → 3.2), the previous regex won't match, so DON'T add this. +// export const isOAS{PREV} = createOnlyOAS{VERSION}SelectorWrapper((state) => () => false) +``` + +### Step 6: Create Version Pragma Filter Component + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/components/version-pragma-filter.jsx` + +**Template:** +```javascript +/** + * @prettier + */ + +import React from "react" +import PropTypes from "prop-types" + +const OAS{VERSION}VersionPragmaFilter = ({ bypass, isSwagger2, isOAS3, isOAS{PREVIOUS_VERSION}, isOAS{VERSION} }) => { + // Handle version detection logic + const isAmbiguous = + (isSwagger2 && isOAS3) || + (isSwagger2 && isOAS{PREVIOUS_VERSION}) || + (isSwagger2 && isOAS{VERSION}) || + (isOAS3 && isOAS{PREVIOUS_VERSION}) || + (isOAS3 && isOAS{VERSION}) || + (isOAS{PREVIOUS_VERSION} && isOAS{VERSION}) + + const isMissing = !isSwagger2 && !isOAS3 && !isOAS{PREVIOUS_VERSION} && !isOAS{VERSION} + + if (bypass) { + return null + } + + if (isAmbiguous) { + return ( +
+
+
+

Unable to render this document, as it contains multiple OpenAPI version fields.

+
+
+
+ ) + } + + if (isMissing) { + return ( +
+
+
+

Unable to render this document, as it doesn't contain an OpenAPI version field.

+
+
+
+ ) + } + + return null +} + +OAS{VERSION}VersionPragmaFilter.propTypes = { + bypass: PropTypes.bool, + isSwagger2: PropTypes.bool.isRequired, + isOAS3: PropTypes.bool.isRequired, + isOAS{PREVIOUS_VERSION}: PropTypes.bool.isRequired, + isOAS{VERSION}: PropTypes.bool.isRequired, +} + +OAS{VERSION}VersionPragmaFilter.defaultProps = { + bypass: false, +} + +export default OAS{VERSION}VersionPragmaFilter +``` + +### Step 7: Implement New Feature Components + +For each new feature in the OAS specification: + +**Example: New Feature Component** +```javascript +/** + * @prettier + */ + +import React from "react" +import PropTypes from "prop-types" + +const NewFeature = ({ getComponent, specSelectors }) => { + const newFeature = specSelectors.selectNewFeature() + + if (!newFeature || !newFeature.size) { + return null + } + + // Render the new feature + return ( +
+

New Feature

+ {/* Render logic here */} +
+ ) +} + +NewFeature.propTypes = { + specSelectors: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, +} + +export default NewFeature +``` + +### Step 8: Create Component Wrappers + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/wrap-components/info.jsx` + +**⚠️ Minor version (e.g. 3.2): don't create a new Info component.** If the Info object is identical to the previous version, reuse `OAS{PREV_VERSION}Info` via `getComponent` instead of copying the whole component. Only create a new Info component for major versions with significant structural changes. + +**Template (minor version — reuse previous Info):** +```javascript +/** + * @prettier + */ + +import React from "react" +import { createOnlyOAS{VERSION}ComponentWrapper } from "../fn.js" + +const InfoWrapper = createOnlyOAS{VERSION}ComponentWrapper(({ getSystem }) => { + const system = getSystem() + const OAS{PREV_VERSION}Info = system.getComponent("OAS{PREV_VERSION}Info", true) + return +}) + +export default InfoWrapper +``` + +**Also — OpenAPIVersion wrapper (minor version — change version badge only):** + +For minor versions where the only Info difference is the version badge, use the OpenAPIVersion wrapper pattern instead of wrapping InfoContainer at all: + +```javascript +// wrap-components/openapi-version.jsx +/** + * @prettier + */ +import React from "react" +import { createOnlyOAS{VERSION}ComponentWrapper } from "../fn.js" + +export default createOnlyOAS{VERSION}ComponentWrapper((props) => { + const { originalComponent: Ori } = props // NOT `Ori` from props directly — use `originalComponent` + return +}) +``` + +Then register it in index.js: +```javascript +wrapComponents: { + OpenAPIVersion: OpenAPIVersionWrapper, // changes the version badge shown in the Info header +} +``` + +### Step 9: Implement afterLoad Hook + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/after-load.js` + +**Template:** +```javascript +/** + * @prettier + */ + +import { wrapOAS{VERSION}Fn } from "./fn.js" + +/** + * afterLoad hook for OAS {VERSION} plugin + * + * This hook runs after all plugins are loaded and allows + * modification of the system's functions and behaviors. + */ +function afterLoad({ fn, getSystem }) { + const system = getSystem() + + // Override functions that differ in OAS {VERSION} + if (typeof fn.sampleFromSchema === "function") { + // Example: Wrap sample generation if JSON Schema version changed + // const wrappedFns = wrapOAS{VERSION}Fn({ + // sampleFromSchema: fn.jsonSchema{VERSION}.sampleFromSchema, + // }, system) + // Object.assign(this.fn, wrappedFns) + } + + // Override other functions as needed + // Example: File upload detection + // if (typeof fn.isFileUploadIntended === "function") { + // const wrappedFns = wrapOAS{VERSION}Fn({ + // isFileUploadIntended: makeIsFileUploadIntended(system), + // }, system) + // Object.assign(this.fn, wrappedFns) + // } +} + +export default afterLoad +``` + +### Step 10: Handle Authentication Changes + +If new authentication types are introduced: + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/components/auth/{auth-type}.jsx` + +**Template:** +```javascript +/** + * @prettier + */ + +import React from "react" +import PropTypes from "prop-types" + +const NewAuthType = ({ schema, name, getComponent, onChange, authorized }) => { + const authValue = authorized && authorized.getIn([name]) + + const Input = getComponent("Input") + const Button = getComponent("Button") + + // Render auth UI + return ( +
+

{schema.get("description") || `${name} (${schema.get("type")})`}

+ {/* Auth UI here */} +
+ ) +} + +NewAuthType.propTypes = { + schema: PropTypes.object.isRequired, + name: PropTypes.string.isRequired, + getComponent: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + authorized: PropTypes.object, +} + +export default NewAuthType +``` + +**File:** `src/core/plugins/oas{VERSION_NUMBER}/auth-extensions/wrap-selectors.js` + +**Template:** +```javascript +/** + * @prettier + */ + +import { createOnlyOAS{VERSION}SelectorWrapper } from "../fn.js" +import { Map } from "immutable" + +/** + * Wraps definitionsToAuthorize to include new auth types + */ +export const definitionsToAuthorize = createOnlyOAS{VERSION}SelectorWrapper( + (state, system) => + (oriSelector) => { + const definitions = oriSelector() + const securityDefinitions = system.specSelectors.securityDefinitions() || Map() + + // Add new auth types to definitions + return definitions.map((definition) => { + const schema = securityDefinitions.get(definition.get("name")) + if (schema && schema.get("type") === "newAuthType") { + // Add new auth type logic + return definition.set("newAuthType", true) + } + return definition + }) + } +) +``` + +### Step 11: Register Plugin in Preset + +**File:** `src/core/presets/apis/index.js` + +**Add import:** +```javascript +import OpenAPI{VERSION}Plugin from "../../plugins/oas{VERSION_NUMBER}/index.js" +``` + +**Add to preset array (IMPORTANT: add at the end):** +```javascript +export default function PresetApis() { + return [ + BasePreset, + OpenAPI30Plugin, + // ... other plugins + OpenAPI{PREVIOUS_VERSION}Plugin, + OpenAPI{VERSION}Plugin, // Load LAST to override previous versions + ] +} +``` + +**Why load last?** +- Wrap-components override previous versions +- Wrap-selectors intercept earlier selectors +- afterLoad modifications apply to dependencies + +### Step 12: Add Unit Tests + +**Directory:** `test/unit/core/plugins/oas{VERSION_NUMBER}/` + +**File:** `test/unit/core/plugins/oas{VERSION_NUMBER}/fn.js` + +**Template:** +```javascript +/** + * @prettier + */ + +import { fromJS } from "immutable" +import { isOAS{VERSION} } from "src/core/plugins/oas{VERSION_NUMBER}/fn.js" + +describe("oas{VERSION_NUMBER} plugin - fn - isOAS{VERSION}", () => { + it("should match OpenAPI {MAJOR}.{MINOR}.0", () => { + const spec = fromJS({ openapi: "{MAJOR}.{MINOR}.0" }) + expect(isOAS{VERSION}(spec)).toBe(true) + }) + + it("should match OpenAPI {MAJOR}.{MINOR}.1", () => { + const spec = fromJS({ openapi: "{MAJOR}.{MINOR}.1" }) + expect(isOAS{VERSION}(spec)).toBe(true) + }) + + it("should match OpenAPI {MAJOR}.{MINOR}.25", () => { + const spec = fromJS({ openapi: "{MAJOR}.{MINOR}.25" }) + expect(isOAS{VERSION}(spec)).toBe(true) + }) + + it("should NOT match OpenAPI {MAJOR}.{MINOR}", () => { + const spec = fromJS({ openapi: "{MAJOR}.{MINOR}" }) + expect(isOAS{VERSION}(spec)).toBe(false) + }) + + it("should NOT match OpenAPI {MAJOR}.{MINOR}.01 (leading zero)", () => { + const spec = fromJS({ openapi: "{MAJOR}.{MINOR}.01" }) + expect(isOAS{VERSION}(spec)).toBe(false) + }) + + it("should NOT match OpenAPI {DIFFERENT_MAJOR}.{DIFFERENT_MINOR}.0", () => { + const spec = fromJS({ openapi: "{DIFFERENT_MAJOR}.{DIFFERENT_MINOR}.0" }) + expect(isOAS{VERSION}(spec)).toBe(false) + }) + + it("should NOT match swagger: 2.0", () => { + const spec = fromJS({ swagger: "2.0" }) + expect(isOAS{VERSION}(spec)).toBe(false) + }) + + it("should handle null spec", () => { + const spec = fromJS({}) + expect(isOAS{VERSION}(spec)).toBe(false) + }) +}) +``` + +**File:** `test/unit/core/plugins/oas{VERSION_NUMBER}/components/version-pragma-filter.jsx` + +**Template:** +```javascript +/** + * @prettier + */ + +import React from "react" +import { shallow } from "enzyme" +import OAS{VERSION}VersionPragmaFilter from "src/core/plugins/oas{VERSION_NUMBER}/components/version-pragma-filter.jsx" + +describe("OAS{VERSION}VersionPragmaFilter", () => { + it("should render nothing when bypass is true", () => { + const wrapper = shallow( + + ) + expect(wrapper.type()).toBe(null) + }) + + it("should render nothing when version is valid", () => { + const wrapper = shallow( + + ) + expect(wrapper.type()).toBe(null) + }) + + it("should render error when version is ambiguous", () => { + const wrapper = shallow( + + ) + expect(wrapper.find(".version-pragma__message--ambiguous")).toHaveLength(1) + }) + + it("should render error when version is missing", () => { + const wrapper = shallow( + + ) + expect(wrapper.find(".version-pragma__message--missing")).toHaveLength(1) + }) +}) +``` + +### Step 13: Add E2E Tests + +**Directory:** `test/e2e-cypress/e2e/features/oas{VERSION_NUMBER}/` + +**Create test spec file:** +```javascript +/** + * @prettier + */ + +describe("OpenAPI {MAJOR}.{MINOR} features", () => { + it("should detect OAS {VERSION} version", () => { + cy.visit("/oas{VERSION_NUMBER}-spec.html") + cy.get(".information-container .version").should("contain", "{MAJOR}.{MINOR}.0") + }) + + // Add tests for new features + it("should render new feature X", () => { + cy.visit("/oas{VERSION_NUMBER}-spec.html") + cy.get(".opblock-tag-section").should("contain", "New Feature") + }) +}) +``` + +**Add test fixtures:** +- Create sample OAS {VERSION} spec in `test/e2e-cypress/static/documents/oas{VERSION_NUMBER}/` +- Create HTML fixture in `test/e2e-cypress/static/` referencing the spec + +### Step 14: Update Documentation + +**Files to update:** + +1. **README.md** - Add OAS {VERSION} to supported versions +2. **CLAUDE.md** - Add plugin to list and update version compatibility +3. **docs/usage/version-detection.md** - Document new version detection +4. **package.json** - Update description if needed + +**Example README.md update:** +```markdown +### OpenAPI Specification Compatibility + +- **OpenAPI 2.0** (Swagger) +- **OpenAPI 3.0.x** +- **OpenAPI 3.1.x** +- **OpenAPI {MAJOR}.{MINOR}.x** ⭐ NEW +``` + +### Step 15: Verify Build + +**Run full build:** +```bash +npm run clean +npm run build +``` + +**Check dist/ output:** +- `dist/swagger-ui.js` - Should include new plugin +- `dist/swagger-ui.css` - Should include new styles (if any) +- Bundle size should be reasonable + +**Run artifact tests:** +```bash +npm run test:artifact +``` + +### Step 16: Run Full Test Suite + +```bash +npm run lint-errors # ESLint errors only +npm run test:unit # Jest unit tests +npm run cy:ci # Cypress E2E tests +``` + +**All tests must pass before submitting PR.** + +### Step 17: Create Pull Request + +**Commit message format:** +``` +feat(oas{VERSION_NUMBER}): add support for OpenAPI {MAJOR}.{MINOR} + +Implement OpenAPI {MAJOR}.{MINOR}.x specification support with: +- Version detection and plugin architecture +- New feature X rendering +- New authentication type Y support +- JSON Schema {VERSION} integration (if applicable) + +Closes #{ISSUE_NUMBER} +``` + +**PR checklist:** +- [ ] All tests passing +- [ ] New feature components implemented +- [ ] Unit tests added +- [ ] E2E tests added +- [ ] Documentation updated +- [ ] Build completes successfully +- [ ] No ESLint errors +- [ ] Follows existing code patterns + +## Common Pitfalls + +1. **Don't forget @prettier pragma** - Add to all new files +2. **Plugin loading order matters** - Load new plugin LAST in preset +3. **Use double quotes** - Project convention +4. **No semicolons** - Project convention +5. **Always use DOMPurify** - For any user-provided HTML +6. **Test with real specs** - Use actual OAS {VERSION} examples +7. **Don't modify core unnecessarily** - Use plugin architecture +8. **Version regex must be exact** - Follow pattern from OAS 3.1 +9. **Component wrappers render ``** - When not active version (handled by `createOnlyOAS{VERSION}ComponentWrapper` automatically) +10. **afterLoad runs after all plugins** - Safe to modify system +11. **`originalComponent` not `Ori`** - `createOnlyOAS{VERSION}ComponentWrapper` passes the original as `originalComponent` prop. Use `const { originalComponent: Ori } = props`. The `Ori` name comes from `OAS30ComponentWrapFactory` which uses a different signature. +12. **Don't copy the entire Info component for minor versions** - Use the OpenAPIVersion wrapper to change the version badge, and reuse the previous version's Info via `getComponent("OAS{PREV}Info", true)` instead of copying. +13. **Don't add version-specific logic to previous version's plugin** - OAS32 logic belongs in the OAS32 plugin, not in OAS31 plugin files (e.g. don't add `isOAS32` checks to `oas31/wrap-components/license.jsx`). +20. **Always use `createOnlyOAS{VERSION}ComponentWrapper` in wrap-components — never inline the version guard** - Each wrap-component in a plugin should use the factory (`createOnlyOAS{VERSION}ComponentWrapper`), not a hand-written `(Original, system) => (props) => { if (system.specSelectors.isOAS{VERSION}()) ... }`. When a later version (OAS32) also needs to reuse the same OAS31 component, it gets its own dedicated wrap-component in the OAS32 plugin — don't expand the guard to `isOAS31 || isOAS32` in the OAS31 file. The OAS31 wrappers for contact/license should use `createOnlyOAS31ComponentWrapper` and nothing else; OAS32 contact/license wrappers live in `oas32/wrap-components/` and handle the OAS32 case. +14. **Don't add `isOAS{PREV}` wrap-selector unless the regex actually overlaps** - For minor versions, the previous version's regex already won't match (e.g. OAS31's `/^3\.1\./` won't match `3.2.x`). Adding the wrapper is dead code. +15. **New HTTP methods: use `OPERATION_METHODS` in `spec/selectors.js`, not `operationsWithRootInherited` wrapper** - Adding the method to `OPERATION_METHODS` makes the core `operations` selector collect those ops. The `validOperationMethods` wrapper (guarded by `createOnlyOAS{VERSION}SelectorWrapper`) then controls whether the UI renders them. No need for an `operationsWithRootInherited` wrapper. +16. **Don't add new HTTP methods to the core `validOperationMethods` constant** - Adding `"query"` to the base constant in `spec/selectors.js` affects ALL OAS versions. Instead, add it only via `createOnlyOAS{VERSION}SelectorWrapper` in your plugin's `spec-extensions/wrap-selectors.js`. +17. **Don't create selectors only used in tests** - Selectors like `selectHasQueryOperations` that are never called from production code should not exist. Remove them. +18. **OAS meta-schema URL ≠ new JSON Schema dialect** - The URL `https://spec.openapis.org/oas/3.2/schema/...` is the OAS 3.2 document structure schema, NOT a new JSON Schema version. OAS 3.2 uses JSON Schema 2020-12, the same as OAS 3.1. Don't list "new JSON Schema version" as a feature unless it actually changes. +19. **Verify "not yet implemented" feature list against previous version** - Features like `pathItems in Components` were already in OAS 3.1, not new in 3.2. Check what's actually new before listing it. + +## JSON Schema Version Changes + +**⚠️ First, verify whether the JSON Schema version actually changed.** The OAS meta-schema URL (e.g. `https://spec.openapis.org/oas/3.2/schema/...`) describes the OAS document structure — it is NOT a new JSON Schema dialect. OAS 3.2 uses JSON Schema 2020-12, the same as OAS 3.1. Only create a new `json-schema-{VERSION}` plugin if the actual JSON Schema dialect changed (as it did from OAS 3.0 Draft-07 → OAS 3.1 JSON Schema 2020-12). + +If the new OAS version uses a different JSON Schema version: + +1. **Create json-schema-{VERSION} plugin** (separate from OAS plugin) +2. **Implement keyword components** for new schema features +3. **Update sample generation** logic +4. **Add schema validation** support +5. **Load json-schema-{VERSION} plugin BEFORE oas{VERSION_NUMBER}** in preset + +Example from OAS 3.1 using JSON Schema 2020-12: +- `src/core/plugins/json-schema-2020-12/` - Main plugin +- `src/core/plugins/json-schema-2020-12-samples/` - Sample generation +- Loaded before `oas31` plugin in preset + +## Major vs Minor Version Considerations + +### Major Version (e.g., 4.0) + +**Likely includes:** +- Breaking changes from previous major version +- New top-level fields +- Removed/deprecated features +- Possibly new JSON Schema version +- New authentication paradigms +- Restructured spec format + +**Implementation approach:** +- Create completely separate plugin +- May need to create new base components +- Extensive wrapping of previous version components +- Significant selector overrides +- New preset configuration options + +### Minor Version (e.g., 3.2) + +**Likely includes:** +- Backward-compatible additions +- New optional fields +- Enhanced existing features +- Same JSON Schema version (verify before assuming it changed) +- Incremental improvements + +**Implementation approach:** +- Lighter plugin with focused additions +- Fewer component wrappers needed +- Selective selector additions +- Reuse most of previous version logic via `getComponent("OAS{PREV}...", true)` +- Simpler afterLoad modifications + +**Key decisions for minor versions (lessons from OAS 3.2):** + +1. **Version badge only changed?** Use `OpenAPIVersion` wrapper + reuse `OAS{PREV}Info` — don't create a new Info component. +2. **New HTTP method (e.g. QUERY)?** Add it to `OPERATION_METHODS` in `src/core/plugins/spec/selectors.js` AND add it to `validOperationMethods` via `createOnlyOAS{VERSION}SelectorWrapper`. Don't add it to the core `validOperationMethods` constant (affects all versions). +3. **`isOAS{PREV}` wrapper needed?** Only if the previous regex also matches the new version string. Minor version bumps (3.1 → 3.2) don't need it. +4. **JSON Schema version comment?** Verify it actually changed. OAS 3.2 uses JSON Schema 2020-12, the same as OAS 3.1. The new OAS meta-schema URL (`https://spec.openapis.org/oas/3.2/schema/...`) describes OAS document structure, not a new JSON Schema dialect. +5. **"Not yet implemented" list?** Double-check each item against the *previous* version's changelog — some may already be implemented. + +## Example: Adding OAS 4.0 (Major) + +**Version detection:** +```javascript +export const isOAS40 = (jsSpec) => { + const oasVersion = jsSpec.get("openapi") + return ( + typeof oasVersion === "string" && /^4\.0\.(?:[1-9]\d*|0)$/.test(oasVersion) + ) +} +``` + +**Directory:** `src/core/plugins/oas40/` + +**Wrap previous version:** +```javascript +// spec-extensions/wrap-selectors.js +export const isOAS31 = createOnlyOAS40SelectorWrapper((state) => () => false) +export const isOAS3 = createOnlyOAS40SelectorWrapper((state) => () => false) +``` + +**Register in preset:** +```javascript +// src/core/presets/apis/index.js +export default function PresetApis() { + return [ + BasePreset, + OpenAPI30Plugin, + OpenAPI31Plugin, + OpenAPI40Plugin, // LAST + ] +} +``` + +## Example: Adding OAS 3.2 (Minor) + +**Version detection:** +```javascript +export const isOAS32 = (jsSpec) => { + const oasVersion = jsSpec.get("openapi") + return ( + typeof oasVersion === "string" && /^3\.2\.(?:[1-9]\d*|0)$/.test(oasVersion) + ) +} +``` + +**Directory:** `src/core/plugins/oas32/` + +**Lighter component wrapping — OAS 3.2 specific patterns:** + +```javascript +// wrap-components/openapi-version.jsx — change the version badge only +// Use `originalComponent` (NOT `Ori`) — that's what createOnlyOAS32ComponentWrapper passes +export default createOnlyOAS32ComponentWrapper((props) => { + const { originalComponent: Ori } = props + return +}) + +// wrap-components/info.jsx — reuse OAS31Info, don't create a new one +export default createOnlyOAS32ComponentWrapper(({ getSystem }) => { + const system = getSystem() + const OAS31Info = system.getComponent("OAS31Info", true) + return +}) + +// wrap-components/contact.jsx and license.jsx — same pattern (OAS32 logic belongs HERE, not in OAS31) +export default createOnlyOAS32ComponentWrapper((props) => { + const { getSystem } = props + const system = getSystem() + const OAS31Contact = system.getComponent("OAS31Contact", true) + return +}) +``` + +**New HTTP method (QUERY) — two-part approach:** + +Part 1: Add to `OPERATION_METHODS` in `src/core/plugins/spec/selectors.js` so the `operations` selector collects QUERY ops: +```javascript +// spec/selectors.js +export const OPERATION_METHODS = [ + "get", "put", "post", "delete", "options", "head", "patch", "trace", "query", +] +``` + +Part 2: Add to `validOperationMethods` via `createOnlyOAS32SelectorWrapper` so the UI only renders them for OAS 3.2: +```javascript +// oas32/spec-extensions/wrap-selectors.js +export const validOperationMethods = createOnlyOAS32SelectorWrapper( + () => (oriSelector, system) => system.oas32Selectors.validOperationMethods() +) + +// oas32/selectors.js +export const validOperationMethods = () => [ + "get", "put", "post", "delete", "options", "head", "patch", "trace", "query", +] +``` + +Do NOT add `"query"` to the core `validOperationMethods` constant — that would affect all versions. + +## Final Checklist + +Before marking the task as complete: + +- [ ] Plugin created in `src/core/plugins/oas{VERSION_NUMBER}/` +- [ ] Version detection function implemented and tested +- [ ] Selector factories created +- [ ] New feature components implemented +- [ ] Component wrappers created for modified components +- [ ] Spec extension selectors implemented +- [ ] Auth extensions implemented (if new auth types) +- [ ] afterLoad hook implemented +- [ ] Plugin registered in preset (LAST position) +- [ ] Unit tests added and passing +- [ ] E2E tests added and passing +- [ ] Documentation updated (README, CLAUDE.md, docs/) +- [ ] Build completes successfully +- [ ] Artifact tests pass +- [ ] No ESLint errors +- [ ] Code follows project conventions (no semicolons, double quotes, @prettier pragma) +- [ ] All security considerations addressed (DOMPurify, input validation) +- [ ] PR created with proper commit message format + +## Practical Workflow: From Spec to Components + +This section demonstrates the complete workflow from specification analysis to component implementation. + +### Workflow Example: Adding a Hypothetical `workflows` Field + +**Scenario:** OAS 4.0 introduces a new `workflows` top-level field for defining reusable workflow definitions. + +#### Phase 1: Specification Research + +**Step 1: Fetch the spec section** +```javascript +WebFetch( + "https://spec.openapis.org/oas/v4.0.0/", + "Describe the 'workflows' field: its structure, purpose, whether it's required, and provide a complete example from the specification." +) +``` + +**Step 2: Analyze the structure** +```javascript +// Response analysis: +// - Type: Object (Map[string, Workflow Object]) +// - Location: Root level (sibling to 'paths') +// - Required: No (optional) +// - Structure: Similar to paths, but defines reusable workflows +// - Example: +workflows: + onNewUser: + steps: + - operationRef: '#/paths/~1users/post' + - operationRef: '#/paths/~1emails~1welcome/post' +``` + +**Step 3: Map to component type** +Using the mapping table from Step 1D: +- Change type: New top-level field (Object) +- Component type: New standalone component +- Similar to: `webhooks` in OAS 3.1 + +#### Phase 2: Component Design + +**File structure decision:** +``` +src/core/plugins/oas40/ +├── components/ +│ └── workflows.jsx # Main component +└── spec-extensions/ + └── selectors.js # Add selectWorkflows +``` + +**Component responsibilities:** +1. Select workflows from spec +2. Render workflow list +3. Display workflow steps +4. Link to referenced operations + +#### Phase 3: Implementation + +**Step 1: Create selector** +```javascript +// src/core/plugins/oas40/spec-extensions/selectors.js + +export const selectWorkflows = createSelector( + (state) => state, + (state) => { + const spec = state.getIn(["spec", "json"]) + return spec.get("workflows") || Map() + } +) +``` + +**Step 2: Create component** +```javascript +// src/core/plugins/oas40/components/workflows.jsx + +/** + * @prettier + */ + +import React from "react" +import PropTypes from "prop-types" +import ImPropTypes from "react-immutable-proptypes" + +const Workflows = ({ specSelectors, getComponent }) => { + const workflows = specSelectors.workflows() + + if (!workflows || !workflows.size) { + return null + } + + const Collapse = getComponent("Collapse") + + return ( +
+

Workflows

+
+

+ Reusable workflow definitions that can be referenced from operations. +

+
+ {workflows.entrySeq().map(([workflowName, workflow]) => ( +
+ +
+

{workflowName}

+ {workflow.get("description") && ( +
+ {workflow.get("description")} +
+ )} +
+
+ {workflow.get("steps") && ( +
    + {workflow.get("steps").map((step, idx) => ( +
  • + {step.get("operationRef") || step.get("workflowRef")} +
  • + ))} +
+ )} +
+
+
+ ))} +
+ ) +} + +Workflows.propTypes = { + specSelectors: PropTypes.shape({ + workflows: PropTypes.func.isRequired, + }).isRequired, + getComponent: PropTypes.func.isRequired, +} + +export default Workflows +``` + +**Step 3: Register in plugin** +```javascript +// src/core/plugins/oas40/index.js + +import Workflows from "./components/workflows.jsx" +import { selectWorkflows } from "./spec-extensions/selectors.js" + +const OpenAPI40Plugin = ({ fn }) => { + return { + components: { + Workflows, + }, + statePlugins: { + spec: { + selectors: { + workflows: createOnlyOAS40Selector(selectWorkflows), + }, + }, + }, + } +} +``` + +**Step 4: Add to layout** +```javascript +// Modify layout to include Workflows component +// This would be done in BaseLayout or a wrapper +``` + +#### Phase 4: Testing + +**Step 1: Create test spec** +```yaml +# test/e2e-cypress/static/documents/oas40/workflows-example.yaml +openapi: 4.0.0 +info: + title: Workflows API + version: 1.0.0 +workflows: + onNewUser: + description: Workflow triggered when a new user is created + steps: + - operationRef: '#/paths/~1users/post' + - operationRef: '#/paths/~1emails~1welcome/post' +paths: + /users: + post: + summary: Create user + # ... +``` + +**Step 2: Create E2E test** +```javascript +// test/e2e-cypress/e2e/features/oas40/workflows.cy.js + +describe("OAS 4.0 - Workflows", () => { + beforeEach(() => { + cy.visit("/e2e-cypress/static/documents/oas40/workflows-example.yaml") + }) + + it("should render workflows section", () => { + cy.get(".opblock-tag").contains("Workflows").should("exist") + }) + + it("should display workflow definitions", () => { + cy.get(".opblock-workflow").should("have.length.greaterThan", 0) + }) + + it("should show workflow steps", () => { + cy.get(".opblock-workflow-steps ul li").should("exist") + }) +}) +``` + +**Step 3: Create unit test** +```javascript +// test/unit/core/plugins/oas40/components/workflows.jsx + +import React from "react" +import { shallow } from "enzyme" +import { fromJS } from "immutable" +import Workflows from "src/core/plugins/oas40/components/workflows.jsx" + +describe("Workflows component", () => { + it("should render workflows from spec", () => { + const mockWorkflows = fromJS({ + onNewUser: { + description: "New user workflow", + steps: [ + { operationRef: "#/paths/~1users/post" } + ] + } + }) + + const props = { + specSelectors: { + workflows: () => mockWorkflows + }, + getComponent: (name) => { + if (name === "Collapse") { + return ({ children }) =>
{children}
+ } + } + } + + const wrapper = shallow() + expect(wrapper.find(".opblock-workflow")).toHaveLength(1) + }) + + it("should render nothing when no workflows", () => { + const props = { + specSelectors: { + workflows: () => fromJS({}) + }, + getComponent: () => null + } + + const wrapper = shallow() + expect(wrapper.type()).toBe(null) + }) +}) +``` + +### Summary: Spec → Component Mapping Checklist + +For each new feature in the specification: + +- [ ] **Analyze spec using WebFetch** + - [ ] Understand structure and purpose + - [ ] Get examples from spec + - [ ] Identify data type and location + +- [ ] **Map to component type** (using Step 1D table) + - [ ] Determine if new component or wrapper needed + - [ ] Identify similar existing components + - [ ] Plan component hierarchy + +- [ ] **Create selector** + - [ ] Extract data from spec + - [ ] Use appropriate selector factory + - [ ] Add to spec-extensions/selectors.js + +- [ ] **Implement component** + - [ ] Follow existing patterns + - [ ] Use getComponent for sub-components + - [ ] Add PropTypes validation + - [ ] Include @prettier pragma + +- [ ] **Register in plugin** + - [ ] Add to components export + - [ ] Register selector in statePlugins + - [ ] Add to layout if top-level feature + +- [ ] **Create tests** + - [ ] Unit tests for component + - [ ] Unit tests for selector + - [ ] E2E tests with example spec + - [ ] Test edge cases (missing data, etc.) + +- [ ] **Verify with real spec** + - [ ] Test with example from official spec + - [ ] Test edge cases mentioned in spec + - [ ] Verify rendering matches expected behavior + +## Resources + +- **OAS Specification:** https://spec.openapis.org/ +- **OAS 2.0:** https://spec.openapis.org/oas/v2.0/ +- **OAS 3.0:** https://spec.openapis.org/oas/v3.0.3/ +- **OAS 3.1:** https://spec.openapis.org/oas/v3.1.0/ +- **JSON Schema:** https://json-schema.org/ +- **Plugin API Docs:** `docs/customization/plugin-api.md` +- **OAS 3.1 Reference:** `src/core/plugins/oas31/` +- **Version Detection:** `src/core/plugins/versions/` +- **CLAUDE.md:** Complete codebase guide + +## Best Practices: Iterative Spec-Driven Development + +### Continuous Specification Reference + +**NEVER implement a feature without checking the spec first.** The specification is your source of truth. + +#### During Initial Planning +1. Read the entire specification document +2. Create your component mapping document +3. Prioritize features by importance and dependency + +#### During Implementation +**For each component:** + +1. **Before writing code:** + ```javascript + WebFetch( + "https://spec.openapis.org/oas/v{VERSION}/", + "Provide the complete specification for the '{FIELD_NAME}' field, including all properties, data types, constraints, and examples." + ) + ``` + +2. **While writing the selector:** + - Verify the exact field path in the spec + - Check if field is required or optional + - Confirm data type (string, object, array, etc.) + - Note any default values + +3. **While writing the component:** + - Reference spec examples for rendering approach + - Implement all specified subfields + - Handle edge cases mentioned in spec + - Follow spec's field naming exactly + +4. **After writing tests:** + - Use spec examples as test fixtures + - Test all scenarios mentioned in spec + - Verify behavior matches spec descriptions + +#### Common Mistakes to Avoid + +❌ **DON'T:** +- Guess field structure without checking spec +- Assume field behavior from field name +- Skip optional fields thinking they're unimportant +- Implement based on previous version without checking changes +- Use hardcoded strings without verifying in spec + +✅ **DO:** +- Read spec section before each component +- Verify field paths with WebFetch +- Check spec examples for edge cases +- Confirm data types and constraints +- Reference spec in code comments + +#### Example: Spec-Driven Implementation + +**Bad approach (guessing):** +```javascript +// ❌ Guessing the structure +const workflows = spec.get("workflows") +return workflows.map(w => w.name) // Assumes 'name' exists +``` + +**Good approach (spec-driven):** +```javascript +// ✅ First check the spec: +// WebFetch result shows workflows is a Map[string, Workflow Object] +// Each workflow has: description, steps (array), parameters (optional) + +// Then implement correctly: +const workflows = spec.get("workflows") // Map[string, Workflow Object] +if (!workflows || !workflows.size) return null + +return workflows.entrySeq().map(([workflowName, workflowObject]) => { + // workflowName is the key, workflowObject contains: description, steps, etc. + const description = workflowObject.get("description") + const steps = workflowObject.get("steps") || List() + // ... +}) +``` + +### Verification Checklist Per Component + +Before marking a component complete, verify against spec: + +- [ ] **Field location matches spec exactly** (path in spec tree) +- [ ] **Data type implemented correctly** (string, object, array, etc.) +- [ ] **Required fields are handled** (error if missing vs optional) +- [ ] **Optional fields have defaults** (as specified in spec) +- [ ] **All subfields are rendered** (no fields missed) +- [ ] **Constraints are enforced** (min/max, enums, formats) +- [ ] **Examples from spec render correctly** (visual verification) +- [ ] **Edge cases are handled** (empty, null, malformed) +- [ ] **Field descriptions match spec** (for documentation) +- [ ] **Cross-references work** ($ref, operationRef, etc.) + +### Documentation Trail + +Leave a trail connecting your code to the spec: + +```javascript +/** + * @prettier + */ + +/** + * Workflows Component + * + * Renders the 'workflows' field from OpenAPI 4.0 specification. + * + * Spec reference: https://spec.openapis.org/oas/v4.0.0/#workflows-object + * + * Structure (from spec): + * workflows: + * {workflowName}: + * description: string (optional) + * steps: array of Step Objects (required) + * parameters: Map[string, Parameter] (optional) + * + * This component renders a list of workflow definitions, showing + * workflow steps and their operation references. + */ +const Workflows = ({ specSelectors, getComponent }) => { + // Implementation referencing spec structure +} +``` + +### Progressive Implementation Strategy + +Implement features in order of dependency and spec structure: + +**Phase 1: Core Fields (from spec root)** +1. Version detection (always first) +2. Top-level required fields +3. Top-level optional fields with high usage + +**Phase 2: Info Object Enhancements** +1. New info fields +2. Modified license/contact fields +3. Extended metadata fields + +**Phase 3: Component Object Extensions** +1. New component types +2. Schema keyword changes (if JSON Schema version changed) +3. Security scheme additions + +**Phase 4: Operation-Level Features** +1. New operation fields +2. Parameter enhancements +3. Request/response modifications +4. Callback/link changes + +**Phase 5: Advanced Features** +1. Complex interactions (workflows, callbacks, links) +2. Conditional rendering +3. Advanced security features + +### Spec Change Impact Analysis + +Before implementing, analyze the impact: + +| Spec Change | Swagger UI Impact | Implementation Effort | User Impact | +|-------------|-------------------|----------------------|-------------| +| New top-level field | New component + layout change | High | High | +| New info subfield | Wrap or extend Info | Medium | Medium | +| New auth type | New auth component | Medium | High | +| New schema keyword | Keyword component | Low-Medium | Low | +| Field description change | Documentation only | Low | Low | +| Deprecated field | Warning message | Low | Medium | +| Breaking change | Major refactor | High | High | + +Prioritize high user impact changes first. + +## Questions to Answer During Implementation + +### Before Starting +1. What is the exact version number? (e.g., 3.2, 4.0) +2. Have I read the complete specification at https://spec.openapis.org/? +3. Is it a major or minor version change? +4. What is the migration guide URL? + +### Specification Analysis +5. Does it use a new JSON Schema version? +6. What are ALL the new top-level fields? +7. What are ALL the modified existing fields? +8. What features are deprecated or removed? + +### Component Planning +9. Which components need to be created from scratch? +10. Which existing components need to be wrapped? +11. What selectors need to be added/wrapped? +12. What functions need to be overridden in afterLoad? + +### Authentication & Security +13. Are there new authentication types? +14. Do security schemes have new fields? +15. Are there new security requirements or flows? + +### Testing Strategy +16. Where are the official example specs? +17. What edge cases are mentioned in the spec? +18. What are the validation rules? + +### During Each Component +19. What is the exact field path in the spec? +20. What is the exact data type? +21. Is this field required or optional? +22. What are the constraints (min/max, enum, format)? +23. What are the examples in the spec? +24. How should this render visually? + +--- + +**Remember:** +1. **Always check https://spec.openapis.org/ first** +2. **Use WebFetch to analyze spec sections before coding** +3. **Follow the patterns established by OAS 3.1 implementation** +4. **Verify each component against the specification** +5. **Test with examples from the official spec** +6. **Document the spec reference in code comments** +7. **When in doubt, check the spec again!** diff --git a/src/main/webapp/swagger/.commitlintrc.json b/src/main/webapp/swagger/.commitlintrc.json new file mode 100644 index 00000000..a55865b3 --- /dev/null +++ b/src/main/webapp/swagger/.commitlintrc.json @@ -0,0 +1,25 @@ +{ + "extends": [ + "@commitlint/config-conventional" + ], + "rules": { + "header-max-length": [ + 2, + "always", + 69 + ], + "scope-case": [ + 2, + "always", + [ + "camel-case", + "kebab-case", + "upper-case" + ] + ], + "subject-case": [ + 0, + "always" + ] + } +} diff --git a/src/main/webapp/swagger/.dockerignore b/src/main/webapp/swagger/.dockerignore new file mode 100644 index 00000000..47c624e5 --- /dev/null +++ b/src/main/webapp/swagger/.dockerignore @@ -0,0 +1,8 @@ +/.git +/.github +/dev-helpers +/docs +/src +/swagger-ui-dist-package +/test +/node_modules \ No newline at end of file diff --git a/src/main/webapp/swagger/.editorconfig b/src/main/webapp/swagger/.editorconfig new file mode 100644 index 00000000..2d959463 --- /dev/null +++ b/src/main/webapp/swagger/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true +[*.md] +trim_trailing_whitespace = false diff --git a/src/main/webapp/swagger/.eslintignore b/src/main/webapp/swagger/.eslintignore new file mode 100644 index 00000000..5e20fb04 --- /dev/null +++ b/src/main/webapp/swagger/.eslintignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +test/e2e-selenium/ diff --git a/src/main/webapp/swagger/.eslintrc.js b/src/main/webapp/swagger/.eslintrc.js new file mode 100644 index 00000000..87d561e1 --- /dev/null +++ b/src/main/webapp/swagger/.eslintrc.js @@ -0,0 +1,78 @@ +/** + * @prettier + */ +const path = require("node:path") + +module.exports = { + parser: "@babel/eslint-parser", + env: { + browser: true, + node: true, + es6: true, + jest: true, + "jest/globals": true, + }, + parserOptions: { + ecmaFeatures: { jsx: true }, + babelOptions: { configFile: path.join(__dirname, "babel.config.js") }, + }, + extends: [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:prettier/recommended", + ], + plugins: ["react", "import", "jest", "prettier"], + settings: { + react: { + pragma: "React", + version: "15.0", + }, + }, + rules: { + semi: [2, "never"], + strict: 0, + quotes: [ + 2, + "double", + { + avoidEscape: true, + allowTemplateLiterals: true, + }, + ], + "no-unused-vars": 2, + "no-multi-spaces": 1, + camelcase: [ + "error", + { + allow: [ + "^UNSAFE_", + "^requestSnippetGenerator_", + "^JsonSchema_", + "^curl_", + "^dom_", + "^api_", + "^client_", + "^grant_", + "^code_", + "^redirect_", + "^spec", + ], + }, + ], + "no-use-before-define": [2, "nofunc"], + "no-underscore-dangle": 0, + "no-unused-expressions": 1, + "comma-dangle": 0, + "no-console": [ + 2, + { + allow: ["warn", "error"], + }, + ], + "react/jsx-no-bind": 1, + "react/jsx-no-target-blank": 2, + "react/display-name": 0, + "import/no-extraneous-dependencies": 2, + "react/jsx-filename-extension": 2, + }, +} diff --git a/src/main/webapp/swagger/.gitattributes b/src/main/webapp/swagger/.gitattributes new file mode 100644 index 00000000..5e49d7fd --- /dev/null +++ b/src/main/webapp/swagger/.gitattributes @@ -0,0 +1,4 @@ +docker-run.sh text eol=lf +/dist/*.map export-ignore +/test export-ignore +/docs export-ignore \ No newline at end of file diff --git a/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Bug_report.md b/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 00000000..48861bf3 --- /dev/null +++ b/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,73 @@ +--- +name: Bug report +about: Report an issue you're experiencing + +--- + + + +### Q&A (please complete the following information) + - OS: [e.g. macOS] + - Browser: [e.g. chrome, safari] + - Version: [e.g. 22] + - Method of installation: [e.g. npm, dist assets] + - Swagger-UI version: [e.g. 3.10.0] + - Swagger/OpenAPI version: [e.g. Swagger 2.0, OpenAPI 3.0] + +### Content & configuration + + +Example Swagger/OpenAPI definition: +```yaml +# your YAML here +``` + +Swagger-UI configuration options: +```js +SwaggerUI({ + // your config options here +}) +``` + +``` +?yourQueryStringConfig +``` + +### Describe the bug you're encountering + + +### To reproduce... + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +### Expected behavior + + +### Screenshots + + +### Additional context or thoughts + diff --git a/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Feature_request.md b/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 00000000..5b18f43f --- /dev/null +++ b/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,42 @@ +--- +name: Feature request +about: Suggest a new feature or enhancement for this project + +--- + + ### Content & configuration + +Swagger/OpenAPI definition: + ```yaml + # your YAML here + ``` + + Swagger-UI configuration options: + ```js + SwaggerUI({ + // your config options here + }) + ``` + + ``` + ?yourQueryStringConfig + ``` + + +### Is your feature request related to a problem? + + +### Describe the solution you'd like + + +### Describe alternatives you've considered + + +### Additional context + diff --git a/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Support.md b/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Support.md new file mode 100644 index 00000000..92eb5e90 --- /dev/null +++ b/src/main/webapp/swagger/.github/ISSUE_TEMPLATE/Support.md @@ -0,0 +1,46 @@ +--- +name: Support +about: Ask a question or request help with your implementation. + +--- + + + +### Q&A (please complete the following information) + - OS: [e.g. macOS] + - Browser: [e.g. chrome, safari] + - Version: [e.g. 22] + - Method of installation: [e.g. npm, dist assets] + - Swagger-UI version: [e.g. 3.10.0] + - Swagger/OpenAPI version: [e.g. Swagger 2.0, OpenAPI 3.0] + +### Content & configuration + + +Swagger/OpenAPI definition: +```yaml +# your YAML here +``` + +Swagger-UI configuration options: +```js +SwaggerUI({ + // your config options here +}) +``` + +``` +?yourQueryStringConfig +``` + +### Screenshots + + +### How can we help? + diff --git a/src/main/webapp/swagger/.github/dependabot.yaml b/src/main/webapp/swagger/.github/dependabot.yaml new file mode 100644 index 00000000..6475663f --- /dev/null +++ b/src/main/webapp/swagger/.github/dependabot.yaml @@ -0,0 +1,35 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "23:00" + commit-message: + prefix: "chore" + include: "scope" + open-pull-requests-limit: 3 + ignore: + # node-fetch must be synced manually + - dependency-name: "node-fetch" + + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `root` directory + directory: "/" + # Check for updates once a week + schedule: + interval: "weekly" + time: "23:00" + + - package-ecosystem: "github-actions" + target-branch: "master" + directory: "/" + schedule: + interval: "daily" + time: "23:00" + commit-message: + prefix: "chore" + include: "scope" + open-pull-requests-limit: 3 + + diff --git a/src/main/webapp/swagger/.github/lock.yml b/src/main/webapp/swagger/.github/lock.yml new file mode 100644 index 00000000..e0b8c11a --- /dev/null +++ b/src/main/webapp/swagger/.github/lock.yml @@ -0,0 +1,15 @@ +daysUntilLock: 365 +skipCreatedBefore: 2017-03-29 # initial release of Swagger UI 3.0.0 +exemptLabels: [] +lockLabel: "locked-by: lock-bot" +setLockReason: false +only: issues +lockComment: false +# lockComment: | +# Locking due to inactivity. + +# This is done to avoid resurrecting old issues and bumping long threads with new, possibly unrelated content. + +# If you think you're experiencing something similar to what you've found here: please [open a new issue](https://github.com/swagger-api/swagger-ui/issues/new/choose), follow the template, and reference this issue in your report. + +# Thanks! diff --git a/src/main/webapp/swagger/.github/pull_request_template.md b/src/main/webapp/swagger/.github/pull_request_template.md new file mode 100644 index 00000000..64e9cf6f --- /dev/null +++ b/src/main/webapp/swagger/.github/pull_request_template.md @@ -0,0 +1,55 @@ + + +### Description + + + + +### Motivation and Context + + + + + + + +### How Has This Been Tested? + + + + + + +### Screenshots (if appropriate): + + + +## Checklist + + + +### My PR contains... + +- [ ] No code changes (`src/` is unmodified: changes to documentation, CI, metadata, etc.) +- [ ] Dependency changes (any modification to dependencies in `package.json`) +- [ ] Bug fixes (non-breaking change which fixes an issue) +- [ ] Improvements (misc. changes to existing features) +- [ ] Features (non-breaking change which adds functionality) + +### My changes... +- [ ] are breaking changes to a public API (config options, System API, major UI change, etc). +- [ ] are breaking changes to a private API (Redux, component props, utility functions, etc.). +- [ ] are breaking changes to a developer API (npm script behavior changes, new dev system dependencies, etc). +- [ ] are not breaking changes. + +### Documentation +- [ ] My changes do not require a change to the project documentation. +- [ ] My changes require a change to the project documentation. +- [ ] If yes to above: I have updated the documentation accordingly. + +### Automated tests +- [ ] My changes can not or do not need to be tested. +- [ ] My changes can and should be tested by unit and/or integration tests. +- [ ] If yes to above: I have added tests to cover my changes. +- [ ] If yes to above: I have taken care to cover edge cases in my tests. +- [ ] All new and existing tests passed. diff --git a/src/main/webapp/swagger/.github/workflows/codeql.yml b/src/main/webapp/swagger/.github/workflows/codeql.yml new file mode 100644 index 00000000..f8ba9eff --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/codeql.yml @@ -0,0 +1,63 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '16 04 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + config: | + paths-ignore: + - 'dist/' + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/src/main/webapp/swagger/.github/workflows/dependabot-merge.yml b/src/main/webapp/swagger/.github/workflows/dependabot-merge.yml new file mode 100644 index 00000000..9495d428 --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/dependabot-merge.yml @@ -0,0 +1,40 @@ +name: Merge me! + +on: + pull_request_target: + branches: [ master, next ] + +permissions: + contents: read + +jobs: + merge-me: + name: Merge me! + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + # This first step will fail if there's no metadata and so the approval + # will not occur. + - name: Dependabot metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@v3.0.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + # Here the PR gets approved. + - name: Approve a PR + if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} + # Finally, tell dependabot to merge the PR if all checks are successful + - name: Instruct dependabot to squash & merge + if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} + uses: mshick/add-pr-comment@v3 + with: + repo-token: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} + allow-repeats: true + message: | + @dependabot squash and merge + env: + GITHUB_TOKEN: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} diff --git a/src/main/webapp/swagger/.github/workflows/docker-build-push-unstable.yml b/src/main/webapp/swagger/.github/workflows/docker-build-push-unstable.yml new file mode 100644 index 00000000..2acfe76a --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/docker-build-push-unstable.yml @@ -0,0 +1,52 @@ +name: Build & Push SwaggerUI unstable Docker image + +on: + workflow_run: + workflows: ["Node.js CI"] + types: + - completed + branches: [master] + +jobs: + + build-push-unstable: + if: github.event.workflow_run.conclusion == 'success' + name: Build & Push SwaggerUI unstable Docker image + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build SwaggerUI + run: npm run build + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_SB_USERNAME }} + password: ${{ secrets.DOCKERHUB_SB_PASSWORD }} + + - name: Build docker image and push + uses: docker/build-push-action@v7 + with: + context: . + push: true + platforms: linux/amd64,linux/arm/v6,linux/arm64,linux/386,linux/ppc64le + provenance: false + tags: swaggerapi/swagger-ui:unstable diff --git a/src/main/webapp/swagger/.github/workflows/docker-build-push.yml b/src/main/webapp/swagger/.github/workflows/docker-build-push.yml new file mode 100644 index 00000000..c6460b53 --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/docker-build-push.yml @@ -0,0 +1,77 @@ +name: Build & Push SwaggerUI Docker image + +on: + workflow_run: + workflows: ["Release SwaggerUI"] + types: + - completed + branches: [master] + +jobs: + + build-push: + if: github.event.workflow_run.conclusion == 'success' + name: Build & Push SwaggerUI Docker image + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build SwaggerUI + run: npm run build + + - name: Determine released version + uses: actions/github-script@v9 + with: + script: | + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + const matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "released-version" + })[0]; + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + const fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/released-version.zip', Buffer.from(download.data)); + - run: | + unzip released-version.zip + RELEASED_VERSION=$(cat released-version.txt) + echo "RELEASED_VERSION=$RELEASED_VERSION" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_SB_USERNAME }} + password: ${{ secrets.DOCKERHUB_SB_PASSWORD }} + + - name: Build docker image and push + uses: docker/build-push-action@v7 + with: + context: . + push: true + platforms: linux/amd64,linux/arm/v6,linux/arm64,linux/386,linux/ppc64le + provenance: false + tags: swaggerapi/swagger-ui:latest,swaggerapi/swagger-ui:v${{ env.RELEASED_VERSION }} diff --git a/src/main/webapp/swagger/.github/workflows/docker-image-check.yml b/src/main/webapp/swagger/.github/workflows/docker-image-check.yml new file mode 100644 index 00000000..dd119606 --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/docker-image-check.yml @@ -0,0 +1,23 @@ +name: Security scan for docker image + +on: + workflow_dispatch: + schedule: + - cron: '30 4 * * *' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'docker.swagger.io/swaggerapi/swagger-ui:unstable' + format: 'table' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' diff --git a/src/main/webapp/swagger/.github/workflows/nodejs.yml b/src/main/webapp/swagger/.github/workflows/nodejs.yml new file mode 100644 index 00000000..0fba4b0c --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/nodejs.yml @@ -0,0 +1,86 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ master, next ] + pull_request: + branches: [ master, next ] + +env: + CYPRESS_CACHE_FOLDER: cypress/cache + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Cache Node Modules and Cypress binary + uses: actions/cache@v5 + id: cache-primes + with: + path: | + node_modules + ${{ env.CYPRESS_CACHE_FOLDER }} + key: ${{ runner.os }}-node-and-cypress-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.cache-primes.outputs.cache-hit != 'true' + run: npm ci + + - name: Lint code for errors only + run: npm run lint-errors + + - name: Run all tests + run: npm run test:unit + env: + CI: true + + - name: Build SwaggerUI + run: npm run build + + - name: Test build artifacts + run: npm run test:artifact + + e2e-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + containers: ['+(a11y|security|bugs)/**/*cy.js', 'features/**/+(o|d)*.cy.js', 'features/**/m*.cy.js', 'features/**/!(o|d|m)*.cy.js'] + + steps: + - uses: actions/checkout@v6 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Cache Node Modules and Cypress binary + uses: actions/cache@v5 + id: cache-primes + with: + path: | + node_modules + ${{ env.CYPRESS_CACHE_FOLDER }} + key: ${{ runner.os }}-node-and-cypress-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.cache-primes.outputs.cache-hit != 'true' + run: npm ci + + - name: Cypress Test + run: npx start-server-and-test cy:start http://localhost:3204 'npm run cy:run -- --spec "test/e2e-cypress/e2e/${{ matrix.containers }}"' diff --git a/src/main/webapp/swagger/.github/workflows/release-swagger-ui-dist.yml b/src/main/webapp/swagger/.github/workflows/release-swagger-ui-dist.yml new file mode 100644 index 00000000..f738b84e --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/release-swagger-ui-dist.yml @@ -0,0 +1,61 @@ +name: Release SwaggerUI Dist + +on: + workflow_run: + workflows: ["Release SwaggerUI"] + types: + - completed + branches: [ master ] + +jobs: + release-swagger-ui-dist: + if: github.event.workflow_run.conclusion == 'success' + name: Release swagger-ui-dist npm package + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: master + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Download build artifact + uses: actions/github-script@v9 + with: + script: | + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + const matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "dist" + })[0]; + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + const fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/dist.zip', Buffer.from(download.data)); + - run: | + unzip -o dist.zip -d dist + + - name: Publish to npmjs.com + run: ./deploy.sh + working-directory: ./swagger-ui-dist-package + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + PUBLISH_DIST: true diff --git a/src/main/webapp/swagger/.github/workflows/release-swagger-ui-packagist.yml b/src/main/webapp/swagger/.github/workflows/release-swagger-ui-packagist.yml new file mode 100644 index 00000000..05d51750 --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/release-swagger-ui-packagist.yml @@ -0,0 +1,21 @@ +name: Release SwaggerUI to Packagist + +on: + workflow_run: + workflows: ["Release SwaggerUI"] + types: + - completed + branches: [ master ] + +jobs: + release-swagger-ui-packagist: + if: github.event.workflow_run.conclusion == 'success' + name: Release swagger-ui to packagist + runs-on: ubuntu-latest + steps: + - name: Update SwaggerUI packagist package + run: | + curl --fail -X POST \ + -H "Content-Type: application/json" \ + "https://packagist.org/api/update-package?username=${{ secrets.PACKAGIST_USER }}&apiToken=${{ secrets.PACKAGIST_API_TOKEN }}" \ + -d "{\"repository\":{\"url\":\"https://packagist.org/packages/swagger-api/swagger-ui\"}}" diff --git a/src/main/webapp/swagger/.github/workflows/release-swagger-ui-react.yml b/src/main/webapp/swagger/.github/workflows/release-swagger-ui-react.yml new file mode 100644 index 00000000..f37cf3ba --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/release-swagger-ui-react.yml @@ -0,0 +1,61 @@ +name: Release SwaggerUI React + +on: + workflow_run: + workflows: ["Release SwaggerUI"] + types: + - completed + branches: [ master ] + +jobs: + release-swagger-ui-react: + if: github.event.workflow_run.conclusion == 'success' + name: Release swagger-ui-react npm package + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: master + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Download build artifact + uses: actions/github-script@v9 + with: + script: | + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + const matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "dist" + })[0]; + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + const fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/dist.zip', Buffer.from(download.data)); + - run: | + unzip -o dist.zip -d dist + + - name: Publish to npmjs.com + run: ./run.sh + working-directory: ./flavors/swagger-ui-react/release + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + PUBLISH_FLAVOR_REACT: true diff --git a/src/main/webapp/swagger/.github/workflows/release-swagger-ui.yml b/src/main/webapp/swagger/.github/workflows/release-swagger-ui.yml new file mode 100644 index 00000000..2c53028d --- /dev/null +++ b/src/main/webapp/swagger/.github/workflows/release-swagger-ui.yml @@ -0,0 +1,95 @@ +name: Release SwaggerUI +on: + workflow_dispatch: + branches: + - master + +jobs: + release: + name: Release swagger-ui npm package + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: master + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: package-lock.json + + - name: Determine the next release version + uses: cycjimmy/semantic-release-action@v6 + with: + dry_run: true + extra_plugins: | + @semantic-release/git + @semantic-release/exec + env: + GITHUB_TOKEN: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Nothing to release + if: ${{ env.NEXT_RELEASE_VERSION == '' }} + uses: actions/github-script@v9 + with: + script: | + core.setFailed('Nothing to release') + + - name: Install dependencies + run: npm ci + + - name: Prepare for the Release + env: + REACT_APP_VERSION: ${{ env.NEXT_RELEASE_VERSION }} + run: | + npm run test + npm run build + + - name: Semantic Release + id: semantic + uses: cycjimmy/semantic-release-action@v6 + with: + dry_run: false + extra_plugins: | + @semantic-release/git + env: + GITHUB_TOKEN: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Release failed + if: steps.semantic.outputs.new_release_published == 'false' + uses: actions/github-script@v9 + with: + script: | + core.setFailed('Release failed') + + - name: Release published + run: | + echo ${{ steps.semantic.outputs.new_release_version }} + echo ${{ steps.semantic.outputs.new_release_major_version }} + echo ${{ steps.semantic.outputs.new_release_minor_version }} + echo ${{ steps.semantic.outputs.new_release_patch_version }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: dist + path: ./dist + + + - name: Prepare released version for uploading + shell: bash + run: | + echo ${{ steps.semantic.outputs.new_release_version }} > released-version.txt + - name: Upload released version + uses: actions/upload-artifact@v7 + with: + name: released-version + path: ./released-version.txt + retention-days: 1 diff --git a/src/main/webapp/swagger/.gitignore b/src/main/webapp/swagger/.gitignore new file mode 100644 index 00000000..e9578575 --- /dev/null +++ b/src/main/webapp/swagger/.gitignore @@ -0,0 +1,29 @@ +node_modules +.idea +.vscode +.deps_check +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local +.nyc_output +npm-debug.log* +.eslintcache +*.iml +selenium-debug.log +chromedriver.log +test/e2e/db.json +docs/_book +dev-helpers/examples + +# dist +flavors/**/dist/* +/lib +/es +dist/log* +/swagger-ui-*.tgz + +# Cypress +test/e2e-cypress/screenshots +test/e2e-cypress/videos diff --git a/src/main/webapp/swagger/.husky/commit-msg b/src/main/webapp/swagger/.husky/commit-msg new file mode 100755 index 00000000..951570f7 --- /dev/null +++ b/src/main/webapp/swagger/.husky/commit-msg @@ -0,0 +1 @@ +npx commitlint -e diff --git a/src/main/webapp/swagger/.husky/pre-commit b/src/main/webapp/swagger/.husky/pre-commit new file mode 100755 index 00000000..2312dc58 --- /dev/null +++ b/src/main/webapp/swagger/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/src/main/webapp/swagger/.lintstagedrc b/src/main/webapp/swagger/.lintstagedrc new file mode 100644 index 00000000..0b1dd617 --- /dev/null +++ b/src/main/webapp/swagger/.lintstagedrc @@ -0,0 +1,4 @@ +{ + "*.{js,jsx}": ["eslint --max-warnings 0"], + "*.scss": ["stylelint '**/*.scss'"] +} diff --git a/src/main/webapp/swagger/.npmignore b/src/main/webapp/swagger/.npmignore new file mode 100644 index 00000000..600fc159 --- /dev/null +++ b/src/main/webapp/swagger/.npmignore @@ -0,0 +1,16 @@ +* +*/ +!README.md +!NOTICE +!package.json +!dist/swagger-ui.js +!dist/swagger-ui.js.map +!dist/swagger-ui-bundle.js +!dist/swagger-ui-standalone-preset.js +!dist/swagger-ui-es-bundle.js +!dist/swagger-ui-es-bundle-core.js +!dist/swagger-ui-es-bundle-core.js.map +!dist/swagger-ui.css +!dist/swagger-ui.css.map +!dist/oauth2-redirect.html +!dist/oauth2-redirect.js \ No newline at end of file diff --git a/src/main/webapp/swagger/.npmrc b/src/main/webapp/swagger/.npmrc new file mode 100644 index 00000000..d508edc4 --- /dev/null +++ b/src/main/webapp/swagger/.npmrc @@ -0,0 +1 @@ +save-prefix="=" diff --git a/src/main/webapp/swagger/.nvmrc b/src/main/webapp/swagger/.nvmrc new file mode 100644 index 00000000..d845d9d8 --- /dev/null +++ b/src/main/webapp/swagger/.nvmrc @@ -0,0 +1 @@ +24.14.0 diff --git a/src/main/webapp/swagger/.prettierrc.yaml b/src/main/webapp/swagger/.prettierrc.yaml new file mode 100644 index 00000000..2e34d757 --- /dev/null +++ b/src/main/webapp/swagger/.prettierrc.yaml @@ -0,0 +1,5 @@ +semi: false +trailingComma: es5 +endOfLine: lf +requirePragma: true +insertPragma: true diff --git a/src/main/webapp/swagger/.releaserc b/src/main/webapp/swagger/.releaserc new file mode 100644 index 00000000..d90e9626 --- /dev/null +++ b/src/main/webapp/swagger/.releaserc @@ -0,0 +1,26 @@ +{ + "branches": [ + {"name": "master"} + ], + "tagFormat": "v${version}", + "plugins": [ + "@semantic-release/commit-analyzer", + [ + "@semantic-release/exec", + { + "verifyReleaseCmd": "echo \"NEXT_RELEASE_VERSION=${nextRelease.version}\" >> $GITHUB_ENV" + } + ], + "@semantic-release/release-notes-generator", + "@semantic-release/npm", + "@semantic-release/github", + ["@semantic-release/git", { + "assets": [ + "dist/*.{js,html,css}", + "package.json", + "package-lock.json" + ], + "message": "chore(release): cut the ${nextRelease.version} release\n\n${nextRelease.notes}" + }] + ] +} diff --git a/src/main/webapp/swagger/CLAUDE.md b/src/main/webapp/swagger/CLAUDE.md new file mode 100644 index 00000000..5148b423 --- /dev/null +++ b/src/main/webapp/swagger/CLAUDE.md @@ -0,0 +1,1072 @@ +# CLAUDE.md - Swagger UI Codebase Guide + +> **Last Updated:** 2026-02-24 +> **Version:** 5.32.0 (in development) +> **Purpose:** Comprehensive guide for AI assistants working with the Swagger UI codebase + +--- + +## Table of Contents + +1. [Repository Overview](#repository-overview) +2. [Project Architecture](#project-architecture) +3. [Development Setup](#development-setup) +4. [Build System](#build-system) +5. [Testing Infrastructure](#testing-infrastructure) +6. [Code Style & Conventions](#code-style--conventions) +7. [Git Workflow](#git-workflow) +8. [Plugin Architecture](#plugin-architecture) +9. [Key Files & Directories](#key-files--directories) +10. [Common Workflows](#common-workflows) +11. [Important Guidelines](#important-guidelines) + +--- + +## Repository Overview + +### What is Swagger UI? + +Swagger UI is a tool that allows developers to visualize and interact with API resources without having implementation logic in place. It's automatically generated from OpenAPI (formerly Swagger) Specification documents. + +### Multi-Package Monorepo Structure + +This repository publishes **three different npm packages**: + +1. **swagger-ui** (main package) + - Traditional npm module for single-page applications + - Entry: `dist/swagger-ui.js` + - ES Module: `dist/swagger-ui-es-bundle-core.js` + - Includes dependency resolution via Webpack/Browserify + +2. **swagger-ui-dist** (distribution package) + - Dependency-free module for server-side projects + - Published separately via GitHub workflow + - Template location: `swagger-ui-dist-package/` + +3. **swagger-ui-react** (React component) + - React wrapper component + - Location: `flavors/swagger-ui-react/` + - Uses React hooks + - Released separately via GitHub workflow + +### OpenAPI Specification Compatibility + +- **Current Support:** OpenAPI 2.0, 3.0.x, 3.1.x +- **Latest Version:** v5.31.0 (supports up to OpenAPI 3.1.2) + +### License + +Apache 2.0 - See LICENSE and NOTICE files for details. + +--- + +## Project Architecture + +### Technology Stack + +**Core Framework:** +- React 18 (>=16.8.0 <20) - UI components +- Redux 5.0.1 - State management +- Redux Immutable 4.0.0 - Immutable state +- Immutable.js 3.x - Immutable data structures +- React Redux 9.2.0 - React-Redux bindings + +**API & Schema Processing:** +- swagger-client 3.36.0 - OpenAPI client +- js-yaml 4.1.1 - YAML parsing +- remarkable 2.0.1 - Markdown rendering + +**Security:** +- DOMPurify 3.2.6 - HTML sanitization (CRITICAL for XSS prevention) +- serialize-error 8.1.0 - Error serialization + +**Build Tools:** +- Webpack 5.97.1 - Module bundling +- Babel 7.26.x - JavaScript transpilation +- sass-embedded 1.86.0 - SCSS compilation +- PostCSS - CSS processing + +**Testing:** +- Jest 29.7.0 - Unit testing +- Cypress 14.2.0 - E2E testing +- Enzyme 3.11.0 - React component testing + +**Development:** +- ESLint 8.57.0 - JavaScript linting +- Prettier 3.5.3 - Code formatting +- Stylelint 16.19.1 - CSS linting +- Husky 9.1.7 - Git hooks +- lint-staged 15.5.0 - Pre-commit linting + +### Plugin-Based Architecture + +Swagger UI uses a **sophisticated plugin system** powered by Redux. The core system (`src/core/system.js`) manages: + +- Plugin registration and lifecycle +- Redux store creation and middleware +- State plugin combination +- Action/selector binding +- Configuration management + +**26 Core Plugins** (in `src/core/plugins/`): +- `auth` - Authentication handling +- `configs` - Configuration management +- `deep-linking` - URL-based navigation +- `download-url` - Spec downloading +- `err` - Error handling and transformation +- `filter` - API filtering +- `icons` - Icon components +- `json-schema-2020-12` - JSON Schema 2020-12 support +- `json-schema-2020-12-samples` - Sample generation +- `json-schema-5` - JSON Schema Draft 5 support +- `json-schema-5-samples` - Sample generation for Draft 5 +- `layout` - Layout system +- `logs` - Logging +- `oas3` - OpenAPI 3.0.x support +- `oas31` - OpenAPI 3.1.x support +- `oas32` - OpenAPI 3.2.x support +- `on-complete` - Completion callbacks +- `request-snippets` - Code snippet generation +- `safe-render` - Safe component rendering +- `spec` - Specification handling +- `swagger-client` - API client integration +- `syntax-highlighting` - Code highlighting +- `util` - Utilities +- `versions` - Version detection +- `view` - View rendering +- `view-legacy` - Legacy view support + +--- + +## Development Setup + +### Prerequisites + +- **Node.js:** >=24.14.0 (Node 24.x recommended, as defined in `.nvmrc`) +- **npm:** >=11.9.0 +- **Git:** Any version +- **JDK 7+:** Required for Nightwatch.js integration tests + +### Installation Steps + +```bash +# Clone the repository +git clone https://github.com/swagger-api/swagger-ui.git +cd swagger-ui + +# Install dependencies +npm install + +# Initialize Husky (optional, for git hooks) +npx husky init + +# Start development server +npm run dev + +# Open http://localhost:3200/ +``` + +### Development Server + +The `npm run dev` command starts a hot-reloading Webpack dev server on **port 3200**. + +### Using Local API Definitions + +Edit `dev-helpers/dev-helper-initializer.js` to change the spec URL: + +```javascript +// Replace +url: "https://petstore.swagger.io/v2/swagger.json", + +// With +url: "./examples/your-local-api-definition.yaml", +``` + +**Important:** Local files must be in the `dev-helpers/` directory or subdirectory. Use `dev-helpers/examples/` (already in `.gitignore`). + +--- + +## Build System + +### Babel Environments + +Three Babel environments configured in `babel.config.js`: + +1. **development/production** - Browser builds with `modules: "auto"` +2. **commonjs** - CommonJS modules with `modules: "commonjs"` for Node.js +3. **esm** - ES modules with `modules: false` for modern bundlers + +### Babel Aliases + +```javascript +{ + root: ".", + core: "./src/core" +} +``` + +### Browserslist Environments + +Defined in `.browserslistrc`: + +- `[browser-production]` - Production browser targets +- `[browser-development]` - Latest Chrome, Firefox, Safari +- `[isomorphic-production]` - Browser + Node targets +- `[node-production]` - Maintained Node versions +- `[node-development]` - Node 24 + +### Build Commands + +```bash +# Full build (stylesheets + all bundles) +npm run build + +# Individual builds +npm run build:core # Core bundle (browser) +npm run build:bundle # Isomorphic bundle +npm run build:standalone # Standalone preset +npm run build:es:bundle # ES module bundle +npm run build:es:bundle:core # ES module core +npm run build-stylesheets # CSS only + +# Clean build artifacts +npm run clean +``` + +### Build Output (dist/) + +- `swagger-ui.js` - Core bundle (CommonJS) +- `swagger-ui.css` - Compiled styles +- `swagger-ui-bundle.js` - Isomorphic bundle +- `swagger-ui-standalone-preset.js` - Standalone preset +- `swagger-ui-es-bundle.js` - ES module bundle +- `swagger-ui-es-bundle-core.js` - ES module core +- `oauth2-redirect.html` - OAuth2 redirect page + +### Webpack Configurations + +Located in `webpack/` directory: + +- `_config-builder.js` - Base configuration +- `core.js` - Core build +- `bundle.js` - Bundle build +- `standalone.js` - Standalone build +- `es-bundle.js` - ES bundle +- `es-bundle-core.js` - ES core bundle +- `stylesheets.js` - CSS build +- `dev.js` - Development server +- `dev-e2e.js` - E2E testing server + +--- + +## Testing Infrastructure + +### Unit Tests (Jest) + +**Configuration:** `config/jest/jest.unit.config.js` + +**Environment:** jsdom (simulates browser environment) + +**Location:** `test/unit/` + +**Command:** +```bash +npm run test:unit +``` + +**Key Features:** +- 37 unit test files +- Tests for core plugins, components, system +- XSS security tests +- Silent mode enabled by default (set to `false` for console output) +- Module name mapper for SVG and standalone imports +- Transform ignore patterns for node_modules exceptions + +**Setup Files:** +- `test/unit/jest-shim.js` - Polyfills and shims +- `test/unit/setup.js` - Test environment setup + +### E2E Tests (Cypress) + +**Configuration:** `cypress.config.js` + +**Location:** `test/e2e-cypress/` + +**Base URL:** http://localhost:3230/ + +**Commands:** +```bash +# Run all E2E tests +npm run cy:ci + +# Interactive Cypress runner +npm run cy:dev + +# Headless run +npm run cy:run + +# Start servers and run tests +npm run cy:start # Starts webpack + mock API +``` + +**Structure:** +- `test/e2e-cypress/e2e/` - Test specs (99 test files) +- `test/e2e-cypress/static/` - Test fixtures and documents +- `test/e2e-cypress/support/` - Test helpers and commands + +**Test Categories:** +- `a11y/**/*cy.js` - Accessibility tests +- `security/**/*cy.js` - Security tests +- `bugs/**/*cy.js` - Bug regression tests +- `features/**/*cy.js` - Feature tests + +**Mock API Server:** +```bash +npm run cy:mock-api # JSON Server on port 3204 +``` + +### Artifact Tests + +**Configuration:** `config/jest/jest.artifact.config.js` + +**Purpose:** Verify build artifacts export correctly + +**Command:** +```bash +npm run test:artifact +``` + +### Complete Test Suite + +```bash +npm test # Runs: lint-errors + test:unit + cy:ci +``` + +### CI/CD Testing + +**GitHub Actions Workflow:** `.github/workflows/nodejs.yml` + +**Two Jobs:** +1. **build** - Lint, unit tests, build, artifact tests +2. **e2e-tests** - Cypress tests (matrix strategy with 3 containers) + +**Branches:** `master`, `next` + +--- + +## Code Style & Conventions + +### ESLint Configuration + +**File:** `.eslintrc.js` + +**Parser:** `@babel/eslint-parser` + +**Key Rules:** +- `semi: [2, "never"]` - **No semicolons** +- `quotes: [2, "double"]` - **Double quotes** (allow template literals) +- `no-unused-vars: 2` - Error on unused variables +- `camelcase: ["error"]` - Enforce camelCase (with exceptions for UNSAFE_, request generators, etc.) +- `no-console: [2, {allow: ["warn", "error"]}]` - Only `console.warn` and `console.error` allowed +- `react/jsx-no-bind: 1` - Warning for JSX bind +- `react/jsx-filename-extension: 2` - JSX only in `.jsx` files +- `import/no-extraneous-dependencies: 2` - Error on extraneous dependencies + +**Extends:** +- `eslint:recommended` +- `plugin:react/recommended` +- `plugin:prettier/recommended` + +### Prettier Configuration + +**File:** `.prettierrc.yaml` + +**Settings:** +```yaml +semi: false # No semicolons +trailingComma: es5 # ES5 trailing commas +endOfLine: lf # Unix line endings +requirePragma: true # Require @prettier pragma +insertPragma: true # Insert @prettier pragma +``` + +**IMPORTANT:** Prettier requires `@prettier` pragma comment at the top of files: +```javascript +/** + * @prettier + */ +``` + +### Stylelint Configuration + +**File:** `stylelint.config.js` + +**Custom Syntax:** `postcss-scss` + +**Rules:** +- Uses `stylelint-prettier` plugin +- Prettier integration without pragma requirement + +### Pre-commit Hooks + +**Husky:** `.husky/pre-commit` runs `npx lint-staged` + +**Lint-staged Configuration:** `.lintstagedrc` +```json +{ + "*.{js,jsx}": ["eslint --max-warnings 0"], + "*.scss": ["stylelint '**/*.scss'"] +} +``` + +**Critical:** All staged JS/JSX/SCSS files are linted with **zero warnings tolerance**. + +### File Structure Conventions + +**Components:** +- Location: `src/core/components/` +- Extension: `.jsx` (React components) +- Format: PascalCase for component names + +**Styles:** +- Location: `src/style/` +- Extension: `.scss` +- Format: SCSS with PostCSS processing +- Dark mode: `_dark-mode.scss` + +**Tests:** +- Unit: `test/unit/` (mirrors source structure) +- E2E: `test/e2e-cypress/e2e/` +- Naming: `*.test.js`, `*.spec.js`, `*.cy.js` (Cypress) + +--- + +## Git Workflow + +### Branch Strategy + +**Main Branches:** +- `master` - Production releases +- `next` - Next version development + +**Feature Branches:** +- Should branch from `master` or `next` +- Use descriptive names + +### Commit Conventions + +**Format:** Conventional Commits (enforced by commitlint) + +**Structure:** +``` +(): + + + +