Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/dart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Dart CI

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
with:
sdk: 'stable'

- name: Get dependencies
run: dart pub get

- name: Run codegen (build_runner)
run: dart run build_runner build --delete-conflicting-outputs

- name: Run tests
run: dart test -r expanded
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ doc/api/

.flutter-plugins
.flutter-plugins-dependencies

# Mac stuff
.DS_Store
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# JsonFeed Parser for Dart

A Dart library for parsing [JSON Feed](https://www.jsonfeed.org/version/1.1/) format feeds. This package provides strongly-typed models and utilities for working with JSON Feed data, including validation and error handling.

## Features

- Fully-typed models for JSON Feed, Item, Author, Attachment, and Hub (using [freezed](https://pub.dev/packages/freezed) and [json_serializable](https://pub.dev/packages/json_serializable))
- Simple API for parsing JSON Feed strings
- Custom exception handling for invalid feeds
- Null safety and modern Dart support

## Getting Started

Add this package to your `pubspec.yaml`:

```yaml
dependencies:
jsonfeed:
git:
url: https://github.com/imprologic/json_feed.git
```

Import and use in your Dart code:

```dart
import 'package:jsonfeed/json_feed.dart';

void main() {
final jsonString = '{ ... }'; // your JSON Feed string
try {
final feed = parseJsonFeed(jsonString);
print(feed.title);
} catch (e) {
print('Failed to parse feed: $e');
}
}
```

## Code Generation

This package uses code generation for immutable models. Run the following command to generate the necessary files:

```bash
dart run build_runner watch -d
```

## Running Tests

To run the unit tests:

```bash
dart test
```

## Project Structure

- `lib/json_feed.dart`: Main library entry point and parser
- `lib/src/models.dart`: Data models for JSON Feed and related types
- `test/json_feed_test.dart`: Unit tests

## License

See [LICENSE](LICENSE).

4 changes: 4 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

analyzer:
errors:
invalid_annotation_target: ignore
9 changes: 9 additions & 0 deletions example/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:json_feed/json_feed.dart';
import 'dart:io';

void main() {
final sample = File('example/sample_feed.json').readAsStringSync();
final feed = parseJsonFeed(sample);
print('Feed title: ${feed.title}');
print('Item count: ${feed.items.length}');
}
34 changes: 34 additions & 0 deletions lib/json_feed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
library json_feed;

export 'src/models.dart';

import 'dart:convert';

import 'src/models.dart';

class JsonFeedParseException implements Exception {
final String message;
JsonFeedParseException(this.message);
@override
String toString() => 'JsonFeedParseException: $message';
}

JsonFeed parseJsonFeed(String jsonString) {
final decoded = json.decode(jsonString);
if (decoded is! Map<String, dynamic>)
throw JsonFeedParseException('Top-level JSON must be an object.');

// validate minimal requirements per spec
final version = decoded['version'];
if (version is! String || version.isEmpty)
throw JsonFeedParseException('Missing required "version" string.');
final items = decoded['items'];
if (items is! List)
throw JsonFeedParseException('Missing required "items" array.');
// authors fallback and extension extraction can be added here

// Use generated fromJson
final feed = JsonFeed.fromJson(decoded);

return feed;
}
78 changes: 78 additions & 0 deletions lib/src/models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'models.freezed.dart';
part 'models.g.dart';

@freezed
abstract class Author with _$Author {
const factory Author({String? name, String? url, String? avatar}) = _Author;

factory Author.fromJson(Map<String, dynamic> json) => _$AuthorFromJson(json);
}

@freezed
abstract class Attachment with _$Attachment {
const factory Attachment({
required String url,
@JsonKey(name: 'mime_type') required String mimeType,
String? title,
@JsonKey(name: 'size_in_bytes') int? sizeInBytes,
@JsonKey(name: 'duration_in_seconds') double? durationInSeconds,
}) = _Attachment;

factory Attachment.fromJson(Map<String, dynamic> json) =>
_$AttachmentFromJson(json);
}

@freezed
abstract class Hub with _$Hub {
const factory Hub({required String type, required String url}) = _Hub;

factory Hub.fromJson(Map<String, dynamic> json) => _$HubFromJson(json);
}

@freezed
abstract class Item with _$Item {
const factory Item({
required String id,
String? url,
@JsonKey(name: 'external_url') String? externalUrl,
String? title,
@JsonKey(name: 'content_html') String? contentHtml,
@JsonKey(name: 'content_text') String? contentText,
String? summary,
String? image,
@JsonKey(name: 'banner_image') String? bannerImage,
@JsonKey(name: 'date_published') String? datePublished,
@JsonKey(name: 'date_modified') String? dateModified,
List<Author>? authors,
List<String>? tags,
String? language,
List<Attachment>? attachments,
}) = _Item;

factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);
}

@freezed
abstract class JsonFeed with _$JsonFeed {
const factory JsonFeed({
required String version,
required String title,
@JsonKey(name: 'home_page_url') String? homePageUrl,
@JsonKey(name: 'feed_url') String? feedUrl,
String? description,
@JsonKey(name: 'user_comment') String? userComment,
@JsonKey(name: 'next_url') String? nextUrl,
String? icon,
String? favicon,
List<Author>? authors,
String? language,
bool? expired,
List<Hub>? hubs,
@Default([]) List<Item> items,
}) = _JsonFeed;

factory JsonFeed.fromJson(Map<String, dynamic> json) =>
_$JsonFeedFromJson(json);
}
Loading