Class: RestEasy::Resource
- Inherits:
-
Object
- Object
- RestEasy::Resource
- Extended by:
- Dry::Configurable
- Includes:
- Types
- Defined in:
- lib/rest_easy/resource.rb
Defined Under Namespace
Classes: AttributeBlockDSL, ConfigureDSL, MetaCollector, ModelProxy, ShadowCopy
Constant Summary collapse
- String =
Shadow Ruby’s built-in type names so that inside a regular class body (not Class.new blocks), ‘String`, `Integer`, etc. resolve to Dry::Types equivalents with coercion and constraint support.
Types::Coercible::String
- Integer =
Types::Coercible::Integer
- Float =
Types::Coercible::Float
- Boolean =
Types::Params::Bool
- Date =
Types::Params::Date
- TYPE_MAP =
Map Ruby’s built-in classes to Dry::Types equivalents. Used by ‘attr` to resolve types passed from Class.new blocks where constant lookup doesn’t find our shadowed constants.
{ ::String => Types::Coercible::String, ::Integer => Types::Coercible::Integer, ::Float => Types::Coercible::Float }.freeze
Instance Attribute Summary collapse
-
#meta ⇒ Object
readonly
Returns the value of attribute meta.
Class Method Summary collapse
- .after_parse(&block) ⇒ Object
- .after_serialise(&block) ⇒ Object
- .all ⇒ Object
- .all_attribute_definitions ⇒ Object
- .all_ignored_fields ⇒ Object
-
.attr(name_or_mapping, *args, &block) ⇒ Object
– attr ———————————————————-.
-
.attribute_convention(value = nil) ⇒ Object
– attribute_convention (deprecated) ——————————-.
-
.attributes ⇒ Object
── Attribute introspection ────────────────────────────────────────.
- .attributes_with_flag(flag) ⇒ Object
-
.before_parse(&block) ⇒ Object
– hooks ———————————————————.
- .before_serialise(&block) ⇒ Object
- .configure(&block) ⇒ Object
- .create(instance) ⇒ Object
- .delete(id) ⇒ Object
-
.find(id) ⇒ Object
CRUD operations.
-
.get(path:, params: {}, headers: {}) ⇒ Object
HTTP primitives — delegate to the parent API module’s connection.
-
.ignore(*api_field_names) ⇒ Object
– ignore ——————————————————–.
-
.json_attribute_converter ⇒ Object
– conversions —————————————————.
-
.key(attr_name, type = nil, *flags) ⇒ Object
– key ———————————————————–.
- .key_attribute_name ⇒ Object
-
.metadata(**kwargs) ⇒ Object
– metadata ——————————————————.
-
.parse(api_data) ⇒ Object
── Class-level operations ─────────────────────────────────────────.
- .post(path:, body: nil, headers: {}) ⇒ Object
- .put(path:, body: nil, headers: {}) ⇒ Object
- .query_parameter_converter ⇒ Object
- .resolve_after_parse_hook ⇒ Object
- .resolve_after_serialise_hook ⇒ Object
-
.resolve_before_parse_hook ⇒ Object
── Hook lookup (walks ancestor chain) ─────────────────────────────.
- .resolve_before_serialise_hook ⇒ Object
- .save(instance) ⇒ Object
-
.settings(&block) ⇒ Object
– settings ——————————————————-.
- .stub(**model_data) ⇒ Object
- .stub_defaults ⇒ Object
- .update(instance) ⇒ Object
-
.with_stub(**defaults) ⇒ Object
– with_stub —————————————————–.
Instance Method Summary collapse
- #==(other) ⇒ Object (also: #eql?)
- #__changes__ ⇒ Object
- #api ⇒ Object
-
#config ⇒ Object
Delegate class-level config so hooks can call it via instance_exec.
- #hash ⇒ Object
-
#initialize(model_data = {}) ⇒ Resource
constructor
A new instance of Resource.
- #model ⇒ Object
- #serialise ⇒ Object
- #to_api ⇒ Object
- #to_json(*_args) ⇒ Object
- #unique_id ⇒ Object
- #update(changes = {}, **kwargs) ⇒ Object
Constructor Details
#initialize(model_data = {}) ⇒ Resource
Returns a new instance of Resource.
553 554 555 |
# File 'lib/rest_easy/resource.rb', line 553 def initialize(model_data = {}) init_from_model(model_data) end |
Instance Attribute Details
#meta ⇒ Object (readonly)
Returns the value of attribute meta.
551 552 553 |
# File 'lib/rest_easy/resource.rb', line 551 def @meta end |
Class Method Details
.after_parse(&block) ⇒ Object
359 360 361 |
# File 'lib/rest_easy/resource.rb', line 359 def after_parse(&block) @after_parse_hook = block end |
.after_serialise(&block) ⇒ Object
367 368 369 |
# File 'lib/rest_easy/resource.rb', line 367 def after_serialise(&block) @after_serialise_hook = block end |
.all ⇒ Object
461 462 463 464 |
# File 'lib/rest_easy/resource.rb', line 461 def all response = get(path: config.path.to_s) parse(response) end |
.all_attribute_definitions ⇒ Object
383 384 385 386 |
# File 'lib/rest_easy/resource.rb', line 383 def all_attribute_definitions parent = superclass.respond_to?(:all_attribute_definitions) ? superclass.all_attribute_definitions : {} parent.merge(own_attribute_definitions) end |
.all_ignored_fields ⇒ Object
392 393 394 395 |
# File 'lib/rest_easy/resource.rb', line 392 def all_ignored_fields parent = superclass.respond_to?(:all_ignored_fields) ? superclass.all_ignored_fields : [] parent + own_ignored_fields end |
.attr(name_or_mapping, *args, &block) ⇒ Object
– attr ———————————————————-
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 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 |
# File 'lib/rest_easy/resource.rb', line 208 def attr(name_or_mapping, *args, &block) # Determine attribute_api_name and attribute_model_name if name_or_mapping.is_a?(::Array) attribute_model_name = name_or_mapping[0].to_sym attribute_api_name = name_or_mapping[1].to_s else attribute_model_name = name_or_mapping.to_sym attribute_api_name = json_attribute_converter.serialise(attribute_model_name) end # Extract type (non-Symbol), flags (Symbols), and optional mapper object type = nil flags = [] mapper = nil args.each do |arg| if arg.is_a?(::Symbol) flags << arg elsif arg.respond_to?(:parse) && arg.respond_to?(:serialise) mapper = arg else type = resolve_type(arg) end end raise AttributeError, "Attribute :#{attribute_model_name} must have a type" if type.nil? # Handle mapper object or block DSL for custom parse/serialise parse_block = nil serialise_block = nil source_fields = [] target_fields = [] if mapper parse_block = mapper.method(:parse) serialise_block = mapper.method(:serialise) # Introspect mapper method parameters the same way we do blocks. # This enables merge/split patterns with mapper objects. parse_params = parse_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if parse_params.length > 1 flags << :synthetic unless flags.include?(:synthetic) source_fields = parse_params.map { |_, pname| pname } end serialise_params = serialise_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if serialise_params.length > 1 flags << :synthetic unless flags.include?(:synthetic) target_fields = serialise_params.map { |_, pname| pname } end elsif block block_params = block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if block_params.any? # Bare block with params = implicit parse block. # The parameter names are API field references (resolved via convention). parse_block = block source_fields = block_params.map { |_, pname| pname } flags << :synthetic unless flags.include?(:synthetic) else # DSL block — evaluate to extract parse/serialise sub-blocks dsl = AttributeBlockDSL.new dsl.instance_eval(&block) parse_block = dsl.parse_block serialise_block = dsl.serialise_block # Introspect parse block parameters: if 2+ params, this is a # synthetic attribute. The parameter names are the source API fields # (e.g. |first_name, last_name| → source_fields [:first_name, :last_name]). if parse_block params = parse_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if params.length > 1 flags << :synthetic unless flags.include?(:synthetic) source_fields = params.map { |_, pname| pname } end end # Introspect serialise block parameters: if 2+ params, the parameter # names are model field references to gather during serialisation. if serialise_block params = serialise_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req } if params.length > 1 flags << :synthetic unless flags.include?(:synthetic) target_fields = params.map { |_, pname| pname } end end # Combine pattern (multi-param serialise, no multi-param parse) # has no inbound api_name on parse. If the user also wrote an # explicit `parse` block, it will be silently ignored — warn so # the inconsistency is visible at load time. if target_fields.any? && source_fields.empty? && parse_block warn "RestEasy: :#{attribute_model_name} declares a combine pattern " \ "(serialise from #{target_fields.inspect}) and also defines a parse block. " \ "Combine attributes have no inbound API field to read, so the parse block " \ "will not run. Remove the parse block, or restructure the declaration if you " \ "intended to read from the API." end end end # Handle :key flag if flags.include?(:key) if @key_attribute_name && @key_attribute_name != attribute_model_name warn "Warning: :#{@key_attribute_name} already defined as :key, ignoring :#{attribute_model_name} as :key" else @key_attribute_name = attribute_model_name end end # Register attribute definition own_attribute_definitions[attribute_model_name] = Attribute.new( model_name: attribute_model_name, api_name: attribute_api_name, type:, flags:, parse_block:, serialise_block:, source_fields:, target_fields: ) # Define accessor method on the class define_method(attribute_model_name) { @model_attributes[attribute_model_name] } end |
.attribute_convention(value = nil) ⇒ Object
– attribute_convention (deprecated) ——————————-
171 172 173 174 175 176 177 |
# File 'lib/rest_easy/resource.rb', line 171 def attribute_convention(value = nil) if value warn "RestEasy: attribute_convention is deprecated, use `configure { conversions.json_attributes = #{value.inspect} }` instead" config.conversions.json_attributes = value end json_attribute_converter end |
.attributes ⇒ Object
── Attribute introspection ────────────────────────────────────────
379 380 381 |
# File 'lib/rest_easy/resource.rb', line 379 def attributes all_attribute_definitions.keys end |
.attributes_with_flag(flag) ⇒ Object
388 389 390 |
# File 'lib/rest_easy/resource.rb', line 388 def attributes_with_flag(flag) all_attribute_definitions.select { |_, attr_def| attr_def.flags.include?(flag) } end |
.before_parse(&block) ⇒ Object
– hooks ———————————————————
355 356 357 |
# File 'lib/rest_easy/resource.rb', line 355 def before_parse(&block) @before_parse_hook = block end |
.before_serialise(&block) ⇒ Object
363 364 365 |
# File 'lib/rest_easy/resource.rb', line 363 def before_serialise(&block) @before_serialise_hook = block end |
.configure(&block) ⇒ Object
138 139 140 141 |
# File 'lib/rest_easy/resource.rb', line 138 def configure(&block) dsl = ConfigureDSL.new(config) dsl.instance_eval(&block) end |
.create(instance) ⇒ Object
474 475 476 477 478 479 480 |
# File 'lib/rest_easy/resource.rb', line 474 def create(instance) response = post( path: "#{config.path}", body: instance.serialise ) parse(response) end |
.delete(id) ⇒ Object
490 491 492 |
# File 'lib/rest_easy/resource.rb', line 490 def delete(id) parent.delete(path: "#{config.path}/#{id}") end |
.find(id) ⇒ Object
CRUD operations
456 457 458 459 |
# File 'lib/rest_easy/resource.rb', line 456 def find(id) response = get(path: "#{config.path}/#{id}") parse(response) end |
.get(path:, params: {}, headers: {}) ⇒ Object
HTTP primitives — delegate to the parent API module’s connection
496 497 498 499 500 |
# File 'lib/rest_easy/resource.rb', line 496 def get(path:, params: {}, headers: {}) converter = query_parameter_converter converted_params = converter ? params.transform_keys { |k| converter.serialise(k) } : params parent.get(path:, params: converted_params, headers:) end |
.ignore(*api_field_names) ⇒ Object
– ignore ——————————————————–
347 348 349 350 351 |
# File 'lib/rest_easy/resource.rb', line 347 def ignore(*api_field_names) api_field_names.each do |field_name| own_ignored_fields << field_name.to_sym end end |
.json_attribute_converter ⇒ Object
– conversions —————————————————
155 156 157 158 159 160 161 |
# File 'lib/rest_easy/resource.rb', line 155 def json_attribute_converter Conventions.resolve( config.conversions.json_attributes || parent&.config&.conversions&.json_attributes || Conventions::DEFAULT ) end |
.key(attr_name, type = nil, *flags) ⇒ Object
– key ———————————————————–
334 335 336 337 338 339 340 341 342 343 |
# File 'lib/rest_easy/resource.rb', line 334 def key(attr_name, type = nil, *flags) if @key_attribute_name warn "Warning: key already defined as :#{@key_attribute_name}, overriding with :#{attr_name}" end if type self.attr(attr_name, type, *flags, :key) else self.attr(attr_name, *flags, :key) end end |
.key_attribute_name ⇒ Object
397 398 399 400 |
# File 'lib/rest_easy/resource.rb', line 397 def key_attribute_name @key_attribute_name || (superclass.respond_to?(:key_attribute_name) ? superclass.key_attribute_name : nil) end |
.metadata(**kwargs) ⇒ Object
– metadata ——————————————————
145 146 147 148 149 150 151 |
# File 'lib/rest_easy/resource.rb', line 145 def (**kwargs) if kwargs.any? .merge!(kwargs) else end end |
.parse(api_data) ⇒ Object
── Class-level operations ─────────────────────────────────────────
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'lib/rest_easy/resource.rb', line 431 def parse(api_data) = MetaCollector.new hook = resolve_before_parse_hook if hook api_data = instance_exec(api_data, , &hook) end = .to_h if api_data.is_a?(::Array) api_data.map { |item| allocate.tap { |instance| instance.send(:init_from_api, item, ) } } else allocate.tap { |instance| instance.send(:init_from_api, api_data, ) } end end |
.post(path:, body: nil, headers: {}) ⇒ Object
502 503 504 |
# File 'lib/rest_easy/resource.rb', line 502 def post(path:, body: nil, headers: {}) parent.post(path:, body:, headers:) end |
.put(path:, body: nil, headers: {}) ⇒ Object
506 507 508 |
# File 'lib/rest_easy/resource.rb', line 506 def put(path:, body: nil, headers: {}) parent.put(path:, body:, headers:) end |
.query_parameter_converter ⇒ Object
163 164 165 166 167 |
# File 'lib/rest_easy/resource.rb', line 163 def query_parameter_converter convention = config.conversions.query_parameters || parent&.config&.conversions&.query_parameters convention && Conventions.resolve(convention) end |
.resolve_after_parse_hook ⇒ Object
414 415 416 417 |
# File 'lib/rest_easy/resource.rb', line 414 def resolve_after_parse_hook @after_parse_hook || (superclass.respond_to?(:resolve_after_parse_hook) ? superclass.resolve_after_parse_hook : nil) end |
.resolve_after_serialise_hook ⇒ Object
424 425 426 427 |
# File 'lib/rest_easy/resource.rb', line 424 def resolve_after_serialise_hook @after_serialise_hook || (superclass.respond_to?(:resolve_after_serialise_hook) ? superclass.resolve_after_serialise_hook : nil) end |
.resolve_before_parse_hook ⇒ Object
── Hook lookup (walks ancestor chain) ─────────────────────────────
409 410 411 412 |
# File 'lib/rest_easy/resource.rb', line 409 def resolve_before_parse_hook @before_parse_hook || (superclass.respond_to?(:resolve_before_parse_hook) ? superclass.resolve_before_parse_hook : nil) end |
.resolve_before_serialise_hook ⇒ Object
419 420 421 422 |
# File 'lib/rest_easy/resource.rb', line 419 def resolve_before_serialise_hook @before_serialise_hook || (superclass.respond_to?(:resolve_before_serialise_hook) ? superclass.resolve_before_serialise_hook : nil) end |
.save(instance) ⇒ Object
466 467 468 469 470 471 472 |
# File 'lib/rest_easy/resource.rb', line 466 def save(instance) if instance..new? create(instance) else update(instance) end end |
.settings(&block) ⇒ Object
– settings ——————————————————-
132 133 134 135 136 |
# File 'lib/rest_easy/resource.rb', line 132 def settings(&block) return super() unless block class_eval(&block) end |
.stub(**model_data) ⇒ Object
448 449 450 451 452 |
# File 'lib/rest_easy/resource.rb', line 448 def stub(**model_data) defaults = stub_defaults || {} data = defaults.merge(model_data) allocate.tap { |instance| instance.send(:init_from_model, data) } end |
.stub_defaults ⇒ Object
402 403 404 405 |
# File 'lib/rest_easy/resource.rb', line 402 def stub_defaults parent = superclass.respond_to?(:stub_defaults) ? superclass.stub_defaults : {} (parent || {}).merge(@stub_defaults || {}) end |
.update(instance) ⇒ Object
482 483 484 485 486 487 488 |
# File 'lib/rest_easy/resource.rb', line 482 def update(instance) response = put( path: "#{config.path}/#{instance.unique_id}", body: instance.serialise ) parse(response) end |
.with_stub(**defaults) ⇒ Object
– with_stub —————————————————–
373 374 375 |
# File 'lib/rest_easy/resource.rb', line 373 def with_stub(**defaults) @stub_defaults = defaults end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
667 668 669 670 |
# File 'lib/rest_easy/resource.rb', line 667 def ==(other) other.is_a?(self.class) && self.class == other.class && @model_attributes == other.send(:model_attributes_hash) end |
#__changes__ ⇒ Object
591 592 593 |
# File 'lib/rest_easy/resource.rb', line 591 def __changes__ @changes || {} end |
#api ⇒ Object
561 562 563 |
# File 'lib/rest_easy/resource.rb', line 561 def api ShadowCopy.new(@api_data) end |
#config ⇒ Object
Delegate class-level config so hooks can call it via instance_exec
547 548 549 |
# File 'lib/rest_easy/resource.rb', line 547 def config self.class.config end |
#hash ⇒ Object
674 675 676 |
# File 'lib/rest_easy/resource.rb', line 674 def hash [self.class, @model_attributes].hash end |
#model ⇒ Object
557 558 559 |
# File 'lib/rest_easy/resource.rb', line 557 def model ModelProxy.new(@model_attributes) end |
#serialise ⇒ Object
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 |
# File 'lib/rest_easy/resource.rb', line 595 def serialise klass = self.class # Run before_serialise hook on the instance # Input: model_attributes. Side-effect only; return value ignored. hook = klass.resolve_before_serialise_hook instance_exec(@model_attributes, &hook) if hook result = {} # Serialise all attributes klass.all_attribute_definitions.each do |_model_name, attr_def| next if attr_def.read_only? if attr_def.target_fields.any? # Multi-param serialise: gather model values by param names, splat into block model_values = attr_def.target_fields.map { |fn| @model_attributes[fn] } attr_def.validate_required!(*model_values) result[attr_def.api_name] = attr_def.serialise_value(*model_values) elsif attr_def.source_fields.any? value = @model_attributes[attr_def.model_name] attr_def.validate_required!(value) serialised = attr_def.serialise_value(value) if serialised.is_a?(::Array) # Array return: zip with source field API names convention = klass.json_attribute_converter attr_def.source_fields.zip(serialised).each do |field_name, field_value| api_key = convention.serialise(field_name) result[api_key] = field_value end elsif serialised.is_a?(::Hash) # Hash return: merge into result result.merge!(serialised) else result[attr_def.api_name] = serialised end else value = @model_attributes[attr_def.model_name] attr_def.validate_required!(value) result[attr_def.api_name] = attr_def.serialise_value(value) end end # Merge ignored fields from shadow copy if @api_data && !@api_data.empty? known_api_names = klass.all_attribute_definitions.values.map(&:api_name) @api_data.each do |api_key, value| unless known_api_names.include?(api_key) || result.key?(api_key) result[api_key] = value end end end # Run after_serialise hook on the instance # Input: serialised_data, model. Output: final serialised_data. hook = klass.resolve_after_serialise_hook if hook result = instance_exec(result, model, &hook) end result end |
#to_api ⇒ Object
663 664 665 |
# File 'lib/rest_easy/resource.rb', line 663 def to_api ::JSON.generate(serialise) end |
#to_json(*_args) ⇒ Object
658 659 660 661 |
# File 'lib/rest_easy/resource.rb', line 658 def to_json(*_args) model_hash = @model_attributes.transform_keys(&:to_s) ::JSON.generate(model_hash) end |
#unique_id ⇒ Object
565 566 567 568 |
# File 'lib/rest_easy/resource.rb', line 565 def unique_id key_name = self.class.key_attribute_name key_name ? @model_attributes[key_name] : nil end |
#update(changes = {}, **kwargs) ⇒ Object
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 |
# File 'lib/rest_easy/resource.rb', line 570 def update(changes = {}, **kwargs) changes = changes.merge(kwargs) unless kwargs.empty? return self if changes.empty? klass = self.class coerced = {} changes.each do |attr_name, value| attr_def = klass.all_attribute_definitions[attr_name] coerced[attr_name] = if attr_def && !value.nil? attr_def.coerce(value) else value end end new_model = @model_attributes.merge(coerced) new_instance = self.class.allocate new_instance.send(:init_from_update, new_model, @api_data, coerced) new_instance end |