From ab96a683ac373fe03e6dcf659bb06079130dde5e Mon Sep 17 00:00:00 2001 From: jcpitre Date: Wed, 27 May 2026 12:52:21 -0400 Subject: [PATCH 1/2] Add GBFS v3.1-RC3 support --- gbfs-validator-java/pom.xml | 6 +- .../validator/GbfsJsonValidator.java | 68 ++++++--- .../validator/versions/Version30.java | 55 ++++--- .../validator/versions/Version31RC3.java | 52 +++++++ .../validator/versions/VersionFactory.java | 27 ++++ .../validator/GbfsJsonValidatorTest.java | 87 ++++++++++- .../resources/fixtures/v3.1-RC3/gbfs.json | 17 +++ .../fixtures/v3.1-RC3/gbfs_versions.json | 17 +++ .../fixtures/v3.1-RC3/geofencing_zones.json | 74 +++++++++ .../resources/fixtures/v3.1-RC3/manifest.json | 115 ++++++++++++++ .../v3.1-RC3/station_information.json | 57 +++++++ .../fixtures/v3.1-RC3/station_status.json | 71 +++++++++ .../fixtures/v3.1-RC3/system_alerts.json | 43 ++++++ .../fixtures/v3.1-RC3/system_information.json | 68 +++++++++ .../v3.1-RC3/system_pricing_plans.json | 46 ++++++ .../fixtures/v3.1-RC3/system_regions.json | 45 ++++++ .../v3.1-RC3/vehicle_availability.json | 27 ++++ .../fixtures/v3.1-RC3/vehicle_status.json | 32 ++++ .../fixtures/v3.1-RC3/vehicle_types.json | 141 ++++++++++++++++++ 19 files changed, 990 insertions(+), 58 deletions(-) create mode 100644 gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version31RC3.java create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs_versions.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/geofencing_zones.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/manifest.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_information.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_status.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_alerts.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_information.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_pricing_plans.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_regions.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_availability.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_status.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_types.json diff --git a/gbfs-validator-java/pom.xml b/gbfs-validator-java/pom.xml index c91a6aa8..78e20724 100644 --- a/gbfs-validator-java/pom.xml +++ b/gbfs-validator-java/pom.xml @@ -51,8 +51,8 @@ 17 - https://github.com/MobilityData/gbfs-json-schema/archive/refs/tags/v4.0.0.zip - 4.0.0 + https://github.com/MobilityData/gbfs-json-schema/archive/refs/tags/v4.3.0.zip + 4.3.0 1.14.6 2.0.17 @@ -310,7 +310,7 @@ maven-javadoc-plugin ${maven-javadoc-plugin.version} - -Xdoclint:none + -Xdoclint:none diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java index 18a83be6..df7d8dbd 100644 --- a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java +++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java @@ -77,12 +77,13 @@ private record ParsedFeedContainer( "station_status", "free_bike_status", "vehicle_status", + "vehicle_availability", + "manifest", "system_hours", - "system_alerts", - "system_alerts", "system_calendar", "system_regions", "system_pricing_plans", + "system_alerts", "geofencing_zones" ); @@ -110,17 +111,10 @@ public ValidationResult validate(Map rawFeeds) { } if (parsedContainer.jsonObject() == null) { - // Parsing failed or stream read error - FileValidationResult result = new FileValidationResult( + FileValidationResult result = createParsingErrorResult( feedName, - version.isFileRequired(feedName), - true, - 0, - version.getSchema(feedName).toString(), - parsedContainer.originalContent(), - null, - Collections.emptyList(), - parsedContainer.parsingErrors() + parsedContainer, + version ); fileValidations.put(feedName, result); } else { @@ -183,19 +177,10 @@ public FileValidationResult validateFile(String fileName, InputStream file) { ParsedFeedContainer parsedContainer = parseFeed(fileName, file); if (parsedContainer.jsonObject() == null) { - // Determine version for schema and requirement - this is tricky for a single file - // For now, using default version. A more robust approach might require context. - Version tempVersion = VersionFactory.createVersion(DEFAULT_VERSION); - return new FileValidationResult( + return createParsingErrorResult( fileName, - tempVersion.isFileRequired(fileName), - true, // File was provided - 0, - tempVersion.getSchema(fileName).toString(), - parsedContainer.originalContent(), - null, // File specific version unknown - Collections.emptyList(), - parsedContainer.parsingErrors() + parsedContainer, + VersionFactory.createVersion(DEFAULT_VERSION) ); } else { return validateFile( @@ -350,4 +335,39 @@ private ParsedFeedContainer parseFeed(String name, InputStream raw) { ); } } + + private FileValidationResult createParsingErrorResult( + String feedName, + ParsedFeedContainer parsedContainer, + Version preferredVersion + ) { + Version schemaVersion = resolveVersionForFeed(feedName, preferredVersion); + boolean supportedFeed = schemaVersion.getFileNames().contains(feedName); + + return new FileValidationResult( + feedName, + supportedFeed && schemaVersion.isFileRequired(feedName), + true, + 0, + supportedFeed ? schemaVersion.getSchema(feedName).toString() : null, + parsedContainer.originalContent(), + null, + Collections.emptyList(), + parsedContainer.parsingErrors() + ); + } + + private Version resolveVersionForFeed( + String feedName, + Version preferredVersion + ) { + if (preferredVersion.getFileNames().contains(feedName)) { + return preferredVersion; + } + + return VersionFactory.createLatestVersionSupportingFeed( + feedName, + preferredVersion.getVersionString() + ); + } } diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version30.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version30.java index 2363611b..64f3a130 100644 --- a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version30.java +++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version30.java @@ -36,7 +36,7 @@ public class Version30 extends AbstractVersion { public static final String VERSION = "3.0"; - private static final List feeds = Arrays.asList( + private static final List FEEDS = Arrays.asList( "gbfs", "gbfs_versions", "system_information", @@ -51,37 +51,36 @@ public class Version30 extends AbstractVersion { "geofencing_zones" ); - private static final Map> customRules = - Map.of( - "vehicle_types", - List.of(new NoInvalidReferenceToPricingPlansInVehicleTypes()), - "station_status", - List.of( - new NoInvalidReferenceToVehicleTypesInStationStatus(), - new NoMissingVehicleTypesAvailableWhenVehicleTypesExists(), - new NoInvalidReferenceToStation("station_information") + static final Map> CUSTOM_RULES = Map.of( + "vehicle_types", + List.of(new NoInvalidReferenceToPricingPlansInVehicleTypes()), + "station_status", + List.of( + new NoInvalidReferenceToVehicleTypesInStationStatus(), + new NoMissingVehicleTypesAvailableWhenVehicleTypesExists(), + new NoInvalidReferenceToStation("station_information") + ), + "vehicle_status", + List.of( + new NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist( + "vehicle_status" ), - "vehicle_status", - List.of( - new NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist( - "vehicle_status" - ), - new NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles( - "vehicle_status" - ), - new NoInvalidReferenceToPricingPlansInVehicleStatus("vehicle_status") + new NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles( + "vehicle_status" ), - "system_information", - List.of(new NoMissingStoreUriInSystemInformation("vehicle_status")), - "station_information", - List.of( - new NoInvalidReferenceToRegionInStationInformation(), - new NoInvalidReferenceToStation("station_status") - ) - ); + new NoInvalidReferenceToPricingPlansInVehicleStatus("vehicle_status") + ), + "system_information", + List.of(new NoMissingStoreUriInSystemInformation("vehicle_status")), + "station_information", + List.of( + new NoInvalidReferenceToRegionInStationInformation(), + new NoInvalidReferenceToStation("station_status") + ) + ); protected Version30() { - super(VERSION, feeds, customRules); + super(VERSION, FEEDS, CUSTOM_RULES); } @Override diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version31RC3.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version31RC3.java new file mode 100644 index 00000000..a7a09555 --- /dev/null +++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/Version31RC3.java @@ -0,0 +1,52 @@ +/* + * + * + * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * * the European Commission - subsequent versions of the EUPL (the "Licence"); + * * You may not use this work except in compliance with the Licence. + * * You may obtain a copy of the Licence at: + * * + * * https://joinup.ec.europa.eu/software/page/eupl + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the Licence is distributed on an "AS IS" basis, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the Licence for the specific language governing permissions and + * * limitations under the Licence. + * + */ + +package org.mobilitydata.gbfs.validation.validator.versions; + +import java.util.Arrays; +import java.util.List; + +public class Version31RC3 extends AbstractVersion { + + public static final String VERSION = "3.1-RC3"; + + private static final List FEEDS = Arrays.asList( + "gbfs", + "gbfs_versions", + "system_information", + "vehicle_types", + "station_information", + "station_status", + "vehicle_status", + "manifest", + "system_regions", + "system_pricing_plans", + "system_alerts", + "geofencing_zones", + "vehicle_availability" + ); + + protected Version31RC3() { + super(VERSION, FEEDS, Version30.CUSTOM_RULES); + } + + @Override + public boolean isFileRequired(String file) { + return super.isFileRequired(file) || "gbfs".equals(file); + } +} diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/VersionFactory.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/VersionFactory.java index 4bfcc2d1..41923984 100644 --- a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/VersionFactory.java +++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/versions/VersionFactory.java @@ -18,8 +18,21 @@ package org.mobilitydata.gbfs.validation.validator.versions; +import java.util.List; + public class VersionFactory { + private static final List SUPPORTED_VERSIONS_DESC = List.of( + "3.1-RC3", + "3.0", + "2.3", + "2.2", + "2.1", + "2.0", + "1.1", + "1.0" + ); + private VersionFactory() {} public static Version createVersion(String version) { @@ -38,8 +51,22 @@ public static Version createVersion(String version) { return new Version23(); case "3.0": return new Version30(); + case "3.1-RC3": + return new Version31RC3(); default: throw new UnsupportedOperationException("Version not implemented"); } } + + public static Version createLatestVersionSupportingFeed( + String feedName, + String fallbackVersion + ) { + return SUPPORTED_VERSIONS_DESC + .stream() + .map(VersionFactory::createVersion) + .filter(version -> version.getFileNames().contains(feedName)) + .findFirst() + .orElse(createVersion(fallbackVersion)); + } } diff --git a/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java b/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java index 8da1d964..b9e601cf 100644 --- a/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java +++ b/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java @@ -19,8 +19,6 @@ package org.mobilitydata.gbfs.validation.validator; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -33,7 +31,6 @@ import org.mobilitydata.gbfs.validation.model.FileValidationResult; import org.mobilitydata.gbfs.validation.model.ValidationResult; import org.mobilitydata.gbfs.validation.model.ValidatorError; -import org.mockito.Mockito; class GbfsJsonValidatorTest { @@ -366,6 +363,73 @@ void testSuccessfulV3_0Validation() { Assertions.assertEquals(0, result.summary().errorsCount()); } + @Test + void testSuccessfulV3_1RC3Validation() { + GbfsJsonValidator validator = new GbfsJsonValidator(); + + Map deliveryMap = new HashMap<>(); + deliveryMap.put("gbfs", getFixture("fixtures/v3.1-RC3/gbfs.json")); + deliveryMap.put( + "gbfs_versions", + getFixture("fixtures/v3.1-RC3/gbfs_versions.json") + ); + deliveryMap.put( + "system_information", + getFixture("fixtures/v3.1-RC3/system_information.json") + ); + deliveryMap.put( + "vehicle_types", + getFixture("fixtures/v3.1-RC3/vehicle_types.json") + ); + deliveryMap.put( + "station_information", + getFixture("fixtures/v3.1-RC3/station_information.json") + ); + deliveryMap.put( + "station_status", + getFixture("fixtures/v3.1-RC3/station_status.json") + ); + deliveryMap.put( + "vehicle_status", + getFixture("fixtures/v3.1-RC3/vehicle_status.json") + ); + deliveryMap.put("manifest", getFixture("fixtures/v3.1-RC3/manifest.json")); + deliveryMap.put( + "system_regions", + getFixture("fixtures/v3.1-RC3/system_regions.json") + ); + deliveryMap.put( + "system_pricing_plans", + getFixture("fixtures/v3.1-RC3/system_pricing_plans.json") + ); + deliveryMap.put( + "system_alerts", + getFixture("fixtures/v3.1-RC3/system_alerts.json") + ); + deliveryMap.put( + "geofencing_zones", + getFixture("fixtures/v3.1-RC3/geofencing_zones.json") + ); + deliveryMap.put( + "vehicle_availability", + getFixture("fixtures/v3.1-RC3/vehicle_availability.json") + ); + + ValidationResult result = validator.validate(deliveryMap); + + printErrors("3.1-RC3", result); + + Assertions.assertEquals("3.1-RC3", result.summary().version()); + Assertions.assertEquals(0, result.summary().errorsCount()); + Assertions.assertTrue(result.files().get("manifest").exists()); + Assertions.assertEquals(0, result.files().get("manifest").errorsCount()); + Assertions.assertTrue(result.files().get("vehicle_availability").exists()); + Assertions.assertEquals( + 0, + result.files().get("vehicle_availability").errorsCount() + ); + } + @Test void testFailed2_3Validation() { GbfsJsonValidator validator = new GbfsJsonValidator(); @@ -382,6 +446,23 @@ void testFailed2_3Validation() { Assertions.assertEquals(6, result.errorsCount()); } + @Test + void testMalformedVehicleAvailabilityReturnsParseError() { + GbfsJsonValidator validator = new GbfsJsonValidator(); + + FileValidationResult result = validator.validateFile( + "vehicle_availability", + new ByteArrayInputStream("{".getBytes(StandardCharsets.UTF_8)) + ); + + Assertions.assertFalse(result.validatorErrors().isEmpty()); + Assertions.assertEquals( + "PARSE_ERROR", + result.validatorErrors().get(0).error() + ); + Assertions.assertNotNull(result.schema()); + } + @Test void testMissingRequiredFile() { GbfsJsonValidator validator = new GbfsJsonValidator(); diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs.json new file mode 100644 index 00000000..2afbc089 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs.json @@ -0,0 +1,17 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 0, + "version": "3.1-RC3", + "data": { + "feeds": [ + { + "name": "system_information", + "url": "https://www.example.com/gbfs/1/system_information" + }, + { + "name": "station_information", + "url": "https://www.example.com/gbfs/1/station_information" + } + ] + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs_versions.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs_versions.json new file mode 100644 index 00000000..34ff0443 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/gbfs_versions.json @@ -0,0 +1,17 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 0, + "version": "3.1-RC3", + "data": { + "versions": [ + { + "version": "2.0", + "url": "https://www.example.com/gbfs/2/gbfs" + }, + { + "version": "3.1-RC3", + "url": "https://www.example.com/gbfs/3.1-RC3/gbfs" + } + ] + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/geofencing_zones.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/geofencing_zones.json new file mode 100644 index 00000000..498319cd --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/geofencing_zones.json @@ -0,0 +1,74 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 60, + "version": "3.1-RC3", + "data": { + "geofencing_zones": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + -122.578067, + 45.562982 + ], + [ + -122.661838, + 45.562741 + ], + [ + -122.661151, + 45.504542 + ], + [ + -122.578926, + 45.5046625 + ], + [ + -122.578067, + 45.562982 + ] + ] + ] + ] + }, + "properties": { + "name": [ + { + "text": "NE 24th/NE Knott", + "language": "en" + } + ], + "start": "2023-07-17T13:34:13+02:00", + "end": "2024-07-18T13:34:13+02:00", + "rules": [ + { + "vehicle_type_ids": [ + "moped1", + "car1" + ], + "ride_start_allowed": true, + "ride_end_allowed": true, + "maximum_speed_kph": 10, + "station_parking": true, + "ride_through_allowed": false + } + ] + } + } + ] + }, + "global_rules": [ + { + "ride_start_allowed": false, + "ride_end_allowed": false, + "ride_through_allowed": true + } + ] + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/manifest.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/manifest.json new file mode 100644 index 00000000..be665e12 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/manifest.json @@ -0,0 +1,115 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl":0, + "version": "3.1-RC3", + "data":{ + "datasets":[ + { + "system_id":"example_berlin", + "versions":[ + { + "version": "2.0", + "url":"https://berlin.example.com/gbfs/2/gbfs" + }, + { + "version": "3.1-RC3", + "url":"https://berlin.example.com/gbfs/3.1-RC3/gbfs" + } + ], + "area": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 13.10821, + 52.58563 + ], + [ + 13.29743, + 52.67046 + ], + [ + 13.48451, + 52.6855 + ], + [ + 13.77993, + 52.43458 + ], + [ + 13.65355, + 52.33048 + ], + [ + 13.08165, + 52.38793 + ], + [ + 13.10821, + 52.58563 + ] + ] + ] + ] + }, + "country_code": "DE" + }, + { + "system_id":"example_paris", + "versions":[ + { + "version": "2.0", + "url":"https://paris.example.com/gbfs/2/gbfs" + }, + { + "version": "3.1-RC3", + "url":"https://paris.example.com/gbfs/3.1-RC3/gbfs" + } + ], + "area": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 2.14306, + 48.89971 + ], + [ + 2.36707, + 48.99455 + ], + [ + 2.60219, + 49.01987 + ], + [ + 2.615, + 48.69025 + ], + [ + 2.52167, + 48.6867 + ], + [ + 2.26838, + 48.73275 + ], + [ + 2.13103, + 48.80833 + ], + [ + 2.14306, + 48.89971 + ] + ] + ] + ] + }, + "country_code": "FR" + } + ] + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_information.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_information.json new file mode 100644 index 00000000..3a47742f --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_information.json @@ -0,0 +1,57 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 0, + "version": "3.1-RC3", + "data": { + "stations": [ + { + "station_id": "station1", + "name": [ + { + "text": "Parking garage A", + "language": "en" + } + ], + "lat": 12.345678, + "lon": 45.678901, + "station_opening_hours": "Su-Th 05:00-22:00; Fr-Sa 05:00-01:00", + "parking_type": "underground_parking", + "parking_hoop": false, + "contact_phone": "+33109874321", + "is_charging_station": true, + "vehicle_docks_capacity": [ + { + "vehicle_type_ids": ["abc123"], + "count": 7 + } + ] + }, + { + "station_id": "station2", + "name": [ + { + "text": "SE Belmont & SE 10th", + "language": "en" + } + ], + "lat": 45.516445, + "lon": -122.655775, + "station_opening_hours": "Su-Th 05:00-22:00; Fr-Sa 05:00-01:00", + "parking_type": "parking_lot", + "parking_hoop": false, + "contact_phone": "+33109874321", + "is_charging_station": false, + "vehicle_docks_capacity": [ + { + "vehicle_type_ids": ["abc123"], + "count": 6 + }, + { + "vehicle_type_ids": ["def456"], + "count": 2 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_status.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_status.json new file mode 100644 index 00000000..20c74c75 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/station_status.json @@ -0,0 +1,71 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 0, + "version": "3.1-RC3", + "data": { + "stations": [ + { + "station_id": "station1", + "is_installed": true, + "is_renting": true, + "is_returning": true, + "last_reported": "2023-07-17T13:34:13+02:00", + "num_docks_available": 3, + "num_docks_disabled" : 1, + "vehicle_docks_available": [ + { + "vehicle_type_ids": [ "abc123", "def456" ], + "count": 2 + }, + { + "vehicle_type_ids": [ "def456" ], + "count": 1 + } + ], + "num_vehicles_available": 1, + "num_vehicles_disabled": 2, + "vehicle_types_available": [ + { + "vehicle_type_id": "abc123", + "count": 1 + }, + { + "vehicle_type_id": "def456", + "count": 0 + } + ] + }, + { + "station_id": "station2", + "is_installed": true, + "is_renting": true, + "is_returning": true, + "last_reported": "2023-07-17T13:34:13+02:00", + "num_docks_available": 8, + "num_docks_disabled" : 1, + "vehicle_docks_available": [ + { + "vehicle_type_ids": [ "abc123" ], + "count": 6 + }, + { + "vehicle_type_ids": [ "def456" ], + "count": 2 + } + ], + "num_vehicles_available": 6, + "num_vehicles_disabled": 1, + "vehicle_types_available": [ + { + "vehicle_type_id": "abc123", + "count": 2 + }, + { + "vehicle_type_id": "def456", + "count": 4 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_alerts.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_alerts.json new file mode 100644 index 00000000..d7bb5bf2 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_alerts.json @@ -0,0 +1,43 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 60, + "version": "3.1-RC3", + "data": { + "alerts": [ + { + "alert_id": "21", + "type": "station_closure", + "station_ids": [ + "123", + "456", + "789" + ], + "times": [ + { + "start": "2023-07-17T13:34:13+02:00", + "end": "2023-07-18T13:34:13+02:00" + } + ], + "url": [ + { + "text": "https://example.com/more-info", + "language": "en" + } + ], + "summary": [ + { + "text": "Disruption of Service", + "language": "en" + } + ], + "description": [ + { + "text": "The three stations on Broadway will be out of service from 12:00am Nov 3 to 3:00pm Nov 6th to accommodate road work", + "language": "en" + } + ], + "last_updated": "2023-07-17T13:34:13+02:00" + } + ] + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_information.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_information.json new file mode 100644 index 00000000..f652ccfd --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_information.json @@ -0,0 +1,68 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 1800, + "version": "3.1-RC3", + "data": { + "system_id": "example_cityname", + "languages": ["en"], + "name": [ + { + "text": "Example Bike Rental", + "language": "en" + } + ], + "short_name": [ + { + "text": "Example Bike", + "language": "en" + } + ], + "operator": [ + { + "text": "Example Sharing, Inc", + "language": "en" + } + ], + "opening_hours": "Apr 1-Nov 3 00:00-24:00", + "start_date": "2010-06-10", + "url": "https://www.example.com", + "purchase_url": "https://www.example.com", + "phone_number": "+18005551234", + "email": "customerservice@example.com", + "feed_contact_email": "datafeed@example.com", + "timezone": "America/Chicago", + "license_url": "https://www.example.com/data-license.html", + "terms_url": [ + { + "text": "https://www.example.com/en/terms", + "language": "en" + } + ], + "terms_last_updated": "2021-06-21", + "privacy_url": [ + { + "text": "https://www.example.com/en/privacy-policy", + "language": "en" + } + ], + "privacy_last_updated": "2019-01-13", + "rental_apps": { + "android": { + "discovery_uri": "com.example.android://", + "store_uri": "https://play.google.com/store/apps/details?id=com.example.android" + }, + "ios": { + "store_uri": "https://apps.apple.com/app/apple-store/id123456789", + "discovery_uri": "com.example.ios://" + } + }, + "brand_assets": { + "brand_last_modified": "2021-06-15", + "brand_image_url": "https://www.example.com/assets/brand_image.svg", + "brand_image_url_dark": "https://www.example.com/assets/brand_image_dark.svg", + "color": "#C2D32C", + "brand_terms_url": "https://www.example.com/assets/brand.pdf" + } + + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_pricing_plans.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_pricing_plans.json new file mode 100644 index 00000000..bc4b7d6f --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_pricing_plans.json @@ -0,0 +1,46 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 0, + "version": "3.1-RC3", + "data": { + "plans": [ + { + "plan_id": "plan2", + "name": [ + { + "text": "One-Way", + "language": "en" + } + ], + "currency": "USD", + "price": 2.00, + "reservation_price_per_min": 0.15, + "is_taxable": false, + "description": [ + { + "text": "Includes 10km, overage fees apply after 10km.", + "language": "en" + } + ], + "per_km_pricing": [ + { + "start": 10, + "rate": 1.00, + "interval": 1, + "end": 25 + }, + { + "start": 25, + "rate": 0.50, + "interval": 1 + }, + { + "start": 25, + "rate": 3.00, + "interval": 5 + } + ] + } + ] + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_regions.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_regions.json new file mode 100644 index 00000000..fb0492a7 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/system_regions.json @@ -0,0 +1,45 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 86400, + "version": "3.1-RC3", + "data": { + "regions": [ + { + "name": [ + { + "text": "North", + "language": "en" + } + ], + "region_id": "3" + }, + { + "name": [ + { + "text": "East", + "language": "en" + } + ], + "region_id": "4" + }, + { + "name": [ + { + "text": "South", + "language": "en" + } + ], + "region_id": "5" + }, + { + "name": [ + { + "text": "West", + "language": "en" + } + ], + "region_id": "6" + } + ] + } + } \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_availability.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_availability.json new file mode 100644 index 00000000..bca74884 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_availability.json @@ -0,0 +1,27 @@ +{ + "last_updated": "2025-07-17T13:34:13+02:00", + "ttl": 0, + "version": "3.1-RC3", + "data": { + "vehicles": [ + { + "vehicle_id": "vehicle_id_1", + "vehicle_type_id": "abc123", + "station_id": "pga", + "pricing_plan_id": "plan2", + "vehicle_equipment": [ + "child_seat_a" + ], + "availabilities": [ + { + "from": "2025-05-24T00:00:00+02:00", + "until": "2025-07-24T23:59:00+02:00" + }, + { + "from": "2025-10-25T00:00:00+02:00" + } + ] + } + ] + } +} diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_status.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_status.json new file mode 100644 index 00000000..14cbf090 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_status.json @@ -0,0 +1,32 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl":0, + "version": "3.1-RC3", + "data":{ + "vehicles":[ + { + "vehicle_id":"973a5c94-c288-4a2b-afa6-de8aeb6ae2e5", + "last_reported": "2023-07-17T13:34:13+02:00", + "lat":12.345678, + "lon":56.789012, + "is_reserved":false, + "is_disabled":false, + "vehicle_type_id":"abc123", + "rental_uris": { + "android": "https://www.example.com/app?vehicle_id=973a5c94-c288-4a2b-afa6-de8aeb6ae2e5&platform=android&", + "ios": "https://www.example.com/app?vehicle_id=973a5c94-c288-4a2b-afa6-de8aeb6ae2e5&platform=ios" + } + }, + { + "vehicle_id":"987fd100-b822-4347-86a4-b3eef8ca8b53", + "last_reported": "2023-07-17T13:34:13+02:00", + "is_reserved":false, + "is_disabled":false, + "vehicle_type_id":"def456", + "current_range_meters":6543.0, + "station_id":"86", + "pricing_plan_id":"plan2" + } + ] + } +} \ No newline at end of file diff --git a/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_types.json b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_types.json new file mode 100644 index 00000000..2a2269aa --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v3.1-RC3/vehicle_types.json @@ -0,0 +1,141 @@ +{ + "last_updated": "2023-07-17T13:34:13+02:00", + "ttl": 0, + "version": "3.1-RC3", + "data": { + "vehicle_types": [ + { + "vehicle_type_id": "abc123", + "form_factor": "bicycle", + "propulsion_type": "human", + "name": [ + { + "text": "Example Basic Bike", + "language": "en" + } + ], + "wheel_count": 2, + "default_reserve_time": 30, + "return_constraint": "any_station", + "min_age": 14, + "vehicle_assets": { + "icon_url": "https://www.example.com/assets/icon_bicycle.svg", + "icon_url_dark": "https://www.example.com/assets/icon_bicycle_dark.svg", + "icon_last_modified": "2021-06-15" + }, + "default_pricing_plan_id": "plan2", + "pricing_plan_ids": [ + "plan2" + ] + }, + { + "vehicle_type_id": "cargo123", + "form_factor": "cargo_bicycle", + "propulsion_type": "human", + "name": [ + { + "text": "Example Cargo Bike", + "language": "en" + } + ], + "description": [ + { + "text": "Extra comfortable seat with additional suspension.\n\nPlease be aware of the cargo box lock: you need to press it down before pulling it up again!", + "language": "en" + } + ], + "wheel_count": 3, + "default_reserve_time": 30, + "return_constraint": "roundtrip_station", + "min_age": 14, + "vehicle_assets": { + "icon_url": "https://www.example.com/assets/icon_cargobicycle.svg", + "icon_url_dark": "https://www.example.com/assets/icon_cargobicycle_dark.svg", + "icon_last_modified": "2021-06-15" + }, + "default_pricing_plan_id": "plan2", + "pricing_plan_ids": [ + "plan2" + ] + }, + { + "vehicle_type_id": "def456", + "form_factor": "scooter_standing", + "propulsion_type": "electric", + "name": [ + { + "text": "Example E-scooter V2", + "language": "en" + } + ], + "wheel_count": 2, + "max_permitted_speed": 25, + "rated_power": 350, + "default_reserve_time": 30, + "max_range_meters": 12345, + "return_constraint": "free_floating", + "min_age": 14, + "vehicle_assets": { + "icon_url": "https://www.example.com/assets/icon_escooter.svg", + "icon_url_dark": "https://www.example.com/assets/icon_escooter_dark.svg", + "icon_last_modified": "2021-06-15" + }, + "default_pricing_plan_id": "plan2" + }, + { + "vehicle_type_id": "car1", + "form_factor": "car", + "rider_capacity": 5, + "cargo_volume_capacity": 200, + "propulsion_type": "combustion_diesel", + "eco_labels": [ + { + "country_code": "FR", + "eco_sticker": "critair_1" + }, + { + "country_code": "DE", + "eco_sticker": "euro_2" + } + ], + "name": [ + { + "text": "Four-door Sedan", + "language": "en" + } + ], + "wheel_count": 4, + "default_reserve_time": 0, + "max_range_meters": 523992, + "return_constraint": "roundtrip_station", + "vehicle_accessories": [ + "doors_4", + "automatic", + "cruise_control" + ], + "g_CO2_km": 120, + "vehicle_image": "https://www.example.com/assets/renault-clio.jpg", + "make": [ + { + "text": "Renault", + "language": "en" + } + ], + "model": [ + { + "text": "Clio", + "language": "en" + } + ], + "color": "white", + "min_age": 14, + "vehicle_assets": { + "icon_url": "https://www.example.com/assets/icon_car.svg", + "icon_url_dark": "https://www.example.com/assets/icon_car_dark.svg", + "icon_last_modified": "2021-06-15" + }, + "default_pricing_plan_id": "plan2" + } + ] + } +} \ No newline at end of file From 9ec6cb4a64de10b6c8dbe570297d5b8ffc6c7181 Mon Sep 17 00:00:00 2001 From: jcpitre Date: Wed, 27 May 2026 13:55:45 -0400 Subject: [PATCH 2/2] Added comments --- .../validator/GbfsJsonValidator.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java index df7d8dbd..db813273 100644 --- a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java +++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java @@ -336,6 +336,16 @@ private ParsedFeedContainer parseFeed(String name, InputStream raw) { } } + /** + * Creates a validation result for a file that could not be parsed. Parsing can fail before + * the exact version for a single file is known, so this resolves the best schema version + * available and still returns file metadata alongside the parse errors. + * + * @param feedName The GBFS feed name + * @param parsedContainer The parsed container holding original content and parse errors + * @param preferredVersion The initially preferred GBFS version + * @return A file validation result containing parse errors and any schema metadata that could be resolved + */ private FileValidationResult createParsingErrorResult( String feedName, ParsedFeedContainer parsedContainer, @@ -357,6 +367,15 @@ private FileValidationResult createParsingErrorResult( ); } + /** + * Resolves the version to use for a feed when building parse-error results. Some feed types + * may not exist in the initially detected version, so this falls back to the newest version + * that supports the feed in order to point the error result at a schema when possible. + * + * @param feedName The GBFS feed name + * @param preferredVersion The initially detected or preferred GBFS version + * @return The preferred version when it supports the feed, otherwise the newest version that does + */ private Version resolveVersionForFeed( String feedName, Version preferredVersion