safrano 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/core_ext/Date/format.rb +47 -0
- data/lib/core_ext/DateTime/format.rb +54 -0
- data/lib/core_ext/Dir/iter.rb +7 -5
- data/lib/core_ext/Hash/transform.rb +14 -6
- data/lib/core_ext/Numeric/convert.rb +25 -0
- data/lib/core_ext/Time/format.rb +71 -0
- data/lib/core_ext/date.rb +5 -0
- data/lib/core_ext/datetime.rb +5 -0
- data/lib/core_ext/numeric.rb +3 -0
- data/lib/core_ext/time.rb +5 -0
- data/lib/odata/attribute.rb +8 -6
- data/lib/odata/batch.rb +3 -3
- data/lib/odata/collection.rb +9 -9
- data/lib/odata/collection_filter.rb +3 -1
- data/lib/odata/collection_media.rb +4 -27
- data/lib/odata/collection_order.rb +1 -1
- data/lib/odata/common_logger.rb +5 -27
- data/lib/odata/complex_type.rb +61 -67
- data/lib/odata/edm/primitive_types.rb +110 -42
- data/lib/odata/entity.rb +14 -47
- data/lib/odata/error.rb +7 -7
- data/lib/odata/expand.rb +2 -2
- data/lib/odata/filter/base.rb +10 -1
- data/lib/odata/filter/error.rb +2 -2
- data/lib/odata/filter/parse.rb +16 -2
- data/lib/odata/filter/sequel.rb +31 -4
- data/lib/odata/filter/sequel_datetime_adapter.rb +21 -0
- data/lib/odata/filter/token.rb +18 -5
- data/lib/odata/filter/tree.rb +83 -9
- data/lib/odata/function_import.rb +19 -18
- data/lib/odata/model_ext.rb +96 -38
- data/lib/odata/request/json.rb +171 -0
- data/lib/odata/transition.rb +13 -9
- data/lib/odata/url_parameters.rb +3 -3
- data/lib/odata/walker.rb +9 -9
- data/lib/safrano/multipart.rb +1 -3
- data/lib/safrano/rack_app.rb +2 -14
- data/lib/safrano/rack_builder.rb +0 -15
- data/lib/safrano/request.rb +3 -3
- data/lib/safrano/response.rb +3 -3
- data/lib/safrano/service.rb +43 -12
- data/lib/safrano/type_mapping.rb +149 -0
- data/lib/safrano/version.rb +1 -2
- data/lib/safrano.rb +3 -0
- metadata +54 -15
    
        data/lib/odata/complex_type.rb
    CHANGED
    
    | @@ -12,153 +12,148 @@ module Safrano | |
| 12 12 | 
             
                  VALUEK = 'value'
         | 
| 13 13 | 
             
                  RESULTSK = 'results'
         | 
| 14 14 | 
             
                  COLLECTION = 'Collection'
         | 
| 15 | 
            -
             | 
| 15 | 
            +
             | 
| 16 16 | 
             
                  def allowed_transitions
         | 
| 17 17 | 
             
                    [Safrano::TransitionEnd]
         | 
| 18 18 | 
             
                  end
         | 
| 19 | 
            -
             | 
| 19 | 
            +
             | 
| 20 20 | 
             
                  def transition_end(_match_result)
         | 
| 21 21 | 
             
                    Safrano::Transition::RESULT_END
         | 
| 22 22 | 
             
                  end
         | 
| 23 | 
            -
             | 
| 23 | 
            +
             | 
| 24 24 | 
             
                  # we will have this on class and instance level for making things simpler first
         | 
| 25 | 
            -
                   | 
| 26 | 
            -
                     | 
| 25 | 
            +
                  class << self
         | 
| 26 | 
            +
                    attr_reader :klassmod
         | 
| 27 27 | 
             
                  end
         | 
| 28 | 
            -
             | 
| 28 | 
            +
             | 
| 29 29 | 
             
                  # return a subclass of ResultAsComplexType
         | 
| 30 30 | 
             
                  def self.asComplexType(klassmod)
         | 
| 31 31 | 
             
                    Class.new(ResultAsComplexType) do
         | 
| 32 32 | 
             
                      @klassmod = klassmod
         | 
| 33 33 | 
             
                    end
         | 
| 34 34 | 
             
                  end
         | 
| 35 | 
            -
             | 
| 35 | 
            +
             | 
| 36 36 | 
             
                  # return a subclass of ResultAsComplexType
         | 
| 37 37 | 
             
                  def self.asComplexTypeColl(klassmod)
         | 
| 38 38 | 
             
                    Class.new(ResultAsComplexTypeColl) do
         | 
| 39 39 | 
             
                      @klassmod = klassmod
         | 
| 40 40 | 
             
                    end
         | 
| 41 41 | 
             
                  end
         | 
| 42 | 
            -
             | 
| 42 | 
            +
             | 
| 43 43 | 
             
                  def self.asPrimitiveType(klassmod)
         | 
| 44 44 | 
             
                    Class.new(ResultAsPrimitiveType) do
         | 
| 45 45 | 
             
                      @klassmod = klassmod
         | 
| 46 46 | 
             
                    end
         | 
| 47 47 | 
             
                  end
         | 
| 48 | 
            -
             | 
| 48 | 
            +
             | 
| 49 49 | 
             
                  def self.asPrimitiveTypeColl(klassmod)
         | 
| 50 50 | 
             
                    Class.new(ResultAsPrimitiveTypeColl) do
         | 
| 51 51 | 
             
                      @klassmod = klassmod
         | 
| 52 52 | 
             
                    end
         | 
| 53 | 
            -
                  end | 
| 54 | 
            -
             | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 55 | 
             
                  def self.asEntity(klassmod)
         | 
| 56 56 | 
             
                    Class.new(ResultAsEntity) do
         | 
| 57 57 | 
             
                      @klassmod = klassmod
         | 
| 58 58 | 
             
                    end
         | 
| 59 59 | 
             
                  end
         | 
| 60 | 
            -
             | 
| 60 | 
            +
             | 
| 61 61 | 
             
                  def self.asEntityColl(klassmod)
         | 
| 62 62 | 
             
                    Class.new(ResultAsEntityColl) do
         | 
| 63 63 | 
             
                      @klassmod = klassmod
         | 
| 64 64 | 
             
                    end
         | 
| 65 65 | 
             
                  end
         | 
| 66 | 
            -
             | 
| 66 | 
            +
             | 
| 67 67 | 
             
                  def initialize(value)
         | 
| 68 68 | 
             
                    @value = value
         | 
| 69 69 | 
             
                  end
         | 
| 70 | 
            -
             | 
| 70 | 
            +
             | 
| 71 71 | 
             
                  def odata_get(req)
         | 
| 72 72 | 
             
                    [200, EMPTY_HASH, [to_odata_json(req)]]
         | 
| 73 73 | 
             
                  end
         | 
| 74 | 
            +
             | 
| 74 75 | 
             
                  def self.type_metadata
         | 
| 75 76 | 
             
                    @klassmod.type_name
         | 
| 76 77 | 
             
                  end
         | 
| 78 | 
            +
             | 
| 77 79 | 
             
                  def type_metadata
         | 
| 78 80 | 
             
                    self.class.type_metadata
         | 
| 79 81 | 
             
                  end
         | 
| 80 | 
            -
             | 
| 82 | 
            +
             | 
| 81 83 | 
             
                  # needed for ComplexType result
         | 
| 82 84 | 
             
                  def to_odata_json(_req)
         | 
| 83 85 | 
             
                    "#{DJ_OPEN}#{@value.odata_h.to_json}#{DJ_CLOSE}"
         | 
| 84 | 
            -
                  end | 
| 85 | 
            -
             | 
| 86 | 
            -
                  # wrapper | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  # wrapper
         | 
| 87 89 | 
             
                  # for OData Entity and Collections, return them directly
         | 
| 88 90 | 
             
                  # for others, ie ComplexType, Prims etc, return the ResultDefinition-subclass wrapped result
         | 
| 89 | 
            -
                  def self.do_execute_func_result(result, _req,  | 
| 90 | 
            -
                     | 
| 91 | 
            +
                  def self.do_execute_func_result(result, _req, _apply_query_params = false)
         | 
| 92 | 
            +
                    new(result)
         | 
| 91 93 | 
             
                  end
         | 
| 92 | 
            -
                  
         | 
| 93 94 | 
             
                end
         | 
| 94 | 
            -
             | 
| 95 | 
            +
             | 
| 95 96 | 
             
                class ResultAsComplexType < ResultDefinition
         | 
| 96 97 | 
             
                  def self.type_metadata
         | 
| 97 98 | 
             
                    @klassmod.type_name
         | 
| 98 99 | 
             
                  end
         | 
| 99 100 | 
             
                end
         | 
| 100 | 
            -
             | 
| 101 | 
            +
             | 
| 101 102 | 
             
                class ResultAsComplexTypeColl < ResultDefinition
         | 
| 102 103 | 
             
                  def self.type_metadata
         | 
| 103 104 | 
             
                    "Collection(#{@klassmod.type_name})"
         | 
| 104 105 | 
             
                  end
         | 
| 105 106 |  | 
| 106 107 | 
             
                  def to_odata_json(req)
         | 
| 107 | 
            -
            #        "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
         | 
| 108 | 
            +
                    #        "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
         | 
| 108 109 | 
             
                    template = self.class.klassmod.output_template
         | 
| 109 110 | 
             
                    # TODO: Error handling if database contains binary BLOB data that cant be
         | 
| 110 111 | 
             
                    # interpreted as UTF-8 then JSON will fail here
         | 
| 111 112 |  | 
| 112 113 | 
             
                    innerh = req.service.get_coll_odata_h(array: @value,
         | 
| 113 114 | 
             
                                                          template: template)
         | 
| 114 | 
            -
             | 
| 115 | 
            +
             | 
| 115 116 | 
             
                    innerj = innerh.to_json
         | 
| 116 117 |  | 
| 117 118 | 
             
                    "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
         | 
| 118 119 | 
             
                  end
         | 
| 119 120 | 
             
                end
         | 
| 120 | 
            -
             | 
| 121 | 
            +
             | 
| 121 122 | 
             
                class ResultAsEntity < ResultDefinition
         | 
| 122 | 
            -
                
         | 
| 123 123 | 
             
                  def self.type_metadata
         | 
| 124 124 | 
             
                    @klassmod.type_name
         | 
| 125 125 | 
             
                  end
         | 
| 126 126 |  | 
| 127 | 
            -
                  
         | 
| 128 | 
            -
                  # wrapper 
         | 
| 127 | 
            +
                  # wrapper
         | 
| 129 128 | 
             
                  # for OData Entity  return them directly
         | 
| 130 | 
            -
                  def self.do_execute_func_result(result, _req, apply_query_params=false)
         | 
| 131 | 
            -
                    # note: Sequel entities instances seem to be thread safe, so we can | 
| 129 | 
            +
                  def self.do_execute_func_result(result, _req, apply_query_params = false)
         | 
| 130 | 
            +
                    # note: Sequel entities instances seem to be thread safe, so we can
         | 
| 132 131 | 
             
                    # safely add request-dependant data (eg. req.params) there
         | 
| 133 132 | 
             
                    apply_query_params ? result : result.inactive_query_params
         | 
| 134 133 | 
             
                  end
         | 
| 135 | 
            -
                  
         | 
| 136 134 | 
             
                end
         | 
| 137 | 
            -
             | 
| 135 | 
            +
             | 
| 138 136 | 
             
                class ResultAsEntityColl < ResultDefinition
         | 
| 139 | 
            -
                
         | 
| 140 137 | 
             
                  def self.type_metadata
         | 
| 141 138 | 
             
                    "Collection(#{@klassmod.type_name})"
         | 
| 142 139 | 
             
                  end
         | 
| 143 | 
            -
             | 
| 144 | 
            -
                  # wrapper | 
| 140 | 
            +
             | 
| 141 | 
            +
                  # wrapper
         | 
| 145 142 | 
             
                  # for OData Entity Collection return them directly
         | 
| 146 | 
            -
                  def self.do_execute_func_result(result, req, apply_query_params=false)
         | 
| 143 | 
            +
                  def self.do_execute_func_result(result, req, apply_query_params = false)
         | 
| 147 144 | 
             
                    coll = Safrano::OData::Collection.new(@klassmod)
         | 
| 148 145 | 
             
                    # instance_exec has other instance variables; @values would be nil in the block below
         | 
| 149 146 | 
             
                    # need to pass a local copy
         | 
| 150 147 | 
             
                    dtset = result
         | 
| 151 148 | 
             
                    coll.instance_exec do
         | 
| 152 | 
            -
             | 
| 153 | 
            -
                      @params =  apply_query_params ?  req.params : EMPTY_HASH
         | 
| 149 | 
            +
                      @params = apply_query_params ? req.params : EMPTY_HASH
         | 
| 154 150 | 
             
                      initialize_dataset(dtset)
         | 
| 155 151 | 
             
                      initialize_uparms
         | 
| 156 152 | 
             
                    end
         | 
| 157 153 | 
             
                    coll
         | 
| 158 154 | 
             
                  end
         | 
| 159 | 
            -
             | 
| 160 155 | 
             
                end
         | 
| 161 | 
            -
             | 
| 156 | 
            +
             | 
| 162 157 | 
             
                class ResultAsPrimitiveType < ResultDefinition
         | 
| 163 158 | 
             
                  def self.type_metadata
         | 
| 164 159 | 
             
                    @klassmod.type_name
         | 
| @@ -169,7 +164,7 @@ module Safrano | |
| 169 164 | 
             
                             VALUEK => self.class.klassmod.odata_value(@value) } }.to_json
         | 
| 170 165 | 
             
                  end
         | 
| 171 166 | 
             
                end
         | 
| 172 | 
            -
             | 
| 167 | 
            +
             | 
| 173 168 | 
             
                class ResultAsPrimitiveTypeColl < ResultDefinition
         | 
| 174 169 | 
             
                  def self.type_metadata
         | 
| 175 170 | 
             
                    "Collection(#{@klassmod.type_name})"
         | 
| @@ -179,7 +174,6 @@ module Safrano | |
| 179 174 | 
             
                    { D => { METAK => { TYPEK => self.class.type_metadata },
         | 
| 180 175 | 
             
                             RESULTSK => self.class.klassmod.odata_collection(@value) } }.to_json
         | 
| 181 176 | 
             
                  end
         | 
| 182 | 
            -
                
         | 
| 183 177 | 
             
                end
         | 
| 184 178 | 
             
              end
         | 
| 185 179 |  | 
| @@ -188,51 +182,51 @@ module Safrano | |
| 188 182 | 
             
              # with added OData functionality
         | 
| 189 183 | 
             
              class ComplexType
         | 
| 190 184 | 
             
                attr_reader :values
         | 
| 185 | 
            +
             | 
| 191 186 | 
             
                EMPTYH = {}.freeze
         | 
| 192 | 
            -
             | 
| 187 | 
            +
             | 
| 193 188 | 
             
                @namespace = nil
         | 
| 194 | 
            -
                 | 
| 195 | 
            -
                   | 
| 189 | 
            +
                class << self
         | 
| 190 | 
            +
                  attr_reader :namespace
         | 
| 196 191 | 
             
                end
         | 
| 197 192 |  | 
| 198 | 
            -
                 | 
| 199 | 
            -
                   | 
| 193 | 
            +
                class << self
         | 
| 194 | 
            +
                  attr_reader :props
         | 
| 200 195 | 
             
                end
         | 
| 201 | 
            -
             | 
| 196 | 
            +
             | 
| 202 197 | 
             
                def type_name
         | 
| 203 198 | 
             
                  self.class.type_name
         | 
| 204 199 | 
             
                end
         | 
| 205 | 
            -
             | 
| 200 | 
            +
             | 
| 206 201 | 
             
                def metadata_h
         | 
| 207 202 | 
             
                  {   type: type_name }
         | 
| 208 203 | 
             
                end
         | 
| 209 | 
            -
             | 
| 204 | 
            +
             | 
| 210 205 | 
             
                def casted_values
         | 
| 211 206 | 
             
                  # MVP... TODO: handle time mappings like in Entity models
         | 
| 212 207 | 
             
                  values
         | 
| 213 208 | 
             
                end
         | 
| 214 | 
            -
             | 
| 209 | 
            +
             | 
| 215 210 | 
             
                # needed for nested json output
         | 
| 216 211 | 
             
                # this is a simpler version of model_ext#output_template
         | 
| 217 212 | 
             
                def self.default_template
         | 
| 218 213 | 
             
                  template = {}
         | 
| 219 214 | 
             
                  expand_e = {}
         | 
| 220 | 
            -
             | 
| 215 | 
            +
             | 
| 221 216 | 
             
                  template[:all_values] = EMPTYH
         | 
| 222 | 
            -
                  @props.each  | 
| 223 | 
            -
                    if kl.respond_to? :default_template
         | 
| 224 | 
            -
             | 
| 225 | 
            -
                    end
         | 
| 226 | 
            -
                  }
         | 
| 217 | 
            +
                  @props.each do |prop, kl|
         | 
| 218 | 
            +
                    expand_e[prop] = kl.default_template if kl.respond_to? :default_template
         | 
| 219 | 
            +
                  end
         | 
| 227 220 | 
             
                  template[:expand_e] = expand_e
         | 
| 228 221 | 
             
                  template
         | 
| 229 222 | 
             
                end
         | 
| 230 | 
            -
             | 
| 223 | 
            +
             | 
| 231 224 | 
             
                def self.output_template
         | 
| 232 225 | 
             
                  default_template
         | 
| 233 226 | 
             
                end
         | 
| 227 | 
            +
             | 
| 234 228 | 
             
                def self.type_name
         | 
| 235 | 
            -
                  @namespace ? "#{@namespace}.#{self | 
| 229 | 
            +
                  @namespace ? "#{@namespace}.#{self}" : to_s
         | 
| 236 230 | 
             
                end
         | 
| 237 231 |  | 
| 238 232 | 
             
                def initialize
         | 
| @@ -244,13 +238,13 @@ module Safrano | |
| 244 238 | 
             
                def odata_h
         | 
| 245 239 | 
             
                  ret = { METAK => { TYPEK => self.class.type_name } }
         | 
| 246 240 |  | 
| 247 | 
            -
                  @values.each  | 
| 241 | 
            +
                  @values.each do |k, v|
         | 
| 248 242 | 
             
                    ret[k] = if v.respond_to? :odata_h
         | 
| 249 243 | 
             
                               v.odata_h
         | 
| 250 244 | 
             
                             else
         | 
| 251 245 | 
             
                               v
         | 
| 252 246 | 
             
                             end
         | 
| 253 | 
            -
                   | 
| 247 | 
            +
                  end
         | 
| 254 248 | 
             
                  ret
         | 
| 255 249 | 
             
                end
         | 
| 256 250 |  | 
| @@ -276,14 +270,14 @@ module Safrano | |
| 276 270 | 
             
                end
         | 
| 277 271 | 
             
              end
         | 
| 278 272 |  | 
| 279 | 
            -
              def  | 
| 273 | 
            +
              def self.ComplexType(**props)
         | 
| 280 274 | 
             
                Class.new(Safrano::ComplexType) do
         | 
| 281 275 | 
             
                  @props = props
         | 
| 282 | 
            -
                  props.each  | 
| 276 | 
            +
                  props.each do |a, _klassmod|
         | 
| 283 277 | 
             
                    asym = a.to_sym
         | 
| 284 | 
            -
                    define_method(asym)  | 
| 285 | 
            -
                    define_method("#{a}=")  | 
| 286 | 
            -
                   | 
| 278 | 
            +
                    define_method(asym) { @values[asym] }
         | 
| 279 | 
            +
                    define_method("#{a}=") { |val| @values[asym] = val }
         | 
| 280 | 
            +
                  end
         | 
| 287 281 | 
             
                  define_method :initialize do |*p, **kwvals|
         | 
| 288 282 | 
             
                    super()
         | 
| 289 283 | 
             
                    p.zip(props.keys).each { |val, a| @values[a] = val } if p
         | 
| @@ -15,36 +15,108 @@ module Safrano | |
| 15 15 | 
             
              # Classes specifying generic types that Sequel will convert to
         | 
| 16 16 | 
             
              # database-specific types.
         | 
| 17 17 | 
             
              DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
         | 
| 18 | 
            -
             | 
| 18 | 
            +
              DB_TYPE_NUMDEC_RGX = /\A(NUMERIC|DECIMAL)\s*(\(\s*((\d+)\s*(,\s*(\d+))?)\s*\))?\s*\z/i.freeze
         | 
| 19 | 
            +
              # thank you rubular
         | 
| 20 | 
            +
              # Test String: DECIMAL (55,2 )
         | 
| 21 | 
            +
              # Match groups
         | 
| 22 | 
            +
              # 1	DECIMAL
         | 
| 23 | 
            +
              # 2	(55,2 )
         | 
| 24 | 
            +
              # 3	55,2
         | 
| 25 | 
            +
              # 4	55
         | 
| 26 | 
            +
              # 5	,2
         | 
| 27 | 
            +
              # 6	2
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              DB_TYPE_FLOATP_RGX = /\A\s*(FLOAT)\s*(\(\s*(\d+)\s*\))?\s*\z/i.freeze
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              # Note: "char" (quoted!) is postgresql's byte type
         | 
| 32 | 
            +
              DB_TYPE_INTLIKE_RGX = /\A\s*(smallserial|smallint|integer|int2|int4|int8|int|mediumint|bigint|serial|bigserial|tinyint)\s*/i.freeze
         | 
| 19 33 | 
             
              # used in $metadata
         | 
| 20 34 | 
             
              # cf. Sequel Database column_schema_default_to_ruby_value
         | 
| 21 35 | 
             
              #                     schema_column_type
         | 
| 22 36 | 
             
              # https://www.odata.org/documentation/odata-version-2-0/overview/
         | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
                 | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 37 | 
            +
             | 
| 38 | 
            +
              # type mappings are hard, especially between "Standards" like SQL and OData V2 (might be a bit better in V4 ?)
         | 
| 39 | 
            +
              # this is all best effort/try to make it work  logic
         | 
| 40 | 
            +
              def self.add_edm_types(metadata, props)
         | 
| 41 | 
            +
                # try num/dec with db_type:
         | 
| 42 | 
            +
                metadata[:edm_type] = if (md = DB_TYPE_NUMDEC_RGX.match(props[:db_type]))
         | 
| 43 | 
            +
                                        prec = md[4]
         | 
| 44 | 
            +
                                        scale = md[6]
         | 
| 45 | 
            +
                                        if scale && prec
         | 
| 46 | 
            +
                                          if scale == '0' # dont force default scale to 0 like SQL standard
         | 
| 47 | 
            +
                                            metadata[:edm_precision] = prec
         | 
| 48 | 
            +
                                            "Edm.Decimal(#{prec})"
         | 
| 49 | 
            +
                                          else
         | 
| 50 | 
            +
                                            # we have precision and scale
         | 
| 51 | 
            +
                                            metadata[:edm_scale] = scale
         | 
| 52 | 
            +
                                            metadata[:edm_precision] = prec
         | 
| 53 | 
            +
                                            "Edm.Decimal(#{prec},#{scale})"
         | 
| 54 | 
            +
                                          end
         | 
| 55 | 
            +
                                        elsif prec
         | 
| 56 | 
            +
                                          # we have precision only
         | 
| 57 | 
            +
                                          metadata[:edm_precision] = prec
         | 
| 58 | 
            +
                                          "Edm.Decimal(#{prec})"
         | 
| 59 | 
            +
                                        else
         | 
| 60 | 
            +
                                          'Edm.Decimal'
         | 
| 61 | 
            +
                                        end
         | 
| 62 | 
            +
                                      end
         | 
| 63 | 
            +
                return if metadata[:edm_type]
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                # try float(prec) with db_type:
         | 
| 66 | 
            +
                metadata[:edm_type] = if (md = DB_TYPE_FLOATP_RGX.match(props[:db_type]))
         | 
| 67 | 
            +
                                        # FLOAT( 22) match groups
         | 
| 68 | 
            +
                                        # 1	FLOAT
         | 
| 69 | 
            +
                                        # 2	(22 )
         | 
| 70 | 
            +
                                        # 3	22
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                                        if (prec = md[3])
         | 
| 73 | 
            +
                                          # we have precision only
         | 
| 74 | 
            +
                                          metadata[:edm_precision] = prec
         | 
| 75 | 
            +
                                          'Edm.Double'
         | 
| 76 | 
            +
                                        end
         | 
| 77 | 
            +
                                      end
         | 
| 78 | 
            +
                return if metadata[:edm_type]
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                # try int-like with db_type:
         | 
| 81 | 
            +
                # smallint|int|integer|bigint|serial|bigserial
         | 
| 82 | 
            +
                metadata[:edm_type] = if (md = DB_TYPE_INTLIKE_RGX.match(props[:db_type]))
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                                        if (itype = md[1])
         | 
| 85 | 
            +
                                          case itype.downcase
         | 
| 86 | 
            +
                                          when 'smallint', 'int2', 'smallserial'
         | 
| 87 | 
            +
                                            'Edm.Int16'
         | 
| 88 | 
            +
                                          when 'int', 'integer', 'serial', 'mediumint', 'int4'
         | 
| 89 | 
            +
                                            'Edm.Int32'
         | 
| 90 | 
            +
                                          when 'bigint', 'bigserial', 'int8'
         | 
| 91 | 
            +
                                            'Edm.Int64'
         | 
| 92 | 
            +
                                          when 'tinyint'
         | 
| 93 | 
            +
                                            'Edm.Byte'
         | 
| 94 | 
            +
                                          end
         | 
| 95 | 
            +
                                        end
         | 
| 96 | 
            +
                                      end
         | 
| 97 | 
            +
                return if metadata[:edm_type]
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                # try with Sequel(ruby) type
         | 
| 100 | 
            +
                metadata[:edm_type] = case props[:type]
         | 
| 101 | 
            +
                                      when :integer
         | 
| 102 | 
            +
                                        'Edm.Int32'
         | 
| 103 | 
            +
                                      when :string
         | 
| 104 | 
            +
                                        'Edm.String'
         | 
| 105 | 
            +
                                      when :date
         | 
| 106 | 
            +
                                        'Edm.DateTime'
         | 
| 107 | 
            +
                                      when :datetime
         | 
| 108 | 
            +
                                        'Edm.DateTime'
         | 
| 109 | 
            +
                                      when :time
         | 
| 110 | 
            +
                                        'Edm.Time'
         | 
| 111 | 
            +
                                      when :boolean
         | 
| 112 | 
            +
                                        'Edm.Boolean'
         | 
| 113 | 
            +
                                      when :float
         | 
| 114 | 
            +
                                        'Edm.Double'
         | 
| 115 | 
            +
                                      when :decimal
         | 
| 116 | 
            +
                                        'Edm.Decimal'
         | 
| 117 | 
            +
                                      when :blob
         | 
| 118 | 
            +
                                        'Edm.Binary'
         | 
| 119 | 
            +
                                      end
         | 
| 48 120 | 
             
              end
         | 
| 49 121 |  | 
| 50 122 | 
             
              # use Edm twice so that we can do include Safrano::Edm and then
         | 
| @@ -69,12 +141,12 @@ module Safrano | |
| 69 141 | 
             
                  class Null < NilClass
         | 
| 70 142 | 
             
                    extend OutputClassMethods
         | 
| 71 143 | 
             
                    # nil --> null convertion is done by to_json
         | 
| 72 | 
            -
                    def self.odata_value( | 
| 144 | 
            +
                    def self.odata_value(_instance)
         | 
| 73 145 | 
             
                      nil
         | 
| 74 146 | 
             
                    end
         | 
| 75 147 |  | 
| 76 148 | 
             
                    def self.convert_from_urlparam(v)
         | 
| 77 | 
            -
                      return Contract::NOK unless  | 
| 149 | 
            +
                      return Contract::NOK unless v == 'null'
         | 
| 78 150 |  | 
| 79 151 | 
             
                      Contract.valid(nil)
         | 
| 80 152 | 
             
                    end
         | 
| @@ -94,7 +166,7 @@ module Safrano | |
| 94 166 | 
             
                  #  or false([nil, false])
         | 
| 95 167 | 
             
                  class Boolean < Object
         | 
| 96 168 | 
             
                    extend OutputClassMethods
         | 
| 97 | 
            -
                    def  | 
| 169 | 
            +
                    def self.odata_value(instance)
         | 
| 98 170 | 
             
                      instance ? true : false
         | 
| 99 171 | 
             
                    end
         | 
| 100 172 |  | 
| @@ -103,7 +175,7 @@ module Safrano | |
| 103 175 | 
             
                    end
         | 
| 104 176 |  | 
| 105 177 | 
             
                    def self.convert_from_urlparam(v)
         | 
| 106 | 
            -
                      return Contract::NOK unless [ | 
| 178 | 
            +
                      return Contract::NOK unless %w[true false].include?(v)
         | 
| 107 179 |  | 
| 108 180 | 
             
                      Contract.valid(v == 'true')
         | 
| 109 181 | 
             
                    end
         | 
| @@ -115,7 +187,7 @@ module Safrano | |
| 115 187 | 
             
                    extend OutputClassMethods
         | 
| 116 188 |  | 
| 117 189 | 
             
                    def self.convert_from_urlparam(v)
         | 
| 118 | 
            -
                      return Contract::NOK unless ( | 
| 190 | 
            +
                      return Contract::NOK unless (bytev = v.to_i) < 256
         | 
| 119 191 |  | 
| 120 192 | 
             
                      Contract.valid(bytev)
         | 
| 121 193 | 
             
                    end
         | 
| @@ -123,7 +195,7 @@ module Safrano | |
| 123 195 |  | 
| 124 196 | 
             
                  class DateTime < ::DateTime
         | 
| 125 197 | 
             
                    extend OutputClassMethods
         | 
| 126 | 
            -
                    def  | 
| 198 | 
            +
                    def self.odata_value(instance)
         | 
| 127 199 | 
             
                      instance.to_datetime
         | 
| 128 200 | 
             
                    end
         | 
| 129 201 |  | 
| @@ -132,11 +204,9 @@ module Safrano | |
| 132 204 | 
             
                    end
         | 
| 133 205 |  | 
| 134 206 | 
             
                    def self.convert_from_urlparam(v)
         | 
| 135 | 
            -
                       | 
| 136 | 
            -
             | 
| 137 | 
            -
                       | 
| 138 | 
            -
                        return convertion_error(v)
         | 
| 139 | 
            -
                      end
         | 
| 207 | 
            +
                      Contract.valid(DateTime.parse(v))
         | 
| 208 | 
            +
                    rescue StandardError
         | 
| 209 | 
            +
                      convertion_error(v)
         | 
| 140 210 | 
             
                    end
         | 
| 141 211 | 
             
                  end
         | 
| 142 212 |  | 
| @@ -172,11 +242,9 @@ module Safrano | |
| 172 242 | 
             
                    extend OutputClassMethods
         | 
| 173 243 |  | 
| 174 244 | 
             
                    def self.convert_from_urlparam(v)
         | 
| 175 | 
            -
                       | 
| 176 | 
            -
             | 
| 177 | 
            -
                       | 
| 178 | 
            -
                        return Contract::NOK
         | 
| 179 | 
            -
                      end
         | 
| 245 | 
            +
                      Contract.valid(v.to_f)
         | 
| 246 | 
            +
                    rescue StandardError
         | 
| 247 | 
            +
                      Contract::NOK
         | 
| 180 248 | 
             
                    end
         | 
| 181 249 | 
             
                  end
         | 
| 182 250 | 
             
                end
         | 
    
        data/lib/odata/entity.rb
    CHANGED
    
    | @@ -2,8 +2,8 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            require 'json'
         | 
| 4 4 | 
             
            require 'rexml/document'
         | 
| 5 | 
            -
            require 'safrano | 
| 6 | 
            -
            require 'odata/model_ext | 
| 5 | 
            +
            require 'safrano'
         | 
| 6 | 
            +
            require 'odata/model_ext' # required for  self.class.entity_type_name ??
         | 
| 7 7 | 
             
            require_relative 'navigation_attribute'
         | 
| 8 8 |  | 
| 9 9 | 
             
            module Safrano
         | 
| @@ -86,9 +86,9 @@ module Safrano | |
| 86 86 | 
             
                  "#{self.class.uri}#{@odata_pk}"
         | 
| 87 87 | 
             
                end
         | 
| 88 88 |  | 
| 89 | 
            -
                D = 'd' | 
| 90 | 
            -
                DJ_OPEN = '{"d":' | 
| 91 | 
            -
                DJ_CLOSE = '}' | 
| 89 | 
            +
                D = 'd'
         | 
| 90 | 
            +
                DJ_OPEN = '{"d":'
         | 
| 91 | 
            +
                DJ_CLOSE = '}'
         | 
| 92 92 |  | 
| 93 93 | 
             
                # Json formatter for a single entity (probably OData V1/V2 like)
         | 
| 94 94 | 
             
                def to_odata_json(request:)
         | 
| @@ -151,11 +151,12 @@ module Safrano | |
| 151 151 | 
             
                  @uparms.check_all.tap_valid { return odata_get_output(req) }
         | 
| 152 152 | 
             
                         .tap_error { |e| return e.odata_get(req) }
         | 
| 153 153 | 
             
                end
         | 
| 154 | 
            +
             | 
| 154 155 | 
             
                def inactive_query_params
         | 
| 155 156 | 
             
                  @inactive_query_params = true
         | 
| 156 157 | 
             
                  self # chaining
         | 
| 157 158 | 
             
                end
         | 
| 158 | 
            -
             | 
| 159 | 
            +
             | 
| 159 160 | 
             
                DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
         | 
| 160 161 | 
             
                  Safrano.remove_nav_relation(assoc, parent)
         | 
| 161 162 | 
             
                  entity.destroy(transaction: false)
         | 
| @@ -175,7 +176,7 @@ module Safrano | |
| 175 176 | 
             
                    destroy(transaction: false)
         | 
| 176 177 | 
             
                  end
         | 
| 177 178 | 
             
                rescue StandardError => e
         | 
| 178 | 
            -
                  raise SequelAdapterError | 
| 179 | 
            +
                  raise SequelAdapterError, e
         | 
| 179 180 | 
             
                end
         | 
| 180 181 |  | 
| 181 182 | 
             
                # TODO: differentiate between POST/PUT/PATCH/MERGE
         | 
| @@ -202,7 +203,7 @@ module Safrano | |
| 202 203 | 
             
                  if req.walker.media_value
         | 
| 203 204 | 
             
                    odata_media_value_put(req)
         | 
| 204 205 | 
             
                  elsif req.accept?(APPJSON)
         | 
| 205 | 
            -
                    data = JSON. | 
| 206 | 
            +
                    data = Safrano::OData::JSON.parse_one(req.body.read, self.class)
         | 
| 206 207 | 
             
                    data.delete('__metadata')
         | 
| 207 208 |  | 
| 208 209 | 
             
                    if req.in_changeset
         | 
| @@ -219,7 +220,7 @@ module Safrano | |
| 219 220 | 
             
                end
         | 
| 220 221 |  | 
| 221 222 | 
             
                def odata_patch(req)
         | 
| 222 | 
            -
                  req.with_parsed_data do |data|
         | 
| 223 | 
            +
                  req.with_parsed_data(self.class) do |data|
         | 
| 223 224 | 
             
                    data.delete('__metadata')
         | 
| 224 225 |  | 
| 225 226 | 
             
                    # validate payload column names
         | 
| @@ -310,40 +311,6 @@ module Safrano | |
| 310 311 | 
             
                end
         | 
| 311 312 | 
             
              end
         | 
| 312 313 |  | 
| 313 | 
            -
              module MappingBeforeOutput
         | 
| 314 | 
            -
                # needed for proper datetime or Decimal output
         | 
| 315 | 
            -
                def casted_values(cols = nil)
         | 
| 316 | 
            -
                  vals = case cols
         | 
| 317 | 
            -
                         when nil
         | 
| 318 | 
            -
                           # we need to dup the model values as we need to change it before passing to_json,
         | 
| 319 | 
            -
                           # but we dont want to interfere with Sequel's owned data
         | 
| 320 | 
            -
                           # (eg because then in worst case it could happen that we write back changed values to DB)
         | 
| 321 | 
            -
                           values_for_odata.dup
         | 
| 322 | 
            -
                         else
         | 
| 323 | 
            -
                           selected_values_for_odata(cols)
         | 
| 324 | 
            -
                         end
         | 
| 325 | 
            -
                  # TODO better design (perf/ do more during startup and less during request runtime )    
         | 
| 326 | 
            -
                  # TODO replace the quick and dirty BigDecimal hack with something better      
         | 
| 327 | 
            -
                  self.class.decimal_cols.each { |dc| vals[dc] = BigDecimal(vals[dc].to_s).to_s('F') if vals.key?(dc) }
         | 
| 328 | 
            -
                         
         | 
| 329 | 
            -
                  self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
         | 
| 330 | 
            -
                  vals
         | 
| 331 | 
            -
                end
         | 
| 332 | 
            -
              end
         | 
| 333 | 
            -
              module NoMappingBeforeOutput
         | 
| 334 | 
            -
                # current model does not have eg. Time fields--> no special mapping, just to_json is fine
         | 
| 335 | 
            -
                # --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
         | 
| 336 | 
            -
                # need to change it, just output as is
         | 
| 337 | 
            -
                def casted_values(cols = nil)
         | 
| 338 | 
            -
                  case cols
         | 
| 339 | 
            -
                  when nil
         | 
| 340 | 
            -
                    values_for_odata
         | 
| 341 | 
            -
                  else
         | 
| 342 | 
            -
                    selected_values_for_odata(cols)
         | 
| 343 | 
            -
                  end
         | 
| 344 | 
            -
                end
         | 
| 345 | 
            -
              end
         | 
| 346 | 
            -
             | 
| 347 314 | 
             
              module MediaEntity
         | 
| 348 315 | 
             
                # media entity metadata for json h
         | 
| 349 316 | 
             
                def metadata_h
         | 
| @@ -434,9 +401,9 @@ module Safrano | |
| 434 401 | 
             
                include Entity
         | 
| 435 402 | 
             
                def pk_uri
         | 
| 436 403 | 
             
                  pku = +''
         | 
| 437 | 
            -
                  self.class.odata_upk_parts.each_with_index  | 
| 404 | 
            +
                  self.class.odata_upk_parts.each_with_index do |upart, i|
         | 
| 438 405 | 
             
                    pku = "#{pku}#{upart}#{pk[i]}"
         | 
| 439 | 
            -
                   | 
| 406 | 
            +
                  end
         | 
| 440 407 | 
             
                  pku
         | 
| 441 408 | 
             
                end
         | 
| 442 409 |  | 
| @@ -452,7 +419,7 @@ module Safrano | |
| 452 419 | 
             
              module EntityCreateStandardOutput
         | 
| 453 420 | 
             
                # Json formatter for a create  entity POST call / Standard version; return as json object
         | 
| 454 421 | 
             
                def to_odata_create_json(request:)
         | 
| 455 | 
            -
                  # TODO Perf: reduce method call overhead
         | 
| 422 | 
            +
                  # TODO: Perf: reduce method call overhead
         | 
| 456 423 | 
             
                  # we added this redirection for readability and flexibility
         | 
| 457 424 | 
             
                  to_odata_json(request: request)
         | 
| 458 425 | 
             
                end
         | 
| @@ -461,7 +428,7 @@ module Safrano | |
| 461 428 | 
             
              module EntityCreateArrayOutput
         | 
| 462 429 | 
             
                # Json formatter for a create  entity POST call Array version
         | 
| 463 430 | 
             
                def to_odata_create_json(request:)
         | 
| 464 | 
            -
                  # TODO Perf: reduce method call overhead
         | 
| 431 | 
            +
                  # TODO: Perf: reduce method call overhead
         | 
| 465 432 | 
             
                  # we added this redirection for readability and flexibility
         | 
| 466 433 | 
             
                  to_odata_array_json(request: request)
         | 
| 467 434 | 
             
                end
         |