diff --git a/.gitignore b/.gitignore index 9591aae..2662521 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,10 @@ # Remove the following pattern if you wish to check in your lock file pubspec.lock -# IntelliJ +# IntelliJ / IDEs *.iml .idea/ +.vscode/ # Conventional directory for build outputs build/ diff --git a/AUTHORS b/AUTHORS index 7d248e8..580b1f6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,4 +3,5 @@ # # Name/Organization -Adaptant Labs \ No newline at end of file +Adaptant Labs +Stemco \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e52f7ed..d33e625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 0.2.1 +- Migrated completely to nullsafety + +## 0.2.0 + +- Expose helpers for querying NHTSA DB and accessing extended vehicle information (requested by @ride4sun, issue #8) + +## 0.1.4+1 + +- Revert null-safety changes from stable release to satisfy SDK constraints + +## 0.1.4 + +- Support VINs with 2-character manufacturer IDs in their WMI (reported by @huangkaichang, issue #7) +- Fix Uri parsing for NHTSA DB REST API ## 0.2.1-nullsafety - Support VINs with 2-character manufacturer IDs in their WMI (reported by @huangkaichang, issue #7) diff --git a/README.md b/README.md index 28972bf..345871b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ vin-decoder-dart ================ -[![Build Status](https://travis-ci.com/adaptant-labs/vin-decoder-dart.svg?branch=master)](https://travis-ci.com/adaptant-labs/vin-decoder-dart#) +[![Build Status](https://travis-ci.com/adaptant-labs/vin-decoder-dart.svg?branch=master)](https://app.travis-ci.com/github/adaptant-labs/vin-decoder-dart) [![Pub](https://img.shields.io/pub/v/vin_decoder.svg)](https://pub.dartlang.org/packages/vin_decoder) A VIN decoding, validation, and generation library for Dart. diff --git a/example/vin_decoder_example.dart b/example/vin_decoder_example.dart index 7da9a33..6b0de2a 100644 --- a/example/vin_decoder_example.dart +++ b/example/vin_decoder_example.dart @@ -1,7 +1,8 @@ -import 'package:vin_decoder/vin_decoder.dart'; +import 'package:custom_vin_decoder/nhtsa.dart'; +import 'package:custom_vin_decoder/vin_decoder.dart'; void main() async { - var vin = VIN(number: 'WP0ZZZ99ZTS392124', extended: true); + var vin = VIN(vin: 'WP0ZZZ99ZTS392124', extended: true); print('WMI: ${vin.wmi}'); print('VDS: ${vin.vds}'); @@ -10,7 +11,7 @@ void main() async { print("Model year is " + vin.modelYear()); print("Serial number is " + vin.serialNumber()); print("Assembly plant is " + vin.assemblyPlant()); - print("Manufacturer is " + vin.getManufacturer()!); + print("Manufacturer is " + (vin.getManufacturer() ?? "")); print("Year is " + vin.getYear().toString()); print("Region is " + vin.getRegion()); print("VIN string is " + vin.toString()); @@ -25,6 +26,12 @@ void main() async { var type = await vin.getVehicleTypeAsync(); print("Type is ${type}"); + var info = await NHTSA.decodeVin(vin.number); + print('Plant Country is ' + (info?.value('Plant Country') ?? "")); + + var values = await NHTSA.decodeVinValues(vin.number); + print('Manufacturer from NHTSA DB is ' + values?['Manufacturer']); + var generated = VINGenerator().generate(); print('Randomly Generated VIN is ${generated}'); } diff --git a/lib/nhtsa.dart b/lib/nhtsa.dart new file mode 100644 index 0000000..44fd877 --- /dev/null +++ b/lib/nhtsa.dart @@ -0,0 +1,4 @@ +/// Support for querying NHTSA database by VIN. +library nhtsa; + +export 'src/nhtsa_model.dart'; diff --git a/lib/src/nhtsa_model.dart b/lib/src/nhtsa_model.dart index c9ffa27..28fa3fb 100644 --- a/lib/src/nhtsa_model.dart +++ b/lib/src/nhtsa_model.dart @@ -2,19 +2,71 @@ import 'package:basic_utils/basic_utils.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +// NHTSA Results not relevant for a specific vehicle can be either null or N/A +const String _RESULT_NOT_APPLICABLE = 'Not Applicable'; + +// ignore: avoid_classes_with_only_static_members +/// A wrapper for the NHTSA REST API +class NHTSA { + static const String _uriBase = 'https://vpic.nhtsa.dot.gov/api/vehicles'; + + /// Obtain information about a given [vin] from the NHTSA DB. + static Future decodeVin(String vin) async { + var path = _uriBase + '/DecodeVin/' + vin + '?format=json'; + final response = await http.get(Uri.parse(path)); + + if (response.statusCode == 200) { + return NHTSAVehicleInfo.fromJson(jsonDecode(response.body) as Map); + } + + return null; + } + + /// Obtain a map of key/value pairs containing known values for a given [vin] + static Future?> decodeVinValues(String vin) async { + var path = _uriBase + '/DecodeVinValues/' + vin + '?format=json'; + final response = await http.get(Uri.parse(path)); + + // The DecodeVinValues endpoint returns a single Results object with all + // variables and values as an array of encapsulated key/value pairs. + // Manually unpack this in order to provide the caller a populated Dart map. + if (response.statusCode == 200) { + final Map data = jsonDecode(response.body) as Map; + final Map map = data['Results'][0] as Map; + // Discard empty and not applicable entries from map + map.removeWhere((key, value) => + value == null || value == _RESULT_NOT_APPLICABLE || value == ''); + return map; + } + + return null; + } +} + +/// The result of a single data point from the NHTSA DB for a specific variable. class NHTSAResult { + /// The value associated with a given [variable] or [variableId] String? value; + + /// The ID number associated with a given [value] String? valueId; + + /// The variable name String? variable; + + /// The ID number of a given [variable] int? variableId; - NHTSAResult({this.value, this.valueId, this.variable, this.variableId}); + NHTSAResult({required this.value, required this.valueId, required this.variable, required this.variableId}); - NHTSAResult.fromJson(Map json) { - value = json['Value']; - valueId = json['ValueId']; - variable = json['Variable']; - variableId = json['VariableId']; + /// Create a new [NHTSAResult] instance from a fixed JSON payload + factory NHTSAResult.fromJson(Map json) { + return NHTSAResult( + value: json['Value'] as String?, + valueId: json['ValueId'] as String?, + variable: json['Variable'] as String?, + variableId: json['VariableId'] as int? + ); } @override @@ -23,81 +75,60 @@ class NHTSAResult { } } +/// Extended vehicle information for a specific VIN obtained from the NHTSA DB. class NHTSAVehicleInfo { - int? count; - String? message; - String? searchCriteria; - List? results; + int count; + String message; + String searchCriteria; + List results = []; NHTSAVehicleInfo( - {this.count, this.message, this.searchCriteria, this.results}); + {required this.count, required this.message, required this.searchCriteria, required this.results}); - NHTSAVehicleInfo.fromJson(Map json) { - count = json['Count']; - message = json['Message']; - searchCriteria = json['SearchCriteria']; + /// Create a new [NHTSAVehicleInfo] instance from a fixed JSON payload. + factory NHTSAVehicleInfo.fromJson(Map json) { + List results = []; if (json['Results'] != null) { - results = []; json['Results'].forEach((v) { - if (v['Value'] != null) { - results!.add(NHTSAResult.fromJson(v)); + if (v['Value'] != null && + v['Value'] != _RESULT_NOT_APPLICABLE && + v['Value'] != '') { + results.add(NHTSAResult.fromJson(v as Map)); } }); } + return NHTSAVehicleInfo( + count: (json['Count'] as int?) ?? 0, + message: (json['Message'] as String?) ?? "", + searchCriteria:( json['SearchCriteria'] as String?) ?? "", + results: results + ); } - static String normalizeStringValue(String s) { + static String? _normalizeStringValue(String? s) { + if (s == null){ + return null; + } return s.splitMapJoin(' ', onNonMatch: (m) => StringUtils.capitalize(m.toLowerCase())); } - ExtendedVehicleInfo toExtendedVehicleInfo() { - final ExtendedVehicleInfo info = ExtendedVehicleInfo(); - - results!.forEach((f) { - switch (f.variable) { - case "Vehicle Type": - info.vehicleType = normalizeStringValue(f.value!); - break; - case "Make": - info.make = normalizeStringValue(f.value!); - break; - case "Model": - info.model = normalizeStringValue(f.value!); - break; - } - }); - - return info; - } - - @override - String toString() { - return 'NHTSAVehicleInfo[count=$count, message=$message, searchCriteria=$searchCriteria, results=$results]'; + /// Lookup the value of a variable by its [variableId] in the NHTSA DB results + String? valueFromId(int? variableId) { + var result = results.singleWhere((e) => e.variableId == variableId, + orElse: () => NHTSAResult(value: null, valueId: null, variable: null, variableId: null)); + return _normalizeStringValue(result.value); } -} - -class ExtendedVehicleInfo { - String? make; - String? model; - String? vehicleType; - static Future getExtendedVehicleInfo(String vin) async { - var path = 'https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVin/' + - vin + - '?format=json'; - final response = await http.get(Uri.parse(path)); - - if (response.statusCode == 200) { - var vehicleInfo = NHTSAVehicleInfo.fromJson(jsonDecode(response.body)); - return vehicleInfo.toExtendedVehicleInfo(); - } - - return null; + /// Lookup the value of a named [variable] in the NHTSA DB results + String? value(String variable) { + var result = + results.singleWhere((e) => e.variable == variable, orElse: () => NHTSAResult(value: null, valueId: null, variable: null, variableId: null)); + return _normalizeStringValue(result.value); } @override String toString() { - return 'ExtendedVehicleInfo[make=$make, model=$model, vehicleType=$vehicleType]'; + return 'NHTSAVehicleInfo[count=$count, message=$message, searchCriteria=$searchCriteria, results=$results]'; } } diff --git a/lib/src/vin_decoder_base.dart b/lib/src/vin_decoder_base.dart index e95caff..8fabf04 100644 --- a/lib/src/vin_decoder_base.dart +++ b/lib/src/vin_decoder_base.dart @@ -3,6 +3,26 @@ import 'nhtsa_model.dart'; import 'year_map.dart'; class VIN { + /// Hashmap containing fuel names and their respective # + static const Map fuelMap = { + "Diesel": 1, + "CNG": 6, + "Gasoline": 4, + "Battery": 2, + "Electric":2, + "BEV":2, + "LNG": 7, + "Hydrogen": 8, + "LPG": 9, + "E85": 10, + "E100": 11, + "M85": 13, + "M100": 14, + "FFV": 15, + "FCEV":18, + "Fuel Cell": 18 + }; + /// The VIN that the class was instantiated with. final String number; @@ -17,15 +37,27 @@ class VIN { /// Try to obtain extended information for the VIN from the NHTSA database. final bool extended; - ExtendedVehicleInfo? _info; - - VIN({required this.number, this.extended = false}) - : wmi = normalize(number).substring(0, 3), - vds = normalize(number).substring(3, 9), - vis = normalize(number).substring(9, 17); + Map _vehicleInfo = {}; + + /// Private named constructor. Creates a new VIN. + /// + /// [wmi], [vds], and [vis] are populated based on [number]. + VIN._({required this.number, required this.extended}) : + wmi = number.substring(0, 3), + vds = number.substring(3, 9), + vis = number.substring(9, 17); + + /// Creates a new VIN. + /// + /// This factory constructor makes sure the string is normallyed + factory VIN({required String vin, bool extended = false}){ + return VIN._(number: normalize(vin), extended: extended); + } /// Carry out VIN validation. A valid [number] must be 17 characters long /// and contain only valid alphanumeric characters. + /// + /// If a number is provided, validates that number. Otherwise, it validates the number this object was initialized with. bool valid([String? number]) { String value = normalize(number != null ? number : this.number); return RegExp(r"^[a-zA-Z0-9]+$").hasMatch(value) && value.length == 17; @@ -36,8 +68,8 @@ class VIN { number.toUpperCase().replaceAll('-', ''); /// Obtain the encoded manufacturing year in YYYY format. - int? getYear() { - return yearMap[modelYear()]; + int getYear() { + return yearMap[modelYear()] ?? 2001; } /// Obtain the 2-character region code for the manufacturing region. @@ -64,6 +96,8 @@ class VIN { } /// Get the full name of the vehicle manufacturer as defined by the [wmi]. + /// + /// If the full name cannot be found, returns null. String? getManufacturer() { // Check for the standard case - a 3 character WMI if (manufacturers.containsKey(this.wmi)) { @@ -75,7 +109,7 @@ class VIN { if (manufacturers.containsKey(id)) { return manufacturers[id]; } else { - return "Unknown (WMI: ${this.wmi.toUpperCase()})"; + return null; } } } @@ -83,45 +117,72 @@ class VIN { /// Returns the checksum for the VIN. Note that in the case of the EU region /// checksums are not implemented, so this becomes a no-op. More information /// is provided in ISO 3779:2009. + /// + /// If the region is EU, returns null String? getChecksum() { - return (getRegion() != "EU") ? normalize(this.number)[8] : null; + return (getRegion() != "EU") ? this.number[8] : null; } /// Extract the single-character model year from the [number]. - String modelYear() => normalize(this.number)[9]; + String modelYear() => this.number[9]; /// Extract the single-character assembly plant designator from the [number]. - String assemblyPlant() => normalize(this.number)[10]; + String assemblyPlant() => this.number[10]; /// Extract the serial number from the [number]. - String serialNumber() => normalize(this.number).substring(12, 17); + String serialNumber() => this.number.substring(12, 17); - Future _fetchExtendedVehicleInfo() async { - if (this._info == null && extended == true) { - this._info = - await ExtendedVehicleInfo.getExtendedVehicleInfo(this.number); + /// Assigns the + Future _fetchExtendedVehicleInfo() async { + if (this._vehicleInfo.isEmpty && extended == true) { + this._vehicleInfo = await NHTSA.decodeVinValues(this.number) ?? {}; } } - /// Get the Make of the vehicle from the NHTSA database if [extended] mode + /// Get the fuel type of the vehicle from the NHTSA database if [extended] mode /// is enabled. - Future getMakeAsync() async { + Future getFuelTypeAsync() async { await _fetchExtendedVehicleInfo(); - return this._info?.make; + final String? fuelType = this._vehicleInfo['FuelTypePrimary'] as String?; + if (fuelType == null) return 0; + + final String fuelLower = fuelType.toLowerCase(); + for (final String fuelKey in fuelMap.keys) { + if (fuelLower.contains(fuelKey.toLowerCase())) { + return fuelMap[fuelKey]!; + } + } + return 0; } - /// Get the Model of the vehicle from the NHTSA database if [extended] mode + /// Get the Make of the vehicle from the NHTSA database if [extended] mode /// is enabled. - Future getModelAsync() async { + Future getMakeAsync() async { + await _fetchExtendedVehicleInfo(); + return this._vehicleInfo['Make'] as String? ?? ""; + } + + /// Get the Make ID of a vehicle from the NHTSA database if the [extended] mode is enabled + Future getMakeIdAsync() async { + await _fetchExtendedVehicleInfo(); + return this._vehicleInfo.keys.contains("MakeID") ? int.parse(this._vehicleInfo["MakeID"]): 0; + } + + /// Get the Model of the vehicle from the NHTSA database if [extended] mode is enabled. + Future getModelAsync() async { + await _fetchExtendedVehicleInfo(); + return (this._vehicleInfo.keys.contains("Model") ? this._vehicleInfo['Model'] as String? ?? "Unknown": "Unknown"); + } + + Future getModelIdAsync() async { await _fetchExtendedVehicleInfo(); - return this._info?.model; + return (this._vehicleInfo.keys.contains("ModelID") ? this._vehicleInfo['ModelID'] as String? ?? "Unknown": "Unknown"); } - /// Get the Vehicle Type from the NHTSA database if [extended] mode is - /// enabled. - Future getVehicleTypeAsync() async { + /// Get the Vehicle Type from the NHTSA database if [extended] mode is enabled. + Future getVehicleTypeAsync() async { await _fetchExtendedVehicleInfo(); - return this._info?.vehicleType; + return (this._vehicleInfo.keys.contains("VehicleType") ? this._vehicleInfo['VehicleType'] as String? ?? "0": "0"); } @override diff --git a/lib/src/year_map.dart b/lib/src/year_map.dart index 075277a..bc988b8 100644 --- a/lib/src/year_map.dart +++ b/lib/src/year_map.dart @@ -1,32 +1,32 @@ const yearMap = { - 'N': 1992, - 'P': 1993, - 'R': 1994, - 'S': 1995, - 'T': 1996, - 'V': 1997, - 'W': 1998, - 'X': 1999, - 'Y': 2000, - '1': 2001, - '2': 2002, - '3': 2003, - '4': 2004, - '5': 2005, - '6': 2006, - '7': 2007, - '8': 2008, - '9': 2009, - 'A': 2010, - 'B': 2011, - 'C': 2012, - 'D': 2013, - 'E': 2014, - 'F': 2015, - 'G': 2016, - 'H': 2017, - 'J': 2018, - 'K': 2019, - 'L': 2020, - 'M': 2021, + "Y": 2000, + "1": 2001, + "2": 2002, + "3": 2003, + "4": 2004, + "5": 2005, + "6": 2006, + "7": 2007, + "8": 2008, + "9": 2009, + "A": 2010, + "B": 2011, + "C": 2012, + "D": 2013, + "E": 2014, + "F": 2015, + "G": 2016, + "H": 2017, + "J": 2018, + "K": 2019, + "L": 2020, + "M": 2021, + "N": 2022, + "P": 2023, + "R": 2024, + "S": 2025, + "T": 2026, + "V": 2027, + "W": 2028, + "X": 2029, }; diff --git a/pubspec.yaml b/pubspec.yaml index ca7c13d..e6cbbe0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,18 @@ -name: vin_decoder +name: custom_vin_decoder description: Dart library for working with Vehicle Identification Numbers (VINs) based on ISO 3779:2009 and World Manufacturer Identifiers (WMIs) based on ISO 3780:2009, enriched by NHTSA data. -version: 0.2.1-nullsafety +version: 0.2.1 repository: https://github.com/adaptant-labs/vin-decoder-dart issue_tracker: https://github.com/adaptant-labs/vin-decoder-dart/issues environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.12.0 <3.0.0' # Important no space allowed between math operator and the numeric digit so < 3 is not the same as <3 dependencies: - basic_utils: ^3.1.0 + meta: ^1.8.0 + basic_utils: ^3.4.0 http: ^0.13.3 random_string: ^2.2.0-nullsafety diff --git a/test/vin_decoder_test.dart b/test/vin_decoder_test.dart index 9448418..e8a190e 100644 --- a/test/vin_decoder_test.dart +++ b/test/vin_decoder_test.dart @@ -1,13 +1,9 @@ -import 'package:vin_decoder/vin_decoder.dart'; +import 'package:custom_vin_decoder/vin_decoder.dart'; import 'package:test/test.dart'; void main() { group('EU VIN Test', () { - late VIN vin; - - setUp(() { - vin = VIN(number: 'WP0ZZZ99ZTS392124'); - }); + final vin = VIN(vin: 'WP0ZZZ99ZTS392124'); test('Validity Test', () { expect(vin.valid(), isTrue); @@ -23,11 +19,7 @@ void main() { }); group('AS VIN Test', () { - late VIN vin; - - setUp(() { - vin = VIN(number: 'JS1VX51L7X2175460'); - }); + final vin = VIN(vin: 'JS1VX51L7X2175460'); test('Validity Test', () { expect(vin.valid(), isTrue); @@ -39,12 +31,13 @@ void main() { }); group('2-character WMI Manufacturer Test', () { + late VIN vin; setUp(() { // Here the first 2 characters refer to the manufacturer, with the 3rd // representing the class of vehicle specific to that manufacturer. - vin = VIN(number: '5TENL42N94Z436445'); + vin = VIN(vin: '5TENL42N94Z436445'); }); test('Validity Test', () { diff --git a/test/vin_generator_test.dart b/test/vin_generator_test.dart index 0bd7695..420f3ac 100644 --- a/test/vin_generator_test.dart +++ b/test/vin_generator_test.dart @@ -1,4 +1,4 @@ -import 'package:vin_decoder/vin_decoder.dart'; +import 'package:custom_vin_decoder/vin_decoder.dart'; import 'package:test/test.dart'; void main() { @@ -8,7 +8,7 @@ void main() { setUp(() { generator = VINGenerator(); - vin = VIN(number: generator.generate()); + vin = VIN(vin: generator.generate()); }); test('Validity Test', () {