diff --git a/.gitignore b/.gitignore index 5212e87..c84d594 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ node_modules /coverage /vendor/ composer.phar -/.idea \ No newline at end of file +/.idea +.aider* +.env diff --git a/schema/README.md b/schema/README.md new file mode 100644 index 0000000..b875bdd --- /dev/null +++ b/schema/README.md @@ -0,0 +1,161 @@ +# Meeting Guide API Schemas + +## Overview + +This directory contains JSON schemas used for validating meeting data structures in the Meeting Guide API. These schemas are designed to ensure data consistency and integrity for applications that manage meeting information. These schemas are based on the JSON Schema Draft-07 specification. + +### Schemas Included + +1. **meeting.schema.json** + - **Description**: Defines the structure for individual meeting entries, including details such as name, location, time, and contact information. Only `name` and `slug` are required but additional properties should be used when possible. + - **Key Properties**: + +| Property | Description | Type | +|------------------------|-----------------------------------------------------------------------------------------------|---------------| +| `name` | The name of the meeting, max 64 characters. | string | +| `slug` | A unique identifier for the meeting, used as the primary key, max 255 characters. | string | +| `day` | The day(s) of the week the meeting occurs (integer or array of integers 0-6). | integer/array of integers | +| `time` | The start time of the meeting in HH:MM 24-hour format. | string | +| `end_time` | The optional end time of the meeting in HH:MM 24-hour format. | string | +| `timezone` | The optional timezone of the meeting in tz database format. | string | +| `formatted_address` | The complete address of the meeting location. | string | +| `address` | The street address of the meeting location. | string | +| `city` | The city where the meeting is held. | string | +| `state` | The state where the meeting is held, 2 uppercase letters. | string | +| `postal_code` | The postal code of the meeting location, 5 digits. | string | +| `country` | The country where the meeting is held. | string | +| `region` | An optional geographical subset of meeting locations. | string | +| `sub_region` | An optional further specification of the region. | string | +| `regions` | A hierarchical array of regions, from general to specific. | array of strings | +| `conference_url` | The URL for an online meeting, URI format. | string | +| `conference_url_notes` | Optional metadata about the conference URL. | string | +| `conference_phone` | The phone number for dialing into an online meeting. | string | +| `conference_phone_notes`| Optional metadata about the conference phone number. | string | +| `types` | An optional array of standardized meeting types. | array of strings | +| `notes` | Optional additional details about the meeting. | string | +| `location` | The name of the building or landmark where the meeting is held. | string | +| `location_notes` | Optional notes applicable to all meetings at the location. | string | +| `latitude` | The optional latitude of the meeting location. | number | +| `longitude` | The optional longitude of the meeting location. | number | +| `approximate` | Indicates if the address is approximate ("yes" or "no"). | string | +| `updated` | The optional timestamp indicating when the listing was last updated, in YYYY-MM-DD HH:MM:SS format. | string | +| `group` | The optional name of the group providing the meeting. | string | +| `group_notes` | Optional group-related notes. | string | +| `venmo` | The optional Venmo handle for contributions. | string | +| `square` | The optional Square Cash App cashtag for contributions. | string | +| `paypal` | The optional PayPal.me username for contributions. | string | +| `url` | The optional URL pointing to the meeting's listing on the area website, URI format. | string | +| `edit_url` | The optional URL that trusted servants can use to edit the meeting's listing, URI format. | string | +| `feedback_emails` | An optional array of feedback email addresses for the service entity. | array of strings | +| `feedback_url` | The optional URL for providing feedback about the meeting, URI format. | string | +| `entity` | The name of the service entity responsible for the listing. | string | +| `entity_email` | The public email address for the service entity, email format. | string | +| `entity_location` | A description of the service area of the entity. | string | +| `entity_logo` | The URL of the logo of the service entity, URI format. | string | +| `entity_phone` | The phone number of the service entity. | string | +| `entity_website_url` | The website homepage of the service entity, URI format. | string | +| `contact_1_name` | The name of the first contact person for the meeting. | string | +| `contact_1_email` | The email address of the first contact person, email format. | string | +| `contact_1_phone` | The phone number of the first contact person. | string | +| `contact_2_name` | The name of the second contact person for the meeting. | string | +| `contact_2_email` | The email address of the second contact person, email format. | string | +| `contact_2_phone` | The phone number of the second contact person. | string | +| `contact_3_name` | The name of the third contact person for the meeting. | string | +| `contact_3_email` | The email address of the third contact person, email format. | string | +| `contact_3_phone` | The phone number of the third contact person. | string | + +2. **meeting-type.schema.json** + - **Description**: Lists available, proposed, and proposed changed meeting types as enumerations. + +3. **feed.schema.json** + - **Description**: Defines the structure of a feed containing multiple meeting entries. + +## Usage + +These schemas can be used to validate JSON data structures in applications that require structured meeting information. They help ensure that data conforms to expected formats and constraints, reducing errors and improving data quality. + +### PHP Example + +Here is an example of how you might use a PHP library to validate data against the `meeting.schema.json`: + +```php +validate($data, $schema, Constraint::CHECK_MODE_APPLY_DEFAULTS); + +if ($validator->isValid()) { + echo "The supplied JSON validates against the schema.\n"; +} else { + echo "JSON does not validate. Violations:\n"; + foreach ($validator->getErrors() as $error) { + echo sprintf("[%s] %s\n", $error['property'], $error['message']); + } +} +``` + +### Typescript Example + +*(tested with `bun`)* + +Here is an example of using json-schema-to-zod to generate typescript types and validation schemas (with zod) from the `meeting.schema.json`: + +```typescript +import { jsonSchemaToZod } from "json-schema-to-zod"; +import { resolveRefs } from "json-refs"; +import { format } from "prettier"; +import {writeFileSync} from 'fs' + +const schemaUrl = 'https://raw.githubusercontent.com/code4recovery/spec/refs/heads/main/schema/meeting.schema.json' + +const generateTypes = async () => { + // Get Json Schema from URL + const schemaObject = await (await fetch(schemaUrl)).json(); + + // Resolve all $refs in the schema object (ie. meeting-type.schema.json) + const { resolved } = await resolveRefs(schemaObject); + + // Convert the schema to Zod types + const code = jsonSchemaToZod(resolved, { name: "MeetingSchema", module: "esm", type: true }); + + // Format the code + const formatted = await format(code, { parser: "typescript" }); + return formatted +} + +generateTypes().then((formatted: string) => { + console.log(formatted) +}) +``` + +### JavaScript Example + +Here is an example of how you might use the ajv library in JavaScript to validate data against the `meeting.schema.json`: + +```javascript +const Ajv = require('ajv'); +const ajv = new Ajv(); +const meetingSchema = require('./spec/meeting.schema.json'); +const data = { + "name": "Weekly Meditation Group", + "slug": "weekly-meditation-group", + "day": 1, + "time": "19:30", + "formatted_address": "123 Main St, Springfield, IL 62701, USA" +}; + +const validate = ajv.compile(meetingSchema); +const valid = validate(data); +if (!valid) console.log(validate.errors); +``` + +## Contribution + +Contributions to improve and expand the schemas are welcome. Please ensure any changes are compatible with JSON Schema Draft-07 and include appropriate documentation. diff --git a/schema/example.json b/schema/example.json new file mode 100644 index 0000000..aca9e17 --- /dev/null +++ b/schema/example.json @@ -0,0 +1,33 @@ +{ + "$schema": "./meeting.schema.json", + "name": "Sunday Serenity", + "slug": "sunday-serenity-14", + "day": 0, + "time": "18:00", + "end_time": "19:30", + "location": "Alano Club", + "group": "The Serenity Group", + "notes": "Ring buzzer. Meeting is on the 2nd floor.", + "updated": "2014-05-31 14:32:23", + "url": "https://district123.org/meetings/sunday-serenity", + "types": [ + "O", + "T", + "LGBTQ" + ], + "address": "123 Main Street", + "city": "Anytown", + "state": "CA", + "postal_code": "98765", + "country": "US", + "approximate": "no", + "entity": "District 123", + "entity_email": "info@district123.org", + "entity_location": "Example County, California", + "entity_logo": "https://district123.org/images/logo.svg", + "entity_phone": "+1-123-456-7890", + "entity_url": "https://district123.org", + "feedback_emails": [ + "meetingupdates@district123.org" + ] +} \ No newline at end of file diff --git a/schema/feed.schema.json b/schema/feed.schema.json new file mode 100644 index 0000000..4cefd94 --- /dev/null +++ b/schema/feed.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/code4recovery/spec/refs/heads/main/schema/feed.schema.json", + "title": "Meeting Guide API Schema", + "description": "The feed of meetings", + "type": "array", + "items": { + "$ref": "https://raw.githubusercontent.com/code4recovery/spec/refs/heads/main/schema/meeting.schema.json" + } +} \ No newline at end of file diff --git a/schema/meeting-type.schema.json b/schema/meeting-type.schema.json new file mode 100644 index 0000000..38f4521 --- /dev/null +++ b/schema/meeting-type.schema.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/code4recovery/spec/refs/heads/main/schema/meeting-type.schema.json", + "title": "Meeting Guide API Schema", + "description": "The meeting types available", + "type": "array", + "items": { + "anyOf": [ + { + "$comment": "Currently Available Types", + "type": "string", + "enum": [ + "11", + "12x12", + "A", + "ABSI", + "AL", + "AL-AN", + "AM", + "ASL", + "B", + "BA", + "BE", + "BI", + "BRK", + "C", + "CAN", + "CF", + "D", + "DA", + "DB", + "DD", + "DE", + "DR", + "EL", + "EN", + "FA", + "FF", + "FR", + "G", + "GR", + "H", + "HE", + "HI", + "HR", + "HU", + "ITA", + "JA", + "KOR", + "L", + "LGBTQ", + "LIT", + "LS", + "LT", + "M", + "MED", + "ML", + "N", + "NB", + "NDG", + "NO", + "O", + "OUT", + "P", + "POA", + "POC", + "POL", + "POR", + "PUN", + "RUS", + "S", + "SEN", + "SK", + "SM", + "SP", + "ST", + "SV", + "T", + "TC", + "TH", + "TL", + "TR", + "TUR", + "UK", + "W", + "X", + "XB", + "XT", + "Y" + ] + }, + { + "$comment": "Proposed Types", + "type": "string", + "enum": [ + "BV-I", + "D-HOH", + "LO-I", + "QSL", + "RSL" + ] + }, + { + "$comment": "Proposed Changed Types", + "type": "string", + "enum": [ + "LGBTQIAA+" + ] + } + ] + } +} \ No newline at end of file diff --git a/schema/meeting.schema.json b/schema/meeting.schema.json new file mode 100644 index 0000000..73c36ac --- /dev/null +++ b/schema/meeting.schema.json @@ -0,0 +1,488 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/code4recovery/spec/refs/heads/main/schema/meeting.schema.json", + "title": "Meeting Guide API Schema", + "description": "The meeting guide API schema", + "type": "object", + "required": [ + "name", + "slug" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "description": "The meeting name, which should be fewer than 64 characters to avoid truncation.", + "examples": [ + "Weekly Meditation Group" + ] + }, + "slug": { + "type": "string", + "maxLength": 64, + "description": "A unique identifier for the meeting, used as the primary key and in bookmark URLs. Must be URL-safe.", + "examples": [ + "weekly-meditation-group" + ] + }, + "day": { + "type": [ + "integer", + "array" + ], + "description": "The day(s) of the week the meeting occurs, represented as an integer (0 for Sunday to 6 for Saturday). Required for weekly meetings.", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 6 + }, + "examples": [ + 1, + [ + 1, + 3, + 5 + ] + ] + }, + "time": { + "type": "string", + "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$", + "description": "The start time of the meeting in HH:MM 24-hour format. Required for weekly meetings.", + "examples": [ + "19:30" + ] + }, + "end_time": { + "type": "string", + "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$", + "description": "The optional end time of the meeting in HH:MM 24-hour format. Defaults to one hour after the start time if omitted.", + "optional": true, + "examples": [ + "20:30" + ] + }, + "timezone": { + "type": "string", + "description": "The optional timezone of the meeting in tz database format (e.g., America/New_York).", + "optional": true, + "examples": [ + "America/New_York" + ] + }, + "formatted_address": { + "type": "string", + "pattern": "^(?:[0-9]+\\s)?[0-9]*\\s?[A-Za-z]+(?:\\s[A-Za-z]+)*,\\s[A-Za-z]+(?:\\s[A-Za-z]+)*,\\s[A-Z]{2}\\s[0-9]{5},\\s[A-Za-z]+$", + "description": "The complete address of the meeting location, used for geocoding.", + "optional": true, + "examples": [ + "123 Main St, Springfield, IL 62701, USA", + "4953 W Addison St, Chicago, IL 60641, USA" + ] + }, + "address": { + "type": "string", + "pattern": "^(?:[0-9]+\\s)?[0-9]*\\s?[A-Za-z]+(?:\\s[A-Za-z]+)*$", + "description": "The street address of the meeting location.", + "optional": true, + "examples": [ + "123 Main St", + "4953 W Addison St", + "Main St" + ] + }, + "city": { + "type": "string", + "pattern": "^[A-Za-z]+(?:\\s[A-Za-z]+)*$", + "description": "The city where the meeting is held.", + "optional": true, + "examples": [ + "Springfield" + ] + }, + "state": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "The state where the meeting is held.", + "optional": true, + "examples": [ + "IL" + ] + }, + "postal_code": { + "type": "string", + "pattern": "^[0-9]{5}$", + "description": "The postal code of the meeting location.", + "optional": true, + "examples": [ + "62701" + ] + }, + "country": { + "type": "string", + "description": "The country where the meeting is held.", + "optional": true, + "examples": [ + "USA" + ] + }, + "region": { + "type": "string", + "description": "An optional geographical subset of meeting locations, such as a neighborhood or city.", + "optional": true, + "examples": [ + "Downtown" + ] + }, + "sub_region": { + "type": "string", + "description": "An optional further specification of the region.", + "optional": true, + "examples": [ + "Historic District" + ] + }, + "regions": { + "type": "array", + "description": "A hierarchical array of regions, from general to specific.", + "items": { + "type": "string" + }, + "optional": true, + "examples": [ + [ + "Springfield", + "Downtown", + "Historic District" + ] + ] + }, + "conference_url": { + "type": "string", + "format": "uri", + "description": "The URL for an online meeting, should launch directly into the meeting.", + "optional": true, + "examples": [ + "https://zoom.us/j/123456789" + ] + }, + "conference_url_notes": { + "type": "string", + "description": "Optional metadata about the conference URL, such as a meeting password.", + "optional": true, + "examples": [ + "Password: 1234" + ] + }, + "conference_phone": { + "type": "string", + "pattern": "^[+]?\\d{1,15}(?:[,*#]\\d+)*$", + "description": "The phone number for dialing into an online meeting. Should be numeric, except a + symbol may be used for international dialers, and ,, *, and # can be used to form one-tap phone links.", + "optional": true, + "examples": [ + "+1234567890", + "+441234567890", + "+8613800138000", + "1234567890*567", + "+1234567890,1234#" + ] + }, + "conference_phone_notes": { + "type": "string", + "description": "Optional metadata about the conference phone number, such as user instructions.", + "optional": true, + "examples": [ + "Dial-in PIN: 5678" + ] + }, + "types": { + "type": "array", + "description": "An optional array of standardized meeting types.", + "$ref": "https://raw.githubusercontent.com/code4recovery/spec/refs/heads/main/schema/meeting-type.schema.json", + "optional": true, + "examples": [ + [ + "O", + "D", + "EN" + ], + [ + "11", + "12x12", + "A", + "O" + ] + ] + }, + "notes": { + "type": "string", + "description": "Optional additional details about the meeting. Plain text only.", + "optional": true, + "examples": [ + "Bring your own coffee." + ] + }, + "location": { + "type": "string", + "description": "The name of the building or landmark where the meeting is held.", + "optional": true, + "examples": [ + "Community Center" + ] + }, + "location_notes": { + "type": "string", + "description": "Optional notes applicable to all meetings at the location.", + "optional": true, + "examples": [ + "Enter through the side door." + ] + }, + "latitude": { + "type": "number", + "description": "The optional latitude of the meeting location.", + "optional": true, + "examples": [ + 39.7817 + ] + }, + "longitude": { + "type": "number", + "description": "The optional longitude of the meeting location.", + "optional": true, + "examples": [ + -89.6501 + ] + }, + "approximate": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "description": "Indicates if the address is approximate.", + "optional": true, + "examples": [ + "no" + ] + }, + "updated": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$", + "description": "Updated is an optional UTC timestamp in the format YYYY-MM-DD HH:MM:SS and indicates when the listing was last updated.", + "optional": true, + "examples": [ + "2023-10-01 14:30:00", + "2022-12-31 23:59:59" + ] + }, + "group": { + "type": "string", + "description": "The optional name of the group providing the meeting.", + "optional": true, + "examples": [ + "Meditation Enthusiasts" + ] + }, + "group_notes": { + "type": "string", + "description": "Optional group-related notes. Plain text only.", + "optional": true, + "examples": [ + "Group meets bi-weekly." + ] + }, + "venmo": { + "type": "string", + "description": "The optional Venmo handle for contributions.", + "optional": true, + "examples": [ + "@MeditationGroup" + ] + }, + "square": { + "type": "string", + "description": "The optional Square Cash App cashtag for contributions.", + "optional": true, + "examples": [ + "$MeditationGroup" + ] + }, + "paypal": { + "type": "string", + "description": "The optional PayPal.me username for contributions.", + "optional": true, + "examples": [ + "paypal.me/MeditationGroup" + ] + }, + "url": { + "type": "string", + "format": "uri", + "description": "The optional URL pointing to the meeting's listing on the area website.", + "optional": true, + "examples": [ + "https://meetings.example.com/meditation" + ] + }, + "edit_url": { + "type": "string", + "format": "uri", + "description": "The optional URL that trusted servants can use to edit the meeting's listing.", + "optional": true, + "examples": [ + "https://meetings.example.com/edit/meditation" + ] + }, + "feedback_emails": { + "type": "array", + "description": "An optional array of feedback email addresses for the service entity.", + "items": { + "type": "string", + "format": "email" + }, + "optional": true, + "examples": [ + [ + "feedback@example.com", + "feedback2@example.com" + ] + ] + }, + "feedback_url": { + "type": "string", + "format": "uri", + "description": "The optional URL for providing feedback about the meeting.", + "optional": true, + "examples": [ + "https://meetings.example.com/feedback" + ] + }, + "entity": { + "type": "string", + "description": "The name of the service entity responsible for the listing. Required if any other entity fields are present.", + "optional": true, + "examples": [ + "Meditation Group Services" + ] + }, + "entity_email": { + "type": "string", + "format": "email", + "description": "The public email address for the service entity.", + "optional": true, + "examples": [ + "contact@meditationgroup.com" + ] + }, + "entity_location": { + "type": "string", + "description": "A description of the service area of the entity.", + "optional": true, + "examples": [ + "Serving the Springfield area" + ] + }, + "entity_logo": { + "type": "string", + "format": "uri", + "description": "The URL of the logo of the service entity.", + "optional": true, + "examples": [ + "https://meditationgroup.com/logo.png" + ] + }, + "entity_phone": { + "type": "string", + "description": "The phone number of the service entity.", + "optional": true, + "examples": [ + "+11234567890" + ] + }, + "entity_website_url": { + "type": "string", + "format": "uri", + "description": "The website homepage of the service entity.", + "optional": true, + "examples": [ + "https://meditationgroup.com" + ] + }, + "contact_1_name": { + "type": "string", + "description": "The name of the first contact person for the meeting.", + "optional": true, + "examples": [ + "John" + ] + }, + "contact_1_email": { + "type": "string", + "format": "email", + "description": "The email address of the first contact person.", + "optional": true, + "examples": [ + "john.doe@example.com" + ] + }, + "contact_1_phone": { + "type": "string", + "description": "The phone number of the first contact person.", + "optional": true, + "examples": [ + "+19876543210" + ] + }, + "contact_2_name": { + "type": "string", + "description": "The name of the second contact person for the meeting.", + "optional": true, + "examples": [ + "Jane" + ] + }, + "contact_2_email": { + "type": "string", + "format": "email", + "description": "The email address of the second contact person.", + "optional": true, + "examples": [ + "jane.smith@example.com" + ] + }, + "contact_2_phone": { + "type": "string", + "description": "The phone number of the second contact person.", + "optional": true, + "examples": [ + "+10987654321" + ] + }, + "contact_3_name": { + "type": "string", + "description": "The name of the third contact person for the meeting.", + "optional": true, + "examples": [ + "Alice" + ] + }, + "contact_3_email": { + "type": "string", + "format": "email", + "description": "The email address of the third contact person.", + "optional": true, + "examples": [ + "alice.johnson@example.com" + ] + }, + "contact_3_phone": { + "type": "string", + "description": "The phone number of the third contact person.", + "optional": true, + "examples": [ + "+10123456789" + ] + } + } +} \ No newline at end of file