Module: RESTHelpers

Defined in:
backend/app/lib/rest.rb

Defined Under Namespace

Modules: ResponseHelpers Classes: BooleanParam, Endpoint, IdSet, NonNegativeInteger, PageSize, UploadFile

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
# File 'backend/app/lib/rest.rb', line 476

def self.included(base)
  base.extend(JSONModel)

  base.helpers do

    def coerce_type(value, type)
      if type == Integer
        Integer(value)
      elsif type == DateTime
        DateTime.parse(value)
      elsif type == Date
        Date.parse(value)
      elsif type.respond_to? :from_json

        # Allow the request to specify how the incoming JSON is encoded, but
        # convert to UTF-8 for processing
        if request.content_charset
          value = value.force_encoding(request.content_charset).encode("UTF-8")
        end
        value = value.to_json unless value.is_a? String
        type.from_json(value)
      elsif type.is_a? Array
        if value.is_a? Array
          value.map {|elt| coerce_type(elt, type[0])}
        else
          raise ArgumentError.new("Not an array")
        end
      elsif type.is_a? Regexp
        raise ArgumentError.new("Value '#{value}' didn't match #{type}") if value !~ type
        value
      elsif type.respond_to? :value
        type.value(value)
      elsif type == String
        value
      elsif type == :body_stream
        value
      else
        raise BadParamsException.new("Type not recognized: #{type}")
      end
    end


    def process_pagination_params(params, known_params, errors, paged)
      known_params['resolve'] = known_params['modified_since'] = true
      params['modified_since'] = coerce_type((params[:modified_since] || '0'),
                                            NonNegativeInteger)

      known_params['sort_field'] = true
      known_params['sort_direction'] = true
      params['sort_field'] = params.fetch('sort_field', 'id').to_sym
      params['sort_direction'] = params.fetch('sort_direction', 'asc').to_sym

      unless [:asc, :desc].include? params['sort_direction']
        errors[:failed_validation] << {
          name: 'sort_direction', validation: "must be either 'asc' or 'desc' but given: #{params['sort_direction']}"
        }
      end

      if params[:page]
        known_params['page_size'] = known_params['page'] = true
        params['page_size'] = coerce_type((params[:page_size] || AppConfig[:default_page_size]), PageSize)
        params['page'] = coerce_type(params[:page], NonNegativeInteger)
      elsif params[:id_set]
        known_params['id_set'] = true
        params['id_set'] = coerce_type(params[:id_set], IdSet)

      elsif params[:all_ids]
        params['all_ids'] = known_params['all_ids'] = true

      else
        # paged and paginated routes both support accessing results a page at a time,
        #   via the page and page_size arguments
        # paginated routes additionally support:
        #   - fetching all database ids as an array via all_ids
        #   - fetching a set of specific known ids via id_set
        if paged
          # Must provide page
          errors[:missing] << {
            :name => 'page',
            :doc => "Must provide 'page' (a number)"
          }
        else
          # Must provide either page, id_set or all_ids
          ['page', 'id_set', 'all_ids'].each do |name|
            errors[:missing] << {
              :name => name,
              :doc => "Must provide either 'page' (a number), 'id_set' (an array of record IDs), or 'all_ids' (a boolean)"
            }
          end
        end
      end
    end


    def process_indexed_params(name, params)
      if params[name] && params[name].is_a?(Hash)
        params[name] = params[name].sort_by(&:first).map(&:last)
      end
    end


    def process_declared_params(declared_params, params, known_params, errors)
      declared_params.each do |definition|
        (name, type, doc, opts) = definition
        opts ||= {}

        if (type.is_a?(Array))
          process_indexed_params(name, params)
        end

        known_params[name] = true

        if opts[:body]
          params[name] = request.body.read
        elsif type == :body_stream
          params[name] = request.body
        end

        if not params[name] and !opts[:optional] and !opts.has_key?(:default)
          errors[:missing] << {:name => name, :doc => doc}
        else

          if type and params[name]
            begin
              params[name] = coerce_type(params[name], type)
            rescue ArgumentError
              errors[:bad_type] << {:name => name, :doc => doc, :type => type}
            end
          elsif type and opts[:default]
            params[name] = opts[:default]
          end

          if opts[:validation]
            if not opts[:validation][1].call(params[name.intern])
              errors[:failed_validation] << {:name => name, :doc => doc, :type => type, :validation => opts[:validation][0]}
            end
          end

        end
      end
    end


    def ensure_params(declared_params, paginated, paged)
      params.delete('captures') # Sinatra 2.x
      errors = {
        :missing => [],
        :bad_type => [],
        :failed_validation => []
      }

      known_params = {}

      process_declared_params(declared_params, params, known_params, errors)
      process_pagination_params(params, known_params, errors, paged) if paginated || paged

      # Any params that were passed in that aren't declared by our endpoint get dropped here.
      unknown_params = params.keys.reject {|p| known_params[p.to_s] }

      unknown_params.each do |p|
        params.delete(p)
      end


      if not errors.values.flatten.empty?
        result = {}

        errors[:missing].each do |missing|
          result[missing[:name]] = ["Parameter required but no value provided"]
        end

        errors[:bad_type].each do |bad|
          provided_value = params[bad[:name]]
          msg = "Wanted type #{bad[:type]} but got '#{provided_value}'"


          if bad[:type].is_a?(Array) &&
             !provided_value.is_a?(Array) &&
             provided_value.is_a?(bad[:type][0])

            # The caller got the right type but didn't wrap it in an array.
            # Provide a more useful error message.
            msg << ".  Perhaps you meant to specify an array like: #{bad[:name]}[]=#{Addressable::URI.escape(provided_value)}"
          end

          result[bad[:name]] = [msg]
        end

        errors[:failed_validation].each do |failed|
          result[failed[:name]] = ["Failed validation -- #{failed[:validation]}"]
        end

        raise BadParamsException.new(result)
      end
    end
  end
end