Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Automatic Code Generation for SPA

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Automatic Code Generation for SPA

Avatar for tanakaworld

tanakaworld

July 03, 2019
Tweet

More Decks by tanakaworld

Other Decks in Technology

Transcript

  1. About Me • Twitter: @_tanakaworld • GitHub: tanakaworld • Merpay

    ◦ Software Engineer (Frontend) ◦ CSTool / MSTool
  2. 1. What is Code Generation? 2. Tell about the Real

    World Rearchtecture project 3. Tips of Generator Agenda
  3. Schema Driven Development • Make Development flow more efficiently •

    Automatic ◦ Source Code Generation ◦ Document Generation ◦ Testing
  4. Microservices in Merpay Protocol Buffers • gRPC • Write Schema

    by Protocol Buffers • Generate Client and Server
  5. Protocol Buffers service SampleAPI { rpc GetUser(GetUsersRequest) returns (GetUsersResponse) {

    option (google.api.http) = { get : "/users" body: "*" }; } } message GetUsersRequest {} message GetUsersResponse { repeated User user = 1; } .proto
  6. .proto to TypeScript export namespace SampleAPI { export class GetUser

    implements APIRequest<GetUsersRequest, GetUsersResponse> { _response?: GetUsersResponse; path = "/users"; method: HTTPMethod = "get"; constructor(public parameter: GetUsersRequest) {} } } export interface GetUsersRequest {} export interface GetUsersResponse { user?: User[]; }
  7. REAL WORLD Rearchitecture Project 1. Overview 2. Backend Implementation 3.

    API schema 4. Generator 5. Frontend Implementation
  8. Why fast_jsonapi was developed? • Netflix ◦ always use JSON:API

    • AMS is too flexible ◦ AMS is designed to serialize JSON in several different formats, not just JSON:API
  9. TL;DR; API • Resource • Request • Response • Method

    • URL • Params RSpec Schema /app/books
  10. • https://github.com/fotinakis/swagger-blocks • Works with all Ruby web frameworks •

    100% support for all features of the Swagger 2.0 spec • Can use Ruby DSL to write OpenAPI schema swagger-blocks
  11. # app/models/concerns/swagger/book_schema.rb module Swagger::BookSchema extend ActiveSupport::Concern include Swagger::Blocks included do

    swagger_schema :Book, required: [:title, :description, :image_url] do property :id, type: :integer property :title, type: :string property :description, type: :string property :created_at, type: :string property :updated_at, type: :string end end end
  12. openapi-generator • https://github.com/OpenAPITools/openapi-generator • It allows generation from OpenAPI Spec

    ◦ API client libraries ◦ Server stubs ◦ Documentation ◦ Configuration
  13. Generate openapi-generator generate \ -i ./tmp/swagger.json \ -g typescript-axios \

    -o ./app/javascript/src/gen/ --additional-properties="modelPropertyNaming=snake_ case" https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/typescript-axios.md
  14. export interface Book { id?: number; title: string; description: string;

    image_url: string; created_at?: string; updated_at?: string; } export interface BookResponse { id: string; type: string; attributes: Book; } export interface CreateBookRequest { title?: string; description?: string; image?: object; }
  15. export class SampleAppApi extends BaseAPI { public createBook(title?: string, description?:

    string, image?: object, options?: any) { ••• } public deleteBook(id: number, options?: any) { ••• } public getBook(id: number, options?: any) { ••• } public getBooks(options?: any) { ••• } public updateBook(id: number, title?: string, description?: string, image?: object, options?: any) { ••• } }
  16. Overview • Render SPA bundle via Rails View • with

    Turbolinks • SPA ◦ CSR ◦ Vue ◦ Vue Router ◦ Vuex API Client /app/books SPA
  17. Rails Routing Rails.application.routes.draw do // path for SPA // app/books

    // app/books/new // app/books/1 // app/books/1/edit // ••• match 'app/(*spa_path)' => 'spa_app#index', via: :get end
  18. How it works with Turbolinks • Make Vue instance on

    'turbolinks:load' • Destroy Vue instance on 'turbolinks:visit' • ref: https://qiita.com/midnightSuyama/items/efc5441a577f3d3abe74
  19. Make/Destroy Vue instances document.addEventListener('turbolinks:load', () => { const templates =

    document.querySelectorAll('[data-vue]'); templates.forEach((el: HTMLElement) => { const option = options[el.dataset.vue] const vm = new Vue(Object.assign(option, { el })); vms.push(vm); }); }); document.addEventListener('turbolinks:visit', () => { for (let vm of vms) { vm.$destroy(); } vms = []; });
  20. Use require.context of webpack // app/javascript/packs/vue-turbolinks.ts const options = {};

    const requireContext = require.context('../options', false, /\.ts$/); requireContext.keys().forEach(key => { const name = key .split('/') .pop() .split('.') .shift(); options[name] = requireContext(key).default; });
  21. Vuex Store Implementation • Use generated API Client in Store

    • FSA = Flux Standard Action ◦ https://github.com/redux-utilities/flux-standard-action ◦ https://github.com/sue71/vuex-typescript-fsa
  22. Vuex + FSA store.dispatch('increment') // or store.dispatch({ type: 'increment', meta:

    null, error: null, payload: { ••• } }) https://vuex.vuejs.org/guide/actions.html#dispatching-actions
  23. import { actionCreatorFactory } from 'vuex-typescript-fsa/lib'; import { UpdateBookRequest }

    from '@gen'; export const namespace = 'books'; const actionCreator = actionCreatorFactory(namespace); export const GetBook = actionCreator<{ id: number; }>( 'GET_BOOK' ); Define FSAs
  24. Actions / Mutations / Getters export const module: Module<State, RootState>

    = { namespaced: true, actions: combineAction( action(GetBook, async function(context, action) { const { data } = await this.sampleAppAPI .getBook(action.payload.id); context.commit(BookReceived(data.data)); }) ) };
  25. Error: URLSearchParams is not defined import * as url from

    'url'; // Error const params = url.URLSearchParams(•••);
  26. Override ‘url’ package (Monkey patch) // webpacker config resolve: {

    alias: { '@': path.resolve(__dirname, '../../app/javascript/src'), urlOriginal: path.resolve(__dirname, '../../node_modules/url'), url: path.resolve(__dirname, './extensions/url') } } // config/webpack/extensions/url.js import { parse, resolve, resolveObject, format, Url } from 'urlOriginal'; import URLSearchParams from '@ungap/url-search-params'; export { parse, resolve, resolveObject, format, Url, URLSearchParams };
  27. Heavy Base64 Image • Easy to upload image as Base64

    String in Rails ◦ https://github.com/y9v/carrierwave-base64 ◦ Able to send image via ‘application/json’ ◦ size tooooooo big • Use multipart/form-data instead
  28. e.g. ‘url’ // ‍♂ This overlaps with import * url

    from 'url' if (url !== undefined) { localVarFormParams.append('url', url as any); } // ‍♂ rename to '_url' if (_url !== undefined) { localVarFormParams.append('_url', _url as any); } // convert in Rails params.tap {|p| p[:url] = p[:_url]}.permit( :title, :description, :url, :image )
  29. fixed now • [TS][Axios] To fix conflict params name 'url'

    ◦ https://github.com/OpenAPITools/openapi-generator/pull/2921
  30. Put .jar into node_modules/ // @openapitools/openapi-generator-cli/bin/openapi-generator const binPath = resolve(__dirname,

    'openapi-generator.jar'); const JAVA_OPTS = process.env['JAVA_OPTS'] || ''; let command = `java ${JAVA_OPTS} -jar "${binPath}"`;
  31. Generator is changed every day. • You can download and

    use snapshot • https://oss.sonatype.org/content/repositories/ snapshots/org/openapitools
  32. Pro/Con • Pro ◦ Able to detect unexpected changes ◦

    Reduce change by hand ◦ Able to reuse API Client for another platform ◦ Automatically detect diff • Con ◦ Took time to setup ◦ Too many things to do ◦ Too much for single person project
  33. OpenAPI + JSON:API is Bad approach? • https://apisyouwonthate.com/blog/json-api-openapi-and-json-schema-working-i n-harmony •

    “At WeWork, we use JSON Schema to describe the data models, OpenAPI to describe everything else, then the message being described is usually JSON API.”
  34. ※ Fast JSON API is not a replacement for AMS

    • AMS does many things and is very flexible ◦ This is why fast_jsonapi is more faster • Netflix still use AMS for non JSON:API serialization and deserialization • ref: Performance methodology