diff --git a/README.md b/README.md index 8c0067c..63bfcfa 100644 --- a/README.md +++ b/README.md @@ -68,25 +68,51 @@ Cada ficha tiene su propio enlace y botones para compartirla por WhatsApp o Face - En *Diseño → Cabecera* se elige cómo sale el logo arriba: símbolo + nombre, logo completo o solo el nombre. -## APIs +## APIs (in English) -| API | Dirección | Qué incluye | +The API is in English (routes, fields, values and messages), whatever the site language. The public website keeps its Spanish/Basque URLs (`/adopta/`, `/especie/perro/`). + +| API | Endpoint | Notes | |---|---|---| -| REST | `/wp-json/wp/v2/animales`, `/wp-json/wp/v2/especies` | Animales con el campo `ficha` (valores y textos legibles en su idioma). Se puede filtrar por idioma con `?lang=es` o `?lang=eu`. | -| GraphQL (WPGraphQL) | `/graphql` | Tipos `Animal` / `Especie` con el campo `ficha`. Explorador GraphiQL en el panel (menú GraphQL). | -| Swagger / OpenAPI 3 | `/rest-api/docs/` (interfaz) y `/rest-api/schema` (esquema) | Documentación interactiva de la API REST. Por defecto solo para usuarios conectados; `API_DOCS_PUBLIC=true` la hace pública. | +| REST | `/wp-json/wp/v2/animals`, `/wp-json/wp/v2/species` | Every animal has a `profile` object (read **and write**). Filter by language with `?lang=es` / `?lang=eu`. | +| GraphQL (WPGraphQL) | `/graphql` | `animals(where: { language: "eu", adoptionStatus: AVAILABLE })`, `speciesList`, type `AnimalProfile`. GraphiQL IDE in the dashboard. | +| OpenAPI 3 / Swagger | `/rest-api/docs/` (UI), `/rest-api/schema` (JSON) | Logged-in users only unless `API_DOCS_PUBLIC=true`. «Try it out» uses your WordPress session. | -Ejemplo de consulta GraphQL: +`profile` fields: + +| Field | Type | Values / notes | +|---|---|---| +| `status` | string | `available`, `pending`, `adopted` (adopted animals are hidden from public listings) | +| `featured` | boolean | shown on the home page | +| `sex` | string | `male`, `female` | +| `birthDate`, `arrivalDate` | date `YYYY-MM-DD` | nullable | +| `age`, `ageUnit` | number, `years`/`months` | approximate age when the birth date is unknown | +| `size` | string | `small`, `medium`, `large` (dogs) | +| `galleryIds` | int[] | extra photos (attachment IDs); `photos` (read-only) = main photo + gallery URLs | +| `video` | URL | YouTube, Vimeo, Instagram or a media file | +| `personality`, `needs`, `compatibility`, `idealHome` | string | **per language** (each translation has its own) | +| `language`, `labels` | read-only | language of the translation and human-readable values in that language | + +Shared fields (status, dates, size, photos…) are copied to every translation when written; texts are not. ```graphql { - animales(first: 10) { - nodes { title uri especies { nodes { name } } ficha { estado edad edadUnidad compatible textos { estado edad } } } + animals(first: 10, where: { language: "es", adoptionStatus: AVAILABLE }) { + nodes { title uri language speciesList { nodes { name } } profile { status sex featured size personality labels { age } } } } } ``` -Para escribir en la API REST (crear o editar animales) se usan las *contraseñas de aplicación* de WordPress (Usuarios → Perfil) con autenticación Basic sobre HTTPS; Swagger lo admite desde el botón «Authorize». Los mensajes del formulario no se exponen en ninguna API. +```bash +# Update an animal (application password: Users → Profile → Application passwords) +curl -u 'user:xxxx xxxx xxxx xxxx' -H 'Content-Type: application/json' \ + -d '{"profile":{"status":"pending","featured":true}}' \ + https://gureametsabilagun.pireon.dev/wp-json/wp/v2/animals/123 +``` + +Contact-form messages are never exposed by any API. + +> Note: the *WP API SwaggerUI* package on wordpress.org ships without its UI assets, so `/rest-api/docs/` is served by our own page (official Swagger UI, pinned version with SRI). The plugin is still used to generate the OpenAPI schema, which we post-process to document `profile`. ## Diseño diff --git a/wp-content/mu-plugins/gure-ametsa/animals.php b/wp-content/mu-plugins/gure-ametsa/animals.php index b09344a..1c9bed4 100644 --- a/wp-content/mu-plugins/gure-ametsa/animals.php +++ b/wp-content/mu-plugins/gure-ametsa/animals.php @@ -37,10 +37,10 @@ add_action( 'init', function () { 'menu_position' => 5, 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), 'show_in_rest' => true, - 'rest_base' => 'animales', + 'rest_base' => 'animals', 'show_in_graphql' => true, 'graphql_single_name' => 'animal', - 'graphql_plural_name' => 'animales', + 'graphql_plural_name' => 'animals', ) ); register_taxonomy( 'especie', 'animal', array( @@ -52,10 +52,10 @@ add_action( 'init', function () { 'hierarchical' => true, 'show_admin_column' => true, 'show_in_rest' => true, - 'rest_base' => 'especies', + 'rest_base' => 'species', 'show_in_graphql' => true, - 'graphql_single_name' => 'especie', - 'graphql_plural_name' => 'especies', + 'graphql_single_name' => 'species', + 'graphql_plural_name' => 'speciesList', 'rewrite' => array( 'slug' => 'especie', 'with_front' => false ), ) ); @@ -78,10 +78,11 @@ add_action( 'init', function () { ) ); foreach ( array_keys( ga_fields() ) as $key ) { + // Los metadatos internos no se exponen tal cual: la API usa el campo «profile» (en inglés, ver api.php). register_post_meta( 'animal', "_ga_$key", array( 'type' => 'string', 'single' => true, - 'show_in_rest' => true, + 'show_in_rest' => false, 'auth_callback' => fn() => current_user_can( 'edit_posts' ), ) ); } diff --git a/wp-content/mu-plugins/gure-ametsa/api.php b/wp-content/mu-plugins/gure-ametsa/api.php index 8e9c801..75d0388 100644 --- a/wp-content/mu-plugins/gure-ametsa/api.php +++ b/wp-content/mu-plugins/gure-ametsa/api.php @@ -1,110 +1,354 @@ ga_status( $post_id ), - 'destacado' => (bool) ga_get( $post_id, 'destacado' ), - 'sexo' => ga_get( $post_id, 'sexo' ), - 'nacimiento' => ga_get( $post_id, 'nacimiento' ), - 'edad' => '' === ga_get( $post_id, 'edad' ) ? null : (float) ga_get( $post_id, 'edad' ), - 'edad_unidad' => ga_get( $post_id, 'edad_unidad' ), - 'tamano' => ga_get( $post_id, 'tamano' ), - 'llegada' => ga_get( $post_id, 'llegada' ), - 'galeria' => array_map( fn( $id ) => wp_get_attachment_image_url( $id, 'large' ), ga_gallery_ids( $post_id ) ), - 'video' => ga_get( $post_id, 'video' ), - 'caracter' => ga_get( $post_id, 'caracter' ), - 'necesidades' => ga_get( $post_id, 'necesidades' ), - 'convivencia' => ga_get( $post_id, 'convivencia' ), - 'hogar' => ga_get( $post_id, 'hogar' ), - 'idioma' => $lang, +/** Is this request to one of the APIs (REST, GraphQL, OpenAPI schema/docs)? */ +function ga_is_api_request() { + static $is = null; + if ( null === $is ) { + $uri = (string) ( $_SERVER['REQUEST_URI'] ?? '' ); + $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); + $is = (bool) preg_match( '#^/(?:eu/)?(?:wp-json|graphql|rest-api/(?:schema|docs))(?:/|$)#', $path ) + || isset( $_GET['rest_route'] ) || isset( $_GET['graphql'] ); + } + return $is; +} + +/* The API speaks English (field descriptions, errors, core schema), whatever the site language. */ +foreach ( array( 'determine_locale', 'locale' ) as $ga_locale_hook ) { + add_filter( $ga_locale_hook, fn( $locale ) => ga_is_api_request() ? 'en_US' : $locale, 1000 ); +} + +/** Internal value => API value, per field. */ +function ga_api_enums() { + return array( + 'estado' => array( 'disponible' => 'available', 'proceso' => 'pending', 'adoptado' => 'adopted' ), + 'sexo' => array( 'macho' => 'male', 'hembra' => 'female' ), + 'tamano' => array( 'pequeno' => 'small', 'mediano' => 'medium', 'grande' => 'large' ), + 'edad_unidad' => array( 'anos' => 'years', 'meses' => 'months' ), ); - $data['textos'] = ga_with_lang( $lang, fn() => array( - 'estado' => ga_label( $post_id, 'estado' ), - 'sexo' => ga_label( $post_id, 'sexo' ), - 'edad' => ga_age( $post_id ), - 'tamano' => ga_label( $post_id, 'tamano' ), +} + +/** API property => internal field. Shared fields are common to every translation of the animal. */ +function ga_api_map() { + return array( + 'status' => 'estado', + 'featured' => 'destacado', + 'sex' => 'sexo', + 'birthDate' => 'nacimiento', + 'age' => 'edad', + 'ageUnit' => 'edad_unidad', + 'size' => 'tamano', + 'arrivalDate' => 'llegada', + 'galleryIds' => 'galeria', + 'video' => 'video', + 'personality' => 'caracter', + 'needs' => 'necesidades', + 'compatibility' => 'convivencia', + 'idealHome' => 'hogar', + ); +} + +function ga_api_to( $field, $value ) { + return ga_api_enums()[ $field ][ $value ] ?? ( '' === $value ? null : $value ); +} + +function ga_api_from( $field, $value ) { + $flip = array_flip( ga_api_enums()[ $field ] ?? array() ); + return $flip[ $value ] ?? null; +} + +/** Animal profile for the APIs (English keys and values, plus readable labels in the animal's language). */ +function ga_animal_api_data( $post_id ) { + $lang = function_exists( 'pll_get_post_language' ) ? ( pll_get_post_language( $post_id ) ?: 'es' ) : 'es'; + $gallery = array_values( array_filter( array_map( 'absint', explode( ',', ga_get( $post_id, 'galeria' ) ) ) ) ); + $text = fn( $key ) => '' === ga_get( $post_id, $key ) ? null : ga_get( $post_id, $key ); + + $data = array( + 'status' => ga_api_to( 'estado', ga_status( $post_id ) ), + 'featured' => (bool) ga_get( $post_id, 'destacado' ), + 'sex' => ga_api_to( 'sexo', ga_get( $post_id, 'sexo' ) ), + 'birthDate' => $text( 'nacimiento' ), + 'age' => '' === ga_get( $post_id, 'edad' ) ? null : (float) ga_get( $post_id, 'edad' ), + 'ageUnit' => ga_api_to( 'edad_unidad', ga_get( $post_id, 'edad_unidad' ) ), + 'size' => ga_api_to( 'tamano', ga_get( $post_id, 'tamano' ) ), + 'arrivalDate' => $text( 'llegada' ), + 'galleryIds' => $gallery, + 'photos' => array_values( array_filter( array_map( fn( $id ) => wp_get_attachment_image_url( $id, 'large' ), ga_gallery_ids( $post_id ) ) ) ), + 'video' => $text( 'video' ), + 'personality' => $text( 'caracter' ), + 'needs' => $text( 'necesidades' ), + 'compatibility' => $text( 'convivencia' ), + 'idealHome' => $text( 'hogar' ), + 'language' => $lang, + ); + $data['labels'] = ga_with_lang( $lang, fn() => array( + 'status' => ga_label( $post_id, 'estado' ), + 'sex' => ga_label( $post_id, 'sexo' ) ?: null, + 'age' => ga_age( $post_id ) ?: null, + 'size' => ga_label( $post_id, 'tamano' ) ?: null, ) ); return $data; } -add_action( 'rest_api_init', function () { - register_rest_field( 'animal', 'ficha', array( - 'get_callback' => fn( $post ) => ga_animal_api_data( $post['id'] ), - 'schema' => array( - 'description' => 'Ficha del animal (valores internos y textos legibles en su idioma).', - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - 'properties' => array( - 'estado' => array( 'type' => 'string', 'enum' => array( 'disponible', 'proceso', 'adoptado' ) ), - 'destacado' => array( 'type' => 'boolean' ), - 'sexo' => array( 'type' => 'string' ), - 'nacimiento' => array( 'type' => 'string' ), - 'edad' => array( 'type' => array( 'number', 'null' ) ), - 'edad_unidad' => array( 'type' => 'string' ), - 'tamano' => array( 'type' => 'string' ), - 'llegada' => array( 'type' => 'string' ), - 'galeria' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'format' => 'uri' ) ), - 'video' => array( 'type' => 'string' ), - 'caracter' => array( 'type' => 'string' ), - 'necesidades' => array( 'type' => 'string' ), - 'convivencia' => array( 'type' => 'string' ), - 'hogar' => array( 'type' => 'string' ), - 'idioma' => array( 'type' => 'string' ), - 'textos' => array( 'type' => 'object' ), - ), +/** Writes an API profile into the animal. Shared fields are copied to its translations. */ +function ga_animal_api_update( array $profile, $post_id ) { + $fields = ga_fields(); + foreach ( ga_api_map() as $prop => $key ) { + if ( ! array_key_exists( $prop, $profile ) ) { + continue; + } + $raw = $profile[ $prop ]; + switch ( $prop ) { + case 'status': + case 'sex': + case 'size': + case 'ageUnit': + $value = null === $raw ? '' : ga_api_from( $key, $raw ); + if ( null === $value ) { + return new WP_Error( 'rest_invalid_param', "Invalid value for profile.$prop.", array( 'status' => 400 ) ); + } + break; + case 'featured': + $value = $raw ? '1' : ''; + break; + case 'age': + $value = null === $raw ? '' : (string) absint( $raw ); + break; + case 'birthDate': + case 'arrivalDate': + $value = (string) $raw; + if ( '' !== $value && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) { + return new WP_Error( 'rest_invalid_param', "profile.$prop must be YYYY-MM-DD.", array( 'status' => 400 ) ); + } + break; + case 'galleryIds': + $value = implode( ',', array_filter( array_map( 'absint', (array) $raw ) ) ); + break; + case 'video': + $value = esc_url_raw( (string) $raw ); + break; + default: + $value = sanitize_textarea_field( (string) $raw ); + } + if ( ! empty( $fields[ $key ]['shared'] ) ) { + ga_update_meta_all_languages( $post_id, "_ga_$key", $value ); + } else { + update_post_meta( $post_id, "_ga_$key", $value ); + } + } + return true; +} + +/** JSON schema of the profile (used by REST validation and the OpenAPI docs). */ +function ga_animal_api_schema() { + $string_or_null = array( 'type' => array( 'string', 'null' ) ); + $enum = fn( $field ) => array( 'type' => array( 'string', 'null' ), 'enum' => array_merge( array_values( ga_api_enums()[ $field ] ), array( null ) ) ); + return array( + 'description' => 'Adoption profile of the animal. Shared fields are common to all translations; texts are per language.', + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'status' => array( 'type' => 'string', 'enum' => array_values( ga_api_enums()['estado'] ), 'description' => 'Adopted animals are hidden from public listings.' ), + 'featured' => array( 'type' => 'boolean', 'description' => 'Shown on the home page.' ), + 'sex' => $enum( 'sexo' ), + 'birthDate' => $string_or_null + array( 'format' => 'date' ), + 'age' => array( 'type' => array( 'number', 'null' ), 'description' => 'Approximate age, used when birthDate is unknown.' ), + 'ageUnit' => $enum( 'edad_unidad' ), + 'size' => $enum( 'tamano' ), + 'arrivalDate' => $string_or_null + array( 'format' => 'date', 'description' => 'Date the animal arrived at the shelter.' ), + 'galleryIds' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ), 'description' => 'Attachment IDs of the extra photos (the featured image is the main photo).' ), + 'photos' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'format' => 'uri' ), 'readonly' => true, 'description' => 'Main photo followed by the gallery.' ), + 'video' => $string_or_null + array( 'format' => 'uri' ), + 'personality' => $string_or_null, + 'needs' => $string_or_null, + 'compatibility' => $string_or_null + array( 'description' => 'Observed behaviour with children and other animals, in context.' ), + 'idealHome' => $string_or_null, + 'language' => array( 'type' => 'string', 'enum' => array( 'es', 'eu' ), 'readonly' => true ), + 'labels' => array( 'type' => 'object', 'readonly' => true, 'description' => 'Human-readable values in the animal\'s language.' ), ), + ); +} + +add_action( 'rest_api_init', function () { + register_rest_field( 'animal', 'profile', array( + 'get_callback' => fn( $post ) => ga_animal_api_data( $post['id'] ), + 'update_callback' => fn( $value, $post ) => ga_animal_api_update( (array) $value, $post->ID ), + 'schema' => ga_animal_api_schema(), ) ); } ); add_action( 'graphql_register_types', function () { - register_graphql_object_type( 'AnimalFichaTextos', array( - 'description' => 'Textos legibles de la ficha, en el idioma del animal.', + foreach ( array( + 'AnimalStatusEnum' => array( 'estado', 'Adoption status' ), + 'AnimalSexEnum' => array( 'sexo', 'Sex' ), + 'AnimalSizeEnum' => array( 'tamano', 'Size (dogs)' ), + 'AnimalAgeUnitEnum' => array( 'edad_unidad', 'Unit of the approximate age' ), + ) as $type => [ $field, $description ] ) { + $values = array(); + foreach ( ga_api_enums()[ $field ] as $api ) { + $values[ strtoupper( $api ) ] = array( 'value' => $api ); + } + register_graphql_enum_type( $type, array( 'description' => $description, 'values' => $values ) ); + } + register_graphql_object_type( 'AnimalProfileLabels', array( + 'description' => 'Human-readable values in the animal\'s language.', 'fields' => array( - 'estado' => array( 'type' => 'String' ), - 'sexo' => array( 'type' => 'String' ), - 'edad' => array( 'type' => 'String' ), - 'tamano' => array( 'type' => 'String' ), + 'status' => array( 'type' => 'String' ), + 'sex' => array( 'type' => 'String' ), + 'age' => array( 'type' => 'String' ), + 'size' => array( 'type' => 'String' ), ), ) ); - register_graphql_object_type( 'AnimalFicha', array( - 'description' => 'Ficha del animal en adopción.', + register_graphql_object_type( 'AnimalProfile', array( + 'description' => 'Adoption profile of the animal.', 'fields' => array( - 'estado' => array( 'type' => 'String', 'description' => 'disponible, proceso o adoptado' ), - 'destacado' => array( 'type' => 'Boolean' ), - 'sexo' => array( 'type' => 'String' ), - 'nacimiento' => array( 'type' => 'String' ), - 'edad' => array( 'type' => 'Float' ), - 'edadUnidad' => array( 'type' => 'String' ), - 'tamano' => array( 'type' => 'String' ), - 'llegada' => array( 'type' => 'String' ), - 'galeria' => array( 'type' => array( 'list_of' => 'String' ) ), - 'video' => array( 'type' => 'String' ), - 'caracter' => array( 'type' => 'String' ), - 'necesidades' => array( 'type' => 'String' ), - 'convivencia' => array( 'type' => 'String' ), - 'hogar' => array( 'type' => 'String' ), - 'idioma' => array( 'type' => 'String' ), - 'textos' => array( 'type' => 'AnimalFichaTextos' ), + 'status' => array( 'type' => 'AnimalStatusEnum' ), + 'featured' => array( 'type' => 'Boolean' ), + 'sex' => array( 'type' => 'AnimalSexEnum' ), + 'birthDate' => array( 'type' => 'String' ), + 'age' => array( 'type' => 'Float' ), + 'ageUnit' => array( 'type' => 'AnimalAgeUnitEnum' ), + 'size' => array( 'type' => 'AnimalSizeEnum' ), + 'arrivalDate' => array( 'type' => 'String' ), + 'photos' => array( 'type' => array( 'list_of' => 'String' ) ), + 'video' => array( 'type' => 'String' ), + 'personality' => array( 'type' => 'String' ), + 'needs' => array( 'type' => 'String' ), + 'compatibility' => array( 'type' => 'String' ), + 'idealHome' => array( 'type' => 'String' ), + 'language' => array( 'type' => 'String' ), + 'labels' => array( 'type' => 'AnimalProfileLabels' ), ), ) ); - register_graphql_field( 'Animal', 'ficha', array( - 'type' => 'AnimalFicha', - 'resolve' => function ( $post ) { - $data = ga_animal_api_data( $post->databaseId ); - $data['edadUnidad'] = $data['edad_unidad']; - return $data; - }, + register_graphql_field( 'Animal', 'profile', array( + 'type' => 'AnimalProfile', + 'resolve' => fn( $post ) => ga_animal_api_data( $post->databaseId ), + ) ); + register_graphql_field( 'Animal', 'language', array( + 'type' => 'String', + 'description' => 'Language of this translation (es or eu).', + 'resolve' => fn( $post ) => function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post->databaseId ) : 'es', + ) ); + register_graphql_field( 'RootQueryToAnimalConnectionWhereArgs', 'language', array( + 'type' => 'String', + 'description' => 'Only animals in this language (es or eu).', + ) ); + register_graphql_field( 'RootQueryToAnimalConnectionWhereArgs', 'adoptionStatus', array( + 'type' => 'AnimalStatusEnum', + 'description' => 'Only animals with this adoption status.', ) ); } ); +/* GraphQL: «where: { language, adoptionStatus }» en la consulta de animales («status» ya es el estado de publicación de WordPress). */ +add_filter( 'graphql_post_object_connection_query_args', function ( $args, $source, $input ) { + if ( 'animal' !== ( $args['post_type'] ?? '' ) && array( 'animal' ) !== ( $args['post_type'] ?? '' ) ) { + return $args; + } + $where = $input['where'] ?? array(); + $args['lang'] = in_array( $where['language'] ?? '', array( 'es', 'eu' ), true ) ? $where['language'] : ''; + if ( ! empty( $where['adoptionStatus'] ) ) { + $internal = ga_api_from( 'estado', $where['adoptionStatus'] ); + $values = 'proceso' === $internal ? array( 'proceso', 'reservado' ) : array( $internal ); + $args['meta_query'][] = 'disponible' === $internal + ? array( 'relation' => 'OR', array( 'key' => '_ga_estado', 'value' => array( 'disponible', 'urgente', 'acogida' ), 'compare' => 'IN' ), array( 'key' => '_ga_estado', 'compare' => 'NOT EXISTS' ) ) + : array( 'key' => '_ga_estado', 'value' => $values, 'compare' => 'IN' ); + } + return $args; +}, 10, 3 ); + +/** Converts a WordPress JSON schema into an OpenAPI 3.0 schema (nullable types, readOnly). */ +function ga_openapi_schema( $schema ) { + if ( isset( $schema['type'] ) && is_array( $schema['type'] ) ) { + $types = array_values( array_diff( $schema['type'], array( 'null' ) ) ); + if ( count( $types ) < count( $schema['type'] ) ) { + $schema['nullable'] = true; + } + $schema['type'] = $types[0] ?? 'string'; + } + if ( isset( $schema['enum'] ) ) { + $schema['enum'] = array_values( array_filter( $schema['enum'], fn( $v ) => null !== $v ) ); + } + if ( ! empty( $schema['readonly'] ) ) { + $schema['readOnly'] = true; + } + unset( $schema['readonly'], $schema['context'] ); + foreach ( array( 'properties' ) as $key ) { + if ( isset( $schema[ $key ] ) ) { + $schema[ $key ] = array_map( 'ga_openapi_schema', $schema[ $key ] ); + } + } + if ( isset( $schema['items'] ) ) { + $schema['items'] = ga_openapi_schema( $schema['items'] ); + } + return $schema; +} + +/* + * OpenAPI schema (/rest-api/schema): the plugin flattens object fields to "string". + * We post-process its output to document «profile» properly (component AnimalProfile). + */ +add_action( 'wp', function () { + if ( 'schema' !== get_query_var( 'swagger_api' ) ) { + return; + } + ob_start( function ( $json ) { + $doc = json_decode( $json, true ); + if ( ! is_array( $doc ) || empty( $doc['paths'] ) ) { + return $json; + } + $profile = ga_openapi_schema( ga_animal_api_schema() ); + $ref = array( '$ref' => '#/components/schemas/AnimalProfile' ); + $doc['components']['schemas']['AnimalProfile'] = $profile; + $doc['components']['schemas']['Animal'] = array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'slug' => array( 'type' => 'string' ), + 'link' => array( 'type' => 'string', 'format' => 'uri' ), + 'title' => array( 'type' => 'object', 'properties' => array( 'rendered' => array( 'type' => 'string' ) ) ), + 'content' => array( 'type' => 'object', 'properties' => array( 'rendered' => array( 'type' => 'string' ) ) ), + 'species' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ) ), + 'lang' => array( 'type' => 'string', 'description' => 'Language of this translation (es or eu).' ), + 'profile' => $ref, + ), + ); + foreach ( $doc['paths'] as $path => &$methods ) { + if ( ! preg_match( '#^/wp/v2/animals(/\{id\})?$#', $path, $m ) ) { + continue; + } + foreach ( $methods as $method => &$op ) { + foreach ( $op['requestBody']['content'] ?? array() as $type => $content ) { + if ( isset( $content['schema']['properties']['profile'] ) ) { + $op['requestBody']['content'][ $type ]['schema']['properties']['profile'] = $ref; + } + } + // JSON first: «profile» is a nested object, which form-encoded bodies can't express well. + if ( ! empty( $op['requestBody']['content'] ) && empty( $op['requestBody']['content']['application/json'] ) ) { + $first = reset( $op['requestBody']['content'] ); + $op['requestBody']['content'] = array( 'application/json' => $first ) + $op['requestBody']['content']; + } + if ( 'get' === $method ) { + $item = array( '$ref' => '#/components/schemas/Animal' ); + $op['responses']['200'] = array( + 'description' => 'OK', + 'content' => array( 'application/json' => array( 'schema' => empty( $m[1] ) ? array( 'type' => 'array', 'items' => $item ) : $item ) ), + ); + } + } + unset( $op ); + } + unset( $methods ); + return wp_json_encode( $doc ); + } ); +}, 0 ); // Antes que el plugin, que emite el esquema en «wp». + /* * Documentación Swagger (/rest-api/docs/). * @@ -132,7 +376,7 @@ add_action( 'template_redirect', function () { header( 'Content-Type: text/html; charset=utf-8' ); ?> - + @@ -143,10 +387,10 @@ add_action( 'template_redirect', function () {
- · API REST · - Esquema OpenAPI · + · REST API · + OpenAPI schema · GraphQL · - Volver al panel + Dashboard