Skip to content

Elegant usage of JSON APIs in Hyperstack #274

Open
@barriehadfield

Description

@barriehadfield

In my app, I want to use traditional REST APIs so I have greater control over which data I fetch and when I fetch it. My app is a mobile app used in a gym, so an internet signal cannot be assumed, so I need to fetch all the data the user might want and hold it in memory.

That said, I did not want to work with big complex multi-nested JSON arrays on the front end! I wanted an ORM for a JSON response, so I wrote this wonderful little recursive class which lets me treat a JSON array as a model which supports has_many relationships out the box!

I thought I would share here in case anyone else found it useful.

(Note there is no security or policy covered here to keep the example as simple as possible).

In the Rails controller:

class ApiController < ApplicationController
  def workout
    params.permit(:workout_id)
    @workout = Workout.find params[:workout_id]

    render "api/workout"
  end
end

I am using the RABL Gem to compose the JSON response, so I have a view which creates the JOSN which is a Workout model which includes two nested has_many associations ( primary_exercises and alternatives) each of which has their own child collection called exsets.

object @workout
attributes :id, :user_id, :created_at, :name, :prescribed_duration_mins
child primary_exercises: :primary_exercises do |e|
  exercise e
  child alternatives: :alternative_exercises do |e|
    exercise e
  end
end

def exercise(e)
  node(:id) { |e| e.id }
  node(:name) { |e| e.exercise_type.name }

  child :exsets do |es|
    node(:id) { |es| es.id }
    node(:prescribed_time_in_s) { |es| es.prescribed_time_in_s }
    node(:prescribed_weight_in_kg) { |es| es.prescribed_weight_in_kg }
  end
end

Then on the client, I have my wonderful recursive class:

class JsonModel
  include Hyperstack::State::Observable

  def initialize(path: nil, name: nil, json: nil)
    return get_data(path) if path
    init name, json
  end

  def loaded?
    observe @loaded
  end

  def inspect
    puts @name
    puts @json
  end

  def method_missing(m)
    return nil unless @json[@name][m]
    return @json[@name][m] unless @json[@name][m].class == Array

    a = []
    @json[@name][m].each do |slice|
      jm = JsonModel.new
      jm.init slice.keys.first, slice
      a << jm
    end
    return a
  end

  private

  def init(name, json)
    @name = name
    @json = json
  end

  def get_data(path)
    HTTP.get(path).then do |resp|
      puts "got #{path}"
      init(resp.json.keys.first, resp.json)
      mutate @loaded = true
    end.fail do |resp|
      puts resp.json
      alert "failed to get #{path}"
    end
  end
end

To use my Workout model in my client code, I load it in the before_mount lifecycle method:

@workout = JsonModel.new(path: "/api/workout/#{id}.json")

Then in my Component I can use the response just like a model:

if @workout.loaded?
  H1 { @workout.name }
  @workout.primary_exercises.each do |pe|
    H2 { pe.name } 
    pe.exsets.each do |set|
      H3 { .... }
    end
  end
else
  P { "Loading..." }
end

And TaDa! ORM-like wrapping of JSON responses :-)

Metadata

Metadata

Labels

discussionneeds on going discussion before resolving what kind of problem it is, and what to do with it.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions