create or replace package checker is
  
  subtype t_exception_no is pls_integer range -20500..-20000;
  
  c_null_value constant t_exception_no := -20001;
  
  procedure raise_if_null(p_value varchar2);
  
end;
/

create or replace package body checker is

  type r_caller_info is record(
    owner   varchar2(250),
    name    varchar2(250),
    line_no positive
  );
  
  function who_called( 
    p_level positiven := 1 
  ) return r_caller_info is
    c_pattern           constant varchar2(500) :=  '^((.*'||CHR(10)||'){'||(p_level+4)||'})(.*)'||CHR(10);
    c_stack             constant varchar2(32767) := dbms_utility.format_call_stack();
    c_stack_offset      constant integer := 4;
    l_stack_line        varchar2(4000);
    l_result            r_caller_info;
  begin
    l_stack_line    := regexp_substr( c_stack, c_pattern, 1, 1, '', 3);
    if l_stack_line like '%.%' then
      l_result.line_no := to_number( regexp_substr(l_stack_line, '(0x)?[0-9a-f]+\s+(\d+)', subexpression => 2) );
      l_result.owner   := regexp_substr( l_stack_line, '([A-Za-z0-9$#_]+)\.([A-Za-z0-9$#_]|\.)+', subexpression => 1);
      l_result.name    := regexp_substr( l_stack_line, '([A-Za-z0-9$#_]+)\.([A-Za-z0-9$#_]+)', subexpression => 2);
    end if;
--    dbms_output.put_line(c_stack);
--    dbms_output.put_line('l_result =('||l_result.owner||', '||l_result.name||', '||l_result.line_no||')' );
    return l_result;
  end;
  
  function get_source_line(p_caller r_caller_info) return varchar2 is
    l_line_text varchar2(4000); 
  begin
    select /*+ first_rows(4) */
           listagg(s.text) within group(order by s.line)
      into l_line_text 
      from all_source s
     where s.name = p_caller.name
       and s.owner = p_caller.owner
       and s.line between p_caller.line_no and p_caller.line_no + 4
       and s.type = 'PACKAGE BODY';
--    dbms_output.put_line(l_line_text);
     return l_line_text;
  end;
  
  function get_call_params(
    p_line varchar2,
    p_self_name varchar2
  ) return varchar2 is
  begin
    return  
      translate(
        regexp_substr(
          p_line
          , '\s*'||$$PLSQL_UNIT||'\.'||p_self_name||'\s*\(([^\)]*)\)'
          , 1, 1, 'i', 1
        )
        /*remove spaces, tabs and newlines*/
        ,'a '||chr(9)||chr(10)||chr(13)
        ,'a'
      );
  end;
  
  procedure raise_if_null(p_value varchar2) is
    l_param_name varchar2(4000);
  begin
    if p_value is null then
      l_param_name := get_call_params(get_source_line(who_called()),'RAISE_IF_NULL');
      raise_application_error(c_null_value,'Call parameter: "'||l_param_name||'" is null');
    end if;
  end;
end;
/

--Example:
create or replace type t_obj as object(
  id integer,
  name varchar2(100)
);
/

create or replace package some_api is

  procedure do_stuff(p_obj t_obj);
end;
/

create or replace package body some_api is
  procedure do_stuff(p_obj t_obj)
  is
  begin
    checker.raise_if_null(
      p_obj.id
    );
    commit;
  exception 
    when others then
    rollback;
    raise;
  end;
end;
/
--Check it
begin
  some_api.do_stuff(t_obj(null,null));
end;
/
--Outcomes
/*************
ORA-20001: Call parameter: "p_obj.id" is null
ORA-06512: at "UT3_TESTER.SOME_API", line 12
ORA-06512: at "UT3_TESTER.CHECKER", line 67
ORA-06512: at "UT3_TESTER.SOME_API", line 5
ORA-06512: at line 2
*************/
