Class: Ballonizer

Inherits:
Object
  • Object
show all
Defined in:
lib/ballonizer.rb

Overview

This gem provides mechanisms to allow ballons (or speech bubbles) to be added/removed/edited over images of a HTML or XHTML document and to be persisted. The edition of the ballons is possible by the javascript module provided by this gem. The persistence is allowed by the Ballonizer class. The Ballonizer class is basically a wrapper around the database used to persist the ballons, and offer methods to process the requests made by the client side (by a form created by the javascript module), and to modify a (X)HTML document adding the ballons of the image over it.

This class lacks a lot of features like: access to an abstraction of the ballons, images and their relationship; control over users who edit the ballons; access to the old versions of the ballon set of a image (that are stored in the database, but only can be accessed directly by the Sequel::Database object). It’s a work in progress, be warned to use carefully and motivated to contribute.

The JavaScript library used to allow edition in the client side works as follows: double click over the image add a ballon, double click over a ballon allow edit the text, when the ballon lose the focus it returns to the non-edition state, a ballon without text (or only with spaces) it’s automatically removed when lose focus, drag the ballon change its position (restricted to image space), drag ballon by the right-bottom handle resize the ballon (also restricted to image space). Any change in the ballons make visible a button fixed in the right-top corner of the browser viewport. Every time a ballons is changed (or added/removed) the json of a hidden form is updated. The button submits this json by POST request to the url configured by :form_handler_url setting.

To the image be ‘ballonized’ it have to match the :img_to_ballonize_css_selector. The ‘ballonized’ term here means: have the ballons added over the image in ballonize_page.

To use this class with your (rack isn’t?) app you need to: create the necessary tables in a Sequel::Database object with Ballonizer.create_tables; create a ballonizer instance with the url where you gonna handle the ballon change requests and where provide the assets. Handle the ballon changes request in that url with process_submit. Call instance.ballonize_page over the html documents that can have the images to be ballonized. Check if the image match the css selector :img_to_ballonize_css_selector.

What’s explained above is basically the example you can access with ‘rake example’ and is in the examples/ballonizer_app/config.ru file. You can reset the database with ‘rake db:reset’ (and if you pass an argument as ‘rake db:reset’ you can create the tables in the database already used by your app). The tables names are: images, ballons, ballonized_image_versions, ballonized_image_ballons.

This documentation is avaliable also in: www.omniref.com/ruby/gems/ballonizer

Changelog:

v0.7.3
 * Solve the annoying arrow key problem. Stop the propagation of
   the key events while editing a balloon. This is meant to solve
   the problem with some sites that use the arrow keys to navigate
   between pages. If you used one of these keys while editing a
   ballon you would lose all work.
v0.7.2
 * Little CSS fix (normalize line-height for ballons)
v0.7.1
 * Fix the big submit button problem and the hidden behind other
   element submit button problem.
v0.7.0
 * Add the jquery_no_conflict option. If this option is true the
   js_load_snippet will restore any previous loaded version of
   jQuery after the ballonizer javascript client code already
   referenced the latest loaded version (use this option if the page
   already use a jQuery version that's different from the used by
   the gem, and you use the add_required_js_libs_for_edition and
   add_js_for_edition options).
v0.6.0
 * Change the jQuery version provided by the gem to the 1.10.2
v0.5.1:
 * js_load_snippet can take a settings arg too. Fixed ballonize_page to
   use the :form_handler_url from the settings argument.
v0.5.0:
 * The *_html_links methods can take a settings argument.
 * Fixed bug where passing a new asset path to the ballonize_page don't
   settings parameter change the asset path that it uses.
 * Asset path settings now are parsed as real URIs (need to have a
   trailing slash if the intent is use as a dir).
 * Updated the rspec version used by the gem (fixed deprecation).
v0.4.0: 
 * Changed the way the Javascript module add containers in the page
   to avoid creating invalid HTML4.0.1/XHTML1.1/HTML5 documents.
 * Now the ballonize_page takes a mime-type argument to decide if
   the page has to be parsed as XML or HTML (trying to be in
   conformance with http://www.w3.org/TR/xhtml-media-types/).
 * The change in the ballon size now change the font-size of the
   ballon text.
 * Database schema change, as consequence of the font-size change,
   the database now stores the font-size. No migration provided for
   databases in the old format, but the font-size field can be null.
   The migration only require adding this column with null value to
   all records (see the create_tables code).
 * Fixed a bug in the Javascript module that give wrong position and
   size values to all ballons that aren't edited/added before submmiting
   (only if the image wasn't loaded before the javascript loading).

Author:

  • Henrique Becker

Defined Under Namespace

Classes: Error, SubmitError

Constant Summary collapse

ASSETS =

The load paths of assets inside the gem and the files inside each path, in the order they need to be included (the files of the first path need to be included before the files in the second path, and the files in the same path need to be included in the specified order). Give preference to the asset(s)_* and *_html_links methods over this constant.

Workaround.deep_freeze([
  ['vendor/assets/javascripts', [
    'jquery-1.10.2.min.js',
    'jquery.json-2.4.min.js',
    'jquery-ui-1.10.3.custom.min.js']],
  ['lib/assets/javascripts', [
    'ballonizer.js']],
  ['vendor/assets/stylesheets', [
    'ui-lightness/jquery-ui-1.10.3.custom.min.css']],
  ['lib/assets/stylesheets', [
    'ballonizer.css']]
])
DEFAULT_SETTINGS =

The default #settings

{
  # The css selector used to define the elements to ballonize.
  img_to_ballonize_css_selector: 'img.to_ballonize',
  # A url to be used in the client-side action attribute of the form for
  # ballon submition. The value will be used in the javascript snippet that
  # initialize the ballonizer client javascript allowing ballon edition
  # (and consequently creating the form).
  form_handler_url: '#',
  # Define if the javascript code that allow edition will be added to the page.
  # (this don't refer to the jquery-* libs and the ballonizer.js only the
  # snippet to execute when the page is ready)
  add_js_for_edition: true,
  # A path string to prefix each href of the css stylesheet links generated
  # by the js_libs_html_links, and, possibly, added by the ballonize_page
  # object. Example: if you use Ballonizer.assets_app mapped to '/assets'
  # then use '/assets' here. This is used with the :add_required_css setting.
  css_asset_path_for_link: nil,
  # If the ballonize_page method will add or not the html generated by
  # #css_html_links (require the :css_asset_path_for_link to be defined).
  add_required_css: false,
  # A path string to prefix each js source src generated by the
  # object. Example: if you use Ballonizer.assets_app mapped to '/assets'
  # then use '/assets' here. This is used with the
  # :add_required_js_libs_for_edition setting.
  js_asset_path_for_link: nil,
  # If the ballonize_page method will add or not the html generated by
  # #js_libs_html_links (require the :js_asset_path_for_link to be defined).
  add_required_js_libs_for_edition: false,
  # If true and the database argument don't have any of the tables used by
  # the class call create_tables over the database argument. If false or the
  # database has at leat one of the tables does nothing.
  create_tables_if_none: false,
  # Use the jQuery.noConflict to restore any previous loaded version of the
  # jQuery in a ballonized page. Only works with add_js_for_edition, and
  # probably only makes sense with add_required_js_libs_for_edition.
  jquery_no_conflict: false
}.freeze.each { | _, v| v.freeze }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(db, settings = {}) ⇒ Ballonizer

Create a new Ballonizer object from a Sequel Database (with the expected tables, that can be created with Ballonizer.create_tables) and a optional hash of settings.

Parameters:

  • db (String, Sequel::Database)

    A Sequel::Database or a String to be used with Sequel::Database.connect. Is necessary to create the tables with Ballonizer.create_tables unless you have set the :create_table_if_none setting to true.

  • settings (Hash{Symbol => String}) (defaults to: {})

    A optional hash of settings. The default value and explanation of each option are documented in the DEFAULT_SETTINGS constant.

See Also:



248
249
250
251
252
253
254
255
256
257
258
# File 'lib/ballonizer.rb', line 248

def initialize(db, settings = {})
  @settings = DEFAULT_SETTINGS.merge(settings)
  if db.is_a? String
    db = Sequel::Database.connect(db)
  end
  if @settings[:create_tables_if_none] &&
       ! (self.class.used_tables.any? { | name | db.table_exists? name })
    self.class.create_tables(db)
  end
  @db = db
end

Instance Attribute Details

#dbObject

Returns the value of attribute db.



116
117
118
# File 'lib/ballonizer.rb', line 116

def db
  @db
end

#settingsObject

Returns the value of attribute settings.



116
117
118
# File 'lib/ballonizer.rb', line 116

def settings
  @settings
end

Class Method Details

.asset_absolute_pathsArray<String>

List of absolute filepaths to the css and js files needed by the client counterpart and provided by the gem. To all who not want to use assets_app.

Returns:

  • (Array<String>)

    A frozen array of frozen strings.

See Also:



662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
# File 'lib/ballonizer.rb', line 662

def self.asset_absolute_paths
  return @asset_absolute_paths if @asset_absolute_paths

  absolute_lib_dir = File.dirname(File.realpath(__FILE__))
  ballonizer_gem_root_dir = File.expand_path('../', absolute_lib_dir)

  @asset_absolute_paths = ASSETS.map do | load_path_and_files |
    relative_load_path, filepaths = *load_path_and_files
    absolute_load_path = File.expand_path(relative_load_path, ballonizer_gem_root_dir)

    filepaths.map do | filepath  |
      File.expand_path(filepath, absolute_load_path)
    end
  end

  @asset_absolute_paths.flatten!
  @asset_absolute_paths.freeze
end

.asset_load_pathsArray<String>

List of paths (relative to the gem root directory) to the directories with the css and js provided by the gem.

Returns:

  • (Array<String>)

    A frozen array of frozen strings.



629
630
631
632
633
634
635
636
637
638
639
640
641
642
# File 'lib/ballonizer.rb', line 629

def self.asset_load_paths
  return @asset_load_paths if @asset_load_paths

  absolute_lib_dir = File.dirname(File.realpath(__FILE__))
  ballonizer_gem_root_dir = File.expand_path('../', absolute_lib_dir)

  @asset_load_paths = ASSETS.map do | load_path_and_files |
    load_path = load_path_and_files.first
    File.expand_path(load_path, ballonizer_gem_root_dir)
  end

  @asset_load_paths.flatten!
  @asset_load_paths.freeze
end

.asset_logical_pathsArray<String>

List of logical paths to the css and js assets. The assets_app respond to any requisition to one of these paths.

Returns:

  • (Array<String>)

    A frozen array of frozen strings.



647
648
649
650
651
652
653
654
655
656
# File 'lib/ballonizer.rb', line 647

def self.asset_logical_paths
  return @asset_logical_paths if @asset_logical_paths

  @asset_logical_paths = ASSETS.map do | load_path_and_files |
    load_path_and_files.last
  end

  @asset_logical_paths.flatten!
  @asset_logical_paths.freeze
end

.assets_appSprockets::Environment

A Rack app that provide the gem css and js. Each call to this method return a new object (clone). The Sprockets::Environment isn’t frozen because it can’t be used with ‘run’ in a rack app if frozen.

Returns:

  • (Sprockets::Environment)

See Also:



686
687
688
689
690
691
692
693
694
# File 'lib/ballonizer.rb', line 686

def self.assets_app
  # dont freeze because run don't work in a frozen sprockets env
  return @assets_app.clone if @assets_app
  @assets_app = Sprockets::Environment.new
  asset_load_paths.each do | load_path |
    @assets_app.prepend_path load_path
  end 
  @assets_app.clone
end

.create_ballon_node(ballon_data) ⇒ Object



486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/ballonizer.rb', line 486

def self.create_ballon_node(ballon_data)
  text = HTMLEntities.new.encode(ballon_data[:text])

  style = ''
  [:top, :left, :width, :height].each do | sym |
    # transform ratio [0,1] to percent [0, 100]
    style = style + "#{sym}: #{(ballon_data[sym] * 100)}%;"
  end
  style = style + "font-size: #{ballon_data[:font_size]}px;"

  "<span class='ballonizer_ballon' style='#{style}'>#{text}</span>"
end

.create_tables(db) ⇒ void

This method returns an undefined value.

Executes the create_table operations over the Sequel::Database argument.

Parameters:

  • db (Sequel::Database)

    The database where create the tables.



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
# File 'lib/ballonizer.rb', line 547

def self.create_tables(db)
  db.create_table(:images) do
    primary_key :id
    String :img_src, :size => 255, :unique => true, :allow_null => false
  end
  db.create_table(:ballons) do
    primary_key :id

    String :text, :size => 255, :allow_null => false
    Float :top, :allow_null => false
    Float :left, :allow_null => false
    Float :width, :allow_null => false
    Float :height, :allow_null => false
    # the font_size allow null to support databases migrated from old versions
    # (that don't have this field)
    Float :font_size, :allow_null => true
  end
  db.create_table(:ballonized_image_versions) do
    Integer :version
    foreign_key :image_id, :images
    DateTime :time, :allow_null => false
    primary_key [:version, :image_id]
  end
  db.create_table(:ballonized_image_ballons) do
    Integer :version
    foreign_key :image_id, :images
    foreign_key :ballon_id, :ballons
    foreign_key [:version, :image_id], :ballonized_image_versions
  end
end

.used_tablesArray<Symbol>

The names (as symbols) of the tables used by instances of the class.

Returns:

  • (Array<Symbol>)

    An frozen array of symbols



232
233
234
# File 'lib/ballonizer.rb', line 232

def self.used_tables
  USED_TABLES
end

Instance Method Details

#ballonize_page(page, page_url, mime_type, settings = {}) ⇒ String

Wrap each image to ballonize with a container, add its ballons to the container and, possibly, add the css and js libs and snippet for the edition initialization. Don’t make any change if the page has no images to ballonize. If the page can’t be parsed (as HTML or X(HT)ML, depending of the mime-type) return the page argument without throwing any exceptions. Throw an exception if the mime-type doesn’t match with html or xhtml.

Parameters:

  • page (String)

    The (X)HTML page.

  • page_url (String)

    The url of the page to be ballonized, necessary to make absolute the src attribute of img (if it’s relative).

  • settings (Hash{Symbol => String}) (defaults to: {})

    Optional. Hash to be merged with the instance #settings (this argument override the #settings ones).

  • mime_type

    A string that have the substring ‘text/html’ or ‘application/xhtml+xml’.

Returns:

  • (String)

    The ballonized page (new string), or the same string, if the parse has failed.

Raises:

  • (Ballonizer::Error)

    If the mime-type don’t match either ‘text/html’ or ‘application/xhtml+xml’.



443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/ballonizer.rb', line 443

def ballonize_page(page, page_url, mime_type, settings = {})
  settings = @settings.merge(settings)

  # can raise Ballonizer::Error if the mime-type is invalid
  parsed_page = Workaround.parse_html_or_xhtml(page, mime_type)
  # if can't parse return the page unaltered
  if parsed_page.nil?
    return page
  end

  selector = settings[:img_to_ballonize_css_selector]
  imgs = parsed_page.css(selector)

  unless imgs.empty?
    imgs.wrap('<span class="ballonizer_image_container" ></span>')

    imgs.each do | img |
      img_src = img['src']
      absolute_normal_src = Addressable::URI.parse(page_url)
                                            .join(img_src)
                                            .normalize.to_s
      ballons = last_ballon_set_of_image(absolute_normal_src)
      ballons.each do | ballon |
        img.add_previous_sibling(self.class.create_ballon_node(ballon))
      end
    end
    
    head = parsed_page.at_css('head')
    if settings[:add_required_css]
      head.children.last.add_next_sibling(css_html_links(settings))
    end
    if settings[:add_required_js_libs_for_edition]
      head.children.last.add_next_sibling(js_libs_html_links(settings))
    end
    if settings[:add_js_for_edition]
      head.children.last.add_next_sibling(js_load_snippet(settings))
    end
  end
  
  parsed_page.to_s
end

The (X)HTML fragment with the link tags that are added to the page by ballonize_page if the :add_required_css setting is true (the default is false).

Parameters:

  • settings (Hash{Symbol => String}) (defaults to: {})

    Optional. Hash to be merged with the instance #settings (this argument override the #settings ones).

Returns:

  • (String, NilClass)

    A String when the :css_asset_path_for_link is defined, nil otherwise.



585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/ballonizer.rb', line 585

def css_html_links(settings = {})
  settings = @settings.merge(settings)
  return nil unless settings[:css_asset_path_for_link]

  link_template = '<link rel="stylesheet" type="text/css" href="PATH" />'
  css_paths = self.class.asset_logical_paths.select do | p |
    /^.+\.css$/.match(p)
  end

  links = css_paths.map do | p |
    p = Workaround.join_uris(settings[:css_asset_path_for_link], p)
    link_template.sub('PATH', p)
  end

  links.join('')
end

The (X)HTML fragment with the script tags that are added to the page by ballonize_page if the :add_required_js_libs_for_edition setting is true (the default is false).

Parameters:

  • settings (Hash{Symbol => String}) (defaults to: {})

    Optional. Hash to be merged with the instance #settings (this argument override the #settings ones).

Returns:

  • (String, NilClass)

    A String when the :js_asset_path_for_link is defined, nil otherwise.



609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/ballonizer.rb', line 609

def js_libs_html_links(settings = {})
  settings = self.settings.merge(settings)
  return nil unless settings[:js_asset_path_for_link]

  link_template = '<script type="text/javascript" src="PATH" ></script>'
  js_libs_paths = self.class.asset_logical_paths.select do | p |
    /^.+\.js$/.match(p)
  end

  links = js_libs_paths.map do | p |
    p = Workaround.join_uris(settings[:js_asset_path_for_link], p)
    link_template.sub('PATH', p)
  end

  links.join('')
end

#js_load_snippet(settings = {}) ⇒ String

Return a String with the snippet added to the pages to allow edition in them.

Parameters:

  • settings (Hash{Symbol => String}) (defaults to: {})

    Optional. Hash to be merged with the instance #settings (this argument override the #settings ones).

Returns:

  • (String)

    The added snippet. Already with the <script/> tag around it.



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/ballonizer.rb', line 525

def js_load_snippet(settings = {})
  settings = @settings.merge(settings)
  # We create a reference to the jQuery because the global variable can be
  # cleaned by the jQuery.noConflict(true), if the jquery_no_conflict
  # setting is defined
  <<-EOF
    <script type="text/javascript">
      var jQueryReference = jQuery;
      jQueryReference(document).ready(function() {
        Ballonizer('#{settings[:form_handler_url]}',
                   '.ballonizer_image_container',
                   jQueryReference('body'),
                   jQueryReference);
      })
      #{ 'jQuery.noConflict(true); ' if settings[:jquery_no_conflict] }
    </script>
  EOF
end

#last_ballon_set_of_image(img_src) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Note:

This method don’t make distinction between a image in the database without any ballons (removed in the last version, by example) or a image that isn’t in the database (both return a empty array).

Don’t use this method. It is for internal use only.



504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/ballonizer.rb', line 504

def last_ballon_set_of_image(img_src)
  db_image = self.db[:images].first({img_src: img_src})
  if db_image
    image_id = db_image[:id]
    version = self.db[:ballonized_image_versions].where({image_id: image_id})
                                                 .max(:version)
    self.db[:ballonized_image_ballons]
        .join(:ballons, { ballonized_image_ballons__version: version,
                          ballonized_image_ballons__image_id: image_id,
                          ballonized_image_ballons__ballon_id: :ballons__id
                        }).select(:text, :top, :left, :width, :height,
                                  :font_size).all
  else
    []
  end
end

#process_submit(env, time = nil) ⇒ Ballonizer

Convenience method for process_submit_json, extract the json from the request, validate and pass to the method.

Parameters:

  • env

    A env Rack hash.

Returns:

Raises:

See Also:



266
267
268
269
270
271
# File 'lib/ballonizer.rb', line 266

def process_submit(env, time = nil)
  request = Rack::Request.new(env)
  submit_json = request['ballonizer_data']
  valid_submit_json?(submit_json, true)
  process_submit_json(submit_json, time)
end

#process_submit_hash(submit_hash, time = nil) ⇒ Ballonizer

Behave as process_submit_json except that takes a already parsed json (hash) and don’t check if it’s tainted.

Parameters:

  • submit_hash (Hash)

    A JSON hash. Validate with #valid_submit_json?.

  • time (Time) (defaults to: nil)

    A Time instance to be used in place of Time.now. Optional.

Returns:



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/ballonizer.rb', line 386

def process_submit_hash(submit_hash, time = nil)
  time = Time.now unless time
  self.db.transaction do
    images = self.db[:images]
    db_ballons = self.db[:ballons]
    ballonized_image_versions = self.db[:ballonized_image_versions]
    ballonized_image_ballons = self.db[:ballonized_image_ballons]

    submit_hash.each do | img_src, ballons |
      img_src = Addressable::URI.parse(img_src).normalize.to_s
      db_image = images.first({img_src: img_src})
      image_id, version = nil, nil

      if db_image
        image_id = db_image[:id]
        version = ballonized_image_versions.where({image_id: image_id})
                                            .max(:version) + 1
      else
        image_id = images.insert({img_src: img_src})
        version = 1
      end

      ballonized_image_versions.insert({
        image_id: image_id,
        version: version,
        time: time
      })
      ballons.each do | ballon |
        db_ballon = db_ballons.first(ballon)
        ballon_id = db_ballon ? db_ballon[:id] : db_ballons.insert(ballon)
        ballonized_image_ballons.insert({
          image_id: image_id,
          version: version,
          ballon_id: ballon_id,
        })
      end
    end
  end
end

#process_submit_json(submit_json, time = nil) ⇒ Ballonizer

Receive a untainted json (assume as validated by #valid_submit_json?) and add it to the database.

Parameters:

  • submit_json (String)

    A untainted JSON string. Validated with #valid_submit_json?.

  • time (Time) (defaults to: nil)

    A Time instance to be used in place of Time.now. Optional.

Returns:

Raises:

  • (SecurityError)

    If the input is tainted.



376
377
378
379
# File 'lib/ballonizer.rb', line 376

def process_submit_json(submit_json, time = nil)
  fail SecurityError, 'the input is tainted' if submit_json.tainted?
  process_submit_hash(JSON.parse(submit_json), time)
end

#valid_submit_hash?(submit_hash, throw_exceptions = false) ⇒ true, false

Note:

This is a instance method because, in the future, the validation can depend of instance settings.

Act as #valid_submit_json, but over a already parsed json and don’t (un)taint the hash.

Parameters:

  • submit_hash (Hash)

    A parsed JSON.

Returns:

  • (true, false)

Raises:

See Also:



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/ballonizer.rb', line 305

def valid_submit_hash?(submit_hash, throw_exceptions=false)
  if submit_hash.empty?
    fail SubmitError, "the submit request is empty"
  end

  submit_hash.each do | img_src, ballons |
    unless img_src.is_a?(String)
      # TODO: validate if valid URI?
      # TODO: define img_src max lenght?
      fail SubmitError, "the image src is a '#{img_src.class}' and not a String"
    end
    unless Addressable::URI.parse(img_src).absolute?
      fail SubmitError, "the image src ('#{img_src.class}') is not an absolute URI"
    end
    unless ballons.is_a?(Array)
      fail SubmitError, "the image with src '#{img_src}' is key of a " +
                        "'#{ballons.class}' and not a Array"
    end

    ballons.each do | ballon |
      unless ballon["text"].is_a?(String)
        fail SubmitError, "the ballon text is a '#{ballon.class}' and not" +
                          " a String"
      end
      if ballon["text"].empty?
        fail SubmitError, "the ballon text is empty"
      end
      [:top, :left, :width, :height, :font_size].each do | numeric_attr_name |
        numeric_attr = ballon[numeric_attr_name.to_s]
        unless numeric_attr.is_a?(Fixnum) || numeric_attr.is_a?(Float)
          fail SubmitError, "the #{numeric_attr_name} " +
                            "(#{numeric_attr}) isn't a Fixnum or " +
                            "Float (is a '#{numeric_attr.class}')"
        end
      end
      [:top, :left, :width, :height].each do | bound_name |
        bound = ballon[bound_name.to_s]
        unless bound >= 0 && bound <= 1
          fail SubmitError, "the #{bound_name.to_s} (#{bound.to_s}) isn't"
                            " between 0 and 1 (both inclusive)"
        end
      end

      ballon_end = {}
      ballon_end[:x] = ballon["left"] + ballon["width"]
      ballon_end[:y] = ballon["top"] + ballon["height"]

      [:x, :y].each do | axis |
        if ballon_end[axis] > 1
          side = { x: "right side", y: "bottom" }[axis]
          fail SubmitError, "the ballon with text #{ballon["text"].to_s} " +
                            "is trespassing the #{side} of the image"
        end
      end
    end
  end

  # if pass everything above return true
  true
rescue SubmitError => exception
  # HACK: "don't use exceptions for flow control", but this is the most DRY
  # way...
  if throw_exceptions then raise exception else false end
end

#valid_submit_json?(submit_json, throw_exceptions = false) ⇒ true, false

Note:

This is a instance method because, in the future, the validation can depend of instance settings.

Verify if the json is a valid output from the client counterpart. If the argument is valid untaint, otherwise taint (unless it’s frozen). If the second parameter argument is true the method will throw exceptions when the input is invalid.

Parameters:

  • submit_json (String)

    A JSON String.

  • throw_exceptions (FalseClass, TrueClass) (defaults to: false)

    Define behaviour when the input is invalid. If true throw exceptions, otherwise only return false. Default value: false (don’t throw exceptions).

Returns:

  • (true, false)

Raises:

See Also:



286
287
288
289
290
291
292
293
294
295
# File 'lib/ballonizer.rb', line 286

def valid_submit_json?(submit_json, throw_exceptions=false)
  parsed_submit = JSON.parse(submit_json)
  valid_submit_hash?(parsed_submit, true)
  submit_json.untaint unless submit_json.frozen?
  true
rescue JSON::ParserError, SubmitError => e
  submit_json.taint unless submit_json.frozen?
  raise e if throw_exceptions
  false
end