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.

Changelog:

v0.6.1
 * 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).

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:



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/ballonizer.rb', line 235

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.



103
104
105
# File 'lib/ballonizer.rb', line 103

def db
  @db
end

#settingsObject

Returns the value of attribute settings.



103
104
105
# File 'lib/ballonizer.rb', line 103

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:



649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/ballonizer.rb', line 649

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.



616
617
618
619
620
621
622
623
624
625
626
627
628
629
# File 'lib/ballonizer.rb', line 616

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.



634
635
636
637
638
639
640
641
642
643
# File 'lib/ballonizer.rb', line 634

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:



673
674
675
676
677
678
679
680
681
# File 'lib/ballonizer.rb', line 673

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



473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/ballonizer.rb', line 473

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.



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

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



219
220
221
# File 'lib/ballonizer.rb', line 219

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’.



430
431
432
433
434
435
436
437
438
439
440
441
442
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
# File 'lib/ballonizer.rb', line 430

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.



572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
# File 'lib/ballonizer.rb', line 572

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.



596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
# File 'lib/ballonizer.rb', line 596

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.



512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'lib/ballonizer.rb', line 512

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
  "    <script type=\"text/javascript\">\n      var jQueryReference = jQuery;\n      jQueryReference(document).ready(function() {\n        Ballonizer('\#{settings[:form_handler_url]}',\n                   '.ballonizer_image_container',\n                   jQueryReference('body'),\n                   jQueryReference);\n      })\n      \#{ 'jQuery.noConflict(true); ' if settings[:jquery_no_conflict] }\n    </script>\n  EOF\nend\n"

#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.



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/ballonizer.rb', line 491

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:



253
254
255
256
257
258
# File 'lib/ballonizer.rb', line 253

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:



373
374
375
376
377
378
379
380
381
382
383
384
385
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
# File 'lib/ballonizer.rb', line 373

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.



363
364
365
366
# File 'lib/ballonizer.rb', line 363

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:



292
293
294
295
296
297
298
299
300
301
302
303
304
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
# File 'lib/ballonizer.rb', line 292

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:



273
274
275
276
277
278
279
280
281
282
# File 'lib/ballonizer.rb', line 273

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