DEV Community

Cover image for HTTP based OOP
Thomas Hansen
Thomas Hansen

Posted on • Originally published at aista.com

HTTP based OOP

There is a reason why I don't like classic OOP. The primary one being that it destroys my creativity and ability to express myself. As proof of that, realise I just invented HTTP based OOP, with inheritance and polymorphism, with the capability of "overriding" an existing HTTP endpoint, and inject additional logic in my "HTTP interceptor", allowing me to "extend" the original HTTP invocation. This allows me to inject additional custom business logic in my original HTTP endpoint, without modifying it. And of course, it's almost impossible to implement using classic OOP.

In the video below I am demonstrating how to implement Stripe payments into "whatever" HTTP API you have from before, using polymorphism, and "overriding" my original HTTP invocation. Basically, I am demonstrating HTTP based OOP, overriding my existing endpoint.

The beauty of this is that it doesn't matter what your original HTTP endpoint is implemented in, as long as it accepts JSON and returns JSON. You can use this with ...

  • Python
  • PHP
  • GraphQL
  • PostgREST
  • Hasura
  • Supabase
  • C#
  • Java
  • "Whatever" really ...

More interestingly, this is almost impossible to achieve with classic OOP, due to OOP's "obsession with strongly typing". You can literally only do this in a non-OOP context, where you completely ignore strongly typing - Hyperlambda being one such example. Implying that Hyperlambda can basically "override" anything you might have from before, as long as it's based upon HTTP and JSON. The technique is easily understood by anyone understanding the concept of YALOA or "Yet Another Layer Of Abstraction".

In the video example above, I need to apply some (tiny) changes to my original backend, to allow for associating users with Stripe customer IDs, create a one to many relationship between users and payment methods, and store payments and subscriptions internally in my app. However, these changes would be microscopic in nature compared to the code required to implement Stripe manually in my own backend. Hence, I basically eliminate 90% of the burden required to implement Stripe. And, if I want to, I can probably create a generalised version of the above example, where I am no longer dependent upon Stripe, but can easily exchange my payment provider with any other payment provider by simply changing my Hyperlambda file. The last part allows me to change my payment provider, without touching my own backend, but instead simply providing an additional "overridden" HTTP method.

Notice, if you want to ensure your intercepted HTTP endpoint becomes the equivalent of "private", you might want to attach some "secret" token to the invocations towards your encapsulated endpoint, and only exposing it to your Magic cloudlet. If you don't do this, people capable of "guessing" the URL to your original endpoint might in theory be able to fake payments, getting product for free.

The process is quite simple.

  1. Create a Hyperlambda endpoint
  2. Invoke your own "extended" business logic in your Hyperlambda endpoint
  3. Invoke the "overridden" HTTP endpoint, optionally with additional data resulting from executing your Hyperlambda
  4. Return to the client whatever data your "encapsulated" endpoint returns

Paradoxically, HTTP based OOP is not only good OOP, but also good SOLID, and for the most parts obeys by the Open-Closed Principle, and there is zero OOP in it. To explain it a bit humorously with some "geek humour" ...

No classes where harmed while inventing HTTP OOP

Watch the above video to understand the concept. Now as to what to refer to this as? I've got no idea, however my initial intuition tells me it is O2, as in Objects to the second exponent or something - Suggestions ...? :D

At least that name would make Bjarne Stroustrup and Anders Hejlsberg choke for a while on their morning coffee ... ;)

I want to emphasise that this is (almost) 100% impossible using classic OOP, and requires a super dynamic programming language such as Hyperlambda.

To reproduce what I am doing, register for a cloudlet below, and start playing with O2 ...

Thank you for reading, now let the debate begin :D

Below is my code ...

echo.post.hl

add:x:+
   get-nodes:x:@.arguments/*
return
Enter fullscreen mode Exit fullscreen mode

customer.post.hl

.arguments:*
.description:Interceptor invoking Stripe to create a Stripe customer, for then to attach the customer ID to specified payload, before invoking intercepted endpoint.

/*
 * Invoking Stripe to create a customer for then to attach
 * the customer ID to the payload we're passing in to the
 * original endpoint, before invoking intercepted endpoint
 * now with a Stripe Customer ID, allowing you to associate
 * the user internally with a Stripe customer object.
 */
.before

   /*
    * If you're using automatic tax calculations, you'll
    * need to pass in the IP address of the client - At
    * which point you'll have to uncomment the line of
    * code below, and pass it into slot invocation.
    */
   request.headers.get:X-Real-IP

   // Sanity checking invocation.
   validators.mandatory:x:@.arguments/*/name
   validators.mandatory:x:@.arguments/*/email
   validators.email:x:@.arguments/*/email

   // Invoking Stripe API.
   unwrap:x:+/*
   signal:stripe.customers.create
      name:x:@.arguments/*/name
      email:x:@.arguments/*/email
      ip_address:x:@request.headers.get

   // Attaching Stripe's customer id to the payload.
   unwrap:x:+/*/*
   add:x:../*/http.post/*/payload
      .
         stripe_customer_id:x:@signal

// Evaluating Stripe lambda object.
eval:x:@.before

// Endpoint we're intercepting.
.endpoint:"https://tiger-polterguy.gb.aista.com/magic/modules/stripe-interceptor/echo"

/*
 * Checking if we've got an Authorization HTTP header,
 * at which point we forward it to the original HTTP endpoint.
 */
request.headers.get:Authorization
if
   not-null:x:@request.headers.get
   .lambda
      add:x:../*/http.post/*/headers
         .
            Authorization:x:@request.headers.get

// Forwarding arguments given to endpoint to intercepted endpoint.
add:x:../*/http.post/*/payload
   get-nodes:x:@.arguments/*

// Invoking the intercepted HTTP endpoint.
http.post:x:@.endpoint
   headers
      Content-Type:application/json
   convert:true
   payload

// Returning the intercepted endpoint's status code.
response.status.set:x:@http.post

// Returning response payload from intercepted endpoint to caller.
add:x:+
   get-nodes:x:@http.post/*/content/*
return
Enter fullscreen mode Exit fullscreen mode

payment-method.post.hl

.arguments:*
.description:Interceptor invoking Stripe to create a payment method for the specified customer, for then to attach the payment method id and payment method data to the specified payload, before invoking intercepted endpoint.

/*
 * Invoking Stripe to create a payment method and associate
 * it with the specified customer, for then to attach the
 * payment data to the original endpoint, before we invoke
 * intercepted endpoint.
 */
.before

   // Sanity checking invocation.
   validators.mandatory:x:@.arguments/*/card_number
   validators.mandatory:x:@.arguments/*/card_exp_month
   validators.mandatory:x:@.arguments/*/card_exp_year
   validators.mandatory:x:@.arguments/*/card_cvs
   validators.mandatory:x:@.arguments/*/customer_id

   // Invoking Stripe to create a payment method.
   unwrap:x:+/*
   signal:stripe.payment_methods.create
      card_number:x:@.arguments/*/card_number
      card_exp_month:x:@.arguments/*/card_exp_month
      card_exp_year:x:@.arguments/*/card_exp_year
      card_cvs:x:@.arguments/*/card_cvs

   // Invoking Stripe to attach the payment method to the customer.
   unwrap:x:+/*
   signal:stripe.payment_methods.attach
      customer_id:x:@.arguments/*/customer_id
      payment_method:x:@.before/*/signal/[0,1]/*/id

   // Making sure we pass in 4 last digits of card to intercepted endpoint.
   strings.length:x:@.arguments/*/card_number
   math.subtract:x:-
      .:int:4
   strings.substring:x:@.arguments/*/card_number
      get-value:x:@math.subtract
      .:int:4

   /* 
    * Passing in brand, payment method id, and 4 last digits of card
    * to intercepted endpoint.
    */
   unwrap:x:+/*/*
   add:x:../*/http.post/*/payload
      .
         brand:x:@.before/*/signal/[0,1]/*/brand
         card:x:@strings.substring
         payment_method_id:x:@.before/*/signal/[0,1]/*/id

   // Removing card data.
   remove-nodes:x:@.arguments/*/card_number
   remove-nodes:x:@.arguments/*/card_exp_month
   remove-nodes:x:@.arguments/*/card_exp_year
   remove-nodes:x:@.arguments/*/card_cvs

// Evaluating [.before] lambda object.
eval:x:@.before

// Endpoint we're intercepting.
.endpoint:"https://tiger-polterguy.gb.aista.com/magic/modules/stripe-interceptor/echo"

/*
 * Checking if we've got an Authorization HTTP header,
 * at which point we forward it to the original HTTP endpoint.
 */
request.headers.get:Authorization
if
   not-null:x:@request.headers.get
   .lambda
      add:x:../*/http.post/*/headers
         .
            Authorization:x:@request.headers.get

// Forwarding arguments given to endpoint to intercepted endpoint.
add:x:../*/http.post/*/payload
   get-nodes:x:@.arguments/*

// Invoking the intercepted HTTP endpoint.
http.post:x:@.endpoint
   headers
      Content-Type:application/json
   convert:true
   payload

// Returning the intercepted endpoint's status code.
response.status.set:x:@http.post

// Returning response payload from intercepted endpoint to caller.
add:x:+
   get-nodes:x:@http.post/*/content/*
return
Enter fullscreen mode Exit fullscreen mode

payment.post.hl

.arguments:*
.description:Interceptor invoking Stripe to create a payment for a customer, for then to attach the payment data to specified payload, before invoking intercepted endpoint.

/*
 * Invoking Stripe to create a payment and associate
 * it with the customer, for then to attach the
 * payment data to the payload we're passing in to
 * the original endpoint.
 */
.before

   validators.mandatory:x:@.arguments/*/amount
   validators.mandatory:x:@.arguments/*/currency
   validators.mandatory:x:@.arguments/*/payment_method
   validators.mandatory:x:@.arguments/*/customer_id

   unwrap:x:+/*
   signal:stripe.payments.create
      amount:x:@.arguments/*/amount
      currency:x:@.arguments/*/currency
      payment_method:x:@.arguments/*/payment_method
      customer_id:x:@.arguments/*/customer_id

   // Passing in payment data to intercepted endpoint.
   unwrap:x:+/*/*
   add:x:../*/http.post/*/payload
      .
         payment_id:x:@signal/*/id

// Evaluating [.before] lambda object.
eval:x:@.before

// Endpoint we're intercepting.
.endpoint:"https://tiger-polterguy.gb.aista.com/magic/modules/stripe-interceptor/echo"

/*
 * Checking if we've got an Authorization HTTP header,
 * at which point we forward it to the original HTTP endpoint.
 */
request.headers.get:Authorization
if
   not-null:x:@request.headers.get
   .lambda
      add:x:../*/http.post/*/headers
         .
            Authorization:x:@request.headers.get

// Forwarding arguments given to endpoint to intercepted endpoint.
add:x:../*/http.post/*/payload
   get-nodes:x:@.arguments/*

// Invoking the intercepted HTTP endpoint.
http.post:x:@.endpoint
   headers
      Content-Type:application/json
   convert:true
   payload

// Returning the intercepted endpoint's status code.
response.status.set:x:@http.post

// Returning response payload from intercepted endpoint to caller.
add:x:+
   get-nodes:x:@http.post/*/content/*
return
Enter fullscreen mode Exit fullscreen mode

subscription.delete.hl

.arguments:*
.description:Interceptor invoking Stripe to create a subscription for a customer, for then to attach the subscription id to specified payload, before invoking intercepted endpoint.

/*
 * Invoking Stripe to create a subscription and associate
 * it with the customer, for then to attach the
 * subscription data to the payload we're passing in to
 * the original endpoint.
 */
.before

   // Sanity checking invocation.
   validators.mandatory:x:@.arguments/*/subscription

   // Invoking Stripe.
   unwrap:x:+/*
   signal:stripe.subscriptions.cancel
      subscription:x:@.arguments/*/subscription

// Evaluating [.before] lambda object.
eval:x:@.before

// Endpoint we're intercepting.
.endpoint:"https://tiger-polterguy.gb.aista.com/magic/modules/stripe-interceptor/echo"

/*
 * Checking if we've got an Authorization HTTP header,
 * at which point we forward it to the original HTTP endpoint.
 */
request.headers.get:Authorization
if
   not-null:x:@request.headers.get
   .lambda
      add:x:../*/http.post/*/headers
         .
            Authorization:x:@request.headers.get

// Forwarding arguments given to endpoint to intercepted endpoint.
add:x:../*/http.post/*/payload
   get-nodes:x:@.arguments/*

// Invoking the intercepted HTTP endpoint.
http.post:x:@.endpoint
   headers
      Content-Type:application/json
   convert:true
   payload

// Returning the intercepted endpoint's status code.
response.status.set:x:@http.post

// Returning response payload from intercepted endpoint to caller.
add:x:+
   get-nodes:x:@http.post/*/content/*
return
Enter fullscreen mode Exit fullscreen mode

subscription.post.hl

.arguments:*
.description:Interceptor invoking Stripe to create a subscription for a customer, for then to attach the subscription id to specified payload, before invoking intercepted endpoint.

/*
 * Invoking Stripe to create a subscription and associate
 * it with the customer, for then to attach the
 * subscription data to the payload we're passing in to
 * the original endpoint.
 */
.before

   // Sanity checking invocation.
   validators.mandatory:x:@.arguments/*/price
   validators.mandatory:x:@.arguments/*/customer_id
   validators.mandatory:x:@.arguments/*/payment_method

   // Invoking Stripe.
   unwrap:x:+/*
   signal:stripe.subscriptions.create
      price:x:@.arguments/*/price
      payment_method:x:@.arguments/*/payment_method
      customer_id:x:@.arguments/*/customer_id

   // Passing in subscription data to intercepted endpoint.
   unwrap:x:+/*/*
   add:x:../*/http.post/*/payload
      .
         subscription_id:x:@signal/*/id
         product:x:@signal/*/product

// Evaluating [.before] lambda object.
eval:x:@.before

// Endpoint we're intercepting.
.endpoint:"https://tiger-polterguy.gb.aista.com/magic/modules/stripe-interceptor/echo"

/*
 * Checking if we've got an Authorization HTTP header,
 * at which point we forward it to the original HTTP endpoint.
 */
request.headers.get:Authorization
if
   not-null:x:@request.headers.get
   .lambda
      add:x:../*/http.post/*/headers
         .
            Authorization:x:@request.headers.get

// Forwarding arguments given to endpoint to intercepted endpoint.
add:x:../*/http.post/*/payload
   get-nodes:x:@.arguments/*

// Invoking the intercepted HTTP endpoint.
http.post:x:@.endpoint
   headers
      Content-Type:application/json
   convert:true
   payload

// Returning the intercepted endpoint's status code.
response.status.set:x:@http.post

// Returning response payload from intercepted endpoint to caller.
add:x:+
   get-nodes:x:@http.post/*/content/*
return
Enter fullscreen mode Exit fullscreen mode

JSON Payloads

POST customer

{
    "name": "Johnny Two Times",
    "email": "johnny@hotmail.com"
}
Enter fullscreen mode Exit fullscreen mode

POST payment-method

{
    "card_number": "4242424242424242",
    "card_exp_month": "10",
    "card_exp_year": "2023",
    "card_cvs": "314",
    "customer_id": "CUSTOMER_ID_FROM_PREVIOUS_INVOCATION"
}
Enter fullscreen mode Exit fullscreen mode

POST payment

{
    "amount": 1234,
    "currency": "USD",
    "payment_method": "PAYMENT_METHOD_ID_FROM_PREVIOUS_INVOCATION",
    "customer_id": "CUSTOMER_ID_FROM_PREVIOUS_INVOCATION"
}
Enter fullscreen mode Exit fullscreen mode

POST subscription

{
    "price": "PRICE_FROM_STRIPE_DASHBOARD",
    "customer_id": "CUSTOMER_ID_FROM_PREVIOUS_INVOCATION",
    "payment_method": "PAYMENT_METHOD_ID_FROM_PREVIOUS_INVOCATION"
}
Enter fullscreen mode Exit fullscreen mode

Top comments (8)

Collapse
 
jmfayard profile image
Jean-Michel ๐Ÿ•ต๐Ÿปโ€โ™‚๏ธ Fayard • Edited

There is more than one way to be a good dev.

I'm a big proponent of static types because I'm lazy

Laziness: The quality that makes you go to great effort to reduce overall energy expenditure. It makes you write labor-saving programs that other people will find useful and document what you wrote so you don't have to answer so many questions about it.

Being lazy, I'm more than ready to accept the initial overhead to design and polish my static types well.

I do it because it means that I know I can rely on JetBrains IntelliJ and its refactoring magic to do so much of the tedious work afterwards.

Collapse
 
polterguy profile image
Thomas Hansen • Edited

I'm a big proponent of static types because I'm lazy

Interestingly, static typing is why this is impractical to do with traditional programming languages. If I search for "How to create a web API using C#", I will find 1,000,000 hits on Google. 999,998 of these will tell me I need to create a "View Model" first, so I create a View Model.

If I was to implement the intercepter using static typing, I'd have to edit it every single time I do a change in my original API. With a dynamic language, such as Hyperlambda I don't have to edit it as long as the fields I am using in it doesn't change, allowing me to create it once, for then to mostly ignore it for the rest of its lifespan - Paradoxically allowing me to "continue being lazy" ... ;)

Because a dynamic API without strong typing simply "forwards whatever" I am feeding it, since there is no typing in it, and it accepts "whatever" ...

FYI, Hyperlambda is the "super lazy framework" arguably. Watch this where I'm creating some 25,000 lines of code, a complete CRM, creating it from scratch, and deploying it into production in 20 minutes to understand ...

My guess is most others, regardless of programming language or platform, would easily need weeks if not months to reproduce what I'm doing in the above video. If you're serious about "being lazy", the first thing you need to drop is static typing ...

If you're serious about "being lazy", there is simply no way you can beat this thing. This was created from scratch in some roughly 20 minutes ... ^_^

CRM system automatically created with Aista Low-Code

Collapse
 
jmfayard profile image
Jean-Michel ๐Ÿ•ต๐Ÿปโ€โ™‚๏ธ Fayard

Hey, I will have a look at the video sometimes later, thanks.

Collapse
 
emmanuel345 profile image
Emmanuel

I want to join you

Collapse
 
polterguy profile image
Thomas Hansen

Great! Go for it :)

Collapse
 
emmanuel345 profile image
Emmanuel

How can I join you

Thread Thread
 
polterguy profile image
Thomas Hansen

Learn Hyperlambda, then let's talk ... :)

Collapse
 
polterguy profile image
Thomas Hansen

Maybe of interest to you guys @coco98 ...?