I'm dealing with this issue by suiting the graphics rendering pipeline and the shader scripts together. A specific kind of render pipeline has its specific stages: Forward lighting, material rendering, screen-space lighting, tone mapping, image post-processing, ... whatever. There are several kinds of properties needed: Material properties for physical based shading (or the suitable NPR properties for NPR rendering; let's call this material, too), camera set-up, screen rect, lights, and so on. Different stages have (partially) distinct requirements, but inside a stage the requirements are fixed.
So here is the deal: A camera provides a uniform block with its parameters, the screen provides one with its own parameters, the model provides one with its material, a post-process provides one with its parameters, a pipeline stage provides one, and perhaps other do so. Each block coming from such a source has an equivalent in the respective shader scripts, and there is a standard name used for it: E.g. CameraSetup, ScreenRect, Material, ... So it is convention that every script implementing forward rendering or material rendering has a slot for e.g. a Material block. Hence one can expect to have blocks with a specific name in a script suitable for a specific kind of rendering stage.
The higher level of rendering is implemented inside the stages of the render pipeline. It fetches the suitable blocks from the various sources and generates suitable binding instructions in the state part of the rendering jobs. The lower level of rendering (i.e. the one that calls OpenGL) then has to process those without the need to know the specific meaning of the parameters in a block.
Perhaps it is worth to mention that I use std140 layout. This makes things easier at least if the block is shared over many calls, like e.g. CameraSetup.
I do it in the following way:
My UniformBuffer has a usage flag, which is "Default" or "Shared". Now we have 2 scenarios on using the UBO:
1) If the UniformBuffer usage is "Shared", then the UBO send an event to the ShaderManager to register this UBO to all the loaded shaders. In my engine it's guaranteed that all the shaders are pre-loaded, so when you create a shared UBO, it's also guaranteed that all the shaders will know about it. When the renderer is binding a shader, it also binds its registered UBOs.
2) If the UniformBuffer usage is "Default", then I manually register the UBO to whichever shader I want.
I use the std140 layout as well.
I am not sure if this approach is 100% correct though. Reading haegarr's approach makes me re-think of my design a bit.
I see, it still doesn't make sense how you would know if the uniform buffers provided fulfill the shader program requirments.