-
Notifications
You must be signed in to change notification settings - Fork 339
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
RFC - Stripity Stripe 2.0 #102
Comments
A few comments:
Generally I would say I am not very afraid of high churn in Stripe's API. This year there have been 8 changes, almost all fairly trivial. API version changes can be tacked pretty well to semantic versioning ranges in this package. And I have vastly more confidence in the ability of a community to respond to any future Stripe API changes than I am in my own ability to respond to those changes personally via a super low-level library. I think some of the above comments have significant impact on your proposed design. Obviously happy to discuss further in the course of this issue. |
I'll ask the same question I did over in #92, with these changes what benefit does this library offer over just using an HTTP client directly? I mean this honestly and not facetiously. I've been working with Elixir for a while now, and I generally discourage people from using libraries that are only lightweight API wrappers in the same way that I recommended not using HTTPoison in favor of using Hackney directly. It creates additional dependency levels that may be better satisfied through in-house code that requires the same level of maintenance. I think what you're suggesting with "a consistent and easy to understand high level API" is important, and that should be a central focus of the change. At the same time, I strongly disagree with your comment "Given that Elixir was born of Ruby, we will seek to closely match the documented Ruby Interface." Elixir takes inspiration from Ruby, but seeking to match implementations in Ruby to implementations in Elixir will can afoul of paradigmatic differences. Also, I have to respond to this:
First, this in no way should impact code readability, and code readability should never be sacrificed for that reason. You are not losing any of the value of Elixir by calling an Erlang library. You are not losing any syntatic sugar. You are losing the HTTPoison response wrapper, and instead you have a standardized tuple that you can match on instead. I've replaced HTTPoison with Hackney a few times. I've done more than my fair share of Hackney tuning. Nothing is being lost except a level of dependency resolution. |
I can see the logic in taking stripe-ruby’s |
I would also agree with the reasoning around Hackney vs HTTPoison. There are good reasons to drop down into Erlang and this seems like one of them. |
I mean, I hesitate to call it "dropping down." The module name is just written differently using atom notation. It's still Elixir. |
I'm very glad that this seems to be the central takeaway from the conversation so far:
What this looks like, exactly, I think is the only point of contention that exists here. @dmvt I'm hoping there's not significant disagreement with the points I had raised above. I know you want to avoid a heavy API here but don't think we're at odds on that. In the final analysis, really just hopeful that we're not envisioning two entirely distinct packages. Given that our goals are flexibility, consistency, testability, and ease of use, I don't think we actually do diverge here in a sense that necessitates forking paths. Disagreeing with modeling this on the Ruby implementation I think hardly qualifies as fundamentally divergent. |
Overall, I share most of the sentiment with @DavidAntaramian and @joshsmith Using the ruby implementation as a sort of a baseline feature goal is good, but I wouldn't insist on a 1-to-1 mapping. It can easily lead to trouble, and push as away from using the advantages elixir offers. I'm OK with the idea of adding some syntactic sugar, but I get the motivation behind not wanting it (yet), so I would strive for 2.0 to get the actual, base functionality done. Maybe a good compromise would be to list what the current library has and decide which of those features are useful to keep around. We might be able to trim it down some, while still keeping most of the usefulness. For example, the library offers I don't really have much to add. I share the sentiments. I'm also not sure about low-level vs high-level API in that I'm not 100% on what that actually means. |
@begedin i agree with you there on delete_all, pretty much never. I'm always for keeping wrappers specifically in line with the API and nothing more. Anytime I build a wrapper I almost never include utility functions myself. I say almost never, because I'm sure there are exceptions, I guess I just haven't found one yet. If you need a Utility function, write it in house. That's pretty much how I see it. I would like to see us keep it with its intended core functionality. |
@begedin With regards to low-level vs high-level API, typically a high-level has greater organization of functions that correspond directly to the resources they operate on instead of being a direct pass-through to the HTTP API. For example, Thankfully, Elixir doesn't force any choice. It's traditional for Elixir packages to provide a high-level API for developer convenience while still exposing their low-level API in case the package doesn't directly offer what the developer needs. |
def create(
attributes,
headers \\ %{},
hackney_options \\ %{},
secret_key \\ ""
), do: api_create
@joshsmith, @begedin - I didn't mean to suggest that we should map our functions one to one with the methods in the Ruby gem, but rather that we should map the functions in the high level API modules one to one with the endpoints that stripe provides. This is mostly because I'd like to maintain feel and interface consistency with the official libs. One of the things that really bugs me about version 1 is the use of @DavidAntaramian - I couldn't agree more with your final statement. Elixir gives us the ability to easily provide both, and I think we should. I may have misspoken when originally talking to @joshsmith. What I was driving at with low level vs high level is the distinction between the core functions that are represented in the official documentation and what we have taken to calling the utility functions which wrap or iterate over those core functions. By my way of thinking, we don't need to provide those at all, but, as I said above, I'm willing to be overruled. Thanks for the thoughtful comments so far everyone. I'll be back on and update the text tonight to account for all of these and any more that come along. |
Hackney Replacement // Low-Level APITotally happy to handle the Hackney replacement. The documentation on Hackney is certainly not revolutionary, but the modular documentation (e.g., If you're up for it, I'd like to coordinate the Hackney replacement as part of the low level API system. I typically start by implementing a Depending on the needs, I expand this to be multiple functions that concentrate on specific contexts. For example, if an API has multiple ways of authenticating depending on the context, each gets its own, properly named request function. I haven't used the Connect system before, so you and Josh may need to point me to anything that is different form the standard API documentation. Lists (as optional parameter data types)Regarding optional lists, this is a standard convention in Elixir for passing optional parameters at the end of a function. If everything was ideal it would be maps, but precedent has been set, largely for interoperability and principle-of-least surprise with the Erlang standard library. I believe it's also because keyword lists can be written without VersioningRe: Versioning, from the documentation:
Function NamingWith regards to function naming, The question is really if the primary audience is going to be targeted towards Elixir developers who are using Stripe (expecting Elixir conventions) or developers who have used Stripe before in other languages that are coming to Elixir (expecting Stripe conventions). Being consistent is the thing that really counts. |
@dmvt @joshsmith Also, I'm happy to use any style guide you want, just would ask that it be decided on prior to anyone starting work. Credo can also be really useful for automated checking |
Thanks for the additional info @DavidAntaramian. I like everything you said. VersioningGiven that it is handled in a header, this would be passed as an option in the second argument of any of the high level functions. No need to do anything special to support this. Lists as optional argumentsMy personal preference would be to use maps. We seem to agree that having pattern matching capabilities would be nice and, while the Function NamingAll of the official Stripe libraries focus on Stripe conventions over language specific conventions so I think we should follow that pattern. To expand on your Hackney / Low Level APII'm fine with you designing this however you see fit, however, I don't think it'll be useful to have two different versions based on Auth types. There are only two auth approaches for Stripe Connect, passing a connected account's secret key or passing your secret key (the defaultly configured one) plus a Given the Low Level rewrite, we may want to go a step further than I already have and perform a full rewrite of everything. Can you propose a testing strategy for the low level stuff? |
One more thought on the Low Level API and List arguments: I could be convinced to make the high level api functions only accept an attributes map and a headers list. Hackney options should never need to be changed inline and the secret key option at the end is a sub-optimal way to handle Connect auth. Eliminating it would force users to either use the low level api or conform to preferred conventions. |
I've been writing a basic API integration for my internal application that I'll be using until the v2 work on this library is done. I have some thoughts from that that apply here. VersioningSee Low-Level API List as Optional ArgumentsI've had a few ideas about this as I've been implementing. Will cover them in the "high-level" api section. Function NamingThis sounds reasonable to me. Low Level APIOK, now I looked at Connect and realized it's a lot less complicated then I thought. In regards to the version and the primary account API key, both of these should be kept in the application configuration in my opinion. That's standard Elixir practice. This is the bulk of my low-level API function that I'm using internally: @spec request(method, String.t, nil | map, %{required(String.t) => String.t}, Keyword.t) :: {:ok, map} | {:error, Exception.t}
def request(method, endpoint, body, headers, opts) do
url = base_url() <> endpoint
headers = Map.merge(headers, %{
"Accept" => "application/json; charset=utf8",
"Accept-Encoding" => "gzip",
"Authorization" => "Bearer " <> get_api_key(),
"Content-Type" => "application/x-www-form-urlencoded",
"Connection" => "keep-alive"
})
req_body = encode_body(body)
req_headers = encode_req_headers(headers)
req_opts = Keyword.merge(opts,[
with_body: true
])
:hackney.request(method, url, req_headers, req_body, req_opts)
|> handle_response()
end For the purposes of Stripity-Stripe, I would update it so that High Level APII've also been playing around with a higher level API in my codebase. What I'm actually doing instead of using maps is accepting and returning structs. This is actually pretty easy and according to the documentation is how Stripe prefers libraries be written:
What this essentially means is that you would have a module def retrieve(id, opts // opts) do
endpoint = @stripe_base_endpoint <> "/" <> id
Stripe.request(:get, endpoint, nil, %{}, opts)
|> decode_api_map()
end Which you could call as %Stripe.Plan{amount: 10000, created: 1477087897, currency: "usd",
id: "oak", interval: "month", interval_count: 1, metadata: %{}, name: "Oak",
statement_descriptor: nil, trial_period_days: nil} You could also call |
@DavidAntaramian quick clarification: I'm assuming in your "Low Level API" section, at the very end, you meant Overall this looks like a strong direction to me and I appreciate the feedback here. Also like that you found that quote from Stripe. I'd like to direct some love here ASAP since I'd personally not like to rely too terribly heavily on the 1.x implementation of this package in our own application, so I think I'm willing to direct that time and attention here first rather than re-implement its consumption all over on my end later. |
@joshsmith Yes…typing too fast 😞 trying to catch up from Friday's outage |
@DavidAntaramian Aren't we all :) Love the comments and the idea of using Structs. Let's do that. |
Woo, had way too much to do this week, but now I'm back in working order. I can get the base-level stuff committed tonight or tomorrow night depending on my schedule. Are you going to do a v2 working branch for this? |
@DavidAntaramian yes, that's the plan! @dmvt got started with a |
This is good timing since we're now circling back to needing to implement this. We've had some interns learning Elixir and Phoenix for the first time (go them!), so I've been spending a lot of time there. But both @begedin and I should have time freeing up to carry this forward quickly, and I believe @dmvt is similarly expecting to free up here shortly. |
@DavidAntaramian This is indeed perfect timing. I also had a crazy busy week last week, took on a new position and got them going on Elixir. @joshsmith, this will be blocking for us in the next week or so as well. @joshleecreates will probably be jumping in on this with me. |
Excited to watch this get moving. |
@Dangeranger would love your comments on the testing strategy |
So, I've been working with Stripe on a corporate project with Elixir for a couple weeks now. In terms of a testing solution, what I've had working so far is a system that uses Bypass. It gets extremely complicated because there's no good way to maintain state. So… First, I would recommend that any test "mock" system be distributed as a separate package. The reason for this is that it will undoubtedly require various testing specific components that people may not want to include in their production builds. This way, people can do the following: [
{:stripity_stripe, "~> 2.0"},
{:stripity_stripe_testing, "~> 2.0", only: :test}
] Second, the test package can distribute a lightweight replica of the Stripe service. That way, testing can be done something like the following: test "retrieves all plans where the storage limit is greater than 30" do
Stripe.Test.create(:plan, 10)
Stripe.Test.create(:plan, 10, metadata: [storage_limit: 10])
Stripe.Test.create(:plan, 10, metadata: [storage_limit: 35])
Stripe.Test.create(:plan, 10, metadata: [storage_limit: 30])
# This is business logic that will call the StripityStripe module
plans = MyApp.Module.get_plans(:gt, 30)
assert Enum.count(plans) == 10
end My thought process currently is that data can be stored on a per-test basis in ETS, allowing users to build up the necessary state and tear it down without needing to know the intricacies of the system. |
@DavidAntaramian are you expecting then that most of our tests here should rely on this external package rather than being self-contained? |
@joshsmith Yes, though I hesitate to think of it as external as much as complementary. Thought it through a bit more. Essentially, what would happen is as follows: defmodule Stripe.CustomerTest do
# this may be able to be true
use ExUnit.Case, async: false
# Most of this setup process can be abstracted into the library, but putting
# it here so that others understand
setup do
# Starts a separate process
# - That process loads a Plug responder that binds to an available port
# - It also seeds an ETS table that it claims ownership of
# - The ETS table and Plug responder are bound to this specific test,
# so they are isolated to this test and any state manipulation on them
# will not affect other tests
# - Returns a struct of some form like %Stripe.Test.Server{port: 4515, pid: #PID<0.35.0>}
{:ok, stripe_server} = Stripe.Test.Server.start()
Application.put_env(:stripity_stripe, :api_base_url, "http://localhost:#{stripe_server.port}")
on_exit fn ->
# Cleans up the ETS table and stops the plug responder and owning process
Stripe.Test.Server.stop(stripe_server)
end
{:ok, stripe: stripe_server}
end
describe "Stripe.Customer.retrieve/1" do
test "retrieves a customer by ID", %{stripe: stripe} do
# Uses the Stripe.Test.Customer module to create a new customer in the
# ETS backed server (essentially, this is a factory method)
%{id: id} = Stripe.Test.Customer.create(stripe)
# Makes an actual API request from the high level API through the low level
# API and through the OS network stack
{:ok, customer} = Stripe.Customer.retrieve(id)
assert customer.id == id
end
end
end |
I'm closing this RFC since much of what we discussed here in imperfect form is now implemented in the |
Start Date: 2016-10-19
Summary
Stripity Stripe is in need of an upgrade to provide a more flexible Elixir wrapper for the amazing Stripe API. There are two main goals of this rewrite. First, Stripity Stripe 2.0 should have a consistent and easy to understand high level API. Given that Elixir was born of Ruby, we will seek to closely match the documented Ruby Interface. Second, there should be full test coverage which does not rely on recorded responses of a specific version of the API. We want to be testing our functionality, not that of the Stripe API itself.
Motivation
The initial desire for 2.0 came out of a conversation between Dan Matthews of Strumber and Josh Smith of Code Corps. Both teams were/are implementing on top of Stripe's Connect functionality and found the existing version's endpoint modules to be generally lacking best practice support for the Stripe-Account header. Additionally, we discovered unnecessary dependencies, unsupported endpoints, inconsistent variable names and a good deal of repetition.
Rob Conery was gracious enough to hand us the reins several days ago.
Detailed design
HTTPoison will be replaced with Hackney at the suggestion of @DavidAntaramian in Issue #100. Along this thought line, dependencies should be generally avoided unless absolutely necessary. Whenever possible, the lowest level dependency should be used.
Stripe provides a common endpoint structure for each primary endpoint. We will attempt to handle the majority of them through the use of common modules supplemented by heavy documentation. An example of this is as follows:
We will also only provide functions documented in the official Stripe API libraries. No additional functions are allowed directly on the High Level API modules.
Some endpoints have dependency ids in their URLs. These values will be extracted from the attributes map.
All public functions on High Level API modules have an arity of 4, the last 3 of which are optional.
Drawbacks
There is next to no backwards compatibility with Stripity Stripe Version 1. We're singularizing nearly every module name (
Stripe.Customers
becomesStripe.Customer
), leaving out (moving?) courtesy functions such asall
and changing the arity of almost every function that is kept.We're also intentionally treating fewer dependancies and DRYness as higher priority concerns than code readability when we choose to use Erlang modules such as Hackney directly, abandoning the syntactic sugar and familiarity of their Elixir wrappers. This choice may deter wider contributions in those areas of the code.
Unresolved questions
Should we provide some or all of the non-standard functions from version 1 in a
Stripe.Utils
module? The main one I personally find useful is theall
function which iterates over thelist
endpoint.The text was updated successfully, but these errors were encountered: