Skip to content
Open
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: 0 additions & 26 deletions crates/core/examples/streaming.rs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,57 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import org.phoenixframework.liveviewnative.core.ClientConnectOpts
import org.phoenixframework.liveviewnative.core.ChangeType
import org.phoenixframework.liveviewnative.core.ConnectOpts
import org.phoenixframework.liveviewnative.core.Document
import org.phoenixframework.liveviewnative.core.DocumentChangeHandler
import org.phoenixframework.liveviewnative.core.LiveFile
import org.phoenixframework.liveviewnative.core.LiveSocket
import org.phoenixframework.liveviewnative.core.LiveViewClient
import org.phoenixframework.liveviewnative.core.LiveViewClientBuilder
import org.phoenixframework.liveviewnative.core.NavOptions
import org.phoenixframework.liveviewnative.core.NodeData
import org.phoenixframework.liveviewnative.core.NodeRef
import org.phoenixframework.liveviewnative.core.Platform

class SocketTest {
@Test
fun simple_connect() = runTest {
var live_socket = LiveSocket.connect("http://127.0.0.1:4001/upload", "jetpack", null)
var live_channel = live_socket.joinLiveviewChannel(null, null)
var opts = ClientConnectOpts()
var builder = LiveViewClientBuilder()

builder.setFormat(Platform.Jetpack)

var client = builder.connect("http://127.0.0.1:4001/upload", opts)

// This is a PNG located at crates/core/tests/support/tinycross.png
var base64TileImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEdFQog0ycfAgAAAIJJREFUOMulU0EOwCAIK2T/f/LYwWAAgZGtJzS1BbVEuEVAAACCQOsKlkOrEicwgeVz5tC5R1yrDdnKuo6j6J5ydgd+npOUHfaGEJkQq+6cQNVqP1oQiCJxvAjGT3Dn3l1sKpAdfhPhqXP5xDYLXz7SkYUuUNnrcBWULkRlFqZxtvwH8zGCEN6LErUAAAAASUVORK5CYII="

val contents = Base64.getDecoder().decode(base64TileImg)
val phx_upload_id = live_channel.getPhxUploadId("avatar")
val phx_upload_id = client.getPhxUploadId("avatar")
var live_file = LiveFile(contents, "image/png", "avatar", "foobar.png", phx_upload_id)
live_channel.uploadFile(live_file)
client.uploadFiles(listOf(live_file))
}
}

class SocketTestOpts {
@Test
fun connect_with_opts() = runTest {
var opts = ConnectOpts()
var live_socket = LiveSocket.connect("http://127.0.0.1:4001/upload", "jetpack", opts)
var live_channel = live_socket.joinLiveviewChannel(null, null)
var opts = ClientConnectOpts()
var builder = LiveViewClientBuilder()

builder.setFormat(Platform.Jetpack)

var client = builder.connect("http://127.0.0.1:4001/upload", opts)

// This is a PNG located at crates/core/tests/support/tinycross.png
var base64TileImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEdFQog0ycfAgAAAIJJREFUOMulU0EOwCAIK2T/f/LYwWAAgZGtJzS1BbVEuEVAAACCQOsKlkOrEicwgeVz5tC5R1yrDdnKuo6j6J5ydgd+npOUHfaGEJkQq+6cQNVqP1oQiCJxvAjGT3Dn3l1sKpAdfhPhqXP5xDYLXz7SkYUuUNnrcBWULkRlFqZxtvwH8zGCEN6LErUAAAAASUVORK5CYII="

val contents = Base64.getDecoder().decode(base64TileImg)
val phx_upload_id = live_channel.getPhxUploadId("avatar")
val phx_upload_id = client.getPhxUploadId("avatar")
var live_file = LiveFile(contents, "image/png", "avatar", "foobar.png", phx_upload_id)
live_channel.uploadFile(live_file)
client.uploadFiles(listOf(live_file))
}
}

Expand Down Expand Up @@ -252,9 +262,14 @@ class DocumentTest {
val host = "127.0.0.1:4001"
val url = "http://$host/nav/first_page"

val liveSocket = LiveSocket.connect(url, "jetpack", null)
val liveChannel = liveSocket.joinLiveviewChannel(null, null)
val doc = liveChannel.document()
var opts = ClientConnectOpts()
var builder = LiveViewClientBuilder()

builder.setFormat(Platform.Jetpack)

var client = builder.connect(url, opts)

val doc = client.document()

val expectedFirstDoc =
"""
Expand All @@ -272,9 +287,9 @@ class DocumentTest {
assertEquals(exp.render(), doc.render())

val secondUrl = "http://$host/nav/second_page"
val secondChannel = liveSocket.navigate(secondUrl, null, NavOptions())
client.navigate(secondUrl, NavOptions())

val secondDoc = secondChannel.document()
val secondDoc = client.document()

val expectedSecondDoc =
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ extension LiveViewNativeCore.Payload: @unchecked Sendable {}
extension LiveViewNativeCore.EventPayload: @unchecked Sendable {}

extension LiveViewNativeCore.LiveChannel: @unchecked Sendable {}
extension LiveViewNativeCore.LiveSocket: @unchecked Sendable {}

extension LiveViewNativeCore.Events: @unchecked Sendable {}
extension LiveViewNativeCore.ChannelStatuses: @unchecked Sendable {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,27 @@ let timeout = TimeInterval(30.0)
let connect_url = "http://127.0.0.1:4001/hello"
final class LiveViewNativeCoreSocketTests: XCTestCase {
func testConnect() async throws {
let live_socket = try await LiveSocket(connect_url, "swiftui", .none)
let _ = try await live_socket.joinLiveviewChannel(.none, .none)
let builder = LiveViewClientBuilder()
let client = try await builder.connect(connect_url, ClientConnectOpts())
}

func testConnectWithOpts() async throws {
let headers = [String: String]()
let options = ConnectOpts(headers: headers)
let live_socket = try await LiveSocket(connect_url, "swiftui", options)
let _ = try await live_socket.joinLiveviewChannel(.none, .none)
let options = ClientConnectOpts(headers: headers)
let builder = LiveViewClientBuilder()
let client = try await builder.connect(connect_url, options)
}

func testStatus() async throws {
let live_socket = try await LiveSocket(connect_url, "swiftui", .none)
let _ = try await live_socket.joinLiveviewChannel(.none, .none)
let socket = live_socket.socket()
let builder = LiveViewClientBuilder()
let client = try await builder.connect(connect_url, ClientConnectOpts())

var status = socket.status()
var status = try client.status()
XCTAssertEqual(status, .connected)

try await socket.disconnect()
status = socket.status()
try await client.disconnect()
status = try client.status()
XCTAssertEqual(status, .disconnected)

try await socket.shutdown()
status = socket.status()
XCTAssertEqual(status, .shutDown)
}

func testBasicConnection() async throws {
Expand Down Expand Up @@ -111,64 +106,14 @@ let base64TileImg =
let upload_url = "http://127.0.0.1:4001/upload"
final class LiveViewNativeCoreUploadTests: XCTestCase {
func testUpload() async throws {
let live_socket = try await LiveSocket(upload_url, "swiftui", .none)
let live_channel = try await live_socket.joinLiveviewChannel(.none, .none)
// Using the new LiveViewClient API
let builder = LiveViewClientBuilder()
let client = try await builder.connect(upload_url, ClientConnectOpts())

let image: Data! = Data(base64Encoded: base64TileImg)

let phx_id: String! = try live_channel.getPhxUploadId("avatar")
let phx_id: String! = try client.getPhxUploadId("avatar")
let live_file = LiveFile(image, "image/png", "avatar", "foobar.png", phx_id)
try await live_channel.uploadFile(live_file)
try await client.uploadFiles([live_file])
}
}

// Test basic navigation flow with LiveSocket
func testBasicNavFlow() async throws {
let url = "http://127.0.0.1:4001/nav/first_page"
let secondUrl = "http://127.0.0.1:4001/nav/second_page"

let liveSocket = try await LiveSocket(url, "swiftui", .none)
let liveChannel = try await liveSocket.joinLiveviewChannel(.none, .none)

let doc = liveChannel.document()

let expectedFirstDoc = """
<Group id="flash-group" />
<VStack>
<Text>
first_page
</Text>
<NavigationLink id="Next" destination="/nav/next">
<Text>
NEXT
</Text>
</NavigationLink>
</VStack>
"""

let exp = try Document.parse(expectedFirstDoc)

XCTAssertEqual(doc.render(), exp.render())

let secondChannel = try await liveSocket.navigate(secondUrl, .none, NavOptions())

let secondDoc = secondChannel.document()

let expectedSecondDoc = """
<Group id="flash-group" />
<VStack>
<Text>
second_page
</Text>
<NavigationLink id="Next" destination="/nav/next">
<Text>
NEXT
</Text>
</NavigationLink>
</VStack>
"""

let secondExp = try Document.parse(expectedSecondDoc)

XCTAssertEqual(secondDoc.render(), secondExp.render())
}
2 changes: 1 addition & 1 deletion crates/core/src/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use phoenix_channels_client::{Socket, SocketStatus};

use crate::dom::{NodeData, NodeRef};
#[cfg(feature = "liveview-channels")]
use crate::{dom::ffi::Document, live_socket::LiveChannel};
use crate::{client::LiveChannel, dom::ffi::Document};

/// Provides secure persistent storage for session data like cookies.
/// Implementations should handle platform-specific storage (e.g. NSUserDefaults on iOS)
Expand Down
102 changes: 101 additions & 1 deletion crates/core/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};

use phoenix_channels_client::JSON;

use crate::{callbacks::*, live_socket::Method};
use crate::callbacks::*;

#[derive(uniffi::Enum, Debug, Clone, Default, Copy)]
pub enum LogLevel {
Expand Down Expand Up @@ -135,3 +135,103 @@ impl std::fmt::Debug for LiveViewClientConfiguration {
.finish()
}
}

/// An action taken with respect to the history stack
/// when [NavCtx::navigate] is executed. defaults to
/// Push behavior.
#[derive(uniffi::Enum, Default, Clone)]
pub enum NavAction {
/// Push the navigation event onto the history stack.
#[default]
Push,
/// Replace the current top of the history stack with this navigation event.
Replace,
}

/// Options for calls to [NavCtx::navigate] and the external [LiveViewClient::navigate] function
/// Slightly different from [NavActionOptions]
#[derive(Default, uniffi::Record)]
pub struct NavOptions {
/// Additional params to be passed upon joining the liveview channel.
#[uniffi(default = None)]
pub join_params: Option<HashMap<String, JSON>>,
/// see [NavAction], defaults to [NavAction::Push].
#[uniffi(default = None)]
pub action: Option<NavAction>,
/// Ephemeral extra information to be pushed to the even handler.
#[uniffi(default = None)]
pub extra_event_info: Option<Vec<u8>>,
/// Persistent state, intended to be deserialized for user specific purposes when
/// revisiting a given view.
#[uniffi(default = None)]
pub state: Option<Vec<u8>>,
}

#[derive(Default, uniffi::Record)]
pub struct NavActionOptions {
/// Additional params to be passed upon joining the liveview channel.
#[uniffi(default = None)]
pub join_params: Option<HashMap<String, JSON>>,
/// Ephemeral extra information to be pushed to the even handler.
#[uniffi(default = None)]
pub extra_event_info: Option<Vec<u8>>,
}

/// Connection Options for the initial dead render fetch
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record, Default)]
pub struct DeadRenderFetchOpts {
#[uniffi(default = None)]
pub headers: Option<HashMap<String, String>>,
#[uniffi(default = None)]
pub body: Option<Vec<u8>>,
#[uniffi(default = None)]
pub method: Option<Method>,
}

#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
#[repr(u8)]
pub enum Method {
Get = 0,
Options,
Post,
Put,
Delete,
Head,
Trace,
Connect,
Patch,
}

use reqwest::Method as ReqMethod;
impl From<Method> for ReqMethod {
fn from(val: Method) -> ReqMethod {
match val {
Method::Options => ReqMethod::OPTIONS,
Method::Get => ReqMethod::GET,
Method::Post => ReqMethod::POST,
Method::Put => ReqMethod::PUT,
Method::Delete => ReqMethod::DELETE,
Method::Head => ReqMethod::HEAD,
Method::Trace => ReqMethod::TRACE,
Method::Connect => ReqMethod::CONNECT,
Method::Patch => ReqMethod::PATCH,
}
}
}

pub struct UploadConfig {
pub chunk_size: u64,
pub max_file_size: u64,
pub max_entries: u64,
}

/// Defaults from https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#allow_upload/3
impl Default for UploadConfig {
fn default() -> Self {
Self {
chunk_size: 64_000,
max_file_size: 8000000,
max_entries: 1,
}
}
}
Loading