Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensibility for Command #92

Open
weissi opened this issue Apr 23, 2020 · 12 comments
Open

Extensibility for Command #92

weissi opened this issue Apr 23, 2020 · 12 comments
Labels
kind/enhancement Improvements to existing feature.
Milestone

Comments

@weissi
Copy link
Member

weissi commented Apr 23, 2020

Commands need to be extensible so they can't be a big enum.

My proposal

public struct RenameCommand {
    public var from: Mailbox
    public var to: Mailbox
    // ...
}
public struct SelectCommand {
    public var mailbox: Mailbox
    // ...
}
public struct Command {
    enum Commands {
        case rename(RenameCommand)
        case select(SelectCommand)
    }
    var command: Commands
    public static func rename(_ command: RenameCommand) -> Command {
        return Command(command: .rename(command))
    }
    public var asRename: RenameCommand? {
        switch self.command {
        case .rename(let command):
            return command
        default:
            return nil
        }
    }
    public var asSelect: SelectCommand? {
        switch self.command {
        case .select(let command):
            return command
        default:
            return nil
        }
    }
    public static func select(_ command: SelectCommand) -> Command {
        return Command(command: .select(command))
    }
}
func switchCommand(_ command: Command) {
    if let rename = command.asRename {
        // handle rename
        print(rename)
    } else if let select = command.asSelect {
        // handle select
        print(select)
    } else {
        // send BAD command
    }
}
@Davidde94 Davidde94 added the imap label Apr 23, 2020
@danieleggert
Copy link
Collaborator

The problem with this approach is when you implement a server though. You’ll basically have to do a giant

    if let rename = command.asRename {
        // handle rename
        print(rename)
    } else if let select = command.asSelect {
        // handle select
        print(select)
    } else {
        // send BAD command
    }

as you indicated — which is not nice and smells like it’s not particularly performant.

I still think that X-commands are so exotic that we can keep the current approach.

@mdiep
Copy link
Collaborator

mdiep commented Apr 23, 2020

I agree with @danieleggert that pattern matching is useful when handling commands in the context of a server. This also applies to testing, where you might write a fake server or want to extract information from the command. Enums are more natural and concise.

The existing design seems like it has extensibility in the right place—in Command.xcommand. Is there a specific type of extensibility that you foresee that isn't handled by that?

@weissi
Copy link
Member Author

weissi commented Apr 23, 2020

@danieleggert / @mdiep extensibility is necessary to add stuff like the IDLE command, IIRC that came in an extension...

Regarding performance, I’m pretty sure we can come up with a spelling that gets us a jump table. It’s unlikely (not impossible though, I’ll check) that LLVM realises that it could turn those tons of ifs into a jump table (over the tag bits of the underlying (private) enum). I have a few ideas that I’ll explore trying to get a jump table but I need some time because it all requires SIL/assembly reading because Swift doesn’t have any guarantees that allow you to use guaranteed optimisations.

Another option of course is to make a “matcher” protocol that has one function per command that you want to match and requires a special one for “unknown command”. To add a new command, we’d provide a default implementation that goes to the “unknown command”.

If only swift had open enums 😅

@weissi
Copy link
Member Author

weissi commented Apr 23, 2020

(It does, but only in library evolution mode which isn’t what SwiftPM uses and also it comes with other very serious drawbacks)

@weissi
Copy link
Member Author

weissi commented Apr 24, 2020

@mdiep / @danieleggert alternative is to keep the enum but "hack it open"

public /* @open */ enum Command {
    case rename(...)
    case select(...)
    @available(*, deprecated, message: "this is an open enum")
    case _doNotPatternMatchThisCaseAndUseADefaultInsteadThisEnumIsOpen🚯(NIONever)
}

@Davidde94
Copy link
Collaborator

I've been thinking about this a little over the last couple of days. What about being able to give the parser additional parsing functions.

E.g. we could implement some function

Parser.addCustomParser(_ func: (ByteBuffer, StackTracker) -> Void) {
    self.customParsers.append(func)
}

// then, inside the parser itself
func parseCommand(buffer: inout ByteBuffer, tracker: StackTracker) throws {
    do {
        // try our normal parsers
    } catch let error as ParserError {
        for parser in self.customParsers {
            try parser(&buffer, tracker)
        }
    }
}

(Note that the above is pseudo-ish code and will not compile)

@weissi
Copy link
Member Author

weissi commented May 7, 2020

@Davidde94 there are two issues:

  1. Custom commands added by users. Those custom commands wouldn't be in the enum, we'd support them either with user events or by having an extra enum case where you'd have a way those custom commands
  2. New command we introduce in NIOIMAP that need to have their own enum cases

Your proposal could be part of a solution to (1) but this ticket is about (2).

@Davidde94
Copy link
Collaborator

I've been thinking a little about this recently. I don't think it matters too much that it's an enum. If we add support for a new command then we can just bump the major version. Adding support for an entirely new set of RFCs seems like good enough cause to push out a new major IMO.

@weissi
Copy link
Member Author

weissi commented Apr 13, 2021

@Davidde94 we can't do that. "An entirely new RFC" sounds like there are only 1 or 2. But there are many so I don't think this is reasonable. It's very common to the IMAP protocol that new commands/responses get added so we need to support that. We can just add a case _doNotMatchThisPleaseAddADefaultToMatchNewUnknownCommands.

@danieleggert
Copy link
Collaborator

I’d argue, though, that there a few RFCs that add new commands. Most of them just change how existing commands work or what responses contain, etc.

Before we move ahead here, I think it would be good to look at any RFCs that aren’t supported, yet. Then see which ones are likely to make sense, and then evaluate what changes would be needed to support them.

I’d assume that for most of them we would have to push out a new major version — not due to commands, but due to how they’d change arguments to existing commands and/or responses.

#286 was our initial “wish list”. David initially had a list of “all” RFCs here https://github.com/apple/swift-nio-imap/blob/bc1e84b3b8ada9bfbcdd098a77a33e565ac0efea/README.md

@Davidde94
Copy link
Collaborator

@weissi your solution is a hack though. If we're concerned about pushing too many majors versions we can always batch RFCs. @danieleggert good idea, I can take care of this.

Also worth noting that this doesn't only apply to commands, but also responses.

@weissi
Copy link
Member Author

weissi commented Apr 13, 2021

@Davidde94 yes, it's a hack until https://bugs.swift.org/browse/SR-12666 is resolved.

@Davidde94 Davidde94 added this to the 0.3 milestone Apr 29, 2021
@danieleggert danieleggert modified the milestones: 0.3, 0.4 Dec 14, 2021
danieleggert added a commit that referenced this issue Feb 19, 2022
This _only_ handles _encoding_ of custom commands.

This simply adds a
```swift
case custom(name: String, payloads: [CustomCommandPayload])
```
such that clients of this library do their own encoding of such commands.

This is related to the discussion in #92
Davidde94 pushed a commit that referenced this issue Feb 21, 2022
This _only_ handles _encoding_ of custom commands.

This simply adds a
```swift
case custom(name: String, payloads: [CustomCommandPayload])
```
such that clients of this library do their own encoding of such commands.

This is related to the discussion in #92
@danieleggert danieleggert added the kind/enhancement Improvements to existing feature. label Feb 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/enhancement Improvements to existing feature.
Projects
None yet
Development

No branches or pull requests

5 participants