#-- encoding: UTF-8 #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2015 the OpenProject Foundation (OPF) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: # Copyright (C) 2006-2013 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See doc/COPYRIGHT.rdoc for more details. #++ module API module Utilities # Since APIv3 uses different names for some properties, there is sometimes the need to convert # names between the "old" Rails/ActiveRecord world of names and the "new" APIv3 world of names. # This class provides methods to cope with the neccessary name conversions # There are multiple reasons for naming differences: # - APIv3 is using camelCase as opposed to snake_case # - APIv3 defines some properties as a different type, which requires a name change # e.g. estimatedTime vs estimated_hours (AR: hours; API: generic duration) # - some names used in AR are even there kind of deprecated # e.g. fixed_version, which everyone refers to as version # - some names in AR are plainly inconsistent, whereas the API tries to be as consistent as # possible, e.g. updated_at vs updated_on # # Callers note: While this class is envisioned to be generally usable, it is currently solely # used for purposes around work packages. Further work might be required for conversions to make # sense in different contexts. class PropertyNameConverter class << self WELL_KNOWN_AR_TO_API_CONVERSIONS = { assigned_to: 'assignee', fixed_version: 'version', done_ratio: 'percentageDone', estimated_hours: 'estimatedTime', created_on: 'createdAt', updated_on: 'updatedAt', remaining_hours: 'remainingTime', spent_hours: 'spentTime', subproject: 'subprojectId' } # Converts the attribute name as refered to by ActiveRecord to a corresponding API-conform # attribute name: # * camelCasing the attribute name # * unifying :status and :status_id to 'status' (and other foo_id fields) # * converting totally different attribute names (e.g. createdAt vs createdOn) def from_ar_name(attribute) attribute = normalize_foreign_key_name attribute attribute = expand_custom_field_name attribute special_conversion = WELL_KNOWN_AR_TO_API_CONVERSIONS[attribute.to_sym] return special_conversion if special_conversion # use the generic conversion rules if there is no special conversion attribute.camelize(:lower) end # Converts the attribute name as refered to by the APIv3 to the source name of the attribute # in ActiveRecord. For that to work properly, an instance of the correct AR-class needs # to be passed as context. def to_ar_name(attribute, context:, refer_to_ids: false) attribute = underscore_attribute attribute.to_s.underscore attribute = collapse_custom_field_name(attribute) special_conversion = special_api_to_ar_conversions[attribute] if special_conversion && context.respond_to?(special_conversion) attribute = special_conversion end attribute = denormalize_foreign_key_name(attribute, context) if refer_to_ids attribute end private def special_api_to_ar_conversions @api_to_ar_conversions ||= WELL_KNOWN_AR_TO_API_CONVERSIONS.inject({}) { |result, (k, v)| result[v.underscore] = k.to_s result } end # Unifies different attributes refering to the same thing via a foreign key # e.g. status_id -> status def normalize_foreign_key_name(attribute) attribute.to_s.sub(/(.+)_id\z/, '\1') end # Adds _id suffix to field names that refer to foreign key relations, # leaves other names untouched. # e.g. status_id -> status def denormalize_foreign_key_name(attribute, context) id_name = "#{attribute}_id" # When appending an ID is valid, the context object will understand that message # in case of a `belongs_to` relation (e.g. status => status_id). The second check is for # `has_many` relations (e.g. watcher => watchers). if context.respond_to?(id_name) || context.respond_to?(attribute.pluralize) id_name else attribute end end # expands short custom field column names to be represented in their long form # (e.g. cf_1 -> custom_field_1) def expand_custom_field_name(attribute) match = attribute.match /\Acf_(?\d+)\z/ if match "custom_field_#{match[:id]}" else attribute end end # collapses long custom field column names to be represented in their short form # (e.g. custom_field_1 -> cf_1) def collapse_custom_field_name(attribute) match = attribute.match /\Acustom_field_(?\d+)\z/ if match "cf_#{match[:id]}" else attribute end end def underscore_attribute(attribute) # vanilla underscore will not puts underscores between letters and digits # we add them with the power of regex (esp. used for custom fields) attribute.underscore.gsub(/([a-z])(\d)/, '\1_\2') end end end end end